-
Notifications
You must be signed in to change notification settings - Fork 14
When adding a record map to source control, automatically add generated classes #995
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,5 @@ | ||
| .vscode/ | ||
| ptd/ | ||
| .gitattributes | ||
| *.code-workspace | ||
| *.code-workspace | ||
| .claude/worktrees/ |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,72 @@ | ||
| Include (%occStatus, %occErrors, SourceControl.Git) | ||
|
|
||
| /// Utilities for keeping the classes generated by a RecordMap (its data/target class and any batch | ||
| /// class) under source control alongside the RecordMap itself. | ||
| Class SourceControl.Git.Util.RecordMap | ||
| { | ||
|
|
||
| /// Ensure the generated data/target class(es) of a RecordMap are under source control. For each | ||
| /// related class reported by <method>GetRelatedClasses</method> that is not already tracked, add it. | ||
| /// Used both when a RecordMap is first added and when it is saved/compiled, so its data class always | ||
| /// comes along. Does nothing for non-RecordMap items. | ||
| ClassMethod AddRelatedClasses(InternalName As %String, refreshUncommitted As %Boolean = 1) As %Status | ||
| { | ||
| set ec = $$$OK | ||
| do ..GetRelatedClasses(InternalName, .relatedClasses) | ||
| set added = 0 | ||
| set relatedKey = $order(relatedClasses("")) | ||
| while (relatedKey '= "") { | ||
| if '##class(SourceControl.Git.Utils).IsInSourceControl(relatedKey) { | ||
| set sc = ##class(SourceControl.Git.Utils).AddToSourceControl(relatedKey, 0) | ||
| set added = 1 | ||
| if 'sc { | ||
| set ec = $$$ADDSC(ec, sc) | ||
| } | ||
| } | ||
| set relatedKey = $order(relatedClasses(relatedKey)) | ||
| } | ||
| if added && refreshUncommitted { | ||
| do ##class(SourceControl.Git.Change).RefreshUncommitted(,,,1) | ||
| } | ||
| quit ec | ||
| } | ||
|
|
||
| /// For a RecordMap class (one that extends EnsLib.RecordMap.RecordMap), report the generated | ||
| /// data/target class(es) that should be source-controlled alongside it. These classes are not | ||
| /// regenerated by recompiling the RecordMap, so they must be tracked independently. | ||
| /// <var>related</var> is returned subscripted by the internal name of each related class. | ||
| ClassMethod GetRelatedClasses(InternalName As %String, Output related) As %Status | ||
| { | ||
| kill related | ||
| set sc = $$$OK | ||
| try { | ||
| if (##class(SourceControl.Git.Utils).Type(.InternalName) '= "cls") quit | ||
| if '##class(%Library.EnsembleMgr).IsEnsembleNamespace() quit | ||
| set className = $piece(##class(SourceControl.Git.Utils).NormalizeInternalName(InternalName), ".CLS", 1) | ||
| if '##class(%Dictionary.CompiledClass).%ExistsId(className) quit | ||
| if '$system.CLS.IsMthd(className, "%Extends") quit | ||
| if '$classmethod(className, "%Extends", "EnsLib.RecordMap.RecordMap") quit | ||
|
|
||
| // GetGeneratedClasses reports the generated classes (target, batch, etc.) plus the RecordMap | ||
| // itself; the RecordMap is excluded below. It is unioned with the OBJECTNAME parameter (the | ||
| // target class), which acts as a fallback for when GetGeneratedClasses is unreliable. | ||
| // GetGeneratedClasses kills its output array, so the OBJECTNAME set must stay after the call. | ||
| kill candidates | ||
| do $classmethod(className, "GetGeneratedClasses", .candidates) | ||
| set objectName = $parameter(className, "OBJECTNAME") | ||
| if (objectName '= "") set candidates(objectName) = "" | ||
|
|
||
| set candidate = $order(candidates("")) | ||
| while (candidate '= "") { | ||
| if (candidate '= className) && $$$defClassDefined(candidate) { | ||
| set related(candidate _ ".CLS") = "" | ||
| } | ||
| set candidate = $order(candidates(candidate)) | ||
| } | ||
| } catch e { | ||
| set sc = e.AsStatus() | ||
| } | ||
| quit sc | ||
| } | ||
|
|
||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,200 @@ | ||
| Import SourceControl.Git | ||
|
|
||
| /// Tests for automatically source-controlling the data/target classes generated by a RecordMap. | ||
| Class UnitTest.SourceControl.Git.RecordMap Extends UnitTest.SourceControl.Git.AbstractTest | ||
| { | ||
|
|
||
| Parameter FIXTUREPACKAGE = "UnitTest.SourceControl.Git.RecordMapFixture"; | ||
|
|
||
| Method %OnClose() As %Status [ Private, ServerOnly = 1 ] | ||
| { | ||
| do $system.OBJ.DeletePackage(..#FIXTUREPACKAGE, "-d") | ||
| quit ##super() | ||
| } | ||
|
|
||
| /// Build a real, compiled RecordMap whose target/record class is generated as a separate class, | ||
| /// mirroring what the RecordMap editor produces. Returns the record map class name. | ||
| ClassMethod CreateRecordMap(pMap As %String, pTarget As %String) As %Status | ||
| { | ||
| do $system.OBJ.Delete(pMap, "-d") | ||
| do $system.OBJ.Delete(pTarget, "-d") | ||
| set model = ##class(EnsLib.RecordMap.Model.Record).%New() | ||
| set model.name = pMap | ||
| set model.type = "delimited" | ||
| set model.targetClassname = pTarget | ||
| set model.recordTerminator = $char(13, 10) | ||
| do model.Separators.Insert(",") | ||
| set field = ##class(EnsLib.RecordMap.Model.Field).%New() | ||
| set field.name = "Field1" | ||
| do model.Contents.Insert(field) | ||
| set sc = model.SaveToClass() | ||
| if $$$ISERR(sc) quit sc | ||
| set target = pTarget | ||
| quit ##class(EnsLib.RecordMap.Generator).GenerateObject(pMap, .target, , , 2) | ||
| } | ||
|
|
||
| /// Build a RecordMap associated with a (simple) Batch class, mirroring what the RecordMap editor | ||
| /// produces when a batch class is configured. The batch class must exist before generation. | ||
| ClassMethod CreateBatchRecordMap(pMap As %String, pTarget As %String, pBatch As %String) As %Status | ||
| { | ||
| do $system.OBJ.Delete(pMap, "-d") | ||
| do $system.OBJ.Delete(pTarget, "-d") | ||
| do $system.OBJ.Delete(pBatch, "-d") | ||
| set batchDef = ##class(%Dictionary.ClassDefinition).%New() | ||
| set batchDef.Name = pBatch | ||
| set batchDef.Super = "EnsLib.RecordMap.Batch" | ||
| set param = ##class(%Dictionary.ParameterDefinition).%New() | ||
| set param.Name = "RECORDMAPGENERATED" | ||
| set param.Default = 1 | ||
| set param.parent = batchDef | ||
| set sc = batchDef.%Save() | ||
| if $$$ISERR(sc) quit sc | ||
| set sc = $system.OBJ.Compile(pBatch, "ck") | ||
| if $$$ISERR(sc) quit sc | ||
| set model = ##class(EnsLib.RecordMap.Model.Record).%New() | ||
| set model.name = pMap | ||
| set model.type = "delimited" | ||
| set model.targetClassname = pTarget | ||
| set model.batchClass = pBatch | ||
| set model.recordTerminator = $char(13, 10) | ||
| do model.Separators.Insert(",") | ||
| set field = ##class(EnsLib.RecordMap.Model.Field).%New() | ||
| set field.name = "Field1" | ||
| do model.Contents.Insert(field) | ||
| set sc = model.SaveToClass() | ||
| if $$$ISERR(sc) quit sc | ||
| set target = pTarget | ||
| quit ##class(EnsLib.RecordMap.Generator).GenerateObject(pMap, .target, , , 2) | ||
| } | ||
|
|
||
| /// Build a trivial compiled class that is not a RecordMap, for exercising the non-RecordMap path. | ||
| ClassMethod CreatePlainClass(pClass As %String) As %Status | ||
| { | ||
| do $system.OBJ.Delete(pClass, "-d") | ||
| set def = ##class(%Dictionary.ClassDefinition).%New() | ||
| set def.Name = pClass | ||
| set def.Super = "%Persistent" | ||
| set sc = def.%Save() | ||
| if $$$ISERR(sc) quit sc | ||
| quit $system.OBJ.Compile(pClass, "ck") | ||
| } | ||
|
|
||
| /// The resolver should report the generated target/data class as related to the RecordMap, | ||
| /// and must not report the RecordMap class itself. | ||
| Method TestResolverIncludesTargetClass() | ||
| { | ||
| set map = ..#FIXTUREPACKAGE_".Map" | ||
| set target = ..#FIXTUREPACKAGE_".Record" | ||
| $$$ThrowOnError(..CreateRecordMap(map, target)) | ||
|
|
||
| do $$$AssertStatusOK(##class(SourceControl.Git.Util.RecordMap).GetRelatedClasses(map_".CLS", .related)) | ||
| do $$$AssertTrue($data(related(target_".CLS")), "target data class should be reported as related to the RecordMap") | ||
| do $$$AssertNotTrue($data(related(map_".CLS")), "the RecordMap class itself should not be reported as related") | ||
| } | ||
|
|
||
| /// Adding a RecordMap class to source control should automatically add its generated data class. | ||
| Method TestAddRecordMapAddsTargetClass() | ||
| { | ||
| set map = ..#FIXTUREPACKAGE_".Map" | ||
| set target = ..#FIXTUREPACKAGE_".Record" | ||
| $$$ThrowOnError(..CreateRecordMap(map, target)) | ||
|
|
||
| do $$$AssertNotTrue(##class(SourceControl.Git.Utils).IsInSourceControl(target_".CLS"), "target should not be in source control before the RecordMap is added") | ||
| $$$ThrowOnError(##class(SourceControl.Git.Utils).AddToSourceControl(map_".CLS")) | ||
| do $$$AssertTrue(##class(SourceControl.Git.Utils).IsInSourceControl(map_".CLS"), "RecordMap class should be in source control") | ||
| do $$$AssertTrue(##class(SourceControl.Git.Utils).IsInSourceControl(target_".CLS"), "target data class should be added to source control automatically") | ||
| } | ||
|
|
||
| /// A RecordMap that is already tracked but whose generated data class is not (e.g. the map was | ||
| /// added before the data class existed) should have its data class pulled back under source control | ||
| /// when the map is saved/compiled. OnAfterSave delegates this to AddRelatedClasses, which is | ||
| /// exercised directly here to avoid depending on a fully configured git working tree. | ||
| Method TestSaveTrackedRecordMapAddsUntrackedTarget() | ||
| { | ||
| set map = ..#FIXTUREPACKAGE_".Map" | ||
| set target = ..#FIXTUREPACKAGE_".Record" | ||
| $$$ThrowOnError(..CreateRecordMap(map, target)) | ||
|
|
||
| // Precondition: the RecordMap is tracked but its generated data class is not. | ||
| $$$ThrowOnError(##class(SourceControl.Git.Utils).AddToSourceControl(map_".CLS", 0)) | ||
| $$$ThrowOnError(##class(SourceControl.Git.Utils).RemoveFromSourceControl(target_".CLS", 0)) | ||
| do $$$AssertTrue(##class(SourceControl.Git.Utils).IsInSourceControl(map_".CLS"), "RecordMap should be tracked before save") | ||
| do $$$AssertNotTrue(##class(SourceControl.Git.Utils).IsInSourceControl(target_".CLS"), "target should not be tracked before save") | ||
|
|
||
| // Saving the RecordMap should bring its data class back under source control. | ||
| $$$ThrowOnError(##class(SourceControl.Git.Util.RecordMap).AddRelatedClasses(map_".CLS", 0)) | ||
| do $$$AssertTrue(##class(SourceControl.Git.Utils).IsInSourceControl(target_".CLS"), "saving the RecordMap should re-add its generated data class to source control") | ||
| } | ||
|
|
||
| /// For a RecordMap that has an associated Batch class, the resolver should report both the target | ||
| /// data class and the batch class as related. The batch class is discovered via GetGeneratedClasses | ||
| /// rather than the OBJECTNAME parameter. | ||
| Method TestResolverIncludesBatchClass() | ||
| { | ||
| set map = ..#FIXTUREPACKAGE_".BMap" | ||
| set target = ..#FIXTUREPACKAGE_".BRecord" | ||
| set batch = ..#FIXTUREPACKAGE_".BBatch" | ||
| $$$ThrowOnError(..CreateBatchRecordMap(map, target, batch)) | ||
|
|
||
| do $$$AssertStatusOK(##class(SourceControl.Git.Util.RecordMap).GetRelatedClasses(map_".CLS", .related)) | ||
| do $$$AssertTrue($data(related(target_".CLS")), "target data class should be reported as related") | ||
| do $$$AssertTrue($data(related(batch_".CLS")), "batch class should be reported as related") | ||
| do $$$AssertNotTrue($data(related(map_".CLS")), "the RecordMap class itself should not be reported as related") | ||
| } | ||
|
|
||
| /// A class that does not extend EnsLib.RecordMap.RecordMap has no related classes, and running the | ||
| /// resolver/adder against it is a no-op that adds nothing to source control. | ||
| Method TestNonRecordMapClassHasNoRelated() | ||
| { | ||
| set plain = ..#FIXTUREPACKAGE_".Plain" | ||
| $$$ThrowOnError(..CreatePlainClass(plain)) | ||
|
|
||
| do $$$AssertStatusOK(##class(SourceControl.Git.Util.RecordMap).GetRelatedClasses(plain_".CLS", .related)) | ||
| do $$$AssertEquals($order(related("")), "", "a non-RecordMap class should report no related classes") | ||
|
|
||
| do $$$AssertStatusOK(##class(SourceControl.Git.Util.RecordMap).AddRelatedClasses(plain_".CLS", 0)) | ||
| do $$$AssertNotTrue(##class(SourceControl.Git.Utils).IsInSourceControl(plain_".CLS"), "a non-RecordMap class should not be added to source control") | ||
| } | ||
|
|
||
| /// A newly compiled RecordMap that isn't yet tracked (e.g. just generated via the RecordMap wizard, | ||
| /// which never issues a "new document" add) should be picked up by CheckClassesToAddExplicitly, the | ||
| /// same fallback that already exists for classes created by other portal UIs like the Business | ||
| /// Process/Rule editors. This should also bring along the generated target class via the | ||
| /// AddToSourceControl cascade. | ||
| Method TestUntrackedRecordMapIsAddedOnCompile() | ||
| { | ||
| set map = ..#FIXTUREPACKAGE_".CMap" | ||
| set target = ..#FIXTUREPACKAGE_".CRecord" | ||
| $$$ThrowOnError(..CreateRecordMap(map, target)) | ||
|
|
||
| do $$$AssertNotTrue(##class(SourceControl.Git.Utils).IsInSourceControl(map_".CLS"), "RecordMap should not be tracked before the compile-time check") | ||
| do $$$AssertNotTrue(##class(SourceControl.Git.Utils).IsInSourceControl(target_".CLS"), "target should not be tracked before the compile-time check") | ||
|
|
||
| new %session, %request, %SourceControl | ||
| set %session = ##class(%CSP.Session).%New("dummysession") | ||
| set %request = ##class(%CSP.Request).%New() | ||
| set %request.Data("pageclass",1) = "EnsPortal.dummy" | ||
| do ##class(%Studio.SourceControl.Interface).SourceControlCreate() | ||
| do %SourceControl.CheckClassesToAddExplicitly(map_".CLS") | ||
|
|
||
| do $$$AssertTrue(##class(SourceControl.Git.Utils).IsInSourceControl(map_".CLS"), "RecordMap should be added to source control by the compile-time fallback check") | ||
| do $$$AssertTrue(##class(SourceControl.Git.Utils).IsInSourceControl(target_".CLS"), "target data class should be added automatically alongside the RecordMap") | ||
| } | ||
|
|
||
| /// Calling AddRelatedClasses more than once is idempotent: the generated data class is added and | ||
| /// remains tracked, and a repeat call succeeds without error. | ||
| Method TestAddRelatedClassesIsIdempotent() | ||
| { | ||
| set map = ..#FIXTUREPACKAGE_".IMap" | ||
| set target = ..#FIXTUREPACKAGE_".IRecord" | ||
| $$$ThrowOnError(..CreateRecordMap(map, target)) | ||
|
|
||
| do $$$AssertNotTrue(##class(SourceControl.Git.Utils).IsInSourceControl(target_".CLS"), "target should not be tracked before the first call") | ||
| $$$ThrowOnError(##class(SourceControl.Git.Util.RecordMap).AddRelatedClasses(map_".CLS", 0)) | ||
| do $$$AssertTrue(##class(SourceControl.Git.Utils).IsInSourceControl(target_".CLS"), "target should be tracked after the first call") | ||
|
|
||
| do $$$AssertStatusOK(##class(SourceControl.Git.Util.RecordMap).AddRelatedClasses(map_".CLS", 0)) | ||
| do $$$AssertTrue(##class(SourceControl.Git.Utils).IsInSourceControl(target_".CLS"), "target should still be tracked after a repeat call, with no error") | ||
| } | ||
|
|
||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: capitalization