@@ -1271,6 +1271,160 @@ def test_class_parents_ancestors(creature_view: SchemaView) -> None:
12711271 }
12721272
12731273
1274+ """
1275+ Tests of the detect_cycles function, which can identify cyclic relationships between classes, types, and other schema elements.
1276+ """
1277+
1278+
1279+ @pytest .mark .parametrize ("dodgy_input" , [None , [], set (), {}, 12345 , 123.45 , "some string" , ()])
1280+ def test_detect_cycles_input_error (dodgy_input : Any ) -> None :
1281+ """Ensure that `detect_cycles` throws an error if input is not supplied in the appropriate form."""
1282+ with pytest .raises (ValueError , match = "detect_cycles requires a list of values to process" ):
1283+ detect_cycles (lambda x : x , dodgy_input )
1284+
1285+
1286+ @pytest .fixture (scope = "module" )
1287+ def sv_cycles_schema () -> SchemaView :
1288+ """A schema containing cycles!"""
1289+ return SchemaView (INPUT_DIR_PATH / "cycles.yaml" )
1290+
1291+
1292+ # metadata for elements in the `sv_cycles_schema`
1293+ CYCLES = {
1294+ TYPES : {
1295+ # types in cycles, either directly or via ancestors
1296+ # key: type, value: node where the cycle starts
1297+ 0 : {
1298+ "curve_type" : "circular_type" ,
1299+ "semi_circular_type" : "circular_type" ,
1300+ "circular_type" : "circular_type" ,
1301+ "type_circular" : "type_circular" ,
1302+ "circle" : "circle" ,
1303+ "circle_of_life" : "circle" ,
1304+ },
1305+ # types not involved in cycles
1306+ # key: the type, value: ancestors of the type
1307+ 1 : {
1308+ "supreme_string" : {"supreme_string" , "super_string" , "string" },
1309+ "super_string" : {"super_string" , "string" },
1310+ "string" : {"string" },
1311+ "integer" : {"integer" },
1312+ "boolean" : {"boolean" },
1313+ },
1314+ },
1315+ CLASSES : {
1316+ # classes involved in cycles
1317+ # key: class name, value: node where the cycle starts
1318+ 0 : {
1319+ "ClassA" : "ClassA" ,
1320+ "ClassB" : "ClassB" ,
1321+ "ClassC" : "ClassC" ,
1322+ "ClassD" : "ClassA" ,
1323+ "ClassE" : "ClassA" ,
1324+ "ClassF" : "ClassF" ,
1325+ "ClassG" : "ClassF" ,
1326+ "Mixin1" : "Mixin1" ,
1327+ "Mixin2" : "Mixin2" ,
1328+ "MixedClass" : "Mixin2" ,
1329+ },
1330+ # class ancestors for classes not in cycles
1331+ # key: class name, value: class ancestors
1332+ 1 : {
1333+ "BaseClass" : {"BaseClass" },
1334+ "MixinA" : {"MixinA" }, # no ID slot
1335+ "MixinB" : {"MixinB" }, # no ID slot
1336+ "NonCycleClassA" : {"NonCycleClassA" , "BaseClass" },
1337+ "NonCycleClassB" : {"MixinA" , "NonCycleClassB" , "NonCycleClassA" , "BaseClass" },
1338+ "NonCycleClassC" : {"MixinB" , "NonCycleClassC" , "NonCycleClassA" , "BaseClass" },
1339+ "IdentifierCycleClassA" : {"IdentifierCycleClassA" },
1340+ "IdentifierCycleClassB" : {"IdentifierCycleClassB" },
1341+ "IdentifierCycleClassC" : {"IdentifierCycleClassC" },
1342+ "IdentifierCycleClassD" : {"IdentifierCycleClassD" },
1343+ },
1344+ },
1345+ }
1346+
1347+
1348+ @pytest .mark .parametrize (("target" , "cycle_start_node" ), list (CYCLES [TYPES ][0 ].items ()))
1349+ @pytest .mark .parametrize ("fn" , ["detect_cycles" , "graph_closure" , "type_ancestors" ])
1350+ def test_detect_type_cycles_error (sv_cycles_schema : SchemaView , target : str , cycle_start_node : str , fn : str ) -> None :
1351+ """Test detection of cycles in the types segment of the cycles schema."""
1352+ if fn == "detect_cycles" :
1353+ with pytest .raises (ValueError , match = f"Cycle detected at node '{ cycle_start_node } '" ):
1354+ detect_cycles (sv_cycles_schema .type_parents , [target ])
1355+ elif fn == "graph_closure" :
1356+ with pytest .raises (ValueError , match = f"Cycle detected at node '{ cycle_start_node } '" ):
1357+ graph_closure (sv_cycles_schema .type_parents , target , detect_cycles = True )
1358+ else :
1359+ with pytest .raises (ValueError , match = f"Cycle detected at node '{ cycle_start_node } '" ):
1360+ sv_cycles_schema .type_ancestors (type_name = target , detect_cycles = True )
1361+
1362+
1363+ @pytest .mark .parametrize (("target" , "expected" ), list (CYCLES [TYPES ][1 ].items ()))
1364+ @pytest .mark .parametrize ("fn" , ["detect_cycles" , "graph_closure" , "type_ancestors" ])
1365+ def test_detect_type_cycles_no_cycles (sv_cycles_schema : SchemaView , target : str , expected : set [str ], fn : str ) -> None :
1366+ """Ensure that types without cycles in their ancestry do not throw an error."""
1367+ if fn == "detect_cycles" :
1368+ detect_cycles (sv_cycles_schema .type_parents , [target ])
1369+ elif fn == "graph_closure" :
1370+ got = graph_closure (sv_cycles_schema .type_parents , target , detect_cycles = True )
1371+ assert set (got ) == expected
1372+ else :
1373+ got = sv_cycles_schema .type_ancestors (target , detect_cycles = True )
1374+ assert set (got ) == expected
1375+
1376+
1377+ @pytest .mark .parametrize (("target" , "cycle_start_node" ), list (CYCLES [CLASSES ][0 ].items ()))
1378+ @pytest .mark .parametrize ("fn" , ["detect_cycles" , "graph_closure" , "class_ancestors" ])
1379+ def test_detect_class_cycles_error (sv_cycles_schema : SchemaView , target : str , cycle_start_node : str , fn : str ) -> None :
1380+ """Test detection of class cycles in the cycles schema."""
1381+ if fn == "detect_cycles" :
1382+ with pytest .raises (ValueError , match = f"Cycle detected at node '{ cycle_start_node } '" ):
1383+ detect_cycles (sv_cycles_schema .class_parents , [target ])
1384+
1385+ elif fn == "graph_closure" :
1386+ with pytest .raises (ValueError , match = f"Cycle detected at node '{ cycle_start_node } '" ):
1387+ graph_closure (sv_cycles_schema .class_parents , target , detect_cycles = True )
1388+ else :
1389+ with pytest .raises (ValueError , match = f"Cycle detected at node '{ cycle_start_node } '" ):
1390+ sv_cycles_schema .class_ancestors (target , detect_cycles = True )
1391+
1392+
1393+ @pytest .mark .parametrize (("target" , "expected" ), list (CYCLES [CLASSES ][1 ].items ()))
1394+ @pytest .mark .parametrize ("fn" , ["detect_cycles" , "graph_closure" , "class_ancestors" ])
1395+ def test_detect_class_cycles_no_cycles (sv_cycles_schema : SchemaView , target : str , expected : set [str ], fn : str ) -> None :
1396+ """Ensure that classes without cycles in their ancestry do not throw an error."""
1397+ if fn == "detect_cycles" :
1398+ detect_cycles (sv_cycles_schema .class_parents , [target ])
1399+ elif fn == "graph_closure" :
1400+ got = graph_closure (sv_cycles_schema .class_parents , target , detect_cycles = True )
1401+ assert set (got ) == expected
1402+ else :
1403+ got = sv_cycles_schema .class_ancestors (target , detect_cycles = True )
1404+ assert set (got ) == expected
1405+
1406+
1407+ @pytest .mark .parametrize ("target" , CYCLES [CLASSES ][1 ].keys ())
1408+ def test_detect_class_as_range_cycles (sv_cycles_schema : SchemaView , target : str ) -> None :
1409+ """Test cycle detection in cases where a class is used as a range."""
1410+
1411+ def check_recursive_id_slots (class_name : str ) -> list [str ]:
1412+ """Given a class, retrieve any classes used as the range for the class identifier slot."""
1413+ id_slot = sv_cycles_schema .get_identifier_slot (class_name , use_key = True )
1414+ if not id_slot :
1415+ return []
1416+ ind_range = sv_cycles_schema .slot_range_as_union (id_slot )
1417+ return [sv_cycles_schema .get_class (x ).name for x in ind_range if sv_cycles_schema .get_class (x )] or []
1418+
1419+ # classes with a cycle in the class identifier slot range are cunningly named
1420+ if "IdentifierCycle" in target :
1421+ with pytest .raises (ValueError , match = "Cycle detected at node " ):
1422+ detect_cycles (check_recursive_id_slots , [target ])
1423+
1424+ else :
1425+ detect_cycles (check_recursive_id_slots , [target ])
1426+
1427+
12741428ORDERING_TESTS = {
12751429 # Bassoon and Abacus are unranked, so appear at the end of the list.
12761430 "rank" : ["wind instrument" , "instrument" , "Didgeridoo" , "counting instrument" , "Clarinet" , "Bassoon" , "Abacus" ],
@@ -2981,160 +3135,6 @@ def test_class_name_mappings() -> None:
29813135 assert {snm_def .name : snm for snm , snm_def in view .slot_name_mappings ().items ()} == slot_names
29823136
29833137
2984- """
2985- Tests of the detect_cycles function, which can identify cyclic relationships between classes, types, and other schema elements.
2986- """
2987-
2988-
2989- @pytest .mark .parametrize ("dodgy_input" , [None , [], set (), {}, 12345 , 123.45 , "some string" , ()])
2990- def test_detect_cycles_input_error (dodgy_input : Any ) -> None :
2991- """Ensure that `detect_cycles` throws an error if input is not supplied in the appropriate form."""
2992- with pytest .raises (ValueError , match = "detect_cycles requires a list of values to process" ):
2993- detect_cycles (lambda x : x , dodgy_input )
2994-
2995-
2996- @pytest .fixture (scope = "module" )
2997- def sv_cycles_schema () -> SchemaView :
2998- """A schema containing cycles!"""
2999- return SchemaView (INPUT_DIR_PATH / "cycles.yaml" )
3000-
3001-
3002- # metadata for elements in the `sv_cycles_schema`
3003- CYCLES = {
3004- TYPES : {
3005- # types in cycles, either directly or via ancestors
3006- # key: type, value: node where the cycle starts
3007- 0 : {
3008- "curve_type" : "circular_type" ,
3009- "semi_circular_type" : "circular_type" ,
3010- "circular_type" : "circular_type" ,
3011- "type_circular" : "type_circular" ,
3012- "circle" : "circle" ,
3013- "circle_of_life" : "circle" ,
3014- },
3015- # types not involved in cycles
3016- # key: the type, value: ancestors of the type
3017- 1 : {
3018- "supreme_string" : {"supreme_string" , "super_string" , "string" },
3019- "super_string" : {"super_string" , "string" },
3020- "string" : {"string" },
3021- "integer" : {"integer" },
3022- "boolean" : {"boolean" },
3023- },
3024- },
3025- CLASSES : {
3026- # classes involved in cycles
3027- # key: class name, value: node where the cycle starts
3028- 0 : {
3029- "ClassA" : "ClassA" ,
3030- "ClassB" : "ClassB" ,
3031- "ClassC" : "ClassC" ,
3032- "ClassD" : "ClassA" ,
3033- "ClassE" : "ClassA" ,
3034- "ClassF" : "ClassF" ,
3035- "ClassG" : "ClassF" ,
3036- "Mixin1" : "Mixin1" ,
3037- "Mixin2" : "Mixin2" ,
3038- "MixedClass" : "Mixin2" ,
3039- },
3040- # class ancestors for classes not in cycles
3041- # key: class name, value: class ancestors
3042- 1 : {
3043- "BaseClass" : {"BaseClass" },
3044- "MixinA" : {"MixinA" }, # no ID slot
3045- "MixinB" : {"MixinB" }, # no ID slot
3046- "NonCycleClassA" : {"NonCycleClassA" , "BaseClass" },
3047- "NonCycleClassB" : {"MixinA" , "NonCycleClassB" , "NonCycleClassA" , "BaseClass" },
3048- "NonCycleClassC" : {"MixinB" , "NonCycleClassC" , "NonCycleClassA" , "BaseClass" },
3049- "IdentifierCycleClassA" : {"IdentifierCycleClassA" },
3050- "IdentifierCycleClassB" : {"IdentifierCycleClassB" },
3051- "IdentifierCycleClassC" : {"IdentifierCycleClassC" },
3052- "IdentifierCycleClassD" : {"IdentifierCycleClassD" },
3053- },
3054- },
3055- }
3056-
3057-
3058- @pytest .mark .parametrize (("target" , "cycle_start_node" ), list (CYCLES [TYPES ][0 ].items ()))
3059- @pytest .mark .parametrize ("fn" , ["detect_cycles" , "graph_closure" , "type_ancestors" ])
3060- def test_detect_type_cycles_error (sv_cycles_schema : SchemaView , target : str , cycle_start_node : str , fn : str ) -> None :
3061- """Test detection of cycles in the types segment of the cycles schema."""
3062- if fn == "detect_cycles" :
3063- with pytest .raises (ValueError , match = f"Cycle detected at node '{ cycle_start_node } '" ):
3064- detect_cycles (sv_cycles_schema .type_parents , [target ])
3065- elif fn == "graph_closure" :
3066- with pytest .raises (ValueError , match = f"Cycle detected at node '{ cycle_start_node } '" ):
3067- graph_closure (sv_cycles_schema .type_parents , target , detect_cycles = True )
3068- else :
3069- with pytest .raises (ValueError , match = f"Cycle detected at node '{ cycle_start_node } '" ):
3070- sv_cycles_schema .type_ancestors (type_name = target , detect_cycles = True )
3071-
3072-
3073- @pytest .mark .parametrize (("target" , "expected" ), list (CYCLES [TYPES ][1 ].items ()))
3074- @pytest .mark .parametrize ("fn" , ["detect_cycles" , "graph_closure" , "type_ancestors" ])
3075- def test_detect_type_cycles_no_cycles (sv_cycles_schema : SchemaView , target : str , expected : set [str ], fn : str ) -> None :
3076- """Ensure that types without cycles in their ancestry do not throw an error."""
3077- if fn == "detect_cycles" :
3078- detect_cycles (sv_cycles_schema .type_parents , [target ])
3079- elif fn == "graph_closure" :
3080- got = graph_closure (sv_cycles_schema .type_parents , target , detect_cycles = True )
3081- assert set (got ) == expected
3082- else :
3083- got = sv_cycles_schema .type_ancestors (target , detect_cycles = True )
3084- assert set (got ) == expected
3085-
3086-
3087- @pytest .mark .parametrize (("target" , "cycle_start_node" ), list (CYCLES [CLASSES ][0 ].items ()))
3088- @pytest .mark .parametrize ("fn" , ["detect_cycles" , "graph_closure" , "class_ancestors" ])
3089- def test_detect_class_cycles_error (sv_cycles_schema : SchemaView , target : str , cycle_start_node : str , fn : str ) -> None :
3090- """Test detection of class cycles in the cycles schema."""
3091- if fn == "detect_cycles" :
3092- with pytest .raises (ValueError , match = f"Cycle detected at node '{ cycle_start_node } '" ):
3093- detect_cycles (sv_cycles_schema .class_parents , [target ])
3094-
3095- elif fn == "graph_closure" :
3096- with pytest .raises (ValueError , match = f"Cycle detected at node '{ cycle_start_node } '" ):
3097- graph_closure (sv_cycles_schema .class_parents , target , detect_cycles = True )
3098- else :
3099- with pytest .raises (ValueError , match = f"Cycle detected at node '{ cycle_start_node } '" ):
3100- sv_cycles_schema .class_ancestors (target , detect_cycles = True )
3101-
3102-
3103- @pytest .mark .parametrize (("target" , "expected" ), list (CYCLES [CLASSES ][1 ].items ()))
3104- @pytest .mark .parametrize ("fn" , ["detect_cycles" , "graph_closure" , "class_ancestors" ])
3105- def test_detect_class_cycles_no_cycles (sv_cycles_schema : SchemaView , target : str , expected : set [str ], fn : str ) -> None :
3106- """Ensure that classes without cycles in their ancestry do not throw an error."""
3107- if fn == "detect_cycles" :
3108- detect_cycles (sv_cycles_schema .class_parents , [target ])
3109- elif fn == "graph_closure" :
3110- got = graph_closure (sv_cycles_schema .class_parents , target , detect_cycles = True )
3111- assert set (got ) == expected
3112- else :
3113- got = sv_cycles_schema .class_ancestors (target , detect_cycles = True )
3114- assert set (got ) == expected
3115-
3116-
3117- @pytest .mark .parametrize ("target" , CYCLES [CLASSES ][1 ].keys ())
3118- def test_detect_class_as_range_cycles (sv_cycles_schema : SchemaView , target : str ) -> None :
3119- """Test cycle detection in cases where a class is used as a range."""
3120-
3121- def check_recursive_id_slots (class_name : str ) -> list [str ]:
3122- """Given a class, retrieve any classes used as the range for the class identifier slot."""
3123- id_slot = sv_cycles_schema .get_identifier_slot (class_name , use_key = True )
3124- if not id_slot :
3125- return []
3126- ind_range = sv_cycles_schema .slot_range_as_union (id_slot )
3127- return [sv_cycles_schema .get_class (x ).name for x in ind_range if sv_cycles_schema .get_class (x )] or []
3128-
3129- # classes with a cycle in the class identifier slot range are cunningly named
3130- if "IdentifierCycle" in target :
3131- with pytest .raises (ValueError , match = "Cycle detected at node " ):
3132- detect_cycles (check_recursive_id_slots , [target ])
3133-
3134- else :
3135- detect_cycles (check_recursive_id_slots , [target ])
3136-
3137-
31383138@pytest .mark .parametrize (
31393139 ("entity_type" , "entity_name" , "type_for_methods" , "get_all_method" ),
31403140 [
0 commit comments