From f0ac85fad84a76a4f0651a99ae6c7714051dfff8 Mon Sep 17 00:00:00 2001 From: Florian Kroenert Date: Tue, 26 May 2015 11:17:51 +0200 Subject: [PATCH 1/4] Wrote tests, added enum types where useful --- src/app/FakeLib/WiXHelper.fs | 265 +++++++++++++++--- .../Test.Fake.Deploy.Web/WixHelperTests.fs | 81 +++++- 2 files changed, 310 insertions(+), 36 deletions(-) diff --git a/src/app/FakeLib/WiXHelper.fs b/src/app/FakeLib/WiXHelper.fs index 660ff689108..8d6f8eff526 100644 --- a/src/app/FakeLib/WiXHelper.fs +++ b/src/app/FakeLib/WiXHelper.fs @@ -151,18 +151,61 @@ let WiXDefaults : WiXParams = AdditionalCandleArgs = [ "-ext WiXNetFxExtension" ] AdditionalLightArgs = [ "-ext WiXNetFxExtension"; "-ext WixUIExtension.dll"; "-ext WixUtilExtension.dll" ] } +type YesOrNo = + | Yes + | No + override y.ToString() = + match y with + | Yes -> "yes" + | No -> "no" + +type FeatureDisplay = + /// Initially shows the feature collapsed. This is the default value. + | Collapse + /// Initially shows the feature expanded. + | Expand + /// Prevents the feature from displaying in the user interface. + | Hidden + override f.ToString() = + match f with + | Collapse -> "collapse" + | Expand -> "expand" + | Hidden -> "hidden" + /// Parameters for creating WiX Feature, use ToString for creating the string xml nodes -type WiXFeatureParams = +type WiXFeature = { + /// Unique identifier of the feature. Id : string + + /// Short string of text identifying the feature. + /// This string is listed as an item by the SelectionTree control of the Selection Dialog. Title : string + + /// Sets the install level of this feature. A value of 0 will disable the feature. + /// Processing the Condition Table can modify the level value (this is set via the Condition child element). + /// The default value is "1". Level : int + + /// Longer string of text describing the feature. This localizable string is displayed by the Text Control of the Selection Dialog. Description : string - Display : string + + ///Determines the initial display of this feature in the feature tree. This attribute's value should be one of the following: + ///collapse + /// Initially shows the feature collapsed. This is the default value. + ///expand + /// Initially shows the feature expanded. + ///hidden + /// Prevents the feature from displaying in the user interface. + /// + /// For advanced users only, it is possible to directly set the integer value of the display value that will appear in the Feature row. + Display : FeatureDisplay + + /// Nest sub features or components in here InnerContent : string } override f.ToString() = "" + f.InnerContent + "" + + f.Display.ToString() + "\" ConfigurableDirectory=\"INSTALLDIR\">" + f.InnerContent + "" /// Default values for creating WiX Feature let WiXFeatureDefaults = @@ -171,28 +214,59 @@ let WiXFeatureDefaults = Title = "Default Feature" Level = 1 Description = "Default Feature" - Display = "expand" + Display = FeatureDisplay.Expand InnerContent = "" } /// Parameters for WiX Script properties, use ToString for creating the string xml nodes -type WiXScriptParams = +type WiXScript = { + /// The product code GUID for the product. ProductCode : Guid + + /// The descriptive name of the product. ProductName : string + + /// Product description Description : string + + /// The decimal language ID (LCID) for the product. ProductLanguage : string + + /// The product's version string. ProductVersion : string + + /// The manufacturer of the product. ProductPublisher : string + + /// The upgrade code GUID for the product. UpgradeGuid : Guid + + /// You can nest upgrade elements in here Upgrade : string + + /// Nest major upgrade elements in here MajorUpgrade : string + + /// Nest UIRefs in here UIRefs : string + + /// Nest WiXVariables in here WiXVariables : string + + /// Nest directories in here Directories : string + + /// Build Number of product BuildNumber : string + + /// You can nest feature elements in here Features : string + + /// You can nest custom actions in here CustomActions : string + + /// You can nest InstallExecuteSequence actions in here ActionSequences : string } @@ -217,52 +291,124 @@ let WiXScriptDefaults = ActionSequences = "" } +type CustomActionExecute = + /// Indicates that the custom action will run after successful completion of the installation script (at the end of the installation). + | Commit + /// Indicates that the custom action runs in-script (possibly with elevated privileges). + | Deferred + /// Indicates that the custom action will only run in the first sequence that runs it. + | FirstSequence + /// Indicates that the custom action will run during normal processing time with user privileges. This is the default. + | Immediate + /// Indicates that the custom action will only run in the first sequence that runs it in the same process. + | OncePerProcess + /// Indicates that a custom action will run in the rollback sequence when a failure occurs during installation, usually to undo changes made by a deferred custom action. + | Rollback + /// Indicates that a custom action should be run a second time if it was previously run in an earlier sequence. + | SecondSequence + override c.ToString() = + match c with + | Commit -> "commit" + | Deferred -> "deferred" + | FirstSequence -> "firstSequence" + | Immediate -> "immediate" + | OncePerProcess -> "oncePerProcess" + | Rollback -> "rollback" + | SecondSequence -> "secondSequence" + +type CustomActionReturn = + /// Indicates that the custom action will run asyncronously and execution may continue after the installer terminates. + | AsyncNoWait + /// Indicates that the custom action will run asynchronously but the installer will wait for the return code at sequence end. + | AsyncWait + /// Indicates that the custom action will run synchronously and the return code will be checked for success. This is the default. + | Check + /// Indicates that the custom action will run synchronously and the return code will not be checked. + | Ignore + override c.ToString() = + match c with + | AsyncNoWait -> "asyncNoWait" + | AsyncWait -> "asyncWait" + | Check -> "check" + | Ignore -> "ignore" + /// Parameters for WiX custom action, use ToString for creating the string xml nodes type WiXCustomAction = { + /// The identifier of the custom action. Id : string + + /// This attribute specifies a reference to a File element with matching Id attribute that will execute the custom action code + /// in the file after the file is installed. This attribute is typically used with the ExeCommand attribute to specify + /// a type 18 custom action that runs an installed executable, with the DllEntry attribute to specify an installed custom action + /// DLL to use for a type 17 custom action, or with the VBScriptCall or JScriptCall attributes to specify a type 21 or 22 custom action. FileKey : string - Execute : string - Impersonate : string + + /// This attribute indicates the scheduling of the custom action. + Execute : CustomActionExecute + /// This attribute specifies whether the Windows Installer, which executes as LocalSystem, should impersonate the user context of + /// the installing user when executing this custom action. Typically the value should be 'yes', except when the custom action needs + /// elevated privileges to apply changes to the machine. + Impersonate : YesOrNo + /// This attribute specifies the command line parameters to supply to an externally run executable. + /// This attribute is typically used with the BinaryKey attribute for a type 2 custom action, the FileKey attribute for a type 18 + /// custom action, the Property attribute for a type 50 custom action, or the Directory attribute for a type 34 custom action that + /// specify the executable to run. ExeCommand : string - Return : string + /// Set this attribute to set the return behavior of the custom action. + Return : CustomActionReturn } - override w.ToString() = "" + override w.ToString() = "" /// Default values for WiX custom actions let WiXCustomActionDefaults = { Id = "" FileKey = "" - Execute = "" - Impersonate = "" + Execute = CustomActionExecute.Immediate + Impersonate = YesOrNo.Yes ExeCommand = "" - Return = "" + Return = CustomActionReturn.Check } +type ActionExecutionVerb = + /// Specifies that action should be executed after some standard or custom action + | After + /// Specifies that action should be executed before some standard or custom action + | Before + override a.ToString() = + match a with + | After -> "after" + | Before -> "before" + /// Parameters for WiX Custom Action executions (In InstallExecuteSequence), use ToString for creating the string xml nodes type WiXCustomActionExecution = { + /// The action to which the Custom element applies. ActionId : string - Verb : string + /// Specify if action should be executed before or after target action + Verb : ActionExecutionVerb + /// Name of the standard or custom action that the verb points to Target : string + /// Conditions that have to be fulfilled for running execution Condition : string } - override w.ToString() = " " + w.Condition + " " + override w.ToString() = " " + w.Condition + " " /// Default values for WiX custom action executions let WixCustomActionExecutionDefaults = { ActionId = "" - Verb = "" + Verb = ActionExecutionVerb.After Target = "" Condition = "" } /// Parameters for WiX UI Reference, use ToString for creating the string xml nodes type WiXUIRef = - { + { + /// Name of referenced UI Id : string } override w.ToString() = "" @@ -276,24 +422,30 @@ let WiXUIRefDefaults = /// Parameters for WiX Variable, use ToString for creating the string xml nodes type WiXVariable = { + /// The name of the variable. Id : string - Overridable : string + /// Set this value to 'yes' in order to make the variable's value overridable either by another WixVariable entry or via the command-line option -d= for light.exe. + /// If the same variable is declared overridable in multiple places it will cause an error (since WiX won't know which value is correct). The default value is 'no'. + Overridable : YesOrNo + /// The value of the variable. The value cannot be an empty string because that would make it possible to accidentally set a column to null. Value : string } - override w.ToString() = "" + override w.ToString() = "" /// Default value for WiX Variable let WiXVariableDefaults = { Id = "" - Overridable = "no" + Overridable = YesOrNo.No Value = "" } /// Parameters for WiX Upgrade type WiXUpgrade = { + /// This value specifies the upgrade code for the products that are to be detected by the FindRelatedProducts action. Id: Guid + /// You can nest WiXUpgradeVersion sequences in here UpgradeVersion: string } override w.ToString() = "" + w.UpgradeVersion + "" @@ -308,42 +460,85 @@ let WiXUpgradeDefaults = /// Parameters for WiX Upgrade Version type WiXUpgradeVersion = { - OnlyDetect : string + /// Set to "yes" to detect products and applications but do not uninstall. + OnlyDetect : YesOrNo + /// Specifies the lower bound on the range of product versions to be detected by FindRelatedProducts. Minimum : string + /// Specifies the upper boundary of the range of product versions detected by FindRelatedProducts. Maximum : string + /// When the FindRelatedProducts action detects a related product installed on the system, it appends the product code to the property specified in this field. + /// Windows Installer documentation for the Upgrade table states that the property specified in this field must be a public property and must be added to the + /// SecureCustomProperties property. WiX automatically appends the property specified in this field to the SecureCustomProperties property when creating an MSI. + /// Each UpgradeVersion must have a unique Property value. After the FindRelatedProducts action is run, the value of this property is a list of product codes, + /// separated by semicolons (;), detected on the system. Property : string - IncludeMinimum : string - IncludeMaximum : string + /// Set to "no" to make the range of versions detected exclude the value specified in Minimum. This attribute is "yes" by default. + IncludeMinimum : YesOrNo + /// Set to "yes" to make the range of versions detected include the value specified in Maximum. + IncludeMaximum : YesOrNo } - override w.ToString() = "" + override w.ToString() = "" /// Default value for WiX Upgrade let WiXUpgradeVersionDefaults = { - OnlyDetect = "" + OnlyDetect = YesOrNo.No Minimum = "" Maximum = "" Property = "" - IncludeMinimum = "" - IncludeMaximum = "" + IncludeMinimum = YesOrNo.Yes + IncludeMaximum = YesOrNo.No } +type MajorUpgradeSchedule = + /// (Default) Schedules RemoveExistingProducts after the InstallValidate standard action. This scheduling removes the installed product entirely before installing the upgrade product. + /// It's slowest but gives the most flexibility in changing components and features in the upgrade product. Note that if the installation of the upgrade product fails, + /// the machine will have neither version installed. + | AfterInstallValidate + /// Schedules RemoveExistingProducts after the InstallInitialize standard action. This is similar to the afterInstallValidate scheduling, but if the installation of the upgrade product fails, + /// Windows Installer also rolls back the removal of the installed product -- in other words, reinstalls it. + | AfterInstallInitialize + /// Schedules RemoveExistingProducts between the InstallExecute and InstallFinalize standard actions. This scheduling installs the upgrade product "on top of" the installed product then lets + /// RemoveExistingProducts uninstall any components that don't also exist in the upgrade product. Note that this scheduling requires strict adherence to the component rules because it relies + /// on component reference counts to be accurate during installation of the upgrade product and removal of the installed product. For more information, see Bob Arnson's blog post + /// "Paying for Upgrades" for details. If installation of the upgrade product fails, Windows Installer also rolls back the removal of the installed product -- in other words, reinstalls it. + | AfterInstallExecute + /// Schedules RemoveExistingProducts between the InstallExecuteAgain and InstallFinalize standard actions. + /// This is identical to the afterInstallExecute scheduling but after the InstallExecuteAgain standard action instead of InstallExecute. + | AfterInstallExecuteAgain + /// Schedules RemoveExistingProducts after the InstallFinalize standard action. This is similar to the afterInstallExecute and afterInstallExecuteAgain schedulings but takes place outside + /// the installation transaction so if installation of the upgrade product fails, Windows Installer does not roll back the removal of the installed product, + /// so the machine will have both versions installed. + | AfterInstallFinalize + override m.ToString() = + match m with + | AfterInstallValidate -> "afterInstallValidate" + | AfterInstallInitialize -> "afterInstallInitialize" + | AfterInstallExecute -> "afterInstallExecute" + | AfterInstallExecuteAgain -> "afterInstallExecuteAgain" + | AfterInstallFinalize -> "afterInstallFinalize" + /// Parameters for WiX Major Upgrade type WiXMajorUpgrade = { - Schedule : string - AllowDowngrades : string + /// Determines the scheduling of the RemoveExistingProducts standard action, which is when the installed product is removed. The default is "afterInstallValidate" which removes the + /// installed product entirely before installing the upgrade product. It's slowest but gives the most flexibility in changing components and features in the upgrade product. + Schedule : MajorUpgradeSchedule + /// When set to no (the default), products with lower version numbers are blocked from installing when a product with a higher version is installed; the DowngradeErrorMessage + /// attribute must also be specified. When set to yes, any version can be installed over any other version. + AllowDowngrades : YesOrNo + /// The message displayed if users try to install a product with a lower version number when a product with a higher version is installed. Used only when AllowDowngrades is no (the default). DowngradeErrorMessage : string } - override w.ToString() = "" + override w.ToString() = "" /// Default value for WiX Major Upgrade let WiXMajorUpgradeDefaults = { - Schedule = "afterInstallValidate" - AllowDowngrades = "no" - DowngradeErrorMessage = "" + Schedule = MajorUpgradeSchedule.AfterInstallValidate + AllowDowngrades = YesOrNo.No + DowngradeErrorMessage = "You can't downgrade this product!" } /// Generates WiX Template with specified file name (you can prepend location too) @@ -470,7 +665,7 @@ let internal FillInWixScript wiXPath setParams = /// InnerContent = otherFeature.ToString() /// }) let generateFeature setParams = - let parameters : WiXFeatureParams = WiXFeatureDefaults |> setParams + let parameters : WiXFeature = WiXFeatureDefaults |> setParams if parameters.Id = "" then failwith "No parameter passed for feature Id!" parameters diff --git a/src/test/Test.Fake.Deploy.Web/WixHelperTests.fs b/src/test/Test.Fake.Deploy.Web/WixHelperTests.fs index 5305cef5230..498f11a1ccc 100644 --- a/src/test/Test.Fake.Deploy.Web/WixHelperTests.fs +++ b/src/test/Test.Fake.Deploy.Web/WixHelperTests.fs @@ -16,4 +16,83 @@ let ``should find correct id`` () = " let executableId = getFileIdFromWiXString wixDirString "\S*.exe" - Assert.Equal("fi_5", executableId) \ No newline at end of file + Assert.Equal("fi_5", executableId) + +[] +let ``should create valid feature node`` () = + let actualFeature = generateFeature (fun f -> + {f with + Id = "TestFeatureID" + Title = "Test Feature Title" + Level = 1 + Description = "Test Feature Description" + Display = FeatureDisplay.Hidden + InnerContent = "" + }) + let expectedFeature = "" + Assert.Equal(expectedFeature, actualFeature.ToString()) +[] +let ``should create valid custom action node`` () = + let actualAction = generateCustomAction (fun f -> + {f with + Id = "TestActionId" + FileKey = "TestFile" + Execute = CustomActionExecute.Deferred + Impersonate = YesOrNo.No + ExeCommand = "test" + Return = CustomActionReturn.Check + }) + let expectedAction = "" + Assert.Equal(expectedAction, actualAction.ToString()) +[] +let ``should create valid custom action execution node`` () = + let actualActionExecution = generateCustomActionExecution (fun f -> + {f with + ActionId = "TestActionId" + Verb = ActionExecutionVerb.After + Target = "InstallFiles" + Condition = "(NOT WIX_UPGRADE_DETECTED) AND " + }) + let expectedActionExecution = " (NOT WIX_UPGRADE_DETECTED) AND " + Assert.Equal(expectedActionExecution, actualActionExecution.ToString()) +[] +let ``should create valid uiref node`` () = + let actualUiRef = generateUIRef (fun f -> + {f with + Id = "WixUI_Mondo" + }) + let expectedUiRef = "" + Assert.Equal(expectedUiRef, actualUiRef.ToString()) +[] +let ``should create valid upgrade node`` () = + let actualUpgrade = generateUpgrade (fun f -> + {f with + Id = Guid("E21A46F0-05AF-45D0-A2D6-5E2E3C77F615") + UpgradeVersion = "" + }) + + let expectedUpgrade = "" + Assert.Equal(expectedUpgrade, actualUpgrade.ToString()) +[] +let ``should create valid upgrade version node`` () = + let actualUpgradeVersion = generateUpgradeVersion (fun f -> + {f with + Minimum = "1.0.0" + Maximum = "10.0.0" + IncludeMinimum = YesOrNo.Yes + IncludeMaximum = YesOrNo.No + OnlyDetect = YesOrNo.Yes + Property = "SomeProperty"}) + let expectedUpgradeVersion = "" + Assert.Equal(expectedUpgradeVersion, actualUpgradeVersion.ToString()) + +[] +let ``should create valid major upgrade node`` () = + let actualMajorUpgrade = generateMajorUpgradeVersion(fun f -> + {f with + Schedule = MajorUpgradeSchedule.AfterInstallInitialize + DowngradeErrorMessage = "A later version is already installed, exiting." + }) + let expectedMajorUpgrade = "" + Assert.Equal(expectedMajorUpgrade, actualMajorUpgrade.ToString()) \ No newline at end of file From 0b7b735d86c69ea3be3bf4d7b61ffb68a2bfb6e4 Mon Sep 17 00:00:00 2001 From: Florian Kroenert Date: Tue, 26 May 2015 11:20:38 +0200 Subject: [PATCH 2/4] Fix test failures --- src/app/FakeLib/WiXHelper.fs | 4 ++-- src/test/Test.Fake.Deploy.Web/WixHelperTests.fs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/app/FakeLib/WiXHelper.fs b/src/app/FakeLib/WiXHelper.fs index 8d6f8eff526..67b9126d831 100644 --- a/src/app/FakeLib/WiXHelper.fs +++ b/src/app/FakeLib/WiXHelper.fs @@ -379,8 +379,8 @@ type ActionExecutionVerb = | Before override a.ToString() = match a with - | After -> "after" - | Before -> "before" + | After -> "After" + | Before -> "Before" /// Parameters for WiX Custom Action executions (In InstallExecuteSequence), use ToString for creating the string xml nodes type WiXCustomActionExecution = diff --git a/src/test/Test.Fake.Deploy.Web/WixHelperTests.fs b/src/test/Test.Fake.Deploy.Web/WixHelperTests.fs index 498f11a1ccc..a4cf2f5ae4b 100644 --- a/src/test/Test.Fake.Deploy.Web/WixHelperTests.fs +++ b/src/test/Test.Fake.Deploy.Web/WixHelperTests.fs @@ -72,7 +72,7 @@ let ``should create valid upgrade node`` () = UpgradeVersion = "" }) - let expectedUpgrade = "" + let expectedUpgrade = "" Assert.Equal(expectedUpgrade, actualUpgrade.ToString()) [] let ``should create valid upgrade version node`` () = From ce7cd1d0909d0bcbbfdc3bf2af7e65537420a34b Mon Sep 17 00:00:00 2001 From: Florian Kroenert Date: Tue, 26 May 2015 11:26:14 +0200 Subject: [PATCH 3/4] Added missing test --- src/test/Test.Fake.Deploy.Web/WixHelperTests.fs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/test/Test.Fake.Deploy.Web/WixHelperTests.fs b/src/test/Test.Fake.Deploy.Web/WixHelperTests.fs index a4cf2f5ae4b..019a4e5dd23 100644 --- a/src/test/Test.Fake.Deploy.Web/WixHelperTests.fs +++ b/src/test/Test.Fake.Deploy.Web/WixHelperTests.fs @@ -18,6 +18,17 @@ let ``should find correct id`` () = let executableId = getFileIdFromWiXString wixDirString "\S*.exe" Assert.Equal("fi_5", executableId) +[] +let ``should set components never overwrite true if desired`` () = + let componentDefault = " + + " + let componentExpected = " + + " + let actualComponent = setComponentsNeverOverwrite componentDefault + Assert.Equal(componentExpected, actualComponent) + [] let ``should create valid feature node`` () = let actualFeature = generateFeature (fun f -> From bd3f8db9049b8343130841bc1f36cb062881bdc1 Mon Sep 17 00:00:00 2001 From: Florian Kroenert Date: Tue, 26 May 2015 11:41:32 +0200 Subject: [PATCH 4/4] Fixed setComponentsNeverOverwrite and tests --- src/app/FakeLib/WiXHelper.fs | 8 +------ .../Test.Fake.Deploy.Web/WixHelperTests.fs | 23 ++++++++++++++++--- 2 files changed, 21 insertions(+), 10 deletions(-) diff --git a/src/app/FakeLib/WiXHelper.fs b/src/app/FakeLib/WiXHelper.fs index 67b9126d831..45a550c6035 100644 --- a/src/app/FakeLib/WiXHelper.fs +++ b/src/app/FakeLib/WiXHelper.fs @@ -127,13 +127,7 @@ let rec wixComponentRefs (directoryInfo : DirectoryInfo) = /// Take a component string and set "neverOverwrite" Tag /// This is useful for config files, since they are not replaced on upgrade like that let setComponentsNeverOverwrite (components : string) = - let lines = split '\n' components - - // Filter for lines which have a name tag matching the given regex, pick the first and return its ID - lines - |> Seq.filter(fun line -> Regex.IsMatch(line, " Seq.map(fun f -> f.Replace("/>", "NeverOverwrite=\"yes\" />")) - |> toLines + components.Replace("] -let ``should find correct id`` () = +let ``should find correct file id`` () = let wixDirString = " @@ -18,14 +18,31 @@ let ``should find correct id`` () = let executableId = getFileIdFromWiXString wixDirString "\S*.exe" Assert.Equal("fi_5", executableId) +[] +let ``should find correct component id`` () = + let wixDirString = " + + + + + + + + + + " + let componentIds = getComponentIdsFromWiXString wixDirString + Assert.Equal("", componentIds) + + [] let ``should set components never overwrite true if desired`` () = let componentDefault = " " let componentExpected = " - - " + + " let actualComponent = setComponentsNeverOverwrite componentDefault Assert.Equal(componentExpected, actualComponent)