diff --git a/scripts/breaking_changes_checker/breaking_changes_tracker.py b/scripts/breaking_changes_checker/breaking_changes_tracker.py index fee9d991467d..d5c5aab352c6 100644 --- a/scripts/breaking_changes_checker/breaking_changes_tracker.py +++ b/scripts/breaking_changes_checker/breaking_changes_tracker.py @@ -149,6 +149,22 @@ def is_client(self, module_name: Any, class_name: Any) -> bool: return True return False + def is_operation_group(self, module_name: Any, class_name: Any) -> bool: + """Determine whether a class should be treated as an operation group. + + Operation group classes follow the codegen convention of a name ending + with ``Operations`` (case-insensitive, which also covers the default + ``Operations`` class) and being defined in an operations module (module + namespace ending with ``operations``). This mirrors the existing + ``attr_type.lower().endswith("operations")`` heuristic used elsewhere to + recognize operation group properties on clients. + """ + if isinstance(class_name, jsondiff.Symbol) or not isinstance(class_name, str): + return False + if isinstance(module_name, jsondiff.Symbol) or not isinstance(module_name, str): + return False + return class_name.lower().endswith("operations") and module_name.endswith("operations") + def run_post_processing(self) -> None: # Remove duplicate reporting of changes that apply to both sync and async package components self.run_async_cleanup(self.breaking_changes) diff --git a/scripts/breaking_changes_checker/changelog_tracker.py b/scripts/breaking_changes_checker/changelog_tracker.py index 3f2c0e6f1832..0c1154f6b8bd 100644 --- a/scripts/breaking_changes_checker/changelog_tracker.py +++ b/scripts/breaking_changes_checker/changelog_tracker.py @@ -22,6 +22,7 @@ class ChangeType(str, Enum): ADDED_ENUM_MEMBER = "AddedEnumMember" ADDED_FUNCTION_PARAMETER = "AddedFunctionParameter" ADDED_OPERATION_GROUP = "AddedOperationGroup" + ADDED_OPERATION_GROUP_CLASS = "AddedOperationGroupClass" class ChangelogTracker(BreakingChangesTracker): ADDED_CLIENT_MSG = \ @@ -46,6 +47,8 @@ class ChangelogTracker(BreakingChangesTracker): "Enum `{}` added member `{}`" ADDED_OPERATION_GROUP_MSG = \ "Client `{}` added operation group `{}`" + ADDED_OPERATION_GROUP_CLASS_MSG = \ + "Added operation group `{}`" def __init__(self, stable: Dict, current: Dict, package_name: str, **kwargs: Any) -> None: @@ -87,6 +90,14 @@ def run_non_breaking_class_level_diff_checks(self, module: Dict) -> None: self.module_name, class_name ) self.features_added.append(fa) + elif self.is_operation_group(self.module_name, self.class_name): + # This is a new operation group class + fa = ( + self.ADDED_OPERATION_GROUP_CLASS_MSG, + ChangeType.ADDED_OPERATION_GROUP_CLASS, + self.module_name, class_name + ) + self.features_added.append(fa) else: # This is a new class fa = ( diff --git a/scripts/breaking_changes_checker/tests/data/expected_apimanagement_5_6b1_changelog.txt b/scripts/breaking_changes_checker/tests/data/expected_apimanagement_5_6b1_changelog.txt index ff9074d58326..77f854ff8a56 100644 --- a/scripts/breaking_changes_checker/tests/data/expected_apimanagement_5_6b1_changelog.txt +++ b/scripts/breaking_changes_checker/tests/data/expected_apimanagement_5_6b1_changelog.txt @@ -672,10 +672,10 @@ - Model `WorkspaceTagOperations` added parameter `match_condition` in method `delete` - Model `WorkspaceTagOperations` added parameter `etag` in method `update` - Model `WorkspaceTagOperations` added parameter `match_condition` in method `update` - - Added model `ApiGatewayHostnameBindingOperations` - - Added model `ApiToolOperations` - - Added model `ClientApplicationOperations` - - Added model `ClientApplicationProductLinkOperations` + - Added operation group `ApiGatewayHostnameBindingOperations` + - Added operation group `ApiToolOperations` + - Added operation group `ClientApplicationOperations` + - Added operation group `ClientApplicationProductLinkOperations` ### Breaking Changes diff --git a/scripts/breaking_changes_checker/tests/test_changelog.py b/scripts/breaking_changes_checker/tests/test_changelog.py index 0846bcbf4d99..5c00fd6161ce 100644 --- a/scripts/breaking_changes_checker/tests/test_changelog.py +++ b/scripts/breaking_changes_checker/tests/test_changelog.py @@ -447,6 +447,59 @@ def test_new_class_property_added_init(): assert args == ['azure.ai.contentsafety', 'AnalyzeTextResult', 'new_class_att'] +def test_added_operation_group_class_not_labeled_as_model(): + # A newly added operation group class must be reported as "Added operation group `X`", + # not "Added model `X`". It appears in both the sync `...operations` module and the async + # `...aio.operations` module, but the async cleanup should collapse it into a single entry. + def _op_group_module(): + return { + "class_nodes": { + "ExistingOperations": { + "type": None, + "methods": {}, + "properties": {} + }, + "PrivateLinkResourcesOperations": { + "type": None, + "methods": {}, + "properties": {} + } + } + } + + def _stable_op_group_module(): + return { + "class_nodes": { + "ExistingOperations": { + "type": None, + "methods": {}, + "properties": {} + } + } + } + + current = { + "azure.mgmt.attestation.operations": _op_group_module(), + "azure.mgmt.attestation.aio.operations": _op_group_module(), + } + stable = { + "azure.mgmt.attestation.operations": _stable_op_group_module(), + "azure.mgmt.attestation.aio.operations": _stable_op_group_module(), + } + + bc = ChangelogTracker(stable, current, "azure-mgmt-attestation") + bc.run_checks() + + op_group_entries = [fa for fa in bc.features_added if fa[0] == ChangelogTracker.ADDED_OPERATION_GROUP_CLASS_MSG] + added_model_entries = [fa for fa in bc.features_added if fa[0] == ChangelogTracker.ADDED_CLASS_MSG] + + # Exactly one operation group entry (sync/async duplicate collapsed) and no "Added model". + assert len(op_group_entries) == 1 + assert not added_model_entries + _, _, _, class_name = op_group_entries[0] + assert class_name == "PrivateLinkResourcesOperations" + + def test_new_class_property_added_init_only(): # Testing if we get a report on a new class property added only in the init current = {