diff --git a/docker/Tests/vswhere.tests.ps1 b/docker/Tests/vswhere.tests.ps1 index 4a2db02..0c2df73 100644 --- a/docker/Tests/vswhere.tests.ps1 +++ b/docker/Tests/vswhere.tests.ps1 @@ -2,57 +2,97 @@ # Licensed under the MIT license. See LICENSE.txt in the project root for license information. Describe 'vswhere' { - Context 'format: text (default)' { - It 'returns 2 instances' { + Context '(no arguments)' { + It 'returns 2 instances using "text"' { $instanceIds = C:\bin\vswhere.exe | Select-String 'instanceId: \w+' $instanceIds.Count | Should Be 2 } - It '-all returns 3 instances' { + It 'returns 2 instances using "json"' { + $instances = C:\bin\vswhere.exe -format json | ConvertFrom-Json + $instances.Count | Should Be 2 + } + } + + Context '-all' { + It 'returns 3 instances using "text"' { $instanceIds = C:\bin\vswhere.exe -all | Select-String 'instanceId: \w+' $instanceIds.Count | Should Be 3 } - It '-products returns 1 instance' { + It 'returns 3 instances using "json"' { + $instances = C:\bin\vswhere.exe -all -format json | ConvertFrom-Json + $instances.Count | Should Be 3 + } + } + + Context '-products' { + It 'returns 1 instance using "text"' { $instanceIds = C:\bin\vswhere.exe -products microsoft.visualstudio.product.buildtools | Select-String 'instanceId: \w+' $instanceIds.Count | Should Be 1 + $instanceIds.Matches[0].Value | Should Be 'instanceId: 4' + } + + It 'returns 1 instance using "json"' { + $instances = C:\bin\vswhere.exe -products microsoft.visualstudio.product.buildtools -format json | ConvertFrom-Json + $instances.Count | Should Be 1 + $instances[0].instanceId | Should Be 4 } + } - It '-requires returns 1 instance' { + Context '-requires' { + It 'returns 1 instance using "text"' { $instanceIds = C:\bin\vswhere.exe -requires microsoft.visualstudio.workload.nativedesktop | Select-String 'instanceId: \w+' $instanceIds.Count | Should Be 1 + $instanceIds.Matches[0].Value | Should Be 'instanceId: 2' } - It '-version returns 1 instance' { - $instanceIds = C:\bin\vswhere.exe -version '(15.0.26116,]' | Select-String 'instanceId: \w+' - $instanceIds.Count | Should Be 1 + It 'returns 1 instance using "json"' { + $instances = C:\bin\vswhere.exe -requires microsoft.visualstudio.workload.nativedesktop -format json | ConvertFrom-Json + $instances.Count | Should Be 1 + $instances[0].instanceId | Should Be 2 } } - Context 'format: json' { - It 'returns 2 instances' { - $instances = C:\bin\vswhere.exe -format json | ConvertFrom-Json - $instances.Count | Should Be 2 + Context '-version' { + It 'returns 1 instance using "text"' { + $instanceIds = C:\bin\vswhere.exe -version '(15.0.26116,]' | Select-String 'instanceId: \w+' + $instanceIds.Count | Should Be 1 + $instanceIds.Matches[0].Value | Should Be 'instanceId: 2' } - It '-all returns 3 instances' { - $instances = C:\bin\vswhere.exe -all -format json | ConvertFrom-Json - $instances.Count | Should Be 3 + It 'returns 1 instance using "json"' { + $instances = C:\bin\vswhere.exe -version '(15.0.26116,]' -format json | ConvertFrom-Json + $instances.Count | Should Be 1 + $instances[0].instanceId | Should Be 2 } + } - It '-products returns 1 instance' { - $instance = C:\bin\vswhere.exe -products microsoft.visualstudio.product.buildtools -format json | ConvertFrom-Json - $instance.Count | Should Be 1 + Context '-latest' { + It 'returns 1 instance using "text"' { + $instanceIds = C:\bin\vswhere.exe -latest | Select-String 'instanceId: \w+' + $instanceIds.Count | Should Be 1 + $instanceIds.Matches[0].Value | Should Be 'instanceId: 2' } - It '-requires returns 1 instance' { - $instances = C:\bin\vswhere.exe -requires microsoft.visualstudio.workload.nativedesktop -format json | ConvertFrom-Json + It 'returns 1 instance using "json"' { + $instances = C:\bin\vswhere.exe -latest -format json | ConvertFrom-Json $instances.Count | Should Be 1 + $instances[0].instanceId | Should Be 2 + } + } + + Context '-latest -all' { + It 'returns 1 instance using "text"' { + $instanceIds = C:\bin\vswhere.exe -latest -all | Select-String 'instanceId: \w+' + $instanceIds.Count | Should Be 1 + $instanceIds.Matches[0].Value | Should Be 'instanceId: 3' } - It '-version returns 1 instance' { - $instances = C:\bin\vswhere.exe -version '(15.0.26116,]' -format json | ConvertFrom-Json + It 'returns 1 instance using "json"' { + $instances = C:\bin\vswhere.exe -latest -all -format json | ConvertFrom-Json $instances.Count | Should Be 1 + $instances[0].instanceId | Should Be 3 } } } diff --git a/src/vswhere.lib/InstanceSelector.cpp b/src/vswhere.lib/InstanceSelector.cpp index 936c11e..9c7db44 100644 --- a/src/vswhere.lib/InstanceSelector.cpp +++ b/src/vswhere.lib/InstanceSelector.cpp @@ -8,17 +8,16 @@ using namespace std; using std::placeholders::_1; -const ci_equal InstanceSelector::s_comparer; - InstanceSelector::InstanceSelector(_In_ const CommandArgs& args, _In_opt_ ISetupHelper* pHelper) : - m_args(args) + m_args(args), + m_ullMinimumVersion(0), + m_ullMaximumVersion(0) { - // Get the ISetupHelper (if implemented) if a version range is specified. - auto version = args.get_Version(); - if (!version.empty()) + m_helper = pHelper; + if (m_helper) { - m_helper = pHelper; - if (m_helper) + auto version = args.get_Version(); + if (!version.empty()) { auto hr = m_helper->ParseVersionRange(version.c_str(), &m_ullMinimumVersion, &m_ullMaximumVersion); if (FAILED(hr)) @@ -31,6 +30,65 @@ InstanceSelector::InstanceSelector(_In_ const CommandArgs& args, _In_opt_ ISetup } } +bool InstanceSelector::Less(const ISetupInstancePtr& a, const ISetupInstancePtr& b) const +{ + static ci_equal equal; + static ci_less less; + + bstr_t bstrVersionA, bstrVersionB; + ULONGLONG ullVersionA, ullVersionB; + FILETIME ftDateA, ftDateB; + + // Compare versions. + auto hrA = a->GetInstallationVersion(bstrVersionA.GetAddress()); + auto hrB = b->GetInstallationVersion(bstrVersionB.GetAddress()); + if (SUCCEEDED(hrA) && SUCCEEDED(hrB)) + { + if (m_helper) + { + hrA = m_helper->ParseVersion(bstrVersionA, &ullVersionA); + hrB = m_helper->ParseVersion(bstrVersionB, &ullVersionB); + if (SUCCEEDED(hrA) && SUCCEEDED(hrB)) + { + if (ullVersionA != ullVersionB) + { + return ullVersionA < ullVersionB; + } + } + else + { + // a is less than b if we can't parse version for a but can for b. + return SUCCEEDED(hrB); + } + } + } + else + { + // a is less than b if we can't get version for a but can for b. + return SUCCEEDED(hrB); + } + + // Compare dates. + hrA = a->GetInstallDate(&ftDateA); + hrB = b->GetInstallDate(&ftDateB); + if (SUCCEEDED(hrA) && SUCCEEDED(hrB)) + { + auto result = ::CompareFileTime(&ftDateA, &ftDateB); + if (0 == result) + { + auto message = ResourceManager::GetString(IDS_E_UNEXPECTEDDATE); + throw win32_error(E_UNEXPECTED, message); + } + + return 0 > result; + } + else + { + // a is less than b if we can't get date for a but can for b. + return SUCCEEDED(hrB); + } +} + vector InstanceSelector::Select(_In_ IEnumSetupInstances* pEnum) const { _ASSERT(pEnum); @@ -55,6 +113,19 @@ vector InstanceSelector::Select(_In_ IEnumSetupInstances* pEn } } while (SUCCEEDED(hr) && celtFetched); + if (m_args.get_Latest() && 1 < instances.size()) + { + sort(instances.begin(), instances.end(), [&](const ISetupInstancePtr& a, const ISetupInstancePtr& b) -> bool + { + return Less(a, b); + }); + + return vector + { + instances.back(), + }; + } + return instances; } @@ -168,8 +239,7 @@ bool InstanceSelector::IsVersionMatch(_In_ ISetupInstance* pInstance) const { _ASSERT(pInstance); - // m_helper will be NULL if no version range was specified or the interface was not yet implemented. - if (!m_helper) + if (!HasVersionRange()) { return true; } diff --git a/src/vswhere.lib/InstanceSelector.h b/src/vswhere.lib/InstanceSelector.h index bc8ca42..1ab701b 100644 --- a/src/vswhere.lib/InstanceSelector.h +++ b/src/vswhere.lib/InstanceSelector.h @@ -17,6 +17,7 @@ class InstanceSelector { } + bool Less(const ISetupInstancePtr& a, const ISetupInstancePtr& b) const; std::vector Select(_In_ IEnumSetupInstances* pEnum) const; private: @@ -25,8 +26,11 @@ class InstanceSelector bool IsProductMatch(_In_ ISetupInstance2* pInstance) const; bool IsWorkloadMatch(_In_ ISetupInstance2* pInstance) const; bool IsVersionMatch(_In_ ISetupInstance* pInstance) const; + bool HasVersionRange() const + { + return m_ullMinimumVersion != 0 && m_ullMaximumVersion != 0; + } - static const ci_equal s_comparer; const CommandArgs& m_args; ULONGLONG m_ullMinimumVersion; ULONGLONG m_ullMaximumVersion; diff --git a/src/vswhere.lib/resource.h b/src/vswhere.lib/resource.h index b437d96..63836cb 100644 Binary files a/src/vswhere.lib/resource.h and b/src/vswhere.lib/resource.h differ diff --git a/src/vswhere.lib/vswhere.lib.rc b/src/vswhere.lib/vswhere.lib.rc index 310b019..88a3f54 100644 Binary files a/src/vswhere.lib/vswhere.lib.rc and b/src/vswhere.lib/vswhere.lib.rc differ diff --git a/test/vswhere.test/InstanceSelectorTests.cpp b/test/vswhere.test/InstanceSelectorTests.cpp index ffcca84..6914dda 100644 --- a/test/vswhere.test/InstanceSelectorTests.cpp +++ b/test/vswhere.test/InstanceSelectorTests.cpp @@ -11,7 +11,7 @@ using namespace Microsoft::VisualStudio::CppUnitTestFramework; TEST_CLASS(InstanceSelectorTests) { public: - TEST_METHOD(NoProduct) + TEST_METHOD(Select_No_Product) { TestInstance instance = { @@ -33,7 +33,7 @@ TEST_CLASS(InstanceSelectorTests) Assert::AreEqual(0, selected.size()); } - TEST_METHOD(Default_Product) + TEST_METHOD(Select_Default_Product) { TestPackageReference product = { @@ -61,7 +61,7 @@ TEST_CLASS(InstanceSelectorTests) Assert::AreEqual(1, selected.size()); } - TEST_METHOD(Other_Product) + TEST_METHOD(Select_Other_Product) { TestPackageReference product = { @@ -89,14 +89,14 @@ TEST_CLASS(InstanceSelectorTests) Assert::AreEqual(1, selected.size()); } - TEST_METHOD(NoVersion) + TEST_METHOD(Select_No_Version) { TestInstance instance = { { L"InstanceId", L"a1b2c3" }, { L"InstallationName", L"test" }, }; - + TestEnumInstances instances = { &instance, @@ -113,7 +113,7 @@ TEST_CLASS(InstanceSelectorTests) Assert::AreEqual(0, selected.size()); } - TEST_METHOD(NoWorkload) + TEST_METHOD(Select_No_Workload) { TestPackageReference product = { @@ -149,7 +149,7 @@ TEST_CLASS(InstanceSelectorTests) Assert::AreEqual(0, selected.size()); } - TEST_METHOD(Requires_Workload) + TEST_METHOD(Select_Requires_Workload) { TestPackageReference product = { @@ -185,7 +185,7 @@ TEST_CLASS(InstanceSelectorTests) Assert::AreEqual(1, selected.size()); } - TEST_METHOD(InvalidVersionRange) + TEST_METHOD(Select_Invalid_Version_Range) { CommandArgs args; args.Parse(L"vswhere.exe -version invalid"); @@ -194,4 +194,199 @@ TEST_CLASS(InstanceSelectorTests) Assert::ExpectException([&] { InstanceSelector(args, &helper); }); } + + TEST_METHOD(Less_No_Version_A) + { + CommandArgs args; + args.Parse(L"vswhere.exe"); + + TestInstance a = + { + }; + + TestInstance b = + { + { L"InstallationVersion", L"2.0" }, + }; + + TestHelper helper(0, 0); + + InstanceSelector sut(args, &helper); + Assert::IsTrue(sut.Less(&a, &b)); + } + + TEST_METHOD(Less_No_Version_B) + { + CommandArgs args; + args.Parse(L"vswhere.exe"); + + TestInstance a = + { + { L"InstallationVersion", L"1.0" }, + }; + + TestInstance b = + { + }; + + TestHelper helper(0, 0); + + InstanceSelector sut(args, &helper); + Assert::IsFalse(sut.Less(&a, &b)); + } + + TEST_METHOD(Less_Version_A) + { + CommandArgs args; + args.Parse(L"vswhere.exe"); + + TestInstance a = + { + { L"InstallationVersion", L"1.0" }, + }; + + TestInstance b = + { + { L"InstallationVersion", L"2.0" }, + }; + + TestHelper helper(0, 0); + + InstanceSelector sut(args, &helper); + Assert::IsTrue(sut.Less(&a, &b)); + } + + TEST_METHOD(Less_Version_B) + { + CommandArgs args; + args.Parse(L"vswhere.exe"); + + TestInstance a = + { + { L"InstallationVersion", L"2.0" }, + }; + + TestInstance b = + { + { L"InstallationVersion", L"1.0" }, + }; + + TestHelper helper(0, 0); + + InstanceSelector sut(args, &helper); + Assert::IsFalse(sut.Less(&a, &b)); + } + + TEST_METHOD(Less_No_Date_A) + { + CommandArgs args; + args.Parse(L"vswhere.exe"); + + TestInstance a = + { + { L"InstallationVersion", L"2.0" }, + }; + + TestInstance b = + { + { L"InstallationVersion", L"2.0" }, + { L"InstallDate", L"2017-02-23T22:00:00Z" }, + }; + + TestHelper helper(0, 0); + + InstanceSelector sut(args, &helper); + Assert::IsTrue(sut.Less(&a, &b)); + } + + TEST_METHOD(Less_No_Date_B) + { + CommandArgs args; + args.Parse(L"vswhere.exe"); + + TestInstance a = + { + { L"InstallationVersion", L"2.0" }, + { L"InstallDate", L"2017-02-23T22:00:00Z" }, + }; + + TestInstance b = + { + { L"InstallationVersion", L"2.0" }, + }; + + TestHelper helper(0, 0); + + InstanceSelector sut(args, &helper); + Assert::IsFalse(sut.Less(&a, &b)); + } + + TEST_METHOD(Less_Date_A) + { + CommandArgs args; + args.Parse(L"vswhere.exe"); + + TestInstance a = + { + { L"InstallationVersion", L"2.0" }, + { L"InstallDate", L"2017-02-23T22:00:00Z" }, + }; + + TestInstance b = + { + { L"InstallationVersion", L"2.0" }, + { L"InstallDate", L"2017-02-23T23:00:00Z" }, + }; + + TestHelper helper(0, 0); + + InstanceSelector sut(args, &helper); + Assert::IsTrue(sut.Less(&a, &b)); + } + + TEST_METHOD(Less_Date_B) + { + CommandArgs args; + args.Parse(L"vswhere.exe"); + + TestInstance a = + { + { L"InstallationVersion", L"2.0" }, + { L"InstallDate", L"2017-02-23T23:00:00Z" }, + }; + + TestInstance b = + { + { L"InstallationVersion", L"2.0" }, + { L"InstallDate", L"2017-02-23T22:00:00Z" }, + }; + + TestHelper helper(0, 0); + + InstanceSelector sut(args, &helper); + Assert::IsFalse(sut.Less(&a, &b)); + } + + TEST_METHOD(Less_Dates_Equal) + { + CommandArgs args; + args.Parse(L"vswhere.exe"); + + TestInstance a = + { + { L"InstallationVersion", L"2.0" }, + { L"InstallDate", L"2017-02-23T22:00:00Z" }, + }; + + TestInstance b = + { + { L"InstallationVersion", L"2.0" }, + { L"InstallDate", L"2017-02-23T22:00:00Z" }, + }; + + TestHelper helper(0, 0); + + InstanceSelector sut(args, &helper); + Assert::ExpectException([&] { sut.Less(&a, &b); }); + } }; diff --git a/test/vswhere.test/TestHelper.h b/test/vswhere.test/TestHelper.h index b2de78c..6cde8c4 100644 --- a/test/vswhere.test/TestHelper.h +++ b/test/vswhere.test/TestHelper.h @@ -68,7 +68,21 @@ class TestHelper : _Out_ PULONGLONG pullVersion ) { - return E_NOTFOUND; + static ci_equal equal; + + if (equal(pwszVersion, L"1.0")) + { + *pullVersion = MAKEVERSION(1, 0, 0, 0); + S_OK; + } + + if (equal(pwszVersion, L"2.0")) + { + *pullVersion = MAKEVERSION(2, 0, 0, 0); + return S_OK; + } + + return E_NOTIMPL; } STDMETHODIMP ParseVersionRange( diff --git a/test/vswhere.test/TestInstance.h b/test/vswhere.test/TestInstance.h index fd4038b..12d77cf 100644 --- a/test/vswhere.test/TestInstance.h +++ b/test/vswhere.test/TestInstance.h @@ -92,20 +92,21 @@ class TestInstance : STDMETHODIMP GetInstallDate( _Out_ LPFILETIME pInstallDate) { - // Rather than parsing the value, return a fixed value for "2017-02-23T01:22:35Z". - auto it = m_properties.find(L"installDate"); - if (it != m_properties.end()) + std::wstring value; + + auto hr = TryGet(L"installDate", value); + if (SUCCEEDED(hr)) { - *pInstallDate = - { - 1343813982, - 30575987, - }; + const int num_fields = 6; + SYSTEMTIME st = {}; - return S_OK; + if (num_fields == ::swscanf_s(value.c_str(), L"%hd-%hd-%hdT%hd:%hd:%hd", &st.wYear, &st.wMonth, &st.wDay, &st.wHour, &st.wMinute, &st.wSecond)) + { + ::SystemTimeToFileTime(&st, pInstallDate); + } } - return E_NOTFOUND; + return hr; } STDMETHODIMP GetInstallationName(