Skip to content

Commit

Permalink
Added output of better exception when a circular reference is found i…
Browse files Browse the repository at this point in the history
…n the Dynamic UOW

Improved string output of directed graph
  • Loading branch information
rob-baillie-ortoo committed Apr 22, 2022
1 parent 0bf55d4 commit be4e4c4
Show file tree
Hide file tree
Showing 8 changed files with 229 additions and 5 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -362,7 +362,16 @@ public inherited sharing class ortoo_DynamicSobjectUnitOfWork extends ortoo_Sobj

private List<SobjectType> generateOrderOfOperations()
{
List<Object> childToParentTypes = graph.generateSorted();
List<Object> childToParentTypes;
try
{
childToParentTypes = graph.generateSorted();
}
catch ( DirectedGraph.GraphContainsCircularReferenceException e )
{

throw new Exceptions.ConfigurationException( 'Cannot resolve the order of work to be done for the commit, there is a circular reference in the data\n' + graph.toString(), e );
}

List<SobjectType> parentToChildTypes = new List<SobjectType>();
for( Integer i = childToParentTypes.size() - 1; i >= 0; i-- )
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -523,8 +523,32 @@ private without sharing class ortoo_DynamicSobjectUnitOfWorkTest
ortoo_Asserts.assertContains( 'SObject type Contact is invalid for publishing within this unit of work', exceptionMessage, 'registerPublishAfterFailureTransaction, when called with a non event, will throw an exception' );
}

@isTest
private static void commitWork_whenGivenCircularReferences_throwsAnException() // NOPMD: Test method name format
{
Contact childContact = new Contact( LastName = 'Child' );
Contact parentContact = new Contact( LastName = 'Parent' );
MockDml dml = new MockDml();

// TODO: when given a circular reference
Test.startTest();
String exceptionMessage;
try
{
ortoo_DynamicSobjectUnitOfWork uow = new ortoo_DynamicSobjectUnitOfWork( dml );
uow.registerNew( childContact );
uow.registerNew( parentContact );
uow.registerRelationship( childContact, Contact.ReportsToId, parentContact );
uow.commitWork();
}
catch ( Exception e )
{
exceptionMessage = e.getMessage();
}
Test.stopTest();

ortoo_Asserts.assertContains( 'Cannot resolve the order of work to be done for the commit, there is a circular reference in the data', exceptionMessage, 'commitWork, when given circular references, will throw an exception' );
ortoo_Asserts.assertContains( 'Contact is a child of Contact', exceptionMessage, 'commitWork, when given circular references, will throw an exception detailing the relationships' );
}

// IDml is defined as an inner class, and so cannot be mocked using Amoss at time of writing
private class MockDml implements fflib_SObjectUnitOfWork.IDml
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,38 @@ public inherited sharing class DirectedGraph
return sortedObjects;
}

/**
* Produces a String representation of the graph
*
* @return String The graph
*/
public override String toString()
{
if ( parentsByChildren.isEmpty() )
{
return 'Empty Graph';
}

List<String> relationships = new List<String>();

for ( Object thisChild : parentsByChildren.keySet() )
{
List<String> parents = ListUtils.convertToListOfStrings( parentsByChildren.get( thisChild ) );

String description;
if ( parents.size() == 0 )
{
description = String.valueOf( thisChild ) + ' is not a child';
}
else
{
description = String.valueOf( thisChild ) + ' is a child of ' + String.join( parents, ', ' );
}
relationships.add( description );
}
return String.join( relationships, '\n' );
}

/**
* A reference to the full list of nodes registered on this graph.
*/
Expand Down
24 changes: 24 additions & 0 deletions framework/default/ortoo-core/default/classes/utils/ListUtils.cls
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,30 @@ public inherited sharing class ListUtils
return returnList;
}

/**
* Given a Set of Objects, will return a List of those Strings
*
* Note this will only work for sets defined as Set<Object>
*
* @param Set<Object> The set of Obejcts to convert
* @return List<String> The converted list
*/
public static List<String> convertToListOfStrings( Set<Object> setOfObjects )
{
if ( setOfObjects == null )
{
return new List<String>();
}

List<String> returnList = new List<String>();
for ( Object thisObject : setOfObjects )
{
returnList.add( thisObject.toString() );
}

return returnList;
}

/**
* Given a Set of any Strings, will return a List of those Strings
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,11 +62,15 @@ public inherited sharing class SobjectUtils
return record.getSObjectType();
}

// TODO: document
// TODO: test
/**
* Given a list of instances of SObjects, will return their SobjectTypes
*
* @param List<Sobject> The SObjects for which to return the SobjectTypes
* @return Set<SobjectType> The SobjectTypes of the SObjects
*/
public static Set<SobjectType> getSobjectTypes( List<Sobject> records )
{
Contract.requires( records != null, 'getSobjectType called with a null records' );
Contract.requires( records != null, 'getSobjectTypes called with a null records' );

Set<SobjectType> types = new Set<SobjectType>();
for ( Sobject thisRecord : records )
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -181,4 +181,47 @@ private without sharing class DirectedGraphTest

ortoo_Asserts.assertContains( 'addRelationship called with a parent that has not been added as a node (UnregisteredParent)', exceptionMessage, 'addRelationship, when given a parent that has not previously been added, will throw an exception' );
}

@isTest
private static void toString_whenCalledAgainstAnGraph_returnsADescriptionOfTheGraph() // NOPMD: Test method name format
{
DirectedGraph graph = new DirectedGraph()
.addNode( 'Grandparent' )
.addNode( 'Parent1' )
.addNode( 'Parent2' )
.addNode( 'Child1' )
.addNode( 'Child2' )
.addNode( 'Child3' )
.addRelationship( 'Child1', 'Parent1' )
.addRelationship( 'Child1', 'Parent2' )
.addRelationship( 'Child2', 'Parent1' )
.addRelationship( 'Child3', 'Parent2' )
.addRelationship( 'Parent1', 'Grandparent' );

String expected = ''
+ 'Grandparent is not a child\n'
+ 'Parent1 is a child of Grandparent\n'
+ 'Parent2 is not a child\n'
+ 'Child1 is a child of Parent1, Parent2\n'
+ 'Child2 is a child of Parent1\n'
+ 'Child3 is a child of Parent2';

Test.startTest();
String got = graph.toString();
Test.stopTest();

System.assertEquals( expected, got, 'toString, when called, will return a description of the graph' );
}

@isTest
private static void toString_whenCalledAgainstAnEmptyGraph_returnsEmpty() // NOPMD: Test method name format
{
DirectedGraph graph = new DirectedGraph();

Test.startTest();
String got = graph.toString();
Test.stopTest();

System.assertEquals( 'Empty Graph', got, 'toString, when called against an empty graph, will return empty' );
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -498,6 +498,44 @@ private without sharing class ListUtilsTest
System.assertEquals( new List<String>(), returnedList, 'convertToListOfStrings, when given null set of strings, will return an empty list' );
}

@isTest
private static void convertToListOfStrings_setObject_whenGivenASetOfStrings_returnsAListOfString() // NOPMD: Test method name format
{
Set<Object> setOfStrings = new Set<Object>{ 'one', 'two', 'three' };

Test.startTest();
List<String> returnedList = ListUtils.convertToListOfStrings( setOfStrings );
Test.stopTest();

List<String> expectedList = new List<String>{ 'one', 'two', 'three' };

System.assertEquals( expectedList, returnedList, 'convertToListOfStrings, when given a set of Object, will return a list of the strings' );
}

@isTest
private static void convertToListOfStrings_setObject_whenGivenAnEmptySet_returnsAnEmptySet() // NOPMD: Test method name format
{
Set<Object> emptySetOfStrings = new Set<Object>();

Test.startTest();
List<String> returnedList = ListUtils.convertToListOfStrings( emptySetOfStrings );
Test.stopTest();

System.assertEquals( new List<String>(), returnedList, 'convertToListOfStrings, when given an empty set of Object, will return an empty list' );
}

@isTest
private static void convertToListOfStrings_setObject_whenGivenNull_returnsAnEmptyList() // NOPMD: Test method name format
{
Set<Object> nullSetOfObjects = null;

Test.startTest();
List<String> returnedList = ListUtils.convertToListOfStrings( nullSetOfObjects );
Test.stopTest();

System.assertEquals( new List<String>(), returnedList, 'convertToListOfStrings, when given null set of Object, will return an empty list' );
}

private inherited sharing class StringableThing {
String name;
public StringableThing( String name )
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,56 @@ private without sharing class SobjectUtilsTest
ortoo_Asserts.assertContains( 'getSobjectType called with a null record', exceptionMessage, 'getSobjectType, when passed a null record, will throw an exception' );
}

@isTest
private static void getSobjectTypes_whenGivenRecords_willReturnTheirTypes() // NOPMD: Test method name format
{
Set<SobjectType> actualTypes = SobjectUtils.getSobjectTypes( new List<Sobject>{ new Contact(), new Account() } );

System.assertEquals( 2, actualTypes.size(), 'getSobjectType, when given SObject records, will return their types' );
System.assert( actualTypes.contains( Contact.sobjectType ), 'getSobjectType, when given SObject records, will return their types - 1' );
System.assert( actualTypes.contains( Account.sobjectType ), 'getSobjectType, when given SObject records, will return their types - 2' );
}

@isTest
private static void getSobjectTypes_whenPassedANullRecord_willThrowAnException() // NOPMD: Test method name format
{
Sobject nullRecord = null;

Test.startTest();
String exceptionMessage;
try
{
SobjectUtils.getSobjectTypes( new List<Sobject>{ new Contact(), new Account(), null } );
}
catch ( Contract.RequiresException e )
{
exceptionMessage = e.getMessage();
}
Test.stopTest();

ortoo_Asserts.assertContains( 'getSobjectType called with a null record', exceptionMessage, 'getSobjectType, when passed a null record, will throw an exception' );
}

@isTest
private static void getSobjectTypes_whenPassedANullList_willThrowAnException() // NOPMD: Test method name format
{
List<Sobject> nullList = null;

Test.startTest();
String exceptionMessage;
try
{
SobjectUtils.getSobjectTypes( nullList );
}
catch ( Contract.RequiresException e )
{
exceptionMessage = e.getMessage();
}
Test.stopTest();

ortoo_Asserts.assertContains( 'getSobjectTypes called with a null records', exceptionMessage, 'getSobjectTypes, when passed a null list, will throw an exception' );
}

@isTest
private static void getSobjectName_whenGivenAnSobject_willReturnTheApiNameOfIt() // NOPMD: Test method name format
{
Expand Down

0 comments on commit be4e4c4

Please sign in to comment.