aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSean Hall <r.sean.hall@gmail.com>2020-12-01 23:02:05 -0600
committerSean Hall <r.sean.hall@gmail.com>2020-12-01 23:02:05 -0600
commit5d0434843c6f307a46fa95139c1e754221cc13af (patch)
tree559f1714b14c08fa09449b62770c9107fae968c3
parent86456524fe4640053616f6fc18311159e6fafea5 (diff)
downloadwix-5d0434843c6f307a46fa95139c1e754221cc13af.tar.gz
wix-5d0434843c6f307a46fa95139c1e754221cc13af.tar.bz2
wix-5d0434843c6f307a46fa95139c1e754221cc13af.zip
Add MSI transaction tests.
-rw-r--r--BurnE2ETests.sln53
-rw-r--r--README.md2
-rw-r--r--appveyor.cmd11
-rw-r--r--appveyor.yml25
-rw-r--r--global.json8
-rw-r--r--nuget.config14
-rw-r--r--src/Directory.Build.props29
-rw-r--r--src/Directory.Build.targets50
-rw-r--r--src/TestData/MsiTransaction/BundleAv1/BundleA.wxi19
-rw-r--r--src/TestData/MsiTransaction/BundleAv1/BundleAv1.wixproj16
-rw-r--r--src/TestData/MsiTransaction/BundleAv1/BundleAv1.wxs15
-rw-r--r--src/TestData/MsiTransaction/BundleAv2/BundleAv2.wixproj17
-rw-r--r--src/TestData/MsiTransaction/BundleAv2/BundleAv2.wxs15
-rw-r--r--src/TestData/MsiTransaction/BundleBv1/BundleB.wxi19
-rw-r--r--src/TestData/MsiTransaction/BundleBv1/BundleBv1.wixproj14
-rw-r--r--src/TestData/MsiTransaction/BundleBv1/BundleBv1.wxs12
-rw-r--r--src/TestData/MsiTransaction/BundleBv2/BundleBv2.wixproj17
-rw-r--r--src/TestData/MsiTransaction/BundleBv2/BundleBv2.wxs15
-rw-r--r--src/TestData/MsiTransaction/PackageA/PackageA.wixproj9
-rw-r--r--src/TestData/MsiTransaction/PackageBv1/PackageB.props9
-rw-r--r--src/TestData/MsiTransaction/PackageBv1/PackageBv1.wixproj4
-rw-r--r--src/TestData/MsiTransaction/PackageBv2/PackageBv2.wixproj7
-rw-r--r--src/TestData/MsiTransaction/PackageCv1/PackageC.props9
-rw-r--r--src/TestData/MsiTransaction/PackageCv1/PackageCv1.wixproj4
-rw-r--r--src/TestData/MsiTransaction/PackageCv2/PackageCv2.wixproj7
-rw-r--r--src/TestData/MsiTransaction/PackageD/PackageD.wixproj9
-rw-r--r--src/TestData/MsiTransaction/PackageF/PackageF.wixproj12
-rw-r--r--src/TestData/Templates/Package.wxs52
-rw-r--r--src/TestData/Templates/PackageFail.wxs55
-rw-r--r--src/TestData/TestBA/TestBAWixlib/TestBA.wxs15
-rw-r--r--src/TestData/TestBA/TestBAWixlib/testbawixlib.wixproj17
-rw-r--r--src/TestData/TestData.proj17
-rw-r--r--src/Utilities/TestBA/Hresult.cs22
-rw-r--r--src/Utilities/TestBA/TestBA.BootstrapperCore.config18
-rw-r--r--src/Utilities/TestBA/TestBA.cs469
-rw-r--r--src/Utilities/TestBA/TestBA.csproj24
-rw-r--r--src/Utilities/TestBA/TestBAFactory.cs22
-rw-r--r--src/Wix.Build.props7
-rw-r--r--src/Wix.Build.targets10
-rw-r--r--src/WixToolsetTest.BurnE2E/BundleInstaller.cs128
-rw-r--r--src/WixToolsetTest.BurnE2E/BurnE2EFixture.cs28
-rw-r--r--src/WixToolsetTest.BurnE2E/BurnE2ETests.cs48
-rw-r--r--src/WixToolsetTest.BurnE2E/MSIExec.cs753
-rw-r--r--src/WixToolsetTest.BurnE2E/MsiTransactionTests.cs111
-rw-r--r--src/WixToolsetTest.BurnE2E/PackageInstaller.cs97
-rw-r--r--src/WixToolsetTest.BurnE2E/TestTool.cs245
-rw-r--r--src/WixToolsetTest.BurnE2E/WixTestBase.cs21
-rw-r--r--src/WixToolsetTest.BurnE2E/WixTestContext.cs70
-rw-r--r--src/WixToolsetTest.BurnE2E/WixToolsetTest.BurnE2E.csproj24
-rw-r--r--version.json11
50 files changed, 2684 insertions, 1 deletions
diff --git a/BurnE2ETests.sln b/BurnE2ETests.sln
new file mode 100644
index 00000000..ad94234c
--- /dev/null
+++ b/BurnE2ETests.sln
@@ -0,0 +1,53 @@
1
2Microsoft Visual Studio Solution File, Format Version 12.00
3# Visual Studio Version 16
4VisualStudioVersion = 16.0.29503.13
5MinimumVisualStudioVersion = 10.0.40219.1
6Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestBA", "src\Utilities\TestBA\TestBA.csproj", "{04022D35-6D75-49D0-91D2-4208E09DBA6D}"
7EndProject
8Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WixToolsetTest.BurnE2E", "src\WixToolsetTest.BurnE2E\WixToolsetTest.BurnE2E.csproj", "{FED9D707-E5C3-4867-87B0-FABDB5EB0823}"
9EndProject
10Global
11 GlobalSection(SolutionConfigurationPlatforms) = preSolution
12 Debug|Any CPU = Debug|Any CPU
13 Debug|x64 = Debug|x64
14 Debug|x86 = Debug|x86
15 Release|Any CPU = Release|Any CPU
16 Release|x64 = Release|x64
17 Release|x86 = Release|x86
18 EndGlobalSection
19 GlobalSection(ProjectConfigurationPlatforms) = postSolution
20 {04022D35-6D75-49D0-91D2-4208E09DBA6D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
21 {04022D35-6D75-49D0-91D2-4208E09DBA6D}.Debug|Any CPU.Build.0 = Debug|Any CPU
22 {04022D35-6D75-49D0-91D2-4208E09DBA6D}.Debug|x64.ActiveCfg = Debug|Any CPU
23 {04022D35-6D75-49D0-91D2-4208E09DBA6D}.Debug|x64.Build.0 = Debug|Any CPU
24 {04022D35-6D75-49D0-91D2-4208E09DBA6D}.Debug|x86.ActiveCfg = Debug|Any CPU
25 {04022D35-6D75-49D0-91D2-4208E09DBA6D}.Debug|x86.Build.0 = Debug|Any CPU
26 {04022D35-6D75-49D0-91D2-4208E09DBA6D}.Release|Any CPU.ActiveCfg = Release|Any CPU
27 {04022D35-6D75-49D0-91D2-4208E09DBA6D}.Release|Any CPU.Build.0 = Release|Any CPU
28 {04022D35-6D75-49D0-91D2-4208E09DBA6D}.Release|x64.ActiveCfg = Release|Any CPU
29 {04022D35-6D75-49D0-91D2-4208E09DBA6D}.Release|x64.Build.0 = Release|Any CPU
30 {04022D35-6D75-49D0-91D2-4208E09DBA6D}.Release|x86.ActiveCfg = Release|Any CPU
31 {04022D35-6D75-49D0-91D2-4208E09DBA6D}.Release|x86.Build.0 = Release|Any CPU
32 {FED9D707-E5C3-4867-87B0-FABDB5EB0823}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
33 {FED9D707-E5C3-4867-87B0-FABDB5EB0823}.Debug|Any CPU.Build.0 = Debug|Any CPU
34 {FED9D707-E5C3-4867-87B0-FABDB5EB0823}.Debug|x64.ActiveCfg = Debug|Any CPU
35 {FED9D707-E5C3-4867-87B0-FABDB5EB0823}.Debug|x64.Build.0 = Debug|Any CPU
36 {FED9D707-E5C3-4867-87B0-FABDB5EB0823}.Debug|x86.ActiveCfg = Debug|Any CPU
37 {FED9D707-E5C3-4867-87B0-FABDB5EB0823}.Debug|x86.Build.0 = Debug|Any CPU
38 {FED9D707-E5C3-4867-87B0-FABDB5EB0823}.Release|Any CPU.ActiveCfg = Release|Any CPU
39 {FED9D707-E5C3-4867-87B0-FABDB5EB0823}.Release|Any CPU.Build.0 = Release|Any CPU
40 {FED9D707-E5C3-4867-87B0-FABDB5EB0823}.Release|x64.ActiveCfg = Release|Any CPU
41 {FED9D707-E5C3-4867-87B0-FABDB5EB0823}.Release|x64.Build.0 = Release|Any CPU
42 {FED9D707-E5C3-4867-87B0-FABDB5EB0823}.Release|x86.ActiveCfg = Release|Any CPU
43 {FED9D707-E5C3-4867-87B0-FABDB5EB0823}.Release|x86.Build.0 = Release|Any CPU
44 EndGlobalSection
45 GlobalSection(SolutionProperties) = preSolution
46 HideSolutionNode = FALSE
47 EndGlobalSection
48 GlobalSection(NestedProjects) = preSolution
49 EndGlobalSection
50 GlobalSection(ExtensibilityGlobals) = postSolution
51 SolutionGuid = {74DE2EED-ECAA-4FDD-9792-9D3B0C0C1321}
52 EndGlobalSection
53EndGlobal
diff --git a/README.md b/README.md
index fdaa3bfe..30003903 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,2 @@
1# Bal.wixext 1# burn-e2e-tests
2burn-e2e-tests - End-to-end tests for WiX bundles, building real installers and running them. 2burn-e2e-tests - End-to-end tests for WiX bundles, building real installers and running them.
diff --git a/appveyor.cmd b/appveyor.cmd
new file mode 100644
index 00000000..b0283678
--- /dev/null
+++ b/appveyor.cmd
@@ -0,0 +1,11 @@
1@setlocal
2@pushd %~dp0
3@set _C=Release
4
5msbuild -p:Configuration=%_C% -Restore || exit /b
6msbuild -p:Configuration=%_C% src\TestData -Restore || exit /b
7
8dotnet test -c %_C% --no-build src\WixToolsetTest.BurnE2E || exit /b
9
10@popd
11@endlocal
diff --git a/appveyor.yml b/appveyor.yml
new file mode 100644
index 00000000..f12cca2d
--- /dev/null
+++ b/appveyor.yml
@@ -0,0 +1,25 @@
1# Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
2
3branches:
4 only:
5 - master
6
7image: Visual Studio 2019
8
9environment:
10 DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true
11 DOTNET_CLI_TELEMETRY_OPTOUT: 1
12 NUGET_XMLDOC_MODE: skip
13 RuntimeTestsEnabled: true
14 WIX_COMPRESSION_LEVEL: none
15
16build_script:
17 - appveyor.cmd
18
19test: off
20
21skip_tags: true
22
23on_finish:
24 - ps: 7z a logs.zip $env:TEMP\*.log $env:TEMP\..\*.log
25 - ps: Push-AppveyorArtifact logs.zip
diff --git a/global.json b/global.json
new file mode 100644
index 00000000..8910563d
--- /dev/null
+++ b/global.json
@@ -0,0 +1,8 @@
1{
2 "msbuild-sdks": {
3 "WixToolset.Sdk": "4.0.0-build-0168"
4 },
5 "sdk": {
6 "allowPrerelease": false
7 }
8}
diff --git a/nuget.config b/nuget.config
new file mode 100644
index 00000000..dd67462a
--- /dev/null
+++ b/nuget.config
@@ -0,0 +1,14 @@
1<?xml version="1.0" encoding="utf-8"?>
2<configuration>
3 <packageSources>
4 <clear />
5 <add key="wixtoolset-balutil" value="https://ci.appveyor.com/nuget/wixtoolset-balutil" />
6 <add key="wixtoolset-bal-wixext" value="https://ci.appveyor.com/nuget/wixtoolset-bal-wixext" />
7 <add key="wixtoolset-dtf" value="https://ci.appveyor.com/nuget/wixtoolset-dtf" />
8 <add key="wixtoolset-netfx-wixext" value="https://ci.appveyor.com/nuget/wixtoolset-netfx-wixext" />
9 <add key="wixtoolset-tools" value="https://ci.appveyor.com/nuget/wixtoolset-tools" />
10 <add key="wixtoolset-util-wixext" value="https://ci.appveyor.com/nuget/wixtoolset-util-wixext" />
11 <add key="wixbuildtools" value="https://ci.appveyor.com/nuget/wixbuildtools" />
12 <add key="api.nuget.org" value="https://api.nuget.org/v3/index.json" protocolVersion="3" />
13 </packageSources>
14</configuration> \ No newline at end of file
diff --git a/src/Directory.Build.props b/src/Directory.Build.props
new file mode 100644
index 00000000..f83cc154
--- /dev/null
+++ b/src/Directory.Build.props
@@ -0,0 +1,29 @@
1<?xml version="1.0" encoding="utf-8"?>
2<!-- Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. -->
3<!--
4 Do NOT modify this file. Update the canonical version in Home\repo-template\src\Directory.Build.props
5 then update all of the repos.
6-->
7<Project>
8 <PropertyGroup>
9 <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
10 <EnableSourceLink Condition=" '$(NCrunch)' == '1' ">false</EnableSourceLink>
11 <MSBuildWarningsAsMessages>MSB3246</MSBuildWarningsAsMessages>
12
13 <ProjectName Condition=" '$(ProjectName)' == '' ">$(MSBuildProjectName)</ProjectName>
14 <BaseOutputPath>$([System.IO.Path]::GetFullPath($(MSBuildThisFileDirectory)..\build\))</BaseOutputPath>
15 <BaseIntermediateOutputPath>$(BaseOutputPath)obj\$(ProjectName)\</BaseIntermediateOutputPath>
16 <OutputPath>$(BaseOutputPath)$(Configuration)\</OutputPath>
17
18 <Authors>WiX Toolset Team</Authors>
19 <Company>WiX Toolset</Company>
20 <Copyright>Copyright (c) .NET Foundation and contributors. All rights reserved.</Copyright>
21 <PackageLicenseExpression>MS-RL</PackageLicenseExpression>
22 <Product>WiX Toolset</Product>
23 </PropertyGroup>
24
25 <Import Project="CSharp.Build.props" Condition=" '$(MSBuildProjectExtension)'=='.csproj' and Exists('CSharp.Build.props') " />
26 <Import Project="Cpp.Build.props" Condition=" Exists('Cpp.Build.props') And '$(MSBuildProjectExtension)'=='.vcxproj' " />
27 <Import Project="Wix.Build.props" Condition=" Exists('Wix.Build.props') And '$(MSBuildProjectExtension)'=='.wixproj' " />
28 <Import Project="Custom.Build.props" Condition=" Exists('Custom.Build.props') " />
29</Project>
diff --git a/src/Directory.Build.targets b/src/Directory.Build.targets
new file mode 100644
index 00000000..265b5cfd
--- /dev/null
+++ b/src/Directory.Build.targets
@@ -0,0 +1,50 @@
1<?xml version="1.0" encoding="utf-8"?>
2<!-- Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. -->
3<!--
4 Do NOT modify this file. Update the canonical version in Home\repo-template\src\Directory.Build.targets
5 then update all of the repos.
6-->
7<!--
8 Replace PackageReferences with ProjectReferences when the projects can be found in .sln.
9 See the original here: https://github.com/dotnet/sdk/issues/1151#issuecomment-385133284
10-->
11<Project>
12 <PropertyGroup>
13 <ReplacePackageReferences>true</ReplacePackageReferences>
14 <TheSolutionPath Condition=" '$(NCrunch)'=='' ">$(SolutionPath)</TheSolutionPath>
15 <TheSolutionPath Condition=" '$(NCrunch)'=='1' ">$(NCrunchOriginalSolutionPath)</TheSolutionPath>
16 </PropertyGroup>
17
18 <Choose>
19 <When Condition="$(ReplacePackageReferences) AND '$(TheSolutionPath)' != '' AND '$(TheSolutionPath)' != '*undefined*' AND Exists('$(TheSolutionPath)')">
20
21 <PropertyGroup>
22 <SolutionFileContent>$([System.IO.File]::ReadAllText($(TheSolutionPath)))</SolutionFileContent>
23 <SmartSolutionDir>$([System.IO.Path]::GetDirectoryName( $(TheSolutionPath) ))</SmartSolutionDir>
24 <RegexPattern>(?&lt;="[PackageName]", ")(.*)(?=", ")</RegexPattern>
25 </PropertyGroup>
26
27 <ItemGroup>
28 <!-- Keep the identity of the PackageReference -->
29 <SmartPackageReference Include="@(PackageReference)">
30 <PackageName>%(Identity)</PackageName>
31 <InSolution>$(SolutionFileContent.Contains('\%(Identity).csproj'))</InSolution>
32 </SmartPackageReference>
33
34 <!-- Filter them by mapping them to another ItemGroup using the WithMetadataValue item function -->
35 <PackageInSolution Include="@(SmartPackageReference->WithMetadataValue('InSolution', True))">
36 <Pattern>$(RegexPattern.Replace('[PackageName]','%(PackageName)') )</Pattern>
37 <SmartPath>$([System.Text.RegularExpressions.Regex]::Match('$(SolutionFileContent)', '%(Pattern)'))</SmartPath>
38 </PackageInSolution>
39
40 <ProjectReference Include="@(PackageInSolution->'$(SmartSolutionDir)\%(SmartPath)' )"/>
41
42 <!-- Remove the package references that are now referenced as projects -->
43 <PackageReference Remove="@(PackageInSolution->'%(PackageName)' )"/>
44 </ItemGroup>
45
46 </When>
47 </Choose>
48
49 <Import Project="Wix.Build.targets" Condition=" Exists('Wix.Build.targets') And '$(MSBuildProjectExtension)'=='.wixproj' " />
50</Project>
diff --git a/src/TestData/MsiTransaction/BundleAv1/BundleA.wxi b/src/TestData/MsiTransaction/BundleAv1/BundleA.wxi
new file mode 100644
index 00000000..83b901cb
--- /dev/null
+++ b/src/TestData/MsiTransaction/BundleAv1/BundleA.wxi
@@ -0,0 +1,19 @@
1<?xml version="1.0" encoding="utf-8"?>
2<!-- Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. -->
3
4<?ifndef Version?>
5<?define Version = 1.0.0.0?>
6<?endif?>
7
8<Include xmlns="http://wixtoolset.org/schemas/v4/wxs">
9 <Bundle Name="~$(var.TestGroupName) - Bundle A" Version="$(var.Version)" UpgradeCode="{90ED10D5-B187-4470-B498-05D80DAB729A}" Compressed="yes">
10 <Log Prefix="~$(var.TestGroupName)_BundleA"/>
11
12 <Variable Name="TestGroupName" Value="$(var.TestGroupName)" />
13
14 <Chain>
15 <PackageGroupRef Id="TestBA" />
16 <PackageGroupRef Id="BundlePackages" />
17 </Chain>
18 </Bundle>
19</Include>
diff --git a/src/TestData/MsiTransaction/BundleAv1/BundleAv1.wixproj b/src/TestData/MsiTransaction/BundleAv1/BundleAv1.wixproj
new file mode 100644
index 00000000..d5845bbe
--- /dev/null
+++ b/src/TestData/MsiTransaction/BundleAv1/BundleAv1.wixproj
@@ -0,0 +1,16 @@
1<!-- Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. -->
2<Project Sdk="WixToolset.Sdk">
3 <PropertyGroup>
4 <OutputType>Bundle</OutputType>
5 </PropertyGroup>
6 <ItemGroup>
7 <ProjectReference Include="..\..\TestBA\TestBAWixlib\testbawixlib.wixproj" />
8 <ProjectReference Include="..\PackageA\PackageA.wixproj" />
9 <ProjectReference Include="..\PackageBv1\PackageBv1.wixproj" />
10 <ProjectReference Include="..\PackageCv1\PackageCv1.wixproj" />
11 </ItemGroup>
12 <ItemGroup>
13 <PackageReference Include="WixToolset.Bal.wixext" Version="4.0.63" />
14 <PackageReference Include="WixToolset.NetFx.wixext" Version="4.0.51" />
15 </ItemGroup>
16</Project> \ No newline at end of file
diff --git a/src/TestData/MsiTransaction/BundleAv1/BundleAv1.wxs b/src/TestData/MsiTransaction/BundleAv1/BundleAv1.wxs
new file mode 100644
index 00000000..24ef3273
--- /dev/null
+++ b/src/TestData/MsiTransaction/BundleAv1/BundleAv1.wxs
@@ -0,0 +1,15 @@
1<?xml version='1.0' encoding='utf-8'?>
2<!-- Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. -->
3
4
5<Wix xmlns='http://wixtoolset.org/schemas/v4/wxs'>
6 <?include BundleA.wxi ?>
7 <Fragment>
8 <PackageGroup Id="BundlePackages">
9 <MsiPackage Id="PackageA" SourceFile="$(var.PackageA.TargetPath)" />
10 <RollbackBoundary Transaction='yes' />
11 <MsiPackage Id="PackageB" SourceFile="$(var.PackageBv1.TargetPath)" />
12 <MsiPackage Id="PackageC" SourceFile="$(var.PackageCv1.TargetPath)" />
13 </PackageGroup>
14 </Fragment>
15</Wix>
diff --git a/src/TestData/MsiTransaction/BundleAv2/BundleAv2.wixproj b/src/TestData/MsiTransaction/BundleAv2/BundleAv2.wixproj
new file mode 100644
index 00000000..2dbb9539
--- /dev/null
+++ b/src/TestData/MsiTransaction/BundleAv2/BundleAv2.wixproj
@@ -0,0 +1,17 @@
1<!-- Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. -->
2<Project Sdk="WixToolset.Sdk">
3 <PropertyGroup>
4 <OutputType>Bundle</OutputType>
5 <Version>2.0.0.0</Version>
6 </PropertyGroup>
7 <ItemGroup>
8 <ProjectReference Include="..\..\TestBA\TestBAWixlib\testbawixlib.wixproj" />
9 <ProjectReference Include="..\PackageBv2\PackageBv2.wixproj" />
10 <ProjectReference Include="..\PackageCv2\PackageCv2.wixproj" />
11 <ProjectReference Include="..\PackageD\PackageD.wixproj" />
12 </ItemGroup>
13 <ItemGroup>
14 <PackageReference Include="WixToolset.Bal.wixext" Version="4.0.63" />
15 <PackageReference Include="WixToolset.NetFx.wixext" Version="4.0.51" />
16 </ItemGroup>
17</Project> \ No newline at end of file
diff --git a/src/TestData/MsiTransaction/BundleAv2/BundleAv2.wxs b/src/TestData/MsiTransaction/BundleAv2/BundleAv2.wxs
new file mode 100644
index 00000000..7feab97b
--- /dev/null
+++ b/src/TestData/MsiTransaction/BundleAv2/BundleAv2.wxs
@@ -0,0 +1,15 @@
1<?xml version='1.0' encoding='utf-8'?>
2<!-- Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. -->
3
4
5<Wix xmlns='http://wixtoolset.org/schemas/v4/wxs'>
6 <?include ..\BundleAv1\BundleA.wxi ?>
7 <Fragment>
8 <PackageGroup Id="BundlePackages">
9 <MsiPackage Id="PackageD" SourceFile="$(var.PackageD.TargetPath)" />
10 <RollbackBoundary Transaction='yes' />
11 <MsiPackage Id="PackageB" SourceFile="$(var.PackageBv2.TargetPath)" />
12 <MsiPackage Id="PackageC" SourceFile="$(var.PackageCv2.TargetPath)" />
13 </PackageGroup>
14 </Fragment>
15</Wix>
diff --git a/src/TestData/MsiTransaction/BundleBv1/BundleB.wxi b/src/TestData/MsiTransaction/BundleBv1/BundleB.wxi
new file mode 100644
index 00000000..5eeaa6d7
--- /dev/null
+++ b/src/TestData/MsiTransaction/BundleBv1/BundleB.wxi
@@ -0,0 +1,19 @@
1<?xml version="1.0" encoding="utf-8"?>
2<!-- Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. -->
3
4<?ifndef Version?>
5<?define Version = 1.0.0.0?>
6<?endif?>
7
8<Include xmlns="http://wixtoolset.org/schemas/v4/wxs">
9 <Bundle Name="~$(var.TestGroupName) - Bundle B" Version="$(var.Version)" UpgradeCode="{552FD011-4DD6-42B2-A4C6-AD1417C829B2}" Compressed="yes">
10 <Log Prefix="~$(var.TestGroupName)_BundleB"/>
11
12 <Variable Name="TestGroupName" Value="$(var.TestGroupName)" />
13
14 <Chain>
15 <PackageGroupRef Id="TestBA" />
16 <PackageGroupRef Id="BundlePackages" />
17 </Chain>
18 </Bundle>
19</Include>
diff --git a/src/TestData/MsiTransaction/BundleBv1/BundleBv1.wixproj b/src/TestData/MsiTransaction/BundleBv1/BundleBv1.wixproj
new file mode 100644
index 00000000..c4704cbd
--- /dev/null
+++ b/src/TestData/MsiTransaction/BundleBv1/BundleBv1.wixproj
@@ -0,0 +1,14 @@
1<!-- Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. -->
2<Project Sdk="WixToolset.Sdk">
3 <PropertyGroup>
4 <OutputType>Bundle</OutputType>
5 </PropertyGroup>
6 <ItemGroup>
7 <ProjectReference Include="..\..\TestBA\TestBAWixlib\testbawixlib.wixproj" />
8 <ProjectReference Include="..\PackageBv1\PackageBv1.wixproj" />
9 </ItemGroup>
10 <ItemGroup>
11 <PackageReference Include="WixToolset.Bal.wixext" Version="4.0.63" />
12 <PackageReference Include="WixToolset.NetFx.wixext" Version="4.0.51" />
13 </ItemGroup>
14</Project> \ No newline at end of file
diff --git a/src/TestData/MsiTransaction/BundleBv1/BundleBv1.wxs b/src/TestData/MsiTransaction/BundleBv1/BundleBv1.wxs
new file mode 100644
index 00000000..3efe2f5d
--- /dev/null
+++ b/src/TestData/MsiTransaction/BundleBv1/BundleBv1.wxs
@@ -0,0 +1,12 @@
1<?xml version='1.0' encoding='utf-8'?>
2<!-- Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. -->
3
4
5<Wix xmlns='http://wixtoolset.org/schemas/v4/wxs'>
6 <?include BundleB.wxi ?>
7 <Fragment>
8 <PackageGroup Id="BundlePackages">
9 <MsiPackage Id="PackageB" SourceFile="$(var.PackageBv1.TargetPath)" />
10 </PackageGroup>
11 </Fragment>
12</Wix>
diff --git a/src/TestData/MsiTransaction/BundleBv2/BundleBv2.wixproj b/src/TestData/MsiTransaction/BundleBv2/BundleBv2.wixproj
new file mode 100644
index 00000000..9a7f890d
--- /dev/null
+++ b/src/TestData/MsiTransaction/BundleBv2/BundleBv2.wixproj
@@ -0,0 +1,17 @@
1<!-- Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. -->
2<Project Sdk="WixToolset.Sdk">
3 <PropertyGroup>
4 <OutputType>Bundle</OutputType>
5 <Version>2.0.0.0</Version>
6 </PropertyGroup>
7 <ItemGroup>
8 <ProjectReference Include="..\..\TestBA\TestBAWixlib\testbawixlib.wixproj" />
9 <ProjectReference Include="..\PackageA\PackageA.wixproj" />
10 <ProjectReference Include="..\PackageBv2\PackageBv2.wixproj" />
11 <ProjectReference Include="..\PackageF\PackageF.wixproj" />
12 </ItemGroup>
13 <ItemGroup>
14 <PackageReference Include="WixToolset.Bal.wixext" Version="4.0.63" />
15 <PackageReference Include="WixToolset.NetFx.wixext" Version="4.0.51" />
16 </ItemGroup>
17</Project> \ No newline at end of file
diff --git a/src/TestData/MsiTransaction/BundleBv2/BundleBv2.wxs b/src/TestData/MsiTransaction/BundleBv2/BundleBv2.wxs
new file mode 100644
index 00000000..d5b88b40
--- /dev/null
+++ b/src/TestData/MsiTransaction/BundleBv2/BundleBv2.wxs
@@ -0,0 +1,15 @@
1<?xml version='1.0' encoding='utf-8'?>
2<!-- Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. -->
3
4
5<Wix xmlns='http://wixtoolset.org/schemas/v4/wxs'>
6 <?include ..\BundleBv1\BundleB.wxi ?>
7 <Fragment>
8 <PackageGroup Id="BundlePackages">
9 <MsiPackage Id="PackageA" SourceFile="$(var.PackageA.TargetPath)" />
10 <RollbackBoundary Transaction='yes' />
11 <MsiPackage Id="PackageB" SourceFile="$(var.PackageBv2.TargetPath)" />
12 <MsiPackage Id="PackageF" SourceFile="$(var.PackageF.TargetPath)" />
13 </PackageGroup>
14 </Fragment>
15</Wix>
diff --git a/src/TestData/MsiTransaction/PackageA/PackageA.wixproj b/src/TestData/MsiTransaction/PackageA/PackageA.wixproj
new file mode 100644
index 00000000..bc18cd44
--- /dev/null
+++ b/src/TestData/MsiTransaction/PackageA/PackageA.wixproj
@@ -0,0 +1,9 @@
1<!-- Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. -->
2<Project Sdk="WixToolset.Sdk">
3 <PropertyGroup>
4 <UpgradeCode>{7772FCDF-5FDB-497D-B5DF-C6D17D667976}</UpgradeCode>
5 </PropertyGroup>
6 <ItemGroup>
7 <Compile Include="..\..\Templates\Package.wxs" Link="Package.wxs" />
8 </ItemGroup>
9</Project> \ No newline at end of file
diff --git a/src/TestData/MsiTransaction/PackageBv1/PackageB.props b/src/TestData/MsiTransaction/PackageBv1/PackageB.props
new file mode 100644
index 00000000..decdfb6a
--- /dev/null
+++ b/src/TestData/MsiTransaction/PackageBv1/PackageB.props
@@ -0,0 +1,9 @@
1<!-- Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. -->
2<Project>
3 <PropertyGroup>
4 <UpgradeCode>{EAFC0C6B-626E-415C-8132-536FBD19F49B}</UpgradeCode>
5 </PropertyGroup>
6 <ItemGroup>
7 <Compile Include="..\..\Templates\Package.wxs" Link="Package.wxs" />
8 </ItemGroup>
9</Project> \ No newline at end of file
diff --git a/src/TestData/MsiTransaction/PackageBv1/PackageBv1.wixproj b/src/TestData/MsiTransaction/PackageBv1/PackageBv1.wixproj
new file mode 100644
index 00000000..7b6f83a3
--- /dev/null
+++ b/src/TestData/MsiTransaction/PackageBv1/PackageBv1.wixproj
@@ -0,0 +1,4 @@
1<!-- Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. -->
2<Project Sdk="WixToolset.Sdk">
3 <Import Project="PackageB.props" />
4</Project> \ No newline at end of file
diff --git a/src/TestData/MsiTransaction/PackageBv2/PackageBv2.wixproj b/src/TestData/MsiTransaction/PackageBv2/PackageBv2.wixproj
new file mode 100644
index 00000000..126d0f53
--- /dev/null
+++ b/src/TestData/MsiTransaction/PackageBv2/PackageBv2.wixproj
@@ -0,0 +1,7 @@
1<!-- Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. -->
2<Project Sdk="WixToolset.Sdk">
3 <Import Project="..\PackageBv1\PackageB.props" />
4 <PropertyGroup>
5 <Version>2.0.0.0</Version>
6 </PropertyGroup>
7</Project> \ No newline at end of file
diff --git a/src/TestData/MsiTransaction/PackageCv1/PackageC.props b/src/TestData/MsiTransaction/PackageCv1/PackageC.props
new file mode 100644
index 00000000..b3d057bd
--- /dev/null
+++ b/src/TestData/MsiTransaction/PackageCv1/PackageC.props
@@ -0,0 +1,9 @@
1<!-- Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. -->
2<Project>
3 <PropertyGroup>
4 <UpgradeCode>{A18BDC12-DAEC-43EE-87D1-31B2C2BC6269}</UpgradeCode>
5 </PropertyGroup>
6 <ItemGroup>
7 <Compile Include="..\..\Templates\Package.wxs" Link="Package.wxs" />
8 </ItemGroup>
9</Project> \ No newline at end of file
diff --git a/src/TestData/MsiTransaction/PackageCv1/PackageCv1.wixproj b/src/TestData/MsiTransaction/PackageCv1/PackageCv1.wixproj
new file mode 100644
index 00000000..45615706
--- /dev/null
+++ b/src/TestData/MsiTransaction/PackageCv1/PackageCv1.wixproj
@@ -0,0 +1,4 @@
1<!-- Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. -->
2<Project Sdk="WixToolset.Sdk">
3 <Import Project="PackageC.props" />
4</Project> \ No newline at end of file
diff --git a/src/TestData/MsiTransaction/PackageCv2/PackageCv2.wixproj b/src/TestData/MsiTransaction/PackageCv2/PackageCv2.wixproj
new file mode 100644
index 00000000..640ad21d
--- /dev/null
+++ b/src/TestData/MsiTransaction/PackageCv2/PackageCv2.wixproj
@@ -0,0 +1,7 @@
1<!-- Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. -->
2<Project Sdk="WixToolset.Sdk">
3 <Import Project="..\PackageCv1\PackageC.props" />
4 <PropertyGroup>
5 <Version>2.0.0.0</Version>
6 </PropertyGroup>
7</Project> \ No newline at end of file
diff --git a/src/TestData/MsiTransaction/PackageD/PackageD.wixproj b/src/TestData/MsiTransaction/PackageD/PackageD.wixproj
new file mode 100644
index 00000000..1df5da24
--- /dev/null
+++ b/src/TestData/MsiTransaction/PackageD/PackageD.wixproj
@@ -0,0 +1,9 @@
1<!-- Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. -->
2<Project Sdk="WixToolset.Sdk">
3 <PropertyGroup>
4 <UpgradeCode>{78B072D5-1C23-4895-9C4C-1B52E3C80621}</UpgradeCode>
5 </PropertyGroup>
6 <ItemGroup>
7 <Compile Include="..\..\Templates\Package.wxs" Link="Package.wxs" />
8 </ItemGroup>
9</Project> \ No newline at end of file
diff --git a/src/TestData/MsiTransaction/PackageF/PackageF.wixproj b/src/TestData/MsiTransaction/PackageF/PackageF.wixproj
new file mode 100644
index 00000000..e83006ac
--- /dev/null
+++ b/src/TestData/MsiTransaction/PackageF/PackageF.wixproj
@@ -0,0 +1,12 @@
1<!-- Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. -->
2<Project Sdk="WixToolset.Sdk">
3 <PropertyGroup>
4 <UpgradeCode>{3D59F8F2-8AC5-403E-B6F7-453870DE7063}</UpgradeCode>
5 </PropertyGroup>
6 <ItemGroup>
7 <Compile Include="..\..\Templates\PackageFail.wxs" Link="PackageFail.wxs" />
8 </ItemGroup>
9 <ItemGroup>
10 <PackageReference Include="WixToolset.Util.wixext" Version="4.0.65" />
11 </ItemGroup>
12</Project> \ No newline at end of file
diff --git a/src/TestData/Templates/Package.wxs b/src/TestData/Templates/Package.wxs
new file mode 100644
index 00000000..813e03d0
--- /dev/null
+++ b/src/TestData/Templates/Package.wxs
@@ -0,0 +1,52 @@
1<!-- Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. -->
2
3
4
5<?ifndef Version?>
6<?define Version = 1.0.0.0?>
7<?endif?>
8
9<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs">
10 <Package Name="~$(var.TestGroupName) - $(var.PackageName)" Language="1033" Version="$(var.Version)" Manufacturer="Microsoft Corporation" UpgradeCode="$(var.UpgradeCode)" Compressed="yes">
11
12
13 <MajorUpgrade DowngradeErrorMessage="A newer version of [ProductName] is already installed." />
14 <MediaTemplate EmbedCab="yes" />
15
16 <Property Id="MsiLogging" Value="voicewarmup" />
17 <PropertyRef Id="TestVersion" />
18
19 <Feature Id="Complete" Level="1">
20 <ComponentRef Id="FileComponent" />
21 <ComponentRef Id="RegistryComponent" />
22 </Feature>
23 </Package>
24
25 <Fragment>
26 <Directory Id="TARGETDIR" Name="SourceDir">
27 <Directory Id="ProgramFilesFolder">
28 <Directory Id="WixDir" Name="~Test WiX">
29 <Directory Id="TestDir" Name="$(var.TestGroupName)">
30 <Directory Id="INSTALLFOLDER" Name="$(var.PackageName)" />
31 </Directory>
32 </Directory>
33 </Directory>
34 </Directory>
35 </Fragment>
36
37 <Fragment>
38 <Component Id="FileComponent" Directory="INSTALLFOLDER">
39 <File Source="$(sys.SOURCEFILEPATH)" />
40 </Component>
41 </Fragment>
42
43 <Fragment>
44 <Component Id="RegistryComponent" Directory="INSTALLFOLDER">
45 <RegistryValue Root="HKLM" Key="Software\WiX\Tests\$(var.TestGroupName)" Name="$(var.PackageName)" Value="!(bind.Property.TestVersion)" Type="string" />
46 </Component>
47 </Fragment>
48
49 <Fragment>
50 <Property Id="TestVersion" Value="$(var.Version)" />
51 </Fragment>
52</Wix>
diff --git a/src/TestData/Templates/PackageFail.wxs b/src/TestData/Templates/PackageFail.wxs
new file mode 100644
index 00000000..8754d3f6
--- /dev/null
+++ b/src/TestData/Templates/PackageFail.wxs
@@ -0,0 +1,55 @@
1<!-- Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. -->
2
3
4
5<?ifndef Version?>
6<?define Version = 1.0.0.0?>
7<?endif?>
8
9<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs" xmlns:util="http://wixtoolset.org/schemas/v4/wxs/util">
10 <Package Name="~$(var.TestGroupName) - $(var.PackageName)" Language="1033" Version="$(var.Version)" Manufacturer="Microsoft Corporation" UpgradeCode="$(var.UpgradeCode)" Compressed="yes">
11
12
13 <MajorUpgrade DowngradeErrorMessage="A newer version of [ProductName] is already installed." />
14 <MediaTemplate EmbedCab="yes" />
15
16 <Property Id="MsiLogging" Value="voicewarmup" />
17 <PropertyRef Id="TestVersion" />
18
19 <Feature Id="Complete" Level="1">
20 <ComponentRef Id="FileComponent" />
21 <ComponentRef Id="RegistryComponent" />
22 </Feature>
23
24 <util:FailWhenDeferred />
25 <Property Id="WIXFAILWHENDEFERRED" Value="1" />
26 </Package>
27
28 <Fragment>
29 <Directory Id="TARGETDIR" Name="SourceDir">
30 <Directory Id="ProgramFilesFolder">
31 <Directory Id="WixDir" Name="~Test WiX">
32 <Directory Id="TestDir" Name="$(var.TestGroupName)">
33 <Directory Id="INSTALLFOLDER" Name="$(var.PackageName)" />
34 </Directory>
35 </Directory>
36 </Directory>
37 </Directory>
38 </Fragment>
39
40 <Fragment>
41 <Component Id="FileComponent" Directory="INSTALLFOLDER">
42 <File Source="$(sys.SOURCEFILEPATH)" />
43 </Component>
44 </Fragment>
45
46 <Fragment>
47 <Component Id="RegistryComponent" Directory="INSTALLFOLDER">
48 <RegistryValue Root="HKLM" Key="Software\WiX\Tests\$(var.TestGroupName)" Name="$(var.PackageName)" Value="!(bind.Property.TestVersion)" Type="string" />
49 </Component>
50 </Fragment>
51
52 <Fragment>
53 <Property Id="TestVersion" Value="$(var.Version)" />
54 </Fragment>
55</Wix>
diff --git a/src/TestData/TestBA/TestBAWixlib/TestBA.wxs b/src/TestData/TestBA/TestBAWixlib/TestBA.wxs
new file mode 100644
index 00000000..c16da2e5
--- /dev/null
+++ b/src/TestData/TestBA/TestBAWixlib/TestBA.wxs
@@ -0,0 +1,15 @@
1<!-- Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. -->
2<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs">
3 <Fragment>
4 <BootstrapperApplicationRef Id='ManagedBootstrapperApplicationHost'>
5 <Payload Name='WixToolset.Mba.Host.config' SourceFile='!(bindpath.x86)\TestBA.BootstrapperCore.config' />
6 <Payload SourceFile='!(bindpath.x86)\TestBA.dll' />
7 <Payload SourceFile="!(bindpath.x86)\mbanative.dll" />
8 <Payload SourceFile="!(bindpath.x86)\WixToolset.Mba.Core.dll" />
9 </BootstrapperApplicationRef>
10
11 <PackageGroup Id="TestBA">
12 <PackageGroupRef Id="NetFx48WebAsPrereq" />
13 </PackageGroup>
14 </Fragment>
15</Wix>
diff --git a/src/TestData/TestBA/TestBAWixlib/testbawixlib.wixproj b/src/TestData/TestBA/TestBAWixlib/testbawixlib.wixproj
new file mode 100644
index 00000000..4875bebf
--- /dev/null
+++ b/src/TestData/TestBA/TestBAWixlib/testbawixlib.wixproj
@@ -0,0 +1,17 @@
1<!-- Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. -->
2<Project Sdk="WixToolset.Sdk">
3 <PropertyGroup>
4 <OutputType>Library</OutputType>
5 <BindFiles>true</BindFiles>
6 <Cultures>en-us</Cultures>
7 </PropertyGroup>
8 <ItemGroup>
9 <BindInputPaths Include="$(OutputPath)..\net35\win-x86" BindName="x86" />
10 </ItemGroup>
11 <ItemGroup>
12 <ProjectReference Include="..\..\..\Utilities\TestBA\TestBA.csproj" />
13 </ItemGroup>
14 <ItemGroup>
15 <PackageReference Include="WixToolset.Bal.wixext" Version="4.0.63" />
16 </ItemGroup>
17</Project> \ No newline at end of file
diff --git a/src/TestData/TestData.proj b/src/TestData/TestData.proj
new file mode 100644
index 00000000..e8e61a94
--- /dev/null
+++ b/src/TestData/TestData.proj
@@ -0,0 +1,17 @@
1<?xml version="1.0" encoding="utf-8"?>
2<!-- Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. -->
3
4
5<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
6 <ItemGroup>
7 <TestDataProject Include="**\*.wixproj" />
8 </ItemGroup>
9
10 <Target Name="Build">
11 <MSBuild Projects="%(TestDataProject.Identity)" />
12 </Target>
13
14 <Target Name="Restore">
15 <MSBuild Projects="%(TestDataProject.Identity)" Targets="Restore" />
16 </Target>
17</Project> \ No newline at end of file
diff --git a/src/Utilities/TestBA/Hresult.cs b/src/Utilities/TestBA/Hresult.cs
new file mode 100644
index 00000000..bc1aa8c0
--- /dev/null
+++ b/src/Utilities/TestBA/Hresult.cs
@@ -0,0 +1,22 @@
1// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
2
3namespace WixToolset.Test.BA
4{
5 using System;
6
7 /// <summary>
8 /// Utility class to work with HRESULTs
9 /// </summary>
10 internal class Hresult
11 {
12 /// <summary>
13 /// Determines if an HRESULT was a success code or not.
14 /// </summary>
15 /// <param name="status">HRESULT to verify.</param>
16 /// <returns>True if the status is a success code.</returns>
17 public static bool Succeeded(int status)
18 {
19 return status >= 0;
20 }
21 }
22}
diff --git a/src/Utilities/TestBA/TestBA.BootstrapperCore.config b/src/Utilities/TestBA/TestBA.BootstrapperCore.config
new file mode 100644
index 00000000..55876a00
--- /dev/null
+++ b/src/Utilities/TestBA/TestBA.BootstrapperCore.config
@@ -0,0 +1,18 @@
1<?xml version="1.0" encoding="utf-8" ?>
2<!-- Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. -->
3
4
5<configuration>
6 <configSections>
7 <sectionGroup name="wix.bootstrapper" type="WixToolset.Mba.Host.BootstrapperSectionGroup, WixToolset.Mba.Host">
8 <section name="host" type="WixToolset.Mba.Host.HostSection, WixToolset.Mba.Host" />
9 </sectionGroup>
10 </configSections>
11 <startup>
12 <supportedRuntime version="v4.0" />
13 <supportedRuntime version="v2.0.50727" />
14 </startup>
15 <wix.bootstrapper>
16 <host assemblyName="TestBA" />
17 </wix.bootstrapper>
18</configuration>
diff --git a/src/Utilities/TestBA/TestBA.cs b/src/Utilities/TestBA/TestBA.cs
new file mode 100644
index 00000000..e3305d33
--- /dev/null
+++ b/src/Utilities/TestBA/TestBA.cs
@@ -0,0 +1,469 @@
1// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
2
3namespace WixToolset.Test.BA
4{
5 using System;
6 using System.Collections.Generic;
7 using System.IO;
8 using System.Linq;
9 using System.Threading;
10 using System.Windows.Forms;
11 using Microsoft.Win32;
12 using WixToolset.Mba.Core;
13
14 /// <summary>
15 /// A minimal UX used for testing.
16 /// </summary>
17 public class TestBA : BootstrapperApplication
18 {
19 private const string BurnBundleVersionVariable = "WixBundleVersion";
20
21 private ApplicationContext appContext;
22 private Form dummyWindow;
23 private LaunchAction action;
24 private int result;
25
26 private string updateBundlePath;
27
28 private int sleepDuringCache;
29 private int cancelCacheAtProgress;
30 private int sleepDuringExecute;
31 private int cancelExecuteAtProgress;
32 private int retryExecuteFilesInUse;
33
34 private IBootstrapperCommand Command { get; }
35
36 private IEngine Engine => this.engine;
37
38 /// <summary>
39 /// Initializes test user experience.
40 /// </summary>
41 public TestBA(IEngine engine, IBootstrapperCommand bootstrapperCommand)
42 : base(engine)
43 {
44 this.Command = bootstrapperCommand;
45 }
46
47 /// <summary>
48 /// Get the version of the install.
49 /// </summary>
50 public string Version { get; private set; }
51
52 /// <summary>
53 /// Indicates if DetectUpdate found a newer version to update.
54 /// </summary>
55 private bool UpdateAvailable { get; set; }
56
57 /// <summary>
58 /// UI Thread entry point for TestUX.
59 /// </summary>
60 protected override void Run()
61 {
62 this.action = this.Command.Action;
63 this.TestVariables();
64
65 this.Version = this.engine.GetVariableVersion(BurnBundleVersionVariable);
66 this.Log("Version: {0}", this.Version);
67
68 List<string> verifyArguments = this.ReadVerifyArguments();
69
70 foreach (string arg in this.Command.CommandLineArgs)
71 {
72 // If we're not in the update already, process the updatebundle.
73 if (this.Command.Relation != RelationType.Update && arg.StartsWith("-updatebundle:", StringComparison.OrdinalIgnoreCase))
74 {
75 this.updateBundlePath = arg.Substring(14);
76 FileInfo info = new FileInfo(this.updateBundlePath);
77 this.Engine.SetUpdate(this.updateBundlePath, null, info.Length, UpdateHashType.None, null);
78 this.UpdateAvailable = true;
79 this.action = LaunchAction.UpdateReplaceEmbedded;
80 }
81 else if (this.Command.Relation != RelationType.Update && arg.StartsWith("-checkupdate", StringComparison.OrdinalIgnoreCase))
82 {
83 this.action = LaunchAction.UpdateReplace;
84 }
85
86 verifyArguments.Remove(arg);
87 }
88 this.Log("Action: {0}", this.action);
89
90 // If there are any verification arguments left, error out.
91 if (0 < verifyArguments.Count)
92 {
93 foreach (string expectedArg in verifyArguments)
94 {
95 this.Log("Failure. Expected command-line to have argument: {0}", expectedArg);
96 }
97
98 this.Engine.Quit(-1);
99 return;
100 }
101
102 this.dummyWindow = new Form();
103 this.dummyWindow.CreateControl();
104 this.appContext = new ApplicationContext();
105
106 int redetectCount = 0;
107 string redetect = this.ReadPackageAction(null, "RedetectCount");
108 if (String.IsNullOrEmpty(redetect) || !Int32.TryParse(redetect, out redetectCount))
109 {
110 redetectCount = 0;
111 }
112
113 do
114 {
115 this.Engine.Detect();
116 this.Log("Completed detection phase: {0} re-runs remaining", redetectCount);
117 } while (0 < redetectCount--);
118
119 Application.Run(this.appContext);
120 this.Engine.Quit(this.result & 0xFFFF); // return plain old Win32 error, not HRESULT.
121 }
122
123 protected override void OnDetectUpdateBegin(DetectUpdateBeginEventArgs args)
124 {
125 this.Log("OnDetectUpdateBegin");
126 if ((LaunchAction.UpdateReplaceEmbedded == this.action)|(LaunchAction.UpdateReplace == this.action))
127 {
128 args.Skip = false;
129 }
130 }
131
132 protected override void OnDetectUpdate(DetectUpdateEventArgs e)
133 {
134 // The list of updates is sorted in descending version, so the first callback should be the largest update available.
135 // This update should be either larger than ours (so we are out of date), the same as ours (so we are current)
136 // or smaller than ours (we have a private build). If we really wanted to, we could leave the e.StopProcessingUpdates alone and
137 // enumerate all of the updates.
138 this.Log(String.Format("Potential update v{0} from '{1}'; current version: v{2}", e.Version, e.UpdateLocation, this.Version));
139 if (this.Engine.CompareVersions(e.Version, this.Version) > 0)
140 {
141 this.Log(String.Format("Selected update v{0}", e.Version));
142 this.Engine.SetUpdate(null, e.UpdateLocation, e.Size, UpdateHashType.None, null);
143 this.UpdateAvailable = true;
144 }
145 else
146 {
147 this.UpdateAvailable = false;
148 }
149 e.StopProcessingUpdates = true;
150 }
151
152 protected override void OnDetectUpdateComplete(DetectUpdateCompleteEventArgs e)
153 {
154 this.Log("OnDetectUpdateComplete");
155
156 // Failed to process an update, allow the existing bundle to still install.
157 if (!Hresult.Succeeded(e.Status))
158 {
159 this.Log(String.Format("Failed to locate an update, status of 0x{0:X8}, updates disabled.", e.Status));
160 e.IgnoreError = true; // But continue on...
161 }
162 }
163
164 protected override void OnDetectComplete(DetectCompleteEventArgs args)
165 {
166 this.result = args.Status;
167
168 if (Hresult.Succeeded(this.result) && (this.UpdateAvailable | (!((LaunchAction.UpdateReplaceEmbedded == this.action) | (LaunchAction.UpdateReplace == this.action)))))
169 {
170 this.Engine.Plan(this.action);
171 }
172 else
173 {
174 this.appContext.ExitThread();
175 }
176 }
177
178 protected override void OnPlanPackageBegin(PlanPackageBeginEventArgs args)
179 {
180 RequestState state;
181 string action = this.ReadPackageAction(args.PackageId, "Requested");
182 if (TryParseEnum<RequestState>(action, out state))
183 {
184 args.State = state;
185 }
186 }
187
188 protected override void OnPlanTargetMsiPackage(PlanTargetMsiPackageEventArgs args)
189 {
190 RequestState state;
191 string action = this.ReadPackageAction(args.PackageId, "Requested");
192 if (TryParseEnum<RequestState>(action, out state))
193 {
194 args.State = state;
195 }
196 }
197
198 protected override void OnPlanMsiFeature(PlanMsiFeatureEventArgs args)
199 {
200 FeatureState state;
201 string action = this.ReadFeatureAction(args.PackageId, args.FeatureId, "Requested");
202 if (TryParseEnum<FeatureState>(action, out state))
203 {
204 args.State = state;
205 }
206 }
207
208 protected override void OnPlanComplete(PlanCompleteEventArgs args)
209 {
210 this.result = args.Status;
211 if (Hresult.Succeeded(this.result))
212 {
213 this.Engine.Apply(this.dummyWindow.Handle);
214 }
215 else
216 {
217 this.appContext.ExitThread();
218 }
219 }
220
221 protected override void OnCachePackageBegin(CachePackageBeginEventArgs args)
222 {
223 this.Log("OnCachePackageBegin() - package: {0}, payloads to cache: {1}", args.PackageId, args.CachePayloads);
224
225 string slowProgress = this.ReadPackageAction(args.PackageId, "SlowCache");
226 if (String.IsNullOrEmpty(slowProgress) || !Int32.TryParse(slowProgress, out this.sleepDuringCache))
227 {
228 this.sleepDuringCache = 0;
229 }
230
231 string cancelCache = this.ReadPackageAction(args.PackageId, "CancelCacheAtProgress");
232 if (String.IsNullOrEmpty(cancelCache) || !Int32.TryParse(cancelCache, out this.cancelCacheAtProgress))
233 {
234 this.cancelCacheAtProgress = -1;
235 }
236 }
237
238 protected override void OnCacheAcquireProgress(CacheAcquireProgressEventArgs args)
239 {
240 this.Log("OnCacheAcquireProgress() - container/package: {0}, payload: {1}, progress: {2}, total: {3}, overall progress: {4}%", args.PackageOrContainerId, args.PayloadId, args.Progress, args.Total, args.OverallPercentage);
241
242 if (this.cancelCacheAtProgress > 0 && this.cancelCacheAtProgress <= args.Progress)
243 {
244 args.Cancel = true;
245 }
246 else if (this.sleepDuringCache > 0)
247 {
248 Thread.Sleep(this.sleepDuringCache);
249 }
250 }
251
252 protected override void OnExecutePackageBegin(ExecutePackageBeginEventArgs args)
253 {
254 this.Log("OnExecutePackageBegin() - package: {0}, rollback: {1}", args.PackageId, !args.ShouldExecute);
255
256 string slowProgress = this.ReadPackageAction(args.PackageId, "SlowExecute");
257 if (String.IsNullOrEmpty(slowProgress) || !Int32.TryParse(slowProgress, out this.sleepDuringExecute))
258 {
259 this.sleepDuringExecute = 0;
260 }
261
262 string cancelExecute = this.ReadPackageAction(args.PackageId, "CancelExecuteAtProgress");
263 if (String.IsNullOrEmpty(cancelExecute) || !Int32.TryParse(cancelExecute, out this.cancelExecuteAtProgress))
264 {
265 this.cancelExecuteAtProgress = -1;
266 }
267
268 string retryBeforeCancel = this.ReadPackageAction(args.PackageId, "RetryExecuteFilesInUse");
269 if (String.IsNullOrEmpty(retryBeforeCancel) || !Int32.TryParse(retryBeforeCancel, out this.retryExecuteFilesInUse))
270 {
271 this.retryExecuteFilesInUse = 0;
272 }
273 }
274
275 protected override void OnExecuteFilesInUse(ExecuteFilesInUseEventArgs args)
276 {
277 this.Log("OnExecuteFilesInUse() - package: {0}, retries remaining: {1}, data: {2}", args.PackageId, this.retryExecuteFilesInUse, String.Join(", ", args.Files.ToArray()));
278
279 if (this.retryExecuteFilesInUse > 0)
280 {
281 --this.retryExecuteFilesInUse;
282 args.Result = Result.Retry;
283 }
284 else
285 {
286 args.Result = Result.Abort;
287 }
288 }
289
290 protected override void OnExecuteProgress(ExecuteProgressEventArgs args)
291 {
292 this.Log("OnExecuteProgress() - package: {0}, progress: {1}%, overall progress: {2}%", args.PackageId, args.ProgressPercentage, args.OverallPercentage);
293
294 if (this.cancelExecuteAtProgress > 0 && this.cancelExecuteAtProgress <= args.ProgressPercentage)
295 {
296 args.Cancel = true;
297 }
298 else if (this.sleepDuringExecute > 0)
299 {
300 Thread.Sleep(this.sleepDuringExecute);
301 }
302 }
303
304 protected override void OnExecutePatchTarget(ExecutePatchTargetEventArgs args)
305 {
306 this.Log("OnExecutePatchTarget - Patch Package: {0}, Target Product Code: {1}", args.PackageId, args.TargetProductCode);
307 }
308
309 protected override void OnProgress(ProgressEventArgs args)
310 {
311 this.Log("OnProgress() - progress: {0}%, overall progress: {1}%", args.ProgressPercentage, args.OverallPercentage);
312 if (this.Command.Display == Display.Embedded)
313 {
314 this.Engine.SendEmbeddedProgress(args.ProgressPercentage, args.OverallPercentage);
315 }
316 }
317
318 protected override void OnResolveSource(ResolveSourceEventArgs args)
319 {
320 if (!String.IsNullOrEmpty(args.DownloadSource))
321 {
322 args.Action = BOOTSTRAPPER_RESOLVESOURCE_ACTION.Download;
323 }
324 }
325
326 protected override void OnApplyComplete(ApplyCompleteEventArgs args)
327 {
328 // Output what the privileges are now.
329 this.Log("After elevation: WixBundleElevated = {0}", this.Engine.GetVariableNumeric("WixBundleElevated"));
330
331 this.result = args.Status;
332 this.appContext.ExitThread();
333 }
334
335 protected override void OnSystemShutdown(SystemShutdownEventArgs args)
336 {
337 // Always prevent shutdown.
338 this.Log("Disallowed system request to shut down the bootstrapper application.");
339 args.Cancel = true;
340
341 this.appContext.ExitThread();
342 }
343
344 private void TestVariables()
345 {
346 // First make sure we can check and get standard variables of each type.
347 {
348 string value = null;
349 if (this.Engine.ContainsVariable("WindowsFolder"))
350 {
351 value = this.Engine.GetVariableString("WindowsFolder");
352 this.Engine.Log(LogLevel.Verbose, "TEST: Successfully retrieved a string variable: WindowsFolder");
353 }
354 else
355 {
356 throw new Exception("Engine did not define a standard variable: WindowsFolder");
357 }
358 }
359
360 {
361 long value = 0;
362 if (this.Engine.ContainsVariable("NTProductType"))
363 {
364 value = this.Engine.GetVariableNumeric("NTProductType");
365 this.Engine.Log(LogLevel.Verbose, "TEST: Successfully retrieved a numeric variable: NTProductType");
366 }
367 else
368 {
369 throw new Exception("Engine did not define a standard variable: NTProductType");
370 }
371 }
372
373 {
374 string value = null;
375 if (this.Engine.ContainsVariable("VersionMsi"))
376 {
377 value = this.Engine.GetVariableVersion("VersionMsi");
378 this.Engine.Log(LogLevel.Verbose, "TEST: Successfully retrieved a version variable: VersionMsi");
379 }
380 else
381 {
382 throw new Exception("Engine did not define a standard variable: VersionMsi");
383 }
384 }
385
386 // Now validate that Contians returns false for non-existant variables of each type.
387 if (this.Engine.ContainsVariable("TestStringVariableShouldNotExist"))
388 {
389 throw new Exception("Engine defined a variable that should not exist: TestStringVariableShouldNotExist");
390 }
391 else
392 {
393 this.Engine.Log(LogLevel.Verbose, "TEST: Successfully checked for non-existent string variable: TestStringVariableShouldNotExist");
394 }
395
396 if (this.Engine.ContainsVariable("TestNumericVariableShouldNotExist"))
397 {
398 throw new Exception("Engine defined a variable that should not exist: TestNumericVariableShouldNotExist");
399 }
400 else
401 {
402 this.Engine.Log(LogLevel.Verbose, "TEST: Successfully checked for non-existent numeric variable: TestNumericVariableShouldNotExist");
403 }
404
405 if (this.Engine.ContainsVariable("TestVersionVariableShouldNotExist"))
406 {
407 throw new Exception("Engine defined a variable that should not exist: TestVersionVariableShouldNotExist");
408 }
409 else
410 {
411 this.Engine.Log(LogLevel.Verbose, "TEST: Successfully checked for non-existent version variable: TestVersionVariableShouldNotExist");
412 }
413
414 // Output what the initially run privileges were.
415 this.Engine.Log(LogLevel.Verbose, String.Format("TEST: WixBundleElevated = {0}", this.Engine.GetVariableNumeric("WixBundleElevated")));
416 }
417
418 private void Log(string format, params object[] args)
419 {
420 string relation = this.Command.Relation != RelationType.None ? String.Concat(" (", this.Command.Relation.ToString().ToLowerInvariant(), ")") : String.Empty;
421 string message = String.Format(format, args);
422
423 this.Engine.Log(LogLevel.Standard, String.Concat("TESTBA", relation, ": ", message));
424 }
425
426 private List<string> ReadVerifyArguments()
427 {
428 string testName = this.Engine.GetVariableString("TestGroupName");
429 using (RegistryKey testKey = Registry.LocalMachine.OpenSubKey(String.Format(@"Software\WiX\Tests\TestBAControl\{0}", testName)))
430 {
431 string verifyArguments = testKey == null ? null : testKey.GetValue("VerifyArguments") as string;
432 return verifyArguments == null ? new List<string>() : new List<string>(verifyArguments.Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries));
433 }
434 }
435
436 private string ReadPackageAction(string packageId, string state)
437 {
438 string testName = this.Engine.GetVariableString("TestGroupName");
439 using (RegistryKey testKey = Registry.LocalMachine.OpenSubKey(String.Format(@"Software\WiX\Tests\TestBAControl\{0}\{1}", testName, String.IsNullOrEmpty(packageId) ? String.Empty : packageId)))
440 {
441 return testKey == null ? null : testKey.GetValue(state) as string;
442 }
443 }
444
445 private string ReadFeatureAction(string packageId, string featureId, string state)
446 {
447 string testName = this.Engine.GetVariableString("TestGroupName");
448 using (RegistryKey testKey = Registry.LocalMachine.OpenSubKey(String.Format(@"Software\WiX\Tests\TestBAControl\{0}\{1}", testName, packageId)))
449 {
450 string registryName = String.Concat(featureId, state);
451 return testKey == null ? null : testKey.GetValue(registryName) as string;
452 }
453 }
454
455 private static bool TryParseEnum<T>(string value, out T t)
456 {
457 try
458 {
459 t = (T)Enum.Parse(typeof(T), value, true);
460 return true;
461 }
462 catch (ArgumentException) { }
463 catch (OverflowException) { }
464
465 t = default(T);
466 return false;
467 }
468 }
469}
diff --git a/src/Utilities/TestBA/TestBA.csproj b/src/Utilities/TestBA/TestBA.csproj
new file mode 100644
index 00000000..ad7a59b4
--- /dev/null
+++ b/src/Utilities/TestBA/TestBA.csproj
@@ -0,0 +1,24 @@
1<?xml version="1.0" encoding="utf-8"?>
2<!-- Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. -->
3
4<Project Sdk="Microsoft.NET.Sdk">
5 <PropertyGroup>
6 <TargetFramework>net35</TargetFramework>
7 <AssemblyName>TestBA</AssemblyName>
8 <RootNamespace>WixToolset.Test.BA</RootNamespace>
9 <DebugType>embedded</DebugType>
10 <RuntimeIdentifier>win-x86</RuntimeIdentifier>
11 </PropertyGroup>
12
13 <ItemGroup>
14 <Content Include="TestBA.BootstrapperCore.config" CopyToOutputDirectory="PreserveNewest" />
15 </ItemGroup>
16
17 <ItemGroup>
18 <PackageReference Include="WixToolset.Mba.Core" Version="4.0.41" />
19 </ItemGroup>
20
21 <ItemGroup>
22 <Reference Include="System.Windows.Forms" />
23 </ItemGroup>
24</Project> \ No newline at end of file
diff --git a/src/Utilities/TestBA/TestBAFactory.cs b/src/Utilities/TestBA/TestBAFactory.cs
new file mode 100644
index 00000000..ba1de367
--- /dev/null
+++ b/src/Utilities/TestBA/TestBAFactory.cs
@@ -0,0 +1,22 @@
1// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
2
3[assembly: WixToolset.Mba.Core.BootstrapperApplicationFactory(typeof(WixToolset.Test.BA.TestBAFactory))]
4namespace WixToolset.Test.BA
5{
6 using WixToolset.Mba.Core;
7
8 public class TestBAFactory : BaseBootstrapperApplicationFactory
9 {
10 private static int loadCount = 0;
11
12 protected override IBootstrapperApplication Create(IEngine engine, IBootstrapperCommand bootstrapperCommand)
13 {
14 if (loadCount > 0)
15 {
16 engine.Log(LogLevel.Standard, $"Reloaded {loadCount} time(s)");
17 }
18 ++loadCount;
19 return new TestBA(engine, bootstrapperCommand);
20 }
21 }
22}
diff --git a/src/Wix.Build.props b/src/Wix.Build.props
new file mode 100644
index 00000000..050ec6d5
--- /dev/null
+++ b/src/Wix.Build.props
@@ -0,0 +1,7 @@
1<!-- Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. -->
2<Project>
3 <PropertyGroup>
4 <TestGroupName Condition=" '$(TestGroupName)'=='' ">$([System.IO.Path]::GetFileName($([System.IO.Path]::GetDirectoryName($(MSBuildProjectDirectory)))))</TestGroupName>
5 <OutputPath>$(OutputPath)$(TestGroupName)\</OutputPath>
6 </PropertyGroup>
7</Project>
diff --git a/src/Wix.Build.targets b/src/Wix.Build.targets
new file mode 100644
index 00000000..e2caf069
--- /dev/null
+++ b/src/Wix.Build.targets
@@ -0,0 +1,10 @@
1<?xml version="1.0" encoding="utf-8"?>
2<!-- Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. -->
3<Project>
4 <PropertyGroup>
5 <PackageName Condition=" '$(PackageName)' == '' ">$(MSBuildProjectName)</PackageName>
6 <DefineConstants>TestGroupName=$(TestGroupName);PackageName=$(PackageName);$(DefineConstants)</DefineConstants>
7 <DefineConstants Condition=" '$(UpgradeCode)' != '' ">UpgradeCode=$(UpgradeCode);$(DefineConstants)</DefineConstants>
8 <DefineConstants Condition=" '$(Version)' != '' ">Version=$(Version);$(DefineConstants)</DefineConstants>
9 </PropertyGroup>
10</Project>
diff --git a/src/WixToolsetTest.BurnE2E/BundleInstaller.cs b/src/WixToolsetTest.BurnE2E/BundleInstaller.cs
new file mode 100644
index 00000000..b708db40
--- /dev/null
+++ b/src/WixToolsetTest.BurnE2E/BundleInstaller.cs
@@ -0,0 +1,128 @@
1// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
2
3namespace WixToolsetTest.BurnE2E
4{
5 using System;
6 using System.IO;
7 using System.Text;
8
9 public class BundleInstaller : IDisposable
10 {
11 public BundleInstaller(WixTestContext testContext, string name)
12 {
13 this.Bundle = Path.Combine(testContext.TestDataFolder, $"{name}.exe");
14 this.TestGroupName = testContext.TestGroupName;
15 this.TestName = testContext.TestName;
16 }
17
18 public string Bundle { get; }
19
20 public string TestGroupName { get; }
21
22 public string TestName { get; }
23
24 /// <summary>
25 /// Installs the bundle with optional arguments.
26 /// </summary>
27 /// <param name="expectedExitCode">Expected exit code, defaults to success.</param>
28 /// <param name="arguments">Optional arguments to pass to the tool.</param>
29 /// <returns>Path to the generated log file.</returns>
30 public string Install(int expectedExitCode = (int)MSIExec.MSIExecReturnCode.SUCCESS, params string[] arguments)
31 {
32 return this.RunBundleWithArguments(expectedExitCode, MSIExec.MSIExecMode.Install, arguments);
33 }
34
35 /// <summary>
36 /// Modify the bundle with optional arguments.
37 /// </summary>
38 /// <param name="expectedExitCode">Expected exit code, defaults to success.</param>
39 /// <param name="arguments">Optional arguments to pass to the tool.</param>
40 /// <returns>Path to the generated log file.</returns>
41 public string Modify(int expectedExitCode = (int)MSIExec.MSIExecReturnCode.SUCCESS, params string[] arguments)
42 {
43 return this.RunBundleWithArguments(expectedExitCode, MSIExec.MSIExecMode.Modify, arguments);
44 }
45
46 /// <summary>
47 /// Repairs the bundle with optional arguments.
48 /// </summary>
49 /// <param name="expectedExitCode">Expected exit code, defaults to success.</param>
50 /// <param name="arguments">Optional arguments to pass to the tool.</param>
51 /// <returns>Path to the generated log file.</returns>
52 public string Repair(int expectedExitCode = (int)MSIExec.MSIExecReturnCode.SUCCESS, params string[] arguments)
53 {
54 return this.RunBundleWithArguments(expectedExitCode, MSIExec.MSIExecMode.Repair, arguments);
55 }
56
57 /// <summary>
58 /// Uninstalls the bundle with optional arguments.
59 /// </summary>
60 /// <param name="expectedExitCode">Expected exit code, defaults to success.</param>
61 /// <param name="arguments">Optional arguments to pass to the tool.</param>
62 /// <returns>Path to the generated log file.</returns>
63 public string Uninstall(int expectedExitCode = (int)MSIExec.MSIExecReturnCode.SUCCESS, params string[] arguments)
64 {
65 return this.RunBundleWithArguments(expectedExitCode, MSIExec.MSIExecMode.Uninstall, arguments);
66 }
67
68 /// <summary>
69 /// Executes the bundle with optional arguments.
70 /// </summary>
71 /// <param name="expectedExitCode">Expected exit code.</param>
72 /// <param name="mode">Install mode.</param>
73 /// <param name="arguments">Optional arguments to pass to the tool.</param>
74 /// <returns>Path to the generated log file.</returns>
75 private string RunBundleWithArguments(int expectedExitCode, MSIExec.MSIExecMode mode, string[] arguments, bool assertOnError = true)
76 {
77 TestTool bundle = new TestTool(this.Bundle);
78 var sb = new StringBuilder();
79
80 // Be sure to run silent.
81 sb.Append(" -quiet");
82
83 // Generate the log file name.
84 string logFile = Path.Combine(Path.GetTempPath(), String.Format("{0}_{1}_{2:yyyyMMddhhmmss}_{4}_{3}.log", this.TestGroupName, this.TestName, DateTime.UtcNow, Path.GetFileNameWithoutExtension(this.Bundle), mode));
85 sb.AppendFormat(" -log \"{0}\"", logFile);
86
87 // Set operation.
88 switch (mode)
89 {
90 case MSIExec.MSIExecMode.Modify:
91 sb.Append(" -modify");
92 break;
93
94 case MSIExec.MSIExecMode.Repair:
95 sb.Append(" -repair");
96 break;
97
98 case MSIExec.MSIExecMode.Cleanup:
99 case MSIExec.MSIExecMode.Uninstall:
100 sb.Append(" -uninstall");
101 break;
102 }
103
104 // Add additional arguments.
105 if (null != arguments)
106 {
107 sb.Append(" ");
108 sb.Append(String.Join(" ", arguments));
109 }
110
111 // Set the arguments.
112 bundle.Arguments = sb.ToString();
113
114 // Run the tool and assert the expected code.
115 bundle.ExpectedExitCode = expectedExitCode;
116 bundle.Run(assertOnError);
117
118 // Return the log file name.
119 return logFile;
120 }
121
122 public void Dispose()
123 {
124 string[] args = { "-burn.ignoredependencies=ALL" };
125 this.RunBundleWithArguments((int)MSIExec.MSIExecReturnCode.SUCCESS, MSIExec.MSIExecMode.Cleanup, args, assertOnError: false);
126 }
127 }
128}
diff --git a/src/WixToolsetTest.BurnE2E/BurnE2EFixture.cs b/src/WixToolsetTest.BurnE2E/BurnE2EFixture.cs
new file mode 100644
index 00000000..babfcbc3
--- /dev/null
+++ b/src/WixToolsetTest.BurnE2E/BurnE2EFixture.cs
@@ -0,0 +1,28 @@
1// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
2
3namespace WixToolsetTest.BurnE2E
4{
5 using System;
6 using System.Security.Principal;
7
8 public class BurnE2EFixture
9 {
10 const string RequiredEnvironmentVariableName = "RuntimeTestsEnabled";
11
12 public BurnE2EFixture()
13 {
14 using var identity = WindowsIdentity.GetCurrent();
15 var principal = new WindowsPrincipal(identity);
16 if (!principal.IsInRole(WindowsBuiltInRole.Administrator))
17 {
18 throw new InvalidOperationException("These tests must run elevated.");
19 }
20
21 var testsEnabledString = Environment.GetEnvironmentVariable(RequiredEnvironmentVariableName);
22 if (!bool.TryParse(testsEnabledString, out var testsEnabled) || !testsEnabled)
23 {
24 throw new InvalidOperationException($"These tests affect machine state. Set the {RequiredEnvironmentVariableName} environment variable to true to accept the consequences.");
25 }
26 }
27 }
28}
diff --git a/src/WixToolsetTest.BurnE2E/BurnE2ETests.cs b/src/WixToolsetTest.BurnE2E/BurnE2ETests.cs
new file mode 100644
index 00000000..7643cc64
--- /dev/null
+++ b/src/WixToolsetTest.BurnE2E/BurnE2ETests.cs
@@ -0,0 +1,48 @@
1// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
2
3namespace WixToolsetTest.BurnE2E
4{
5 using System;
6 using System.Collections.Generic;
7 using Xunit;
8 using Xunit.Abstractions;
9
10 [Collection("BurnE2E")]
11 public abstract class BurnE2ETests : WixTestBase, IDisposable
12 {
13 protected BurnE2ETests(ITestOutputHelper testOutputHelper, string testGroupName) : base(testOutputHelper, testGroupName) { }
14
15 private Queue<IDisposable> Installers { get; } = new Queue<IDisposable>();
16
17 protected BundleInstaller CreateBundleInstaller(string name)
18 {
19 var installer = new BundleInstaller(this.TestContext, name);
20 this.Installers.Enqueue(installer);
21 return installer;
22 }
23
24 protected PackageInstaller CreatePackageInstaller(string name)
25 {
26 var installer = new PackageInstaller(this.TestContext, name);
27 this.Installers.Enqueue(installer);
28 return installer;
29 }
30
31 public void Dispose()
32 {
33 while (this.Installers.TryDequeue(out var installer))
34 {
35 try
36 {
37 installer.Dispose();
38 }
39 catch { }
40 }
41 }
42 }
43
44 [CollectionDefinition("BurnE2E", DisableParallelization = true)]
45 public class BurnE2ECollectionDefinition : ICollectionFixture<BurnE2EFixture>
46 {
47 }
48}
diff --git a/src/WixToolsetTest.BurnE2E/MSIExec.cs b/src/WixToolsetTest.BurnE2E/MSIExec.cs
new file mode 100644
index 00000000..659d91ea
--- /dev/null
+++ b/src/WixToolsetTest.BurnE2E/MSIExec.cs
@@ -0,0 +1,753 @@
1// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
2
3namespace WixToolsetTest.BurnE2E
4{
5 using System;
6 using System.IO;
7 using System.Text;
8 using WixBuildTools.TestSupport;
9
10 public class MSIExec : TestTool
11 {
12 /// <summary>
13 /// The expected exit code of the tool
14 /// </summary>
15 public new MSIExecReturnCode ExpectedExitCode
16 {
17 get { return (MSIExecReturnCode)base.ExpectedExitCode; }
18 set { base.ExpectedExitCode = (int?)value; }
19 }
20
21 /// <summary>
22 /// Mode of execution (install, uninstall, or repair)
23 /// </summary>
24 public MSIExecMode ExecutionMode { get; set; }
25
26 /// <summary>
27 /// Path to msi or ProductCode
28 /// </summary>
29 public string Product { get; set; }
30
31 /// <summary>
32 /// Logging Options
33 /// </summary>
34 public MSIExecLoggingOptions LoggingOptions { get; set; }
35
36 /// <summary>
37 /// Path to the log file
38 /// </summary>
39 public string LogFile { get; set; }
40
41 /// <summary>
42 /// Unattended mode - progress bar only
43 /// </summary>
44 public bool Passive { get; set; }
45
46 /// <summary>
47 /// Quiet mode, no user interaction
48 /// </summary>
49 public bool Quiet { get; set; }
50
51 /// <summary>
52 /// Sets user interface level
53 /// </summary>
54 public MSIExecUserInterfaceLevel UserInterfaceLevel { get; set; }
55
56 /// <summary>
57 /// Do not restart after the installation is complete
58 /// </summary>
59 public bool NoRestart { get; set; }
60
61 /// <summary>
62 /// Prompts the user for restart if necessary
63 /// </summary>
64 public bool PromptRestart { get; set; }
65
66 /// <summary>
67 /// Always restart the computer after installation
68 /// </summary>
69 public bool ForceRestart { get; set; }
70
71 /// <summary>
72 /// Other arguments.
73 /// </summary>
74 public string OtherArguments { get; set; }
75
76 /// <summary>
77 /// Constructor that uses the default location for MSIExec.
78 /// </summary>
79 public MSIExec()
80 : this(Environment.SystemDirectory)
81 {
82 }
83
84 /// <summary>
85 /// Constructor that accepts a path to the MSIExec location.
86 /// </summary>
87 /// <param name="toolDirectory">The directory of MSIExec.exe.</param>
88 public MSIExec(string toolDirectory)
89 : base(Path.Combine(toolDirectory, "MSIExec.exe"))
90 {
91 this.SetDefaultArguments();
92 }
93
94 public override ExternalExecutableResult Run(bool assertOnError)
95 {
96 this.Arguments = this.GetArguments();
97 return base.Run(assertOnError);
98 }
99
100 /// <summary>
101 /// Clears all of the assigned arguments and resets them to the default values.
102 /// </summary>
103 public void SetDefaultArguments()
104 {
105 this.ExecutionMode = MSIExecMode.Install;
106 this.Product = String.Empty;
107 this.Quiet = true;
108 this.Passive = false;
109 this.UserInterfaceLevel = MSIExecUserInterfaceLevel.None;
110 this.NoRestart = true;
111 this.ForceRestart = false;
112 this.PromptRestart = false;
113 this.LogFile = string.Empty;
114 this.LoggingOptions = MSIExecLoggingOptions.VOICEWARMUP;
115 this.OtherArguments = String.Empty;
116 }
117
118 public string GetArguments()
119 {
120 var arguments = new StringBuilder();
121
122 // quiet
123 if (this.Quiet)
124 {
125 arguments.Append(" /quiet ");
126 }
127
128 // passive
129 if (this.Passive)
130 {
131 arguments.Append(" /passive ");
132 }
133
134 // UserInterfaceLevel
135 switch (this.UserInterfaceLevel)
136 {
137 case MSIExecUserInterfaceLevel.None:
138 arguments.Append(" /qn ");
139 break;
140 case MSIExecUserInterfaceLevel.Basic:
141 arguments.Append(" /qb ");
142 break;
143 case MSIExecUserInterfaceLevel.Reduced:
144 arguments.Append(" /qr ");
145 break;
146 case MSIExecUserInterfaceLevel.Full:
147 arguments.Append(" /qf ");
148 break;
149 }
150
151 // NoRestart
152 if (this.NoRestart)
153 {
154 arguments.Append(" /norestart ");
155 }
156
157 // PromptRestart
158 if (this.PromptRestart)
159 {
160 arguments.Append(" /promptrestart ");
161 }
162
163 // ForceRestart
164 if (this.ForceRestart)
165 {
166 arguments.Append(" /forcerestart ");
167 }
168
169 // Logging options
170 var loggingOptionsString = new StringBuilder();
171 if ((this.LoggingOptions & MSIExecLoggingOptions.Status_Messages) == MSIExecLoggingOptions.Status_Messages)
172 {
173 loggingOptionsString.Append("i");
174 }
175 if ((this.LoggingOptions & MSIExecLoggingOptions.Nonfatal_Warnings) == MSIExecLoggingOptions.Nonfatal_Warnings)
176 {
177 loggingOptionsString.Append("w");
178 }
179 if ((this.LoggingOptions & MSIExecLoggingOptions.All_Error_Messages) == MSIExecLoggingOptions.All_Error_Messages)
180 {
181 loggingOptionsString.Append("e");
182 }
183 if ((this.LoggingOptions & MSIExecLoggingOptions.Start_Up_Of_Actions) == MSIExecLoggingOptions.Start_Up_Of_Actions)
184 {
185 loggingOptionsString.Append("a");
186 }
187 if ((this.LoggingOptions & MSIExecLoggingOptions.Action_Specific_Records) == MSIExecLoggingOptions.Action_Specific_Records)
188 {
189 loggingOptionsString.Append("r");
190 }
191 if ((this.LoggingOptions & MSIExecLoggingOptions.User_Requests) == MSIExecLoggingOptions.User_Requests)
192 {
193 loggingOptionsString.Append("u");
194 }
195 if ((this.LoggingOptions & MSIExecLoggingOptions.Initial_UI_Parameters) == MSIExecLoggingOptions.Initial_UI_Parameters)
196 {
197 loggingOptionsString.Append("c");
198 }
199 if ((this.LoggingOptions & MSIExecLoggingOptions.OutOfMemory_Or_Fatal_Exit_Information) == MSIExecLoggingOptions.OutOfMemory_Or_Fatal_Exit_Information)
200 {
201 loggingOptionsString.Append("m");
202 }
203 if ((this.LoggingOptions & MSIExecLoggingOptions.OutOfDiskSpace_Messages) == MSIExecLoggingOptions.OutOfDiskSpace_Messages)
204 {
205 loggingOptionsString.Append("o");
206 }
207 if ((this.LoggingOptions & MSIExecLoggingOptions.Terminal_Properties) == MSIExecLoggingOptions.Terminal_Properties)
208 {
209 loggingOptionsString.Append("p");
210 }
211 if ((this.LoggingOptions & MSIExecLoggingOptions.Verbose_Output) == MSIExecLoggingOptions.Verbose_Output)
212 {
213 loggingOptionsString.Append("v");
214 }
215 if ((this.LoggingOptions & MSIExecLoggingOptions.Extra_Debugging_Information) == MSIExecLoggingOptions.Extra_Debugging_Information)
216 {
217 loggingOptionsString.Append("x");
218 }
219 if ((this.LoggingOptions & MSIExecLoggingOptions.Append_To_Existing_Log_File) == MSIExecLoggingOptions.Append_To_Existing_Log_File)
220 {
221 loggingOptionsString.Append("+");
222 }
223 if ((this.LoggingOptions & MSIExecLoggingOptions.Flush_Each_line) == MSIExecLoggingOptions.Flush_Each_line)
224 {
225 loggingOptionsString.Append("!");
226 }
227 if ((this.LoggingOptions & MSIExecLoggingOptions.Log_All_Information) == MSIExecLoggingOptions.Log_All_Information)
228 {
229 loggingOptionsString.Append("*");
230 }
231
232 // logfile and logging options
233 if (0 != loggingOptionsString.Length || !string.IsNullOrEmpty(this.LogFile))
234 {
235 arguments.Append(" /l");
236 if (0 != loggingOptionsString.Length)
237 {
238 arguments.AppendFormat("{0} ", loggingOptionsString);
239 }
240 if (!string.IsNullOrEmpty(this.LogFile))
241 {
242 arguments.AppendFormat(" \"{0}\" ", this.LogFile);
243 }
244 }
245
246 // OtherArguments
247 if (!String.IsNullOrEmpty(this.OtherArguments))
248 {
249 arguments.AppendFormat(" {0} ", this.OtherArguments);
250 }
251
252 // execution mode
253 switch (this.ExecutionMode)
254 {
255 case MSIExecMode.Install:
256 arguments.Append(" /package ");
257 break;
258 case MSIExecMode.AdministrativeInstall:
259 arguments.Append(" /a ");
260 break;
261 case MSIExecMode.Repair:
262 arguments.Append(" /f ");
263 break;
264 case MSIExecMode.Cleanup:
265 case MSIExecMode.Uninstall:
266 arguments.Append(" /uninstall ");
267 break;
268 };
269
270 // product
271 if (!string.IsNullOrEmpty(this.Product))
272 {
273 arguments.AppendFormat(" \"{0}\" ", this.Product);
274 }
275
276 return arguments.ToString();
277 }
278
279 /// <summary>
280 /// Return codes from an MSI install or uninstall
281 /// </summary>
282 /// <remarks>
283 /// Error codes indicative of success are:
284 /// ERROR_SUCCESS, ERROR_SUCCESS_REBOOT_INITIATED, and ERROR_SUCCESS_REBOOT_REQUIRED
285 /// </remarks>
286 public enum MSIExecReturnCode
287 {
288 /// <summary>
289 /// ERROR_SUCCESS 0
290 /// Action completed successfully.
291 /// </summary>
292 SUCCESS = 0,
293
294 /// <summary>
295 /// ERROR_INVALID_DATA 13
296 /// The data is invalid.
297 /// </summary>
298 ERROR_INVALID_DATA = 13,
299
300 /// <summary>
301 /// ERROR_INVALID_PARAMETER 87
302 /// One of the parameters was invalid.
303 /// </summary>
304 ERROR_INVALID_PARAMETER = 87,
305
306 /// <summary>
307 /// ERROR_CALL_NOT_IMPLEMENTED 120
308 /// This value is returned when a custom action attempts to call a function that cannot be called from custom actions.
309 /// The function returns the value ERROR_CALL_NOT_IMPLEMENTED. Available beginning with Windows Installer version 3.0.
310 /// </summary>
311 ERROR_CALL_NOT_IMPLEMENTED = 120,
312
313 /// <summary>
314 /// ERROR_APPHELP_BLOCK 1259
315 /// If Windows Installer determines a product may be incompatible with the current operating system,
316 /// it displays a dialog box informing the user and asking whether to try to install anyway.
317 /// This error code is returned if the user chooses not to try the installation.
318 /// </summary>
319 ERROR_APPHELP_BLOCK = 1259,
320
321 /// <summary>
322 /// ERROR_INSTALL_SERVICE_FAILURE 1601
323 /// The Windows Installer service could not be accessed.
324 /// Contact your support personnel to verify that the Windows Installer service is properly registered.
325 /// </summary>
326 ERROR_INSTALL_SERVICE_FAILURE = 1601,
327
328
329 /// <summary>
330 /// ERROR_INSTALL_USEREXIT 1602
331 /// The user cancels installation.
332 /// </summary>
333 ERROR_INSTALL_USEREXIT = 1602,
334
335 /// <summary>
336 /// ERROR_INSTALL_FAILURE 1603
337 /// A fatal error occurred during installation.
338 /// </summary>
339 ERROR_INSTALL_FAILURE = 1603,
340
341 /// <summary>
342 /// ERROR_INSTALL_SUSPEND 1604
343 /// Installation suspended, incomplete.
344 /// </summary>
345 ERROR_INSTALL_SUSPEND = 1604,
346
347 /// <summary>
348 /// ERROR_UNKNOWN_PRODUCT 1605
349 /// This action is only valid for products that are currently installed.
350 /// </summary>
351 ERROR_UNKNOWN_PRODUCT = 1605,
352
353 /// <summary>
354 /// ERROR_UNKNOWN_FEATURE 1606
355 /// The feature identifier is not registered.
356 /// </summary>
357 ERROR_UNKNOWN_FEATURE = 1606,
358
359 /// <summary>
360 /// ERROR_UNKNOWN_COMPONENT 1607
361 /// The component identifier is not registered.
362 /// </summary>
363 ERROR_UNKNOWN_COMPONENT = 1607,
364
365 /// <summary>
366 /// ERROR_UNKNOWN_PROPERTY 1608
367 /// This is an unknown property.
368 /// </summary>
369 ERROR_UNKNOWN_PROPERTY = 1608,
370
371 /// <summary>
372 /// ERROR_INVALID_HANDLE_STATE 1609
373 /// The handle is in an invalid state.
374 /// </summary>
375 ERROR_INVALID_HANDLE_STATE = 1609,
376
377 /// <summary>
378 /// ERROR_BAD_CONFIGURATION 1610
379 /// The configuration data for this product is corrupt. Contact your support personnel.
380 /// </summary>
381 ERROR_BAD_CONFIGURATION = 1610,
382
383 /// <summary>
384 /// ERROR_INDEX_ABSENT 1611
385 /// The component qualifier not present.
386 /// </summary>
387 ERROR_INDEX_ABSENT = 1611,
388
389 /// <summary>ERROR_INSTALL_SOURCE_ABSENT 1612
390 /// The installation source for this product is not available.
391 /// Verify that the source exists and that you can access it.
392 /// </summary>
393 ERROR_INSTALL_SOURCE_ABSENT = 1612,
394
395 /// <summary>
396 /// ERROR_INSTALL_PACKAGE_VERSION 1613
397 /// This installation package cannot be installed by the Windows Installer service.
398 /// You must install a Windows service pack that contains a newer version of the Windows Installer service.
399 /// </summary>
400 ERROR_INSTALL_PACKAGE_VERSION = 1613,
401
402 /// <summary>
403 /// ERROR_PRODUCT_UNINSTALLED 1614
404 /// The product is uninstalled.
405 /// </summary>
406 ERROR_PRODUCT_UNINSTALLED = 1614,
407
408 /// <summary>
409 /// ERROR_BAD_QUERY_SYNTAX 1615
410 /// The SQL query syntax is invalid or unsupported.
411 /// </summary>
412 ERROR_BAD_QUERY_SYNTAX = 1615,
413
414 /// <summary>
415 /// ERROR_INVALID_FIELD 1616
416 /// The record field does not exist.
417 /// </summary>
418 ERROR_INVALID_FIELD = 1616,
419
420 /// <summary>
421 /// ERROR_INSTALL_ALREADY_RUNNING 1618
422 /// Another installation is already in progress. Complete that installation before proceeding with this install.
423 /// For information about the mutex, see _MSIExecute Mutex.
424 /// </summary>
425 ERROR_INSTALL_ALREADY_RUNNING = 1618,
426
427 /// <summary>
428 /// ERROR_INSTALL_PACKAGE_OPEN_FAILED 1619
429 /// This installation package could not be opened. Verify that the package exists and is accessible, or contact the
430 /// application vendor to verify that this is a valid Windows Installer package.
431 /// </summary>
432 ERROR_INSTALL_PACKAGE_OPEN_FAILED = 1619,
433
434
435 /// <summary>
436 /// ERROR_INSTALL_PACKAGE_INVALID 1620
437 /// This installation package could not be opened.
438 /// Contact the application vendor to verify that this is a valid Windows Installer package.
439 /// </summary>
440 ERROR_INSTALL_PACKAGE_INVALID = 1620,
441
442 /// <summary>
443 /// ERROR_INSTALL_UI_FAILURE 1621
444 /// There was an error starting the Windows Installer service user interface.
445 /// Contact your support personnel.
446 /// </summary>
447 ERROR_INSTALL_UI_FAILURE = 1621,
448
449 /// <summary>
450 /// ERROR_INSTALL_LOG_FAILURE 1622
451 /// There was an error opening installation log file.
452 /// Verify that the specified log file location exists and is writable.
453 /// </summary>
454 ERROR_INSTALL_LOG_FAILURE = 1622,
455
456 /// <summary>
457 /// ERROR_INSTALL_LANGUAGE_UNSUPPORTED 1623
458 /// This language of this installation package is not supported by your system.
459 /// </summary>
460 ERROR_INSTALL_LANGUAGE_UNSUPPORTED = 1623,
461
462 /// <summary>
463 /// ERROR_INSTALL_TRANSFORM_FAILURE 1624
464 /// There was an error applying transforms.
465 /// Verify that the specified transform paths are valid.
466 /// </summary>
467 ERROR_INSTALL_TRANSFORM_FAILURE = 1624,
468
469
470 /// <summary>
471 /// ERROR_INSTALL_PACKAGE_REJECTED 1625
472 /// This installation is forbidden by system policy.
473 /// Contact your system administrator.
474 /// </summary>
475 ERROR_INSTALL_PACKAGE_REJECTED = 1625,
476
477 /// <summary>
478 /// ERROR_FUNCTION_NOT_CALLED 1626
479 /// The function could not be executed.
480 /// </summary>
481 ERROR_FUNCTION_NOT_CALLED = 1626,
482
483 /// <summary>
484 /// ERROR_FUNCTION_FAILED 1627
485 /// The function failed during execution.
486 /// </summary>
487 ERROR_FUNCTION_FAILED = 1627,
488
489 /// <summary>
490 /// ERROR_INVALID_TABLE 1628
491 /// An invalid or unknown table was specified.
492 /// </summary>
493 ERROR_INVALID_TABLE = 1628,
494
495 /// <summary>
496 /// ERROR_DATATYPE_MISMATCH 1629
497 /// The data supplied is the wrong type.
498 /// </summary>
499 ERROR_DATATYPE_MISMATCH = 1629,
500
501 /// <summary>
502 /// ERROR_UNSUPPORTED_TYPE 1630
503 /// Data of this type is not supported.
504 /// </summary>
505 ERROR_UNSUPPORTED_TYPE = 1630,
506
507 /// <summary>
508 /// ERROR_CREATE_FAILED 1631
509 /// The Windows Installer service failed to start.
510 /// Contact your support personnel.
511 /// </summary>
512 ERROR_CREATE_FAILED = 1631,
513
514 /// <summary>
515 /// ERROR_INSTALL_TEMP_UNWRITABLE 1632
516 /// The Temp folder is either full or inaccessible.
517 /// Verify that the Temp folder exists and that you can write to it.
518 /// </summary>
519 ERROR_INSTALL_TEMP_UNWRITABLE = 1632,
520
521 /// <summary>
522 /// ERROR_INSTALL_PLATFORM_UNSUPPORTED 1633
523 /// This installation package is not supported on this platform. Contact your application vendor. </summary>
524 ERROR_INSTALL_PLATFORM_UNSUPPORTED = 1633,
525
526 /// <summary>
527 /// ERROR_INSTALL_NOTUSED 1634
528 /// Component is not used on this machine.
529 /// </summary>
530 ERROR_INSTALL_NOTUSED = 1634,
531
532 /// <summary>
533 /// ERROR_PATCH_PACKAGE_OPEN_FAILED 1635
534 /// This patch package could not be opened. Verify that the patch package exists and is accessible,
535 /// or contact the application vendor to verify that this is a valid Windows Installer patch package.
536 /// </summary>
537 ERROR_PATCH_PACKAGE_OPEN_FAILED = 1635,
538
539 /// <summary>
540 /// ERROR_PATCH_PACKAGE_INVALID 1636
541 /// This patch package could not be opened.
542 /// Contact the application vendor to verify that this is a valid Windows Installer patch package.
543 /// </summary>
544 ERROR_PATCH_PACKAGE_INVALID = 1636,
545
546 /// <summary>
547 /// ERROR_PATCH_PACKAGE_UNSUPPORTED 1637
548 /// This patch package cannot be processed by the Windows Installer service.
549 /// You must install a Windows service pack that contains a newer version of the Windows Installer service.
550 /// </summary>
551 ERROR_PATCH_PACKAGE_UNSUPPORTED = 1637,
552
553 /// <summary>
554 /// ERROR_PRODUCT_VERSION 1638
555 /// Another version of this product is already installed.
556 /// Installation of this version cannot continue. To configure or remove the existing version of this product,
557 /// use Add/Remove Programs in Control Panel.
558 /// </summary>
559 ERROR_PRODUCT_VERSION = 1638,
560
561 /// <summary>
562 /// ERROR_INVALID_COMMAND_LINE 1639
563 /// Invalid command line argument.
564 /// Consult the Windows Installer SDK for detailed command-line help.
565 /// </summary>
566 ERROR_INVALID_COMMAND_LINE = 1639,
567
568 /// <summary>
569 /// ERROR_INSTALL_REMOTE_DISALLOWED 1640
570 /// The current user is not permitted to perform installations from a client session of a server running the
571 /// Terminal Server role service.
572 /// </summary>
573 ERROR_INSTALL_REMOTE_DISALLOWED = 1640,
574
575 /// <summary>
576 /// ERROR_SUCCESS_REBOOT_INITIATED 1641
577 /// The installer has initiated a restart.
578 /// This message is indicative of a success.
579 /// </summary>
580 ERROR_SUCCESS_REBOOT_INITIATED = 1641,
581
582 /// <summary>
583 /// ERROR_PATCH_TARGET_NOT_FOUND 1642
584 /// The installer cannot install the upgrade patch because the program being upgraded may be missing or the
585 /// upgrade patch updates a different version of the program.
586 /// Verify that the program to be upgraded exists on your computer and that you have the correct upgrade patch.
587 /// </summary>
588 ERROR_PATCH_TARGET_NOT_FOUND = 1642,
589
590 /// <summary>
591 /// ERROR_PATCH_PACKAGE_REJECTED 1643
592 /// The patch package is not permitted by system policy.
593 /// </summary>
594 ERROR_PATCH_PACKAGE_REJECTED = 1643,
595
596 /// <summary>
597 /// ERROR_INSTALL_TRANSFORM_REJECTED 1644
598 /// One or more customizations are not permitted by system policy.
599 /// </summary>
600 ERROR_INSTALL_TRANSFORM_REJECTED = 1644,
601
602 /// <summary>
603 /// ERROR_INSTALL_REMOTE_PROHIBITED 1645
604 /// Windows Installer does not permit installation from a Remote Desktop Connection.
605 /// </summary>
606 ERROR_INSTALL_REMOTE_PROHIBITED = 1645,
607
608 /// <summary>
609 /// ERROR_PATCH_REMOVAL_UNSUPPORTED 1646
610 /// The patch package is not a removable patch package. Available beginning with Windows Installer version 3.0.
611 /// </summary>
612 ERROR_PATCH_REMOVAL_UNSUPPORTED = 1646,
613
614 /// <summary>
615 /// ERROR_UNKNOWN_PATCH 1647
616 /// The patch is not applied to this product. Available beginning with Windows Installer version 3.0.
617 /// </summary>
618 ERROR_UNKNOWN_PATCH = 1647,
619
620 /// <summary>
621 /// ERROR_PATCH_NO_SEQUENCE 1648
622 /// No valid sequence could be found for the set of patches. Available beginning with Windows Installer version 3.0.
623 /// </summary>
624 ERROR_PATCH_NO_SEQUENCE = 1648,
625
626 /// <summary>
627 /// ERROR_PATCH_REMOVAL_DISALLOWED 1649
628 /// Patch removal was disallowed by policy. Available beginning with Windows Installer version 3.0. </summary>
629 ERROR_PATCH_REMOVAL_DISALLOWED = 1649,
630
631 /// <summary>
632 /// ERROR_INVALID_PATCH_XML = 1650
633 /// The XML patch data is invalid. Available beginning with Windows Installer version 3.0.
634 /// </summary>
635 ERROR_INVALID_PATCH_XML = 1650,
636
637 /// <summary>
638 /// ERROR_PATCH_MANAGED_ADVERTISED_PRODUCT 1651
639 /// Administrative user failed to apply patch for a per-user managed or a per-machine application that is in advertise state.
640 /// Available beginning with Windows Installer version 3.0. </summary>
641 ERROR_PATCH_MANAGED_ADVERTISED_PRODUCT = 1651,
642
643 /// <summary>
644 /// ERROR_INSTALL_SERVICE_SAFEBOOT 1652
645 /// Windows Installer is not accessible when the computer is in Safe Mode.
646 /// Exit Safe Mode and try again or try using System Restore to return your computer to a previous state.
647 /// Available beginning with Windows Installer version 4.0.
648 /// </summary>
649 ERROR_INSTALL_SERVICE_SAFEBOOT = 1652,
650
651 /// <summary>
652 /// ERROR_ROLLBACK_DISABLED 1653
653 /// Could not perform a multiple-package transaction because rollback has been disabled.
654 /// Multiple-Package Installations cannot run if rollback is disabled. Available beginning with Windows Installer version 4.5.
655 /// </summary>
656 ERROR_ROLLBACK_DISABLED = 1653,
657
658 /// <summary>
659 /// ERROR_SUCCESS_REBOOT_REQUIRED 3010
660 /// A restart is required to complete the install. This message is indicative of a success.
661 /// This does not include installs where the ForceReboot action is run.
662 /// </summary>
663 ERROR_SUCCESS_REBOOT_REQUIRED = 3010
664 }
665
666 /// <summary>
667 /// Modes of operations for MSIExec; install, administrator install, uninstall .. etc
668 /// </summary>
669 public enum MSIExecMode
670 {
671 /// <summary>
672 /// Installs or configures a product
673 /// </summary>
674 Install = 0,
675
676 /// <summary>
677 /// Administrative install - Installs a product on the network
678 /// </summary>
679 AdministrativeInstall,
680
681 /// <summary>
682 /// Uninstalls the product
683 /// </summary>
684 Uninstall,
685
686 /// <summary>
687 /// Repairs a product
688 /// </summary>
689 Repair,
690
691 /// <summary>
692 /// Modifies a product
693 /// </summary>
694 Modify,
695
696 /// <summary>
697 /// Uninstalls the product as part of cleanup
698 /// </summary>
699 Cleanup,
700 }
701
702 /// <summary>
703 /// User interfave levels
704 /// </summary>
705 public enum MSIExecUserInterfaceLevel
706 {
707 /// <summary>
708 /// No UI
709 /// </summary>
710 None = 0,
711
712 /// <summary>
713 /// Basic UI
714 /// </summary>
715 Basic,
716
717 /// <summary>
718 /// Reduced UI
719 /// </summary>
720 Reduced,
721
722 /// <summary>
723 /// Full UI (default)
724 /// </summary>
725 Full
726 }
727
728 /// <summary>
729 /// Logging options
730 /// </summary>
731 [Flags]
732 public enum MSIExecLoggingOptions
733 {
734 Status_Messages = 0x0001,
735 Nonfatal_Warnings = 0x0002,
736 All_Error_Messages = 0x0004,
737 Start_Up_Of_Actions = 0x0008,
738 Action_Specific_Records = 0x0010,
739 User_Requests = 0x0020,
740 Initial_UI_Parameters = 0x0040,
741 OutOfMemory_Or_Fatal_Exit_Information = 0x0080,
742 OutOfDiskSpace_Messages = 0x0100,
743 Terminal_Properties = 0x0200,
744 Verbose_Output = 0x0400,
745 Append_To_Existing_Log_File = 0x0800,
746
747 Flush_Each_line = 0x1000,
748 Extra_Debugging_Information = 0x2000,
749 Log_All_Information = 0x4000,
750 VOICEWARMUP = 0x0FFF
751 }
752 }
753}
diff --git a/src/WixToolsetTest.BurnE2E/MsiTransactionTests.cs b/src/WixToolsetTest.BurnE2E/MsiTransactionTests.cs
new file mode 100644
index 00000000..3c9261a7
--- /dev/null
+++ b/src/WixToolsetTest.BurnE2E/MsiTransactionTests.cs
@@ -0,0 +1,111 @@
1// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
2
3namespace WixToolsetTest.BurnE2E
4{
5 using System;
6 using System.IO;
7 using Xunit;
8 using Xunit.Abstractions;
9
10 public class MsiTransactionTests : BurnE2ETests
11 {
12 public MsiTransactionTests(ITestOutputHelper testOutputHelper) : base(testOutputHelper, "MsiTransaction") { }
13
14 [Fact]
15 public void CanUpgradeBundleWithMsiTransaction()
16 {
17 var packageA = this.CreatePackageInstaller("PackageA");
18 var packageBv1 = this.CreatePackageInstaller("PackageBv1");
19 var packageBv2 = this.CreatePackageInstaller("PackageBv2");
20 var packageCv1 = this.CreatePackageInstaller("PackageCv1");
21 var packageCv2 = this.CreatePackageInstaller("PackageCv2");
22 var packageD = this.CreatePackageInstaller("PackageD");
23
24 var bundleAv1 = this.CreateBundleInstaller("BundleAv1");
25 var bundleAv2 = this.CreateBundleInstaller("BundleAv2");
26
27 var packageASourceCodeInstalled = packageA.GetInstalledFilePath("Package.wxs");
28 var packageBv1SourceCodeInstalled = packageBv1.GetInstalledFilePath("Package.wxs");
29 var packageBv2SourceCodeInstalled = packageBv2.GetInstalledFilePath("Package.wxs");
30 var packageCv1SourceCodeInstalled = packageCv1.GetInstalledFilePath("Package.wxs");
31 var packageCv2SourceCodeInstalled = packageCv2.GetInstalledFilePath("Package.wxs");
32 var packageDSourceCodeInstalled = packageD.GetInstalledFilePath("Package.wxs");
33
34 // Source file should *not* be installed
35 Assert.False(File.Exists(packageASourceCodeInstalled), $"Package A payload should not be there on test start: {packageASourceCodeInstalled}");
36 Assert.False(File.Exists(packageBv1SourceCodeInstalled), $"Package Bv1 payload should not be there on test start: {packageBv1SourceCodeInstalled}");
37 Assert.False(File.Exists(packageBv2SourceCodeInstalled), $"Package Bv2 payload should not be there on test start: {packageBv2SourceCodeInstalled}");
38 Assert.False(File.Exists(packageCv1SourceCodeInstalled), $"Package Cv1 payload should not be there on test start: {packageCv1SourceCodeInstalled}");
39 Assert.False(File.Exists(packageCv2SourceCodeInstalled), $"Package Cv2 payload should not be there on test start: {packageCv2SourceCodeInstalled}");
40 Assert.False(File.Exists(packageDSourceCodeInstalled), $"Package D payload should not be there on test start: {packageDSourceCodeInstalled}");
41
42 bundleAv1.Install();
43
44 // Source file should be installed
45 Assert.True(File.Exists(packageASourceCodeInstalled), String.Concat("Should have found Package A payload installed at: ", packageASourceCodeInstalled));
46 Assert.True(File.Exists(packageBv1SourceCodeInstalled), String.Concat("Should have found Package Bv1 payload installed at: ", packageBv1SourceCodeInstalled));
47 Assert.True(File.Exists(packageCv1SourceCodeInstalled), String.Concat("Should have found Package Cv1 payload installed at: ", packageCv1SourceCodeInstalled));
48
49 bundleAv2.Install();
50
51 // Source file should be upgraded
52 Assert.True(File.Exists(packageDSourceCodeInstalled), String.Concat("Should have found Package D payload installed at: ", packageDSourceCodeInstalled));
53 Assert.True(File.Exists(packageBv2SourceCodeInstalled), String.Concat("Should have found Package Bv2 payload installed at: ", packageBv2SourceCodeInstalled));
54 Assert.True(File.Exists(packageCv2SourceCodeInstalled), String.Concat("Should have found Package Cv2 payload installed at: ", packageCv2SourceCodeInstalled));
55 Assert.False(File.Exists(packageCv1SourceCodeInstalled), String.Concat("Package Cv1 payload should have been removed by upgrade uninstall from: ", packageCv1SourceCodeInstalled));
56 Assert.False(File.Exists(packageBv1SourceCodeInstalled), String.Concat("Package Bv1 payload should have been removed by upgrade uninstall from: ", packageBv1SourceCodeInstalled));
57 Assert.False(File.Exists(packageASourceCodeInstalled), String.Concat("Package A payload should have been removed by upgrade uninstall from: ", packageASourceCodeInstalled));
58
59 // Uninstall everything.
60 bundleAv2.Uninstall();
61
62 // Source file should *not* be installed
63 Assert.False(File.Exists(packageDSourceCodeInstalled), String.Concat("Package D payload should have been removed by uninstall from: ", packageDSourceCodeInstalled));
64 Assert.False(File.Exists(packageBv2SourceCodeInstalled), String.Concat("Package Bv2 payload should have been removed by uninstall from: ", packageBv2SourceCodeInstalled));
65 Assert.False(File.Exists(packageCv2SourceCodeInstalled), String.Concat("Package Cv2 payload should have been removed by uninstall from: ", packageCv2SourceCodeInstalled));
66 }
67
68 /// <summary>
69 /// Installs 2 bundles:
70 /// BundleBv1- installs package Bv1
71 /// BundleBv2- installs packages A, Bv2, F
72 /// package Bv2 performs a major upgrade of package Bv1
73 /// package F fails
74 /// Thus, rolling back the transaction should reinstall package Bv1
75 /// </summary>
76 [Fact]
77 public void CanRelyOnMsiTransactionRollback()
78 {
79 var packageA = this.CreatePackageInstaller("PackageA");
80 var packageBv1 = this.CreatePackageInstaller("PackageBv1");
81 var packageBv2 = this.CreatePackageInstaller("PackageBv2");
82 this.CreatePackageInstaller("PackageF");
83
84 var bundleBv1 = this.CreateBundleInstaller("BundleBv1");
85 var bundleBv2 = this.CreateBundleInstaller("BundleBv2");
86
87 var packageASourceCodeInstalled = packageA.GetInstalledFilePath("Package.wxs");
88 var packageBv1SourceCodeInstalled = packageBv1.GetInstalledFilePath("Package.wxs");
89 var packageBv2SourceCodeInstalled = packageBv2.GetInstalledFilePath("Package.wxs");
90
91 // Source file should *not* be installed
92 Assert.False(File.Exists(packageASourceCodeInstalled), $"Package A payload should not be there on test start: {packageASourceCodeInstalled}");
93 Assert.False(File.Exists(packageBv1SourceCodeInstalled), $"Package Bv1 payload should not be there on test start: {packageBv1SourceCodeInstalled}");
94 Assert.False(File.Exists(packageBv2SourceCodeInstalled), $"Package Bv2 payload should not be there on test start: {packageBv2SourceCodeInstalled}");
95
96 bundleBv1.Install();
97
98 // Source file should be installed
99 Assert.True(File.Exists(packageBv1SourceCodeInstalled), String.Concat("Should have found Package Bv1 payload installed at: ", packageBv1SourceCodeInstalled));
100
101 bundleBv2.Install((int)MSIExec.MSIExecReturnCode.ERROR_INSTALL_FAILURE);
102
103 // Source file should be installed
104 Assert.True(File.Exists(packageASourceCodeInstalled), String.Concat("Should have found Package A payload installed at: ", packageASourceCodeInstalled));
105
106 // Previous source file should be installed
107 Assert.True(File.Exists(packageBv1SourceCodeInstalled), String.Concat("Should have found Package Bv1 payload installed at: ", packageBv1SourceCodeInstalled));
108 Assert.False(File.Exists(packageBv2SourceCodeInstalled), String.Concat("Should not have found Package Bv2 payload installed at: ", packageBv2SourceCodeInstalled));
109 }
110 }
111}
diff --git a/src/WixToolsetTest.BurnE2E/PackageInstaller.cs b/src/WixToolsetTest.BurnE2E/PackageInstaller.cs
new file mode 100644
index 00000000..e49d010d
--- /dev/null
+++ b/src/WixToolsetTest.BurnE2E/PackageInstaller.cs
@@ -0,0 +1,97 @@
1// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
2
3namespace WixToolsetTest.BurnE2E
4{
5 using System;
6 using System.IO;
7 using static WixToolsetTest.BurnE2E.MSIExec;
8
9 public class PackageInstaller : IDisposable
10 {
11 public PackageInstaller(WixTestContext testContext, string name)
12 {
13 this.Package = Path.Combine(testContext.TestDataFolder, $"{name}.msi");
14 this.PackageName = name;
15 this.TestContext = testContext;
16 }
17
18 public string Package { get; }
19
20 private string PackageName { get; }
21
22 private WixTestContext TestContext { get; }
23
24 public string TestGroupName => this.TestContext.TestGroupName;
25
26 public string TestName => this.TestContext.TestName;
27
28 public string GetInstalledFilePath(string filename)
29 {
30 return this.TestContext.GetTestInstallFolder(Path.Combine(this.PackageName, filename));
31 }
32
33 /// <summary>
34 /// Installs a .msi file
35 /// </summary>
36 /// <param name="expectedExitCode">Expected exit code</param>
37 /// <param name="otherArguments">Other arguments to pass to MSIExec.</param>
38 /// <returns>MSIExec log File</returns>
39 public string InstallProduct(MSIExecReturnCode expectedExitCode = MSIExecReturnCode.SUCCESS, params string[] otherArguments)
40 {
41 return this.RunMSIExec(MSIExecMode.Install, otherArguments, expectedExitCode);
42 }
43
44 /// <summary>
45 /// Uninstalls a .msi file
46 /// </summary>
47 /// <param name="expectedExitCode">Expected exit code</param>
48 /// <param name="otherArguments">Other arguments to pass to MSIExec.</param>
49 /// <returns>MSIExec log File</returns>
50 public string UninstallProduct(MSIExecReturnCode expectedExitCode, params string[] otherArguments)
51 {
52 return this.RunMSIExec(MSIExecMode.Uninstall, otherArguments, expectedExitCode);
53 }
54
55 /// <summary>
56 /// Repairs a .msi file
57 /// </summary>
58 /// <param name="expectedExitCode">Expected exit code</param>
59 /// <param name="otherArguments">Other arguments to pass to msiexe.exe.</param>
60 /// <returns>MSIExec log File</returns>
61 public string RepairProduct(MSIExecReturnCode expectedExitCode, params string[] otherArguments)
62 {
63 return this.RunMSIExec(MSIExecMode.Repair, otherArguments, expectedExitCode);
64 }
65
66 /// <summary>
67 /// Executes MSIExec on a .msi file
68 /// </summary>
69 /// <param name="mode">Mode of execution for MSIExec</param>
70 /// <param name="otherArguments">Other arguments to pass to MSIExec.</param>
71 /// <param name="expectedExitCode">Expected exit code</param>
72 /// <returns>MSIExec exit code</returns>
73 private string RunMSIExec(MSIExecMode mode, string[] otherArguments, MSIExecReturnCode expectedExitCode, bool assertOnError = true)
74 {
75 // Generate the log file name.
76 var logFile = Path.Combine(Path.GetTempPath(), String.Format("{0}_{1}_{2:yyyyMMddhhmmss}_{4}_{3}.log", this.TestGroupName, this.TestName, DateTime.UtcNow, Path.GetFileNameWithoutExtension(this.Package), mode));
77
78 var msiexec = new MSIExec
79 {
80 Product = this.Package,
81 ExecutionMode = mode,
82 OtherArguments = null != otherArguments ? String.Join(" ", otherArguments) : null,
83 ExpectedExitCode = expectedExitCode,
84 LogFile = logFile,
85 };
86
87 msiexec.Run(assertOnError);
88 return msiexec.LogFile;
89 }
90
91 public void Dispose()
92 {
93 string[] args = { "IGNOREDEPENDENCIES=ALL", "WIXFAILWHENDEFERRED=0" };
94 this.RunMSIExec(MSIExecMode.Cleanup, args, MSIExecReturnCode.SUCCESS, assertOnError: false);
95 }
96 }
97}
diff --git a/src/WixToolsetTest.BurnE2E/TestTool.cs b/src/WixToolsetTest.BurnE2E/TestTool.cs
new file mode 100644
index 00000000..e35c5c4b
--- /dev/null
+++ b/src/WixToolsetTest.BurnE2E/TestTool.cs
@@ -0,0 +1,245 @@
1// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
2
3namespace WixToolsetTest.BurnE2E
4{
5 using System;
6 using System.Collections.Generic;
7 using System.Text;
8 using System.Text.RegularExpressions;
9 using WixBuildTools.TestSupport;
10 using Xunit;
11
12 public class TestTool : ExternalExecutable
13 {
14 /// <summary>
15 /// Constructor for a TestTool
16 /// </summary>
17 public TestTool()
18 : this(null)
19 {
20 }
21
22 /// <summary>
23 /// Constructor for a TestTool
24 /// </summary>
25 /// <param name="toolFile">The full path to the tool. Eg. c:\bin\candle.exe</param>
26 public TestTool(string toolFile)
27 : base(toolFile)
28 {
29 this.PrintOutputToConsole = true;
30 }
31
32 /// <summary>
33 /// The arguments to pass to the tool
34 /// </summary>
35 public virtual string Arguments { get; set; }
36
37 /// <summary>
38 /// Stores the errors that occurred when a run was checked against its expected results
39 /// </summary>
40 public List<string> Errors { get; set; }
41
42 /// <summary>
43 /// A list of Regex's that are expected to match stderr
44 /// </summary>
45 public List<Regex> ExpectedErrorRegexs { get; set; } = new List<Regex>();
46
47 /// <summary>
48 /// The expected error strings to stderr
49 /// </summary>
50 public List<string> ExpectedErrorStrings { get; set; } = new List<string>();
51
52 /// <summary>
53 /// The expected exit code of the tool
54 /// </summary>
55 public int? ExpectedExitCode { get; set; }
56
57 /// <summary>
58 /// A list of Regex's that are expected to match stdout
59 /// </summary>
60 public List<Regex> ExpectedOutputRegexs { get; set; } = new List<Regex>();
61
62 /// <summary>
63 /// The expected output strings to stdout
64 /// </summary>
65 public List<string> ExpectedOutputStrings { get; set; } = new List<string>();
66
67 /// <summary>
68 /// Print output from the tool execution to the console
69 /// </summary>
70 public bool PrintOutputToConsole { get; set; }
71
72 /// <summary>
73 /// The working directory of the tool
74 /// </summary>
75 public string WorkingDirectory { get; set; }
76
77 /// <summary>
78 /// Print the errors from the last run
79 /// </summary>
80 public void PrintErrors()
81 {
82 if (null != this.Errors)
83 {
84 Console.WriteLine("Errors:");
85
86 foreach (string error in this.Errors)
87 {
88 Console.WriteLine(error);
89 }
90 }
91 }
92
93 /// <summary>
94 /// Run the tool
95 /// </summary>
96 /// <returns>The results of the run</returns>
97 public ExternalExecutableResult Run()
98 {
99 return this.Run(true);
100 }
101
102 /// <summary>
103 /// Run the tool
104 /// </summary>
105 /// <param name="exceptionOnError">Throw an exception if the expected results don't match the actual results</param>
106 /// <exception cref="System.Exception">Thrown when the expected results don't match the actual results</exception>
107 /// <returns>The results of the run</returns>
108 public virtual ExternalExecutableResult Run(bool assertOnError)
109 {
110 var result = this.Run(this.Arguments, workingDirectory: this.WorkingDirectory ?? String.Empty);
111
112 if (this.PrintOutputToConsole)
113 {
114 Console.WriteLine(FormatResult(result));
115 }
116
117 this.Errors = this.CheckResult(result);
118
119 if (assertOnError && 0 < this.Errors.Count)
120 {
121 if (this.PrintOutputToConsole)
122 {
123 this.PrintErrors();
124 }
125
126 Assert.Empty(this.Errors);
127 }
128
129 return result;
130 }
131
132 /// <summary>
133 /// Checks that the result from a run matches the expected results
134 /// </summary>
135 /// <param name="result">A result from a run</param>
136 /// <returns>A list of errors</returns>
137 public virtual List<string> CheckResult(ExternalExecutableResult result)
138 {
139 List<string> errors = new List<string>();
140
141 // Verify that the expected return code matched the actual return code
142 if (null != this.ExpectedExitCode && this.ExpectedExitCode != result.ExitCode)
143 {
144 errors.Add(String.Format("Expected exit code {0} did not match actual exit code {1}", this.ExpectedExitCode, result.ExitCode));
145 }
146
147 var standardErrorString = string.Join(Environment.NewLine, result.StandardError);
148
149 // Verify that the expected error string are in stderr
150 if (null != this.ExpectedErrorStrings)
151 {
152 foreach (string expectedString in this.ExpectedErrorStrings)
153 {
154 if (!standardErrorString.Contains(expectedString))
155 {
156 errors.Add(String.Format("The text '{0}' was not found in stderr", expectedString));
157 }
158 }
159 }
160
161 var standardOutputString = string.Join(Environment.NewLine, result.StandardOutput);
162
163 // Verify that the expected output string are in stdout
164 if (null != this.ExpectedOutputStrings)
165 {
166 foreach (string expectedString in this.ExpectedOutputStrings)
167 {
168 if (!standardOutputString.Contains(expectedString))
169 {
170 errors.Add(String.Format("The text '{0}' was not found in stdout", expectedString));
171 }
172 }
173 }
174
175 // Verify that the expected regular expressions match stderr
176 if (null != this.ExpectedOutputRegexs)
177 {
178 foreach (Regex expectedRegex in this.ExpectedOutputRegexs)
179 {
180 if (!expectedRegex.IsMatch(standardOutputString))
181 {
182 errors.Add(String.Format("Regex {0} did not match stdout", expectedRegex.ToString()));
183 }
184 }
185 }
186
187 // Verify that the expected regular expressions match stdout
188 if (null != this.ExpectedErrorRegexs)
189 {
190 foreach (Regex expectedRegex in this.ExpectedErrorRegexs)
191 {
192 if (!expectedRegex.IsMatch(standardErrorString))
193 {
194 errors.Add(String.Format("Regex {0} did not match stderr", expectedRegex.ToString()));
195 }
196 }
197 }
198
199 return errors;
200 }
201
202 /// <summary>
203 /// Clears all of the expected results and resets them to the default values
204 /// </summary>
205 public virtual void SetDefaultExpectedResults()
206 {
207 this.ExpectedErrorRegexs = new List<Regex>();
208 this.ExpectedErrorStrings = new List<string>();
209 this.ExpectedExitCode = null;
210 this.ExpectedOutputRegexs = new List<Regex>();
211 this.ExpectedOutputStrings = new List<string>();
212 }
213
214 /// <summary>
215 /// Returns a string with data contained in the result.
216 /// </summary>
217 /// <returns>A string</returns>
218 private static string FormatResult(ExternalExecutableResult result)
219 {
220 var returnValue = new StringBuilder();
221 returnValue.AppendLine();
222 returnValue.AppendLine("----------------");
223 returnValue.AppendLine("Tool run result:");
224 returnValue.AppendLine("----------------");
225 returnValue.AppendLine("Command:");
226 returnValue.AppendLine($"\"{result.StartInfo.FileName}\" {result.StartInfo.Arguments}");
227 returnValue.AppendLine();
228 returnValue.AppendLine("Standard Output:");
229 foreach (var line in result.StandardOutput ?? new string[0])
230 {
231 returnValue.AppendLine(line);
232 }
233 returnValue.AppendLine("Standard Error:");
234 foreach (var line in result.StandardError ?? new string[0])
235 {
236 returnValue.AppendLine(line);
237 }
238 returnValue.AppendLine("Exit Code:");
239 returnValue.AppendLine(Convert.ToString(result.ExitCode));
240 returnValue.AppendLine("----------------");
241
242 return returnValue.ToString();
243 }
244 }
245}
diff --git a/src/WixToolsetTest.BurnE2E/WixTestBase.cs b/src/WixToolsetTest.BurnE2E/WixTestBase.cs
new file mode 100644
index 00000000..a7fd752f
--- /dev/null
+++ b/src/WixToolsetTest.BurnE2E/WixTestBase.cs
@@ -0,0 +1,21 @@
1// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
2
3namespace WixToolsetTest.BurnE2E
4{
5 using System;
6 using System.IO;
7 using Xunit.Abstractions;
8
9 public abstract class WixTestBase
10 {
11 protected WixTestBase(ITestOutputHelper testOutputHelper, string testGroupName)
12 {
13 this.TestContext = new WixTestContext(testOutputHelper, testGroupName);
14 }
15
16 /// <summary>
17 /// The test context for the current test.
18 /// </summary>
19 public WixTestContext TestContext { get; }
20 }
21}
diff --git a/src/WixToolsetTest.BurnE2E/WixTestContext.cs b/src/WixToolsetTest.BurnE2E/WixTestContext.cs
new file mode 100644
index 00000000..97856089
--- /dev/null
+++ b/src/WixToolsetTest.BurnE2E/WixTestContext.cs
@@ -0,0 +1,70 @@
1// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
2
3namespace WixToolsetTest.BurnE2E
4{
5 using System;
6 using System.IO;
7 using System.Reflection;
8 using Microsoft.Win32;
9 using WixBuildTools.TestSupport;
10 using Xunit.Abstractions;
11
12 public class WixTestContext
13 {
14 static readonly string RootDataPath = Path.GetFullPath(TestData.Get(".."));
15
16 public WixTestContext(ITestOutputHelper testOutputHelper, string testGroupName)
17 {
18 var test = GetTest(testOutputHelper);
19
20 this.TestDataFolder = Path.Combine(RootDataPath, testGroupName);
21 this.TestGroupName = testGroupName;
22 this.TestName = test.TestCase.TestMethod.Method.Name;
23 }
24
25 public string TestDataFolder { get; }
26
27 /// <summary>
28 /// Gets the name of the current test group.
29 /// </summary>
30 public string TestGroupName { get; }
31
32 public string TestName { get; }
33
34 /// <summary>
35 /// Gets the test install directory for the current test.
36 /// </summary>
37 /// <param name="additionalPath">Additional subdirectories under the test install directory.</param>
38 /// <returns>Full path to the test install directory.</returns>
39 /// <remarks>
40 /// The package or bundle must install into [ProgramFilesFolder]\~Test WiX\[TestGroupName]\([Additional]).
41 /// </remarks>
42 public string GetTestInstallFolder(string additionalPath = null)
43 {
44 return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86), "~Test WiX", this.TestGroupName, additionalPath ?? String.Empty);
45 }
46
47 /// <summary>
48 /// Gets the test registry key for the current test.
49 /// </summary>
50 /// <param name="additionalPath">Additional subkeys under the test registry key.</param>
51 /// <returns>Full path to the test registry key.</returns>
52 /// <remarks>
53 /// The package must write into HKLM\Software\WiX\Tests\[TestGroupName]\([Additional]).
54 /// </remarks>
55 public RegistryKey GetTestRegistryRoot(string additionalPath = null)
56 {
57 var key = String.Format(@"Software\WiX\Tests\{0}\{1}", this.TestName, additionalPath ?? String.Empty);
58 return Registry.LocalMachine.OpenSubKey(key, true);
59 }
60
61 private static ITest GetTest(ITestOutputHelper output)
62 {
63 // https://github.com/xunit/xunit/issues/416#issuecomment-378512739
64 var type = output.GetType();
65 var testMember = type.GetField("test", BindingFlags.Instance | BindingFlags.NonPublic);
66 var test = (ITest)testMember.GetValue(output);
67 return test;
68 }
69 }
70}
diff --git a/src/WixToolsetTest.BurnE2E/WixToolsetTest.BurnE2E.csproj b/src/WixToolsetTest.BurnE2E/WixToolsetTest.BurnE2E.csproj
new file mode 100644
index 00000000..78dbaf74
--- /dev/null
+++ b/src/WixToolsetTest.BurnE2E/WixToolsetTest.BurnE2E.csproj
@@ -0,0 +1,24 @@
1<?xml version="1.0" encoding="utf-8"?>
2<!-- Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. -->
3
4<Project Sdk="Microsoft.NET.Sdk">
5 <PropertyGroup>
6 <TargetFramework>netcoreapp3.1</TargetFramework>
7 </PropertyGroup>
8
9 <PropertyGroup>
10 <NoWarn>NU1701</NoWarn>
11 </PropertyGroup>
12
13 <ItemGroup>
14 <PackageReference Include="Microsoft.Win32.Registry" Version="5.0.0" />
15 <PackageReference Include="System.Security.Principal.Windows" Version="5.0.0" />
16 <PackageReference Include="WixBuildTools.TestSupport" Version="4.0.*" />
17 </ItemGroup>
18
19 <ItemGroup>
20 <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.3.0" />
21 <PackageReference Include="xunit" Version="2.4.1" />
22 <PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" PrivateAssets="All" />
23 </ItemGroup>
24</Project>
diff --git a/version.json b/version.json
new file mode 100644
index 00000000..5f857771
--- /dev/null
+++ b/version.json
@@ -0,0 +1,11 @@
1{
2 "version": "4.0",
3 "publicReleaseRefSpec": [
4 "^refs/heads/master$"
5 ],
6 "cloudBuild": {
7 "buildNumber": {
8 "enabled": true
9 }
10 }
11}