diff --git a/CHANGELOG.md b/CHANGELOG.md index a76cc7a1..09988b31 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - #1122: Packaging should recognize resources in dependency modules set to deploy - #1119: The update command should check version requirements using post-update values instead of what's currently installed - #1097: The Test resource processor now supports nested tests +- #1116: Fix behavior inconsistencies between install and uninstall for package name casing. ### Security - requests Python wheel updated to 2.33.0 diff --git a/src/cls/IPM/Storage/ResourceReference.cls b/src/cls/IPM/Storage/ResourceReference.cls index 0a7d977e..4831adb2 100644 --- a/src/cls/IPM/Storage/ResourceReference.cls +++ b/src/cls/IPM/Storage/ResourceReference.cls @@ -125,13 +125,15 @@ ClassMethod GetChildren( set tResourceCache($$$lcase(tCacheResult.%Get("UniqueName"))) = "" } } - // include hidden files by temporarily enable showHidden global for Studio search + // Include hidden files by temporarily enabling showHidden global for Studio search. + // Use %SQLUPPER for case-insensitive package prefix matching instead of relying on + // StudioOpenDialog's built-in prefix filter, which is case-sensitive. set tHidden = $get(^%SYS("Studio", "ShowHidden"), $char(0)) set ^%SYS("Studio","ShowHidden") = 1 - // PKG extension should only cover .CLS files set tFilesResult = ##class(%SQL.Statement).%ExecDirect(, - "select Name from %Library.RoutineMgr_StudioOpenDialog(?,'',1,1,1,0,0)", - tPackage_"*.cls,"_tPackage_"*.mac,"_tPackage_"*.int,"_tPackage_"*.inc") + "select Name from %Library.RoutineMgr_StudioOpenDialog(?,'',1,1,1,0,0) " + _"where %SQLUPPER(Name) %STARTSWITH %SQLUPPER(?)", + "*.cls,*.mac,*.int,*.inc", tPackage) if (tFilesResult.%SQLCODE < 0) { set tSC = $$$ERROR($$$SQLCode,tFilesResult.%SQLCODE,tFilesResult.%Message) quit diff --git a/tests/integration_tests/Test/PM/Integration/PkgCaseUninstall.cls b/tests/integration_tests/Test/PM/Integration/PkgCaseUninstall.cls new file mode 100644 index 00000000..939da212 --- /dev/null +++ b/tests/integration_tests/Test/PM/Integration/PkgCaseUninstall.cls @@ -0,0 +1,50 @@ +/// Tests that uninstall correctly handles PKG resources where the casing of the package +/// name in module.xml differs from the casing of the stored class/routine/include names, +/// and that resources owned by other modules within the same package are not deleted. +Class Test.PM.Integration.PkgCaseUninstall Extends Test.PM.Integration.Base +{ + +/// module.xml declares Test.PKGCASE.PKG (uppercase) but actual resources are Test.PkgCase.* +Parameter ModuleName = "pkg-case-test"; + +Method TestPkgCaseMismatchUninstall() +{ + set moduleDir = ..GetModuleDir(..#ModuleName) + do $$$AssertStatusOK(##class(%IPM.Main).Shell("load " _ moduleDir), "Loaded pkg-case-test") + + // Confirm resources were loaded + do $$$AssertTrue(##class(%Dictionary.ClassDefinition).%ExistsId("Test.PkgCase.MyClass"), "Class exists after load") + do $$$AssertTrue(##class(%Library.Routine).Exists("Test.PkgCase.MyRoutine.MAC"), "Routine exists after load") + do $$$AssertTrue(##class(%Library.Routine).Exists("Test.PkgCase.MyInclude.INC"), "Include exists after load") + + do $$$AssertStatusOK(##class(%IPM.Main).Shell("uninstall " _ ..#ModuleName), "Uninstalled pkg-case-test") + + // All three resource types must be removed despite the casing mismatch in module.xml + do $$$AssertNotTrue(##class(%Dictionary.ClassDefinition).%ExistsId("Test.PkgCase.MyClass"), "Class removed after uninstall") + do $$$AssertNotTrue(##class(%Library.Routine).Exists("Test.PkgCase.MyRoutine.MAC"), "Routine removed after uninstall") + do $$$AssertNotTrue(##class(%Library.Routine).Exists("Test.PkgCase.MyInclude.INC"), "Include removed after uninstall") +} + +/// Verifies that uninstalling a PKG module with a casing mismatch does not delete resources +/// owned by a different module that happen to fall within the same package prefix. +Method TestPkgCaseOwnershipExclusion() +{ + set moduleDirA = ..GetModuleDir(..#ModuleName) + set moduleDirB = ..GetModuleDir("pkg-case-extra") + do $$$AssertStatusOK(##class(%IPM.Main).Shell("load " _ moduleDirA), "Loaded pkg-case-test") + do $$$AssertStatusOK(##class(%IPM.Main).Shell("load " _ moduleDirB), "Loaded pkg-case-extra") + + do $$$AssertTrue(##class(%Dictionary.ClassDefinition).%ExistsId("Test.PkgCase.ExtraClass"), "ExtraClass exists after load") + + do $$$AssertStatusOK(##class(%IPM.Main).Shell("uninstall " _ ..#ModuleName), "Uninstalled pkg-case-test") + + // ExtraClass belongs to pkg-case-extra and must survive uninstall of pkg-case-test + do $$$AssertTrue(##class(%Dictionary.ClassDefinition).%ExistsId("Test.PkgCase.ExtraClass"), "ExtraClass not removed by uninstall of pkg-case-test") + + do $$$AssertStatusOK(##class(%IPM.Main).Shell("uninstall pkg-case-extra"), "Uninstalled pkg-case-extra") + + // ExtraClass should now be removed after uninstalling its owning module + do $$$AssertNotTrue(##class(%Dictionary.ClassDefinition).%ExistsId("Test.PkgCase.ExtraClass"), "ExtraClass removed by uninstall of pkg-case-extra") +} + +} diff --git a/tests/integration_tests/Test/PM/Integration/_data/pkg-case-extra/cls/Test/PkgCase/ExtraClass.cls b/tests/integration_tests/Test/PM/Integration/_data/pkg-case-extra/cls/Test/PkgCase/ExtraClass.cls new file mode 100644 index 00000000..17145677 --- /dev/null +++ b/tests/integration_tests/Test/PM/Integration/_data/pkg-case-extra/cls/Test/PkgCase/ExtraClass.cls @@ -0,0 +1,4 @@ +Class Test.PkgCase.ExtraClass +{ + +} diff --git a/tests/integration_tests/Test/PM/Integration/_data/pkg-case-extra/module.xml b/tests/integration_tests/Test/PM/Integration/_data/pkg-case-extra/module.xml new file mode 100644 index 00000000..628ebc03 --- /dev/null +++ b/tests/integration_tests/Test/PM/Integration/_data/pkg-case-extra/module.xml @@ -0,0 +1,10 @@ + + + + pkg-case-extra + 0.0.1 + + + + + diff --git a/tests/integration_tests/Test/PM/Integration/_data/pkg-case-test/cls/Test/PkgCase/MyClass.cls b/tests/integration_tests/Test/PM/Integration/_data/pkg-case-test/cls/Test/PkgCase/MyClass.cls new file mode 100644 index 00000000..2f1d3097 --- /dev/null +++ b/tests/integration_tests/Test/PM/Integration/_data/pkg-case-test/cls/Test/PkgCase/MyClass.cls @@ -0,0 +1,9 @@ +Class Test.PkgCase.MyClass +{ + +ClassMethod Hello() As %String +{ + quit "hello" +} + +} diff --git a/tests/integration_tests/Test/PM/Integration/_data/pkg-case-test/cls/Test/PkgCase/MyInclude.inc b/tests/integration_tests/Test/PM/Integration/_data/pkg-case-test/cls/Test/PkgCase/MyInclude.inc new file mode 100644 index 00000000..2514d5b2 --- /dev/null +++ b/tests/integration_tests/Test/PM/Integration/_data/pkg-case-test/cls/Test/PkgCase/MyInclude.inc @@ -0,0 +1,4 @@ +^INC^Save for Source Control^^~Format=Cache.S~^UTF8 +%RO +Test.PkgCase.MyInclude^INC^^^0 +#define PkgCaseValue 1 diff --git a/tests/integration_tests/Test/PM/Integration/_data/pkg-case-test/cls/Test/PkgCase/MyRoutine.mac b/tests/integration_tests/Test/PM/Integration/_data/pkg-case-test/cls/Test/PkgCase/MyRoutine.mac new file mode 100644 index 00000000..bd2430fb --- /dev/null +++ b/tests/integration_tests/Test/PM/Integration/_data/pkg-case-test/cls/Test/PkgCase/MyRoutine.mac @@ -0,0 +1,2 @@ +ROUTINE Test.PkgCase.MyRoutine + write !, "Hello from Test.PkgCase.MyRoutine" \ No newline at end of file diff --git a/tests/integration_tests/Test/PM/Integration/_data/pkg-case-test/module.xml b/tests/integration_tests/Test/PM/Integration/_data/pkg-case-test/module.xml new file mode 100644 index 00000000..d5bf62e5 --- /dev/null +++ b/tests/integration_tests/Test/PM/Integration/_data/pkg-case-test/module.xml @@ -0,0 +1,10 @@ + + + + pkg-case-test + 0.0.1 + + + + +