-
Notifications
You must be signed in to change notification settings - Fork 2
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
computed circular reference error messages #355
Changes from 13 commits
7790d16
3d8b897
a0f9c28
ff3f7fe
1777753
d31c491
29fe05e
c946ad8
c746197
f61062d
63fb492
b09c848
a0f13f9
83a1605
d00715b
443a8f6
c31cdf0
7c01e87
491fec7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -14,8 +14,6 @@ | |
|
||
namespace Firely.Fhir.Validation | ||
{ | ||
|
||
|
||
/// <summary> | ||
/// This class is used to track which resources/contained resources/bundled resources are already | ||
/// validated, and what the previous result was. Since it keeps track of running validations, it | ||
|
@@ -68,9 +66,10 @@ public override string ToString() => | |
/// <param name="state">The validation state</param> | ||
/// <param name="profileUrl">Profile against which we are validating</param> | ||
/// <param name="validator">Validation to start when it has not been run before.</param> | ||
/// <param name="refCycleVisualizer">A function which visualises reference cycles when called</param> | ||
/// <returns>The result of calling the validator, or a historic result if there is one.</returns> | ||
#pragma warning disable CS0618 // Type or member is obsolete | ||
public ResultReport Start(ValidationState state, string profileUrl, Func<ResultReport> validator) | ||
public ResultReport Start(ValidationState state, string profileUrl, Func<ResultReport> validator, Func<string?> refCycleVisualizer) | ||
#pragma warning restore CS0618 // Type or member is obsolete | ||
{ | ||
var resourceUrl = state.Instance.ResourceUrl; | ||
|
@@ -80,17 +79,17 @@ public ResultReport Start(ValidationState state, string profileUrl, Func<ResultR | |
|
||
if (_data.TryGetValue(key, out var existing)) | ||
{ | ||
if (existing.Result is null) | ||
if (existing.Result is null && refCycleVisualizer() is { } referenceCycle) // check for loops: we do not include references to containers in the cycle | ||
return new IssueAssertion(Issue.CONTENT_REFERENCE_CYCLE_DETECTED, | ||
$"Detected a loop: instance data inside '{fullLocation}' refers back to itself.") | ||
$"Detected a loop: instance data inside '{fullLocation}' refers back to itself (cycle structure: {referenceCycle}).") | ||
.AsResult(fullLocation); | ||
else | ||
{ | ||
// If the validation has been run before, return an outcome with the same result. | ||
// Note: we don't keep a copy of the original outcome since it has been included in the | ||
// total result (at the first run) and keeping them around costs a lot of memory. | ||
return new ResultReport(existing.Result.Value, new TraceAssertion(fullLocation, $"Repeated validation at {fullLocation} against profile {profileUrl}.")); | ||
} | ||
|
||
// If the validation has been run before, return an outcome with the same result. | ||
// Note: we don't keep a copy of the original outcome since it has been included in the | ||
// total result (at the first run) and keeping them around costs a lot of memory. | ||
return existing.Result is not null | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What happens if existing.Result is null but the refCycleVisualizer() call returns null, it does not report anything? Maybe that makes sense after I have seen refCycleVisualizer(), but it feels wrong. Cannot this remain a string if()/else? |
||
? new ResultReport(existing.Result.Value, new TraceAssertion(fullLocation, $"Repeated validation at {fullLocation} against profile {profileUrl}.")) | ||
: ResultReport.SUCCESS; | ||
} | ||
else | ||
{ | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -93,6 +93,44 @@ internal static bool IsExactlyEqualTo(this IScopedNode left, ITypedElement right | |
(childL, childR) => childL.Name == childR.Name && childL.IsExactlyEqualTo(childR, ignoreOrder)).All(t => t); | ||
} | ||
|
||
internal static string? ComputeReferenceCycle(this IScopedNode current, ValidationSettings vc) => | ||
current.ToScopedNode().ComputeReferenceCycle(vc); | ||
|
||
internal static string? ComputeReferenceCycle(this ScopedNode current, ValidationSettings vc, IList<(string, string)>? followed = null) // this is expensive, but only executed when a loop is detected. We accept this | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this should be a private utility method inside the validationlogger, not an internal extension methods to ensure everyone understands this should never be called by anyone other than the author of the ValidationLogger. |
||
{ | ||
followed ??= []; | ||
|
||
foreach (var child in current.Children()) | ||
{ | ||
var childNode = child.ToScopedNode(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since the parent (current) is a ScopedNode, its children will be scoped nodes. This is a guarantee that the children will have their parent set correctly. It is better to explictly stating this knowledge (and assumption) by making this line |
||
|
||
if (childNode.InstanceType == "Reference") | ||
{ | ||
var target = childNode.Resolve(url => vc.ResolveExternalReference is { } resolve ? resolve(url, childNode.Location) : null)?.ToScopedNode(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same is true here |
||
if (target is null || (childNode.Location.StartsWith(target.Location + ".contained["))) // external reference, or reference to parent container: we do not include these in the cycle | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why not? |
||
{ | ||
continue; | ||
} | ||
|
||
followed.Add((childNode.Location, target.Location)); // add the reference to the list of followed references | ||
|
||
if (followed.Count(tuple => tuple == (childNode.Location, target.Location)) is 2) // if we followed the same reference twice, we have a loop | ||
{ | ||
return string.Join(" | ", followed.Select(reference => $"{reference.Item1} -> {reference.Item2}")); | ||
} | ||
|
||
if(ComputeReferenceCycle(target, vc, followed) is { } result) | ||
return result; // if multiple paths are found, we only return the first one. Rerunning will show the next one. Let's hope that never happens. | ||
} | ||
|
||
if (ComputeReferenceCycle(childNode, vc, followed) is { } result2) // why is result still in scope? that makes no sense | ||
{ | ||
return result2; | ||
} | ||
} | ||
|
||
return null; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Actually, I don't think we should do this. This function is simply too ugly and too inefficient. It will resolve every single reference in an instance, it uses fragile string matches on location, and it just looks like a hack. Cannot we expand the ValidationLogger or the location state so we keep track of the full path instead of constructing it? |
||
} | ||
} | ||
} | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -32,7 +32,7 @@ public class ValidationManifestTest | |
/// </summary> | ||
//[Ignore] | ||
[DataTestMethod] | ||
[ValidationManifestDataSource(TEST_CASES_MANIFEST, singleTest: "mr-m-simple-nossystem")] | ||
[ValidationManifestDataSource(TEST_CASES_MANIFEST, singleTest: "message-empty-entry")] | ||
public void RunSingleTest(TestCase testCase, string baseDirectory) | ||
=> _runner.RunTestCase(testCase, DotNetValidator.Create(), baseDirectory); | ||
|
||
|
@@ -59,7 +59,7 @@ public void RunFirelySdkTests(TestCase testCase, string baseDirectory) | |
/// - The method `ClassCleanup` will gather all the testcases and serialize those to disk. The filename can be altered in | ||
/// that method | ||
/// </summary> | ||
[Ignore("This test is only used to generate the Firely SDK results in the manifest. See the method for more info")] | ||
// [Ignore("This test is only used to generate the Firely SDK results in the manifest. See the method for more info")] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Pretty sure you want to re-enable this Ignore[]? |
||
[TestMethod] | ||
public void AddFirelySdkValidatorResults() | ||
=> _runner.AddOrEditValidatorResults(TEST_CASES_MANIFEST, new[] { DotNetValidator.Create() }); | ||
|
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.
I must be blind but I don't see anything in this PR that actually supplies this extra parameter to the Start() call. It cannot be, as this PR compiles, but I just don't see it.