commit b7dc50ed8b8e8f59fcdde9a4fe99b5929b9835f8 parent a363d7d416c958ada7e81c8c359a2e3414d8ea08 Author: JohnMcPMS <johnmcp@microsoft.com> Date: Tue, 14 Jul 2020 13:45:18 -0700 Change show and install to use a narrower search (#492) Diffstat:
16 files changed, 298 insertions(+), 95 deletions(-)
diff --git a/src/AppInstallerCLICore/Commands/SearchCommand.cpp b/src/AppInstallerCLICore/Commands/SearchCommand.cpp @@ -44,7 +44,7 @@ namespace AppInstaller::CLI { context << Workflow::OpenSource << - Workflow::SearchSource << + Workflow::SearchSourceForMany << Workflow::EnsureMatchesFromSearchResult << Workflow::ReportSearchResult; } diff --git a/src/AppInstallerCLICore/Commands/ShowCommand.cpp b/src/AppInstallerCLICore/Commands/ShowCommand.cpp @@ -54,7 +54,7 @@ namespace AppInstaller::CLI { context << Workflow::OpenSource << - Workflow::SearchSource << + Workflow::SearchSourceForSingle << Workflow::EnsureOneMatchFromSearchResult << Workflow::ReportSearchResultIdentity << Workflow::ShowAppVersions; diff --git a/src/AppInstallerCLICore/Resources.h b/src/AppInstallerCLICore/Resources.h @@ -91,9 +91,11 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(MonikerArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(MsixArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(MsixSignatureHashFailed); + WINGET_DEFINE_RESOURCE_STRINGID(MultiplePackagesFound); WINGET_DEFINE_RESOURCE_STRINGID(NameArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(NoApplicableInstallers); WINGET_DEFINE_RESOURCE_STRINGID(NoExperimentalFeaturesMessage); + WINGET_DEFINE_RESOURCE_STRINGID(NoPackageFound); WINGET_DEFINE_RESOURCE_STRINGID(NoVTArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(Options); WINGET_DEFINE_RESOURCE_STRINGID(OverrideArgumentDescription); diff --git a/src/AppInstallerCLICore/Workflows/WorkflowBase.cpp b/src/AppInstallerCLICore/Workflows/WorkflowBase.cpp @@ -32,6 +32,41 @@ namespace AppInstaller::CLI::Workflow { context.Reporter.Info() << "Found " << Execution::NameEmphasis << name << " [" << Execution::IdEmphasis << id << ']' << std::endl; } + + void SearchSourceApplyFilters(Execution::Context& context, SearchRequest& searchRequest, MatchType matchType) + { + const auto& args = context.Args; + + if (args.Contains(Execution::Args::Type::Id)) + { + searchRequest.Filters.emplace_back(ApplicationMatchFilter(ApplicationMatchField::Id, matchType, args.GetArg(Execution::Args::Type::Id))); + } + + if (args.Contains(Execution::Args::Type::Name)) + { + searchRequest.Filters.emplace_back(ApplicationMatchFilter(ApplicationMatchField::Name, matchType, args.GetArg(Execution::Args::Type::Name))); + } + + if (args.Contains(Execution::Args::Type::Moniker)) + { + searchRequest.Filters.emplace_back(ApplicationMatchFilter(ApplicationMatchField::Moniker, matchType, args.GetArg(Execution::Args::Type::Moniker))); + } + + if (args.Contains(Execution::Args::Type::Tag)) + { + searchRequest.Filters.emplace_back(ApplicationMatchFilter(ApplicationMatchField::Tag, matchType, args.GetArg(Execution::Args::Type::Tag))); + } + + if (args.Contains(Execution::Args::Type::Command)) + { + searchRequest.Filters.emplace_back(ApplicationMatchFilter(ApplicationMatchField::Command, matchType, args.GetArg(Execution::Args::Type::Command))); + } + + if (args.Contains(Execution::Args::Type::Count)) + { + searchRequest.MaximumResults = std::stoi(std::string(args.GetArg(Execution::Args::Type::Count))); + } + } } bool WorkflowTask::operator==(const WorkflowTask& other) const @@ -102,11 +137,10 @@ namespace AppInstaller::CLI::Workflow context.Add<Execution::Data::Source>(std::move(source)); } - void SearchSource(Execution::Context& context) + void SearchSourceForMany(Execution::Context& context) { - auto& args = context.Args; + const auto& args = context.Args; - // Construct query MatchType matchType = MatchType::Substring; if (args.Contains(Execution::Args::Type::Exact)) { @@ -119,37 +153,45 @@ namespace AppInstaller::CLI::Workflow searchRequest.Query.emplace(RequestMatch(matchType, args.GetArg(Execution::Args::Type::Query))); } - if (args.Contains(Execution::Args::Type::Id)) - { - searchRequest.Filters.emplace_back(ApplicationMatchFilter(ApplicationMatchField::Id, matchType, args.GetArg(Execution::Args::Type::Id))); - } + SearchSourceApplyFilters(context, searchRequest, matchType); - if (args.Contains(Execution::Args::Type::Name)) - { - searchRequest.Filters.emplace_back(ApplicationMatchFilter(ApplicationMatchField::Name, matchType, args.GetArg(Execution::Args::Type::Name))); - } + Logging::Telemetry().LogSearchRequest( + "many", + args.GetArg(Execution::Args::Type::Query), + args.GetArg(Execution::Args::Type::Id), + args.GetArg(Execution::Args::Type::Name), + args.GetArg(Execution::Args::Type::Moniker), + args.GetArg(Execution::Args::Type::Tag), + args.GetArg(Execution::Args::Type::Command), + searchRequest.MaximumResults, + searchRequest.ToString()); - if (args.Contains(Execution::Args::Type::Moniker)) - { - searchRequest.Filters.emplace_back(ApplicationMatchFilter(ApplicationMatchField::Moniker, matchType, args.GetArg(Execution::Args::Type::Moniker))); - } + context.Add<Execution::Data::SearchResult>(context.Get<Execution::Data::Source>()->Search(searchRequest)); + } - if (args.Contains(Execution::Args::Type::Tag)) - { - searchRequest.Filters.emplace_back(ApplicationMatchFilter(ApplicationMatchField::Tag, matchType, args.GetArg(Execution::Args::Type::Tag))); - } + void SearchSourceForSingle(Execution::Context& context) + { + const auto& args = context.Args; - if (args.Contains(Execution::Args::Type::Command)) + MatchType matchType = MatchType::CaseInsensitive; + if (args.Contains(Execution::Args::Type::Exact)) { - searchRequest.Filters.emplace_back(ApplicationMatchFilter(ApplicationMatchField::Command, matchType, args.GetArg(Execution::Args::Type::Command))); + matchType = MatchType::Exact; } - if (args.Contains(Execution::Args::Type::Count)) + SearchRequest searchRequest; + if (args.Contains(Execution::Args::Type::Query)) { - searchRequest.MaximumResults = std::stoi(std::string(args.GetArg(Execution::Args::Type::Count))); + std::string_view query = args.GetArg(Execution::Args::Type::Query); + searchRequest.Inclusions.emplace_back(ApplicationMatchFilter(ApplicationMatchField::Id, matchType, query)); + searchRequest.Inclusions.emplace_back(ApplicationMatchFilter(ApplicationMatchField::Name, matchType, query)); + searchRequest.Inclusions.emplace_back(ApplicationMatchFilter(ApplicationMatchField::Moniker, matchType, query)); } + SearchSourceApplyFilters(context, searchRequest, matchType); + Logging::Telemetry().LogSearchRequest( + "single", args.GetArg(Execution::Args::Type::Query), args.GetArg(Execution::Args::Type::Id), args.GetArg(Execution::Args::Type::Name), @@ -192,7 +234,7 @@ namespace AppInstaller::CLI::Workflow if (searchResult.Matches.size() == 0) { Logging::Telemetry().LogNoAppMatch(); - context.Reporter.Info() << "No app found matching input criteria." << std::endl; + context.Reporter.Info() << Resource::String::NoPackageFound << std::endl; AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_NO_APPLICATIONS_FOUND); } } @@ -208,7 +250,7 @@ namespace AppInstaller::CLI::Workflow if (searchResult.Matches.size() > 1) { Logging::Telemetry().LogMultiAppMatch(); - context.Reporter.Warn() << "Multiple apps found matching input criteria. Please refine the input." << std::endl; + context.Reporter.Warn() << Resource::String::MultiplePackagesFound << std::endl; context << ReportSearchResult; AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_MULTIPLE_APPLICATIONS_FOUND); } @@ -300,7 +342,7 @@ namespace AppInstaller::CLI::Workflow { context << OpenSource << - SearchSource << + SearchSourceForSingle << EnsureOneMatchFromSearchResult << ReportSearchResultIdentity << GetManifestFromSearchResult; diff --git a/src/AppInstallerCLICore/Workflows/WorkflowBase.h b/src/AppInstallerCLICore/Workflows/WorkflowBase.h @@ -52,7 +52,13 @@ namespace AppInstaller::CLI::Workflow // Required Args: None // Inputs: Source // Outputs: SearchResult - void SearchSource(Execution::Context& context); + void SearchSourceForMany(Execution::Context& context); + + // Performs a search on the source with the semantics of targeting a single application. + // Required Args: None + // Inputs: Source + // Outputs: SearchResult + void SearchSourceForSingle(Execution::Context& context); // Outputs the search results. // Required Args: None diff --git a/src/AppInstallerCLIE2ETests/SearchCommand.cs b/src/AppInstallerCLIE2ETests/SearchCommand.cs @@ -48,7 +48,7 @@ namespace AppInstallerCLIE2ETests // Search through name. No app found because name is "Visual Studio Code" result = TestCommon.RunAICLICommand("search", "--name VisualStudioCode"); Assert.AreEqual(Constants.ErrorCode.ERROR_NO_APPLICATIONS_FOUND, result.ExitCode); - Assert.True(result.StdOut.Contains("No app found matching input criteria.")); + Assert.True(result.StdOut.Contains("No package found matching input criteria.")); // Search Microsoft should return multiple result = TestCommon.RunAICLICommand("search", "Microsoft"); @@ -59,7 +59,7 @@ namespace AppInstallerCLIE2ETests // Search Microsoft with exact arg should return none result = TestCommon.RunAICLICommand("search", "Microsoft -e"); Assert.AreEqual(Constants.ErrorCode.ERROR_NO_APPLICATIONS_FOUND, result.ExitCode); - Assert.True(result.StdOut.Contains("No app found matching input criteria.")); + Assert.True(result.StdOut.Contains("No package found matching input criteria.")); } } } \ No newline at end of file diff --git a/src/AppInstallerCLIE2ETests/ShowCommand.cs b/src/AppInstallerCLIE2ETests/ShowCommand.cs @@ -32,30 +32,28 @@ namespace AppInstallerCLIE2ETests // Show with no arg lists every app and a warning message var result = TestCommon.RunAICLICommand("show", $"-s {ShowTestSourceName}"); Assert.AreEqual(Constants.ErrorCode.ERROR_MULTIPLE_APPLICATIONS_FOUND, result.ExitCode); - Assert.True(result.StdOut.Contains("Multiple apps found matching input criteria. Please refine the input.")); - Assert.True(result.StdOut.Contains("Microsoft.PowerToys")); - Assert.True(result.StdOut.Contains("Microsoft.VisualStudioCode")); - - // Show with multiple search matches shows a "please refine input" - result = TestCommon.RunAICLICommand("show", $"Microsoft -s {ShowTestSourceName}"); - Assert.AreEqual(Constants.ErrorCode.ERROR_MULTIPLE_APPLICATIONS_FOUND, result.ExitCode); - Assert.True(result.StdOut.Contains("Multiple apps found matching input criteria. Please refine the input.")); + Assert.True(result.StdOut.Contains("Multiple packages found matching input criteria. Please refine the input.")); Assert.True(result.StdOut.Contains("Microsoft.PowerToys")); Assert.True(result.StdOut.Contains("Microsoft.VisualStudioCode")); // Show with 0 search match shows a "please refine input" result = TestCommon.RunAICLICommand("show", $"DoesNotExist -s {ShowTestSourceName}"); Assert.AreEqual(Constants.ErrorCode.ERROR_NO_APPLICATIONS_FOUND, result.ExitCode); - Assert.True(result.StdOut.Contains("No app found matching input criteria.")); + Assert.True(result.StdOut.Contains("No package found matching input criteria.")); + + // Show with a substring match still returns 0 results + result = TestCommon.RunAICLICommand("show", $"Microsoft -s {ShowTestSourceName}"); + Assert.AreEqual(Constants.ErrorCode.ERROR_NO_APPLICATIONS_FOUND, result.ExitCode); + Assert.True(result.StdOut.Contains("No package found matching input criteria.")); // Show with 1 search match shows detailed manifest info - result = TestCommon.RunAICLICommand("show", $"VisualStudioCode -s {ShowTestSourceName}"); + result = TestCommon.RunAICLICommand("show", $"Microsoft.VisualStudioCode -s {ShowTestSourceName}"); Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); Assert.True(result.StdOut.Contains("Microsoft.VisualStudioCode")); Assert.True(result.StdOut.Contains("Visual Studio Code")); // Show with --versions list the versions - result = TestCommon.RunAICLICommand("show", $"VisualStudioCode --versions -s {ShowTestSourceName}"); + result = TestCommon.RunAICLICommand("show", $"Microsoft.VisualStudioCode --versions -s {ShowTestSourceName}"); Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); Assert.True(result.StdOut.Contains("Microsoft.VisualStudioCode")); Assert.True(result.StdOut.Contains("1.41.1")); diff --git a/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw b/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw @@ -157,7 +157,7 @@ <value>Done</value> </data> <data name="ExactArgumentDescription" xml:space="preserve"> - <value>Find app using exact match</value> + <value>Find package using exact match</value> </data> <data name="ExperimentalArgumentDescription" xml:space="preserve"> <value>Experimental argument for demonstration purposes</value> @@ -237,13 +237,14 @@ They can be configured through the settings file 'winget settings'.</value> <value>Microsoft is not responsible for, nor does it grant any licenses to, third-party packages.</value> </data> <data name="InstallationRequiresHigherWindows" xml:space="preserve"> - <value>Cannot install application, as it requires a higher version of Windows:</value> + <value>Cannot install package, as it requires a higher version of Windows:</value> </data> <data name="InstallCommandLongDescription" xml:space="preserve"> - <value>Installs the selected application, either found by searching a configured source or directly from a manifest.</value> + <value>Installs the selected package, either found by searching a configured source or directly from a manifest. By default, the query must case-insensitively match the id, name, or moniker of the package. Other fields can be used by passing their appropriate option.</value> + <comment>id, name, and moniker are all named values in our context, and may benefit from not being translated.</comment> </data> <data name="InstallCommandShortDescription" xml:space="preserve"> - <value>Installs the given application</value> + <value>Installs the given package</value> </data> <data name="InstallerHashMismatchAdminBlock" xml:space="preserve"> <value>Installer hash does not match; this cannot be overridden when running as admin</value> @@ -298,7 +299,7 @@ They can be configured through the settings file 'winget settings'.</value> <comment>The primary webpage for the software</comment> </data> <data name="ManifestArgumentDescription" xml:space="preserve"> - <value>The path to the manifest of the application</value> + <value>The path to the manifest of the package</value> </data> <data name="ManifestValidationFail" xml:space="preserve"> <value>Manifest validation failed.</value> @@ -313,7 +314,7 @@ They can be configured through the settings file 'winget settings'.</value> <value>Argument value required, but none found</value> </data> <data name="MonikerArgumentDescription" xml:space="preserve"> - <value>Filter results by app moniker</value> + <value>Filter results by moniker</value> </data> <data name="MsixArgumentDescription" xml:space="preserve"> <value>Input file will be treated as msix; signature hash will be provided if signed</value> @@ -321,6 +322,9 @@ They can be configured through the settings file 'winget settings'.</value> <data name="MsixSignatureHashFailed" xml:space="preserve"> <value>Failed to calculate MSIX signature hash.</value> </data> + <data name="MultiplePackagesFound" xml:space="preserve"> + <value>Multiple packages found matching input criteria. Please refine the input.</value> + </data> <data name="NameArgumentDescription" xml:space="preserve"> <value>Filter results by name</value> </data> @@ -330,6 +334,9 @@ They can be configured through the settings file 'winget settings'.</value> <data name="NoExperimentalFeaturesMessage" xml:space="preserve"> <value>There are currently no exprimental features available. </value> </data> + <data name="NoPackageFound" xml:space="preserve"> + <value>No package found matching input criteria.</value> + </data> <data name="NoVTArgumentDescription" xml:space="preserve"> <value>Disables VirtualTerminal display</value> <comment>{Locked="VirtualTerminal"}</comment> @@ -355,7 +362,7 @@ They can be configured through the settings file 'winget settings'.</value> <value>Privacy Statement</value> </data> <data name="QueryArgumentDescription" xml:space="preserve"> - <value>The query used to search for an app</value> + <value>The query used to search for a package</value> </data> <data name="RainbowArgumentDescription" xml:space="preserve"> <value>Progress display a rainbow of colors</value> @@ -367,10 +374,10 @@ They can be configured through the settings file 'winget settings'.</value> <value>Progress display as the default color</value> </data> <data name="SearchCommandLongDescription" xml:space="preserve"> - <value>Searches for applications from configured sources.</value> + <value>Searches for pacakges from configured sources.</value> </data> <data name="SearchCommandShortDescription" xml:space="preserve"> - <value>Find and show basic info of apps</value> + <value>Find and show basic info of packages</value> </data> <data name="SearchId" xml:space="preserve"> <value>Id</value> @@ -404,10 +411,10 @@ They can be configured through the settings file 'winget settings'.</value> <value>Channel</value> </data> <data name="ShowCommandLongDescription" xml:space="preserve"> - <value>Shows information on a specific application.</value> + <value>Shows information on a specific pacakge. By default, the query must case-insensitively match the id, name, or moniker of the package. Other fields can be used by passing their appropriate option.</value> </data> <data name="ShowCommandShortDescription" xml:space="preserve"> - <value>Shows info about an application</value> + <value>Shows info about an package</value> </data> <data name="ShowVersion" xml:space="preserve"> <value>Version</value> @@ -431,7 +438,7 @@ They can be configured through the settings file 'winget settings'.</value> <value>Adding source:</value> </data> <data name="SourceAddCommandLongDescription" xml:space="preserve"> - <value>Add a new source. A source provides the data for you to discover and install applications. Only add a new source if you trust it as a secure location.</value> + <value>Add a new source. A source provides the data for you to discover and install packages. Only add a new source if you trust it as a secure location.</value> </data> <data name="SourceAddCommandShortDescription" xml:space="preserve"> <value>Add a new source</value> @@ -440,13 +447,13 @@ They can be configured through the settings file 'winget settings'.</value> <value>Argument given to the source</value> </data> <data name="SourceArgumentDescription" xml:space="preserve"> - <value>Find app using the specified source</value> + <value>Find package using the specified source</value> </data> <data name="SourceCommandLongDescription" xml:space="preserve"> - <value>Manage sources with the sub-commands. A source provides the data for you to discover and install applications. Only add a new source if you trust it as a secure location.</value> + <value>Manage sources with the sub-commands. A source provides the data for you to discover and install packages. Only add a new source if you trust it as a secure location.</value> </data> <data name="SourceCommandShortDescription" xml:space="preserve"> - <value>Manage sources of applications</value> + <value>Manage sources of packages</value> </data> <data name="SourceListArg" xml:space="preserve"> <value>Argument</value> @@ -551,7 +558,7 @@ They can be configured through the settings file 'winget settings'.</value> <value>Third Party Notices</value> </data> <data name="ToolDescription" xml:space="preserve"> - <value>The winget command line utility enables installing applications from the command line.</value> + <value>The winget command line utility enables installing applications and other packages from the command line.</value> </data> <data name="ToolInfoArgumentDescription" xml:space="preserve"> <value>Display general info of the tool</value> @@ -594,6 +601,6 @@ They can be configured through the settings file 'winget settings'.</value> <value>Use the specified version; default is the latest version</value> </data> <data name="VersionsArgumentDescription" xml:space="preserve"> - <value>Show available versions of the app</value> + <value>Show available versions of the package</value> </data> </root> \ No newline at end of file diff --git a/src/AppInstallerCLITests/SQLiteIndex.cpp b/src/AppInstallerCLITests/SQLiteIndex.cpp @@ -1244,3 +1244,101 @@ TEST_CASE("SQLiteIndex_Search_MaximumResults_Greater", "[sqliteindex]") REQUIRE(results.Matches.size() == 3); REQUIRE(!results.Truncated); } + +TEST_CASE("SQLiteIndex_Search_QueryAndInclusion", "[sqliteindex]") +{ + TempFile tempFile{ "repolibtest_tempdb"s, ".db"s }; + INFO("Using temporary file named: " << tempFile.GetPath()); + + SQLiteIndex index = SearchTestSetup(tempFile, { + { "Nope", "Name", "Moniker", "Version", "Channel", { "Tag" }, { "Command" }, "Path1" }, + { "Id2", "Na", "Moniker", "Version", "Channel", { "Tag" }, { "Command" }, "Path2" }, + { "Id3", "No", "Moniker", "Version", "Channel", { "Tag" }, { "Command" }, "Path3" }, + }); + + SearchRequest request; + request.Query = RequestMatch(MatchType::CaseInsensitive, "id3"); + request.Inclusions.emplace_back(ApplicationMatchField::Name, MatchType::Substring, "Na"); + + auto results = index.Search(request); + REQUIRE(results.Matches.size() == 3); +} + +TEST_CASE("SQLiteIndex_Search_InclusionOnly", "[sqliteindex]") +{ + TempFile tempFile{ "repolibtest_tempdb"s, ".db"s }; + INFO("Using temporary file named: " << tempFile.GetPath()); + + SQLiteIndex index = SearchTestSetup(tempFile, { + { "Nope", "Name", "Moniker", "Version", "Channel", { "Tag" }, { "Command" }, "Path1" }, + { "Id2", "Na", "Moniker", "Version", "Channel", { "Tag" }, { "Command" }, "Path2" }, + { "Id3", "No", "Moniker", "Version", "Channel", { "Tag" }, { "Command" }, "Path3" }, + }); + + SearchRequest request; + request.Inclusions.emplace_back(ApplicationMatchField::Name, MatchType::Substring, "Na"); + + auto results = index.Search(request); + REQUIRE(results.Matches.size() == 2); +} + +TEST_CASE("SQLiteIndex_Search_InclusionAndFilter", "[sqliteindex]") +{ + TempFile tempFile{ "repolibtest_tempdb"s, ".db"s }; + INFO("Using temporary file named: " << tempFile.GetPath()); + + SQLiteIndex index = SearchTestSetup(tempFile, { + { "Nope", "Name", "Moniker", "Version", "Channel", { "Tag" }, { "Command" }, "Path1" }, + { "Id2", "Na", "Moniker", "Version", "Channel", { "Tag" }, { "Command" }, "Path2" }, + { "Id3", "No", "Moniker", "Version", "Channel", { "Tag" }, { "Command" }, "Path3" }, + }); + + SearchRequest request; + request.Inclusions.emplace_back(ApplicationMatchField::Name, MatchType::Substring, "Na"); + request.Filters.emplace_back(ApplicationMatchField::Name, MatchType::CaseInsensitive, "name"); + + auto results = index.Search(request); + REQUIRE(results.Matches.size() == 1); + + auto result = index.GetIdStringById(results.Matches[0].first); + REQUIRE(result.has_value()); + REQUIRE(result.value() == "Nope"); +} + +TEST_CASE("SQLiteIndex_Search_QueryInclusionAndFilter", "[sqliteindex]") +{ + TempFile tempFile{ "repolibtest_tempdb"s, ".db"s }; + INFO("Using temporary file named: " << tempFile.GetPath()); + + SQLiteIndex index = SearchTestSetup(tempFile, { + { "Nope", "Name", "Moniker", "Version", "Channel", { "Tag" }, { "Command" }, "Path1" }, + { "Id2", "Na", "monicka", "Version", "Channel", { "Tag" }, { "Command" }, "Path2" }, + { "Id3", "No", "moniker", "Version", "Channel", { "Tag" }, { "Command" }, "Path3" }, + }); + + SearchRequest request; + request.Query = RequestMatch(MatchType::Substring, "id3"); + request.Inclusions.emplace_back(ApplicationMatchField::Name, MatchType::Substring, "na"); + request.Filters.emplace_back(ApplicationMatchField::Moniker, MatchType::CaseInsensitive, "MONIKER"); + + auto results = index.Search(request); + REQUIRE(results.Matches.size() == 2); +} + +TEST_CASE("SQLiteIndex_Search_CaseInsensitive", "[sqliteindex]") +{ + TempFile tempFile{ "repolibtest_tempdb"s, ".db"s }; + INFO("Using temporary file named: " << tempFile.GetPath()); + + SQLiteIndex index = SearchTestSetup(tempFile, { + { "Nope", "id3", "Moniker", "Version", "Channel", { "Tag" }, { "Command" }, "Path1" }, + { "Id2", "Na", "Moniker", "Version", "Channel", { "ID3" }, { "Command" }, "Path2" }, + { "Id3", "No", "Moniker", "Version", "Channel", { "Tag" }, { "Command" }, "Path3" }, + }); + + SearchRequest request; + request.Query = RequestMatch(MatchType::CaseInsensitive, "id3"); + + auto results = index.Search(request); + REQUIRE(results.Matches.size() == 3); +} diff --git a/src/AppInstallerCLITests/WorkFlow.cpp b/src/AppInstallerCLITests/WorkFlow.cpp @@ -15,6 +15,7 @@ #include <Commands/InstallCommand.h> #include <Commands/ShowCommand.h> #include <winget/LocIndependent.h> +#include <Resources.h> using namespace winrt::Windows::Foundation; using namespace winrt::Windows::Management::Deployment; @@ -65,31 +66,41 @@ struct TestSource : public ISource SearchResult Search(const SearchRequest& request) override { SearchResult result; - if (request.Query.has_value()) + + std::string input; + + if (request.Query) { - if (request.Query.value().Value == "TestQueryReturnOne") - { - auto manifest = Manifest::CreateFromPath(TestDataFile("InstallFlowTest_Exe.yaml")); - result.Matches.emplace_back( - ResultMatch( - std::make_unique<TestApplication>(manifest), - ApplicationMatchFilter(ApplicationMatchField::Id, MatchType::Exact, "TestQueryReturnOne"))); - } - else if (request.Query.value().Value == "TestQueryReturnTwo") - { - auto manifest = Manifest::CreateFromPath(TestDataFile("InstallFlowTest_Exe.yaml")); - result.Matches.emplace_back( - ResultMatch( - std::make_unique<TestApplication>(manifest), - ApplicationMatchFilter(ApplicationMatchField::Id, MatchType::Exact, "TestQueryReturnTwo"))); - - auto manifest2 = Manifest::CreateFromPath(TestDataFile("Manifest-Good.yaml")); - result.Matches.emplace_back( - ResultMatch( - std::make_unique<TestApplication>(manifest2), - ApplicationMatchFilter(ApplicationMatchField::Id, MatchType::Exact, "TestQueryReturnTwo"))); - } + input = request.Query->Value; + } + else if (!request.Inclusions.empty()) + { + input = request.Inclusions[0].Value; + } + + if (input == "TestQueryReturnOne") + { + auto manifest = Manifest::CreateFromPath(TestDataFile("InstallFlowTest_Exe.yaml")); + result.Matches.emplace_back( + ResultMatch( + std::make_unique<TestApplication>(manifest), + ApplicationMatchFilter(ApplicationMatchField::Id, MatchType::Exact, "TestQueryReturnOne"))); } + else if (input == "TestQueryReturnTwo") + { + auto manifest = Manifest::CreateFromPath(TestDataFile("InstallFlowTest_Exe.yaml")); + result.Matches.emplace_back( + ResultMatch( + std::make_unique<TestApplication>(manifest), + ApplicationMatchFilter(ApplicationMatchField::Id, MatchType::Exact, "TestQueryReturnTwo"))); + + auto manifest2 = Manifest::CreateFromPath(TestDataFile("Manifest-Good.yaml")); + result.Matches.emplace_back( + ResultMatch( + std::make_unique<TestApplication>(manifest2), + ApplicationMatchFilter(ApplicationMatchField::Id, MatchType::Exact, "TestQueryReturnTwo"))); + } + return result; } @@ -444,7 +455,7 @@ TEST_CASE("InstallFlow_SearchFoundNoApp", "[InstallFlow]") INFO(installOutput.str()); // Verify proper message is printed - REQUIRE(installOutput.str().find("No app found matching input criteria.") != std::string::npos); + REQUIRE(installOutput.str().find(Resource::LocString(Resource::String::NoPackageFound).get()) != std::string::npos); } TEST_CASE("InstallFlow_SearchFoundMultipleApp", "[InstallFlow]") @@ -459,7 +470,7 @@ TEST_CASE("InstallFlow_SearchFoundMultipleApp", "[InstallFlow]") INFO(installOutput.str()); // Verify proper message is printed - REQUIRE(installOutput.str().find("Multiple apps found matching input criteria. Please refine the input.") != std::string::npos); + REQUIRE(installOutput.str().find(Resource::LocString(Resource::String::MultiplePackagesFound).get()) != std::string::npos); } TEST_CASE("InstallFlow_SearchAndShowAppInfo", "[ShowFlow]") diff --git a/src/AppInstallerCommonCore/AppInstallerTelemetry.cpp b/src/AppInstallerCommonCore/AppInstallerTelemetry.cpp @@ -284,6 +284,7 @@ namespace AppInstaller::Logging } void TelemetryTraceLogger::LogSearchRequest( + std::string_view type, std::string_view query, std::string_view id, std::string_view name, @@ -299,6 +300,7 @@ namespace AppInstaller::Logging "SearchRequest", GetActivityId(), nullptr, + AICLI_TraceLoggingStringView(type, "Type"), AICLI_TraceLoggingStringView(query, "Query"), AICLI_TraceLoggingStringView(id, "Id"), AICLI_TraceLoggingStringView(name, "Name"), diff --git a/src/AppInstallerCommonCore/Errors.cpp b/src/AppInstallerCommonCore/Errors.cpp @@ -51,11 +51,11 @@ namespace AppInstaller case APPINSTALLER_CLI_ERROR_SOURCE_ARG_ALREADY_EXISTS: return "The source location is already configured under another name"; case APPINSTALLER_CLI_ERROR_NO_APPLICATIONS_FOUND: - return "No applications found"; + return "No packages found"; case APPINSTALLER_CLI_ERROR_NO_SOURCES_DEFINED: return "No sources are configured"; case APPINSTALLER_CLI_ERROR_MULTIPLE_APPLICATIONS_FOUND: - return "Multiple applications found matching the criteria"; + return "Multiple packages found matching the criteria"; case APPINSTALLER_CLI_ERROR_NO_MANIFEST_FOUND: return "No manifest found matching the criteria"; case APPINSTALLER_CLI_ERROR_COMMAND_REQUIRES_ADMIN: diff --git a/src/AppInstallerCommonCore/Public/AppInstallerTelemetry.h b/src/AppInstallerCommonCore/Public/AppInstallerTelemetry.h @@ -60,6 +60,7 @@ namespace AppInstaller::Logging // Logs details of a search request. void LogSearchRequest( + std::string_view type, std::string_view query, std::string_view id, std::string_view name, diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/1_0/Interface.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/1_0/Interface.cpp @@ -143,14 +143,16 @@ namespace AppInstaller::Repository::Microsoft::Schema::V1_0 { case MatchType::Exact: return { MatchType::Exact }; + case MatchType::CaseInsensitive: + return { MatchType::Exact, MatchType::CaseInsensitive }; case MatchType::Substring: - return { MatchType::Exact, MatchType::Substring }; + return { MatchType::Exact, MatchType::CaseInsensitive, MatchType::Substring }; case MatchType::Wildcard: return { MatchType::Wildcard }; case MatchType::Fuzzy: - return { MatchType::Exact, MatchType::Fuzzy }; + return { MatchType::Exact, MatchType::CaseInsensitive, MatchType::Fuzzy }; case MatchType::FuzzySubstring: - return { MatchType::Exact, MatchType::Fuzzy, MatchType::Substring, MatchType::FuzzySubstring }; + return { MatchType::Exact, MatchType::CaseInsensitive, MatchType::Fuzzy, MatchType::Substring, MatchType::FuzzySubstring }; default: THROW_HR(E_UNEXPECTED); } @@ -366,8 +368,8 @@ namespace AppInstaller::Repository::Microsoft::Schema::V1_0 ISQLiteIndex::SearchResult Interface::Search(SQLite::Connection& connection, const SearchRequest& request) { - // If no query or filters, get everything - if (!request.Query && request.Filters.empty()) + // If an empty request, get everything + if (!request.Query && request.Inclusions.empty() && request.Filters.empty()) { std::vector<SQLite::rowid_t> ids = IdTable::GetAllRowIds(connection, request.MaximumResults); @@ -384,9 +386,10 @@ namespace AppInstaller::Repository::Microsoft::Schema::V1_0 // First phase, create the search results table and populate it with the initial results. // If the Query is provided, we search across many fields and put results in together. - // If not, we take the first filter and use it as the initial results search. + // If Inclusions has fields, we add these to the data. + // If neither is defined, we take the first filter and use it as the initial results search. SearchResultsTable resultsTable(connection); - size_t filterIndex = 0; + bool inclusionsAttempted = false; if (request.Query) { @@ -401,8 +404,25 @@ namespace AppInstaller::Repository::Microsoft::Schema::V1_0 resultsTable.SearchOnField(ApplicationMatchField::Command, match, query.Value); resultsTable.SearchOnField(ApplicationMatchField::Tag, match, query.Value); } + + inclusionsAttempted = true; } - else + + if (!request.Inclusions.empty()) + { + for (const auto& include : request.Inclusions) + { + for (MatchType match : GetMatchTypeOrder(include.Type)) + { + resultsTable.SearchOnField(include.Field, match, include.Value); + } + } + + inclusionsAttempted = true; + } + + size_t filterIndex = 0; + if (!inclusionsAttempted) { THROW_HR_IF(E_UNEXPECTED, request.Filters.empty()); diff --git a/src/AppInstallerRepositoryCore/Public/AppInstallerRepositorySearch.h b/src/AppInstallerRepositoryCore/Public/AppInstallerRepositorySearch.h @@ -19,6 +19,7 @@ namespace AppInstaller::Repository enum class MatchType { Exact, + CaseInsensitive, Substring, Wildcard, Fuzzy, @@ -53,12 +54,20 @@ namespace AppInstaller::Repository }; // Container for data used to filter the available manifests in a source. + // It can be thought of as: + // (Query || Inclusions...) && Filters... + // If Query and Inclusions are both empty, the starting data set will be the entire database. + // Everything && Filters... struct SearchRequest { // The generic query matches against a source defined set of fields. - // If not provided, the filters should be used against the entire dataset. std::optional<RequestMatch> Query; + // Specific fields used to include more data. + // If Query is defined, this can add more rows afterward. + // If Query is not defined, this is the only set of data included. + std::vector<ApplicationMatchFilter> Inclusions; + // Specific fields used to filter the data further. std::vector<ApplicationMatchFilter> Filters; @@ -122,6 +131,8 @@ namespace AppInstaller::Repository { case MatchType::Exact: return "Exact"sv; + case MatchType::CaseInsensitive: + return "CaseInsensitive"sv; case MatchType::Substring: return "Substring"sv; case MatchType::Wildcard: diff --git a/src/AppInstallerRepositoryCore/RepositorySource.cpp b/src/AppInstallerRepositoryCore/RepositorySource.cpp @@ -652,6 +652,11 @@ namespace AppInstaller::Repository result << "[none]"; } + for (const auto& include : Inclusions) + { + result << " Inclusions:" << ApplicationMatchFieldToString(include.Field) << "='" << include.Value << "'[" << MatchTypeToString(include.Type) << "]"; + } + for (const auto& filter : Filters) { result << " Filter:" << ApplicationMatchFieldToString(filter.Field) << "='" << filter.Value << "'[" << MatchTypeToString(filter.Type) << "]";