-
Notifications
You must be signed in to change notification settings - Fork 46
Memoize safety evaluator #2812
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
base: develop
Are you sure you want to change the base?
Memoize safety evaluator #2812
Changes from all commits
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 | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -366,6 +366,119 @@ void testEmptyEnum() { | |||||||||||||||
| .hasValue(LogSafety.SAFE); | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| @Test | ||||||||||||||||
| void testCyclicTypes_bothSafe() { | ||||||||||||||||
| // Foo has a field referencing Bar, Bar has a field referencing Foo. | ||||||||||||||||
| // Both fields are marked safe, so both types should evaluate as safe. | ||||||||||||||||
| TypeDefinition foo = TypeDefinition.object(ObjectDefinition.builder() | ||||||||||||||||
| .typeName(FOO) | ||||||||||||||||
| .fields(FieldDefinition.builder() | ||||||||||||||||
| .fieldName(FieldName.of("bar")) | ||||||||||||||||
| .type(Type.reference(BAR)) | ||||||||||||||||
| .build()) | ||||||||||||||||
| .build()); | ||||||||||||||||
| TypeDefinition bar = TypeDefinition.object(ObjectDefinition.builder() | ||||||||||||||||
| .typeName(BAR) | ||||||||||||||||
| .fields(FieldDefinition.builder() | ||||||||||||||||
| .fieldName(FieldName.of("foo")) | ||||||||||||||||
| .type(Type.reference(FOO)) | ||||||||||||||||
| .build()) | ||||||||||||||||
| .build()); | ||||||||||||||||
|
Comment on lines
+371
to
+386
Contributor
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. The fields are not marked as safe. Shouldn't we be returning unknown/empty in this case? 🤔 (I don't think your change changes this, but this seems weird to me 🤔 ) |
||||||||||||||||
| ConjureDefinition conjureDef = | ||||||||||||||||
| ConjureDefinition.builder().version(1).types(foo).types(bar).build(); | ||||||||||||||||
| SafetyEvaluator evaluator = new SafetyEvaluator(conjureDef); | ||||||||||||||||
| // Evaluation order should not matter for the result | ||||||||||||||||
| assertThat(evaluator.evaluate(foo)).hasValue(LogSafety.SAFE); | ||||||||||||||||
| assertThat(evaluator.evaluate(bar)).hasValue(LogSafety.SAFE); | ||||||||||||||||
|
Comment on lines
+389
to
+392
Contributor
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. You're not checking the evaluation order here. Since you're already doing it in a dedicated test, might as well remove the comment?
Suggested change
|
||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| @Test | ||||||||||||||||
| void testCyclicTypes_withUnsafeField() { | ||||||||||||||||
| // Foo references Bar, Bar references Foo and also has an unsafe string field. | ||||||||||||||||
| // The unsafe field should propagate through the cycle. | ||||||||||||||||
| TypeDefinition foo = TypeDefinition.object(ObjectDefinition.builder() | ||||||||||||||||
| .typeName(FOO) | ||||||||||||||||
| .fields(FieldDefinition.builder() | ||||||||||||||||
| .fieldName(FieldName.of("bar")) | ||||||||||||||||
| .type(Type.reference(BAR)) | ||||||||||||||||
| .build()) | ||||||||||||||||
| .build()); | ||||||||||||||||
| TypeDefinition bar = TypeDefinition.object(ObjectDefinition.builder() | ||||||||||||||||
| .typeName(BAR) | ||||||||||||||||
| .fields(FieldDefinition.builder() | ||||||||||||||||
| .fieldName(FieldName.of("foo")) | ||||||||||||||||
| .type(Type.reference(FOO)) | ||||||||||||||||
| .build()) | ||||||||||||||||
| .fields(FieldDefinition.builder() | ||||||||||||||||
| .fieldName(FieldName.of("unsafeField")) | ||||||||||||||||
| .type(Type.primitive(PrimitiveType.STRING)) | ||||||||||||||||
| .safety(LogSafety.UNSAFE) | ||||||||||||||||
| .build()) | ||||||||||||||||
| .build()); | ||||||||||||||||
| ConjureDefinition conjureDef = | ||||||||||||||||
| ConjureDefinition.builder().version(1).types(foo).types(bar).build(); | ||||||||||||||||
| SafetyEvaluator evaluator = new SafetyEvaluator(conjureDef); | ||||||||||||||||
| assertThat(evaluator.evaluate(bar)).hasValue(LogSafety.UNSAFE); | ||||||||||||||||
| assertThat(evaluator.evaluate(foo)).hasValue(LogSafety.UNSAFE); | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| @Test | ||||||||||||||||
| void testCyclicTypes_evaluationOrderIndependent() { | ||||||||||||||||
| // Verifies that the cache does not cause different results depending on which | ||||||||||||||||
| // type in a cycle is evaluated first. | ||||||||||||||||
| TypeDefinition foo = TypeDefinition.object(ObjectDefinition.builder() | ||||||||||||||||
| .typeName(FOO) | ||||||||||||||||
| .fields(FieldDefinition.builder() | ||||||||||||||||
| .fieldName(FieldName.of("bar")) | ||||||||||||||||
| .type(Type.reference(BAR)) | ||||||||||||||||
| .build()) | ||||||||||||||||
| .fields(FieldDefinition.builder() | ||||||||||||||||
| .fieldName(FieldName.of("unsafeField")) | ||||||||||||||||
| .type(Type.primitive(PrimitiveType.STRING)) | ||||||||||||||||
| .safety(LogSafety.UNSAFE) | ||||||||||||||||
| .build()) | ||||||||||||||||
| .build()); | ||||||||||||||||
| TypeDefinition bar = TypeDefinition.object(ObjectDefinition.builder() | ||||||||||||||||
| .typeName(BAR) | ||||||||||||||||
| .fields(FieldDefinition.builder() | ||||||||||||||||
| .fieldName(FieldName.of("foo")) | ||||||||||||||||
| .type(Type.reference(FOO)) | ||||||||||||||||
| .build()) | ||||||||||||||||
| .build()); | ||||||||||||||||
| ConjureDefinition conjureDef = | ||||||||||||||||
| ConjureDefinition.builder().version(1).types(foo).types(bar).build(); | ||||||||||||||||
|
|
||||||||||||||||
| // Evaluate foo first, then bar | ||||||||||||||||
| SafetyEvaluator evaluator1 = new SafetyEvaluator(conjureDef); | ||||||||||||||||
| Optional<LogSafety> fooFirst = evaluator1.evaluate(foo); | ||||||||||||||||
| Optional<LogSafety> barAfterFoo = evaluator1.evaluate(bar); | ||||||||||||||||
|
|
||||||||||||||||
| // Evaluate bar first, then foo | ||||||||||||||||
| SafetyEvaluator evaluator2 = new SafetyEvaluator(conjureDef); | ||||||||||||||||
| Optional<LogSafety> barFirst = evaluator2.evaluate(bar); | ||||||||||||||||
| Optional<LogSafety> fooAfterBar = evaluator2.evaluate(foo); | ||||||||||||||||
|
|
||||||||||||||||
| // Results should be identical regardless of evaluation order | ||||||||||||||||
| assertThat(fooFirst).isEqualTo(fooAfterBar); | ||||||||||||||||
| assertThat(barAfterFoo).isEqualTo(barFirst); | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| @Test | ||||||||||||||||
| void testSelfReferentialType() { | ||||||||||||||||
| // A type that references itself (e.g. a linked list node) | ||||||||||||||||
| TypeDefinition node = TypeDefinition.object(ObjectDefinition.builder() | ||||||||||||||||
| .typeName(FOO) | ||||||||||||||||
| .fields(FieldDefinition.builder() | ||||||||||||||||
| .fieldName(FieldName.of("next")) | ||||||||||||||||
| .type(Type.reference(FOO)) | ||||||||||||||||
| .build()) | ||||||||||||||||
| .build()); | ||||||||||||||||
| ConjureDefinition conjureDef = | ||||||||||||||||
| ConjureDefinition.builder().version(1).types(node).build(); | ||||||||||||||||
| SafetyEvaluator evaluator = new SafetyEvaluator(conjureDef); | ||||||||||||||||
| assertThat(evaluator.evaluate(node)).hasValue(LogSafety.SAFE); | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| private static Stream<Arguments> getTypes(Type externalReference) { | ||||||||||||||||
| TypeDefinition objectType = TypeDefinition.object(ObjectDefinition.builder() | ||||||||||||||||
| .typeName(FOO) | ||||||||||||||||
|
|
||||||||||||||||
Uh oh!
There was an error while loading. Please reload this page.
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.
It's slightly confusing (to me) that we store the cyclic state in a field, unset it before calling the method, then reset it afterwards.
How would you feel about returning a pair from this method e.g.
record(Optional<LogSafety> safety, boolean cycleDetected)and then use that instead here? (and maybe make a second method that extracts the safety result for usage in the rest of the code)Edit: We'd want to check the memory impact of doing these allocations though
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.
Yeah I wanted to keep this lean. Want me to try that way?
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.
If you have a bit of time to try this out, I'd be curious about the memory impact yeah (especially around the total allocations). If it's low enough, I feel like this would make the cognitive overhead of this piece of code a bit lower, and thus more maintainable.
If it's not, I think this should be fine