Skip to content
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

Feature/sg 1057 add breaking change tests #1373

Merged
merged 52 commits into from
Aug 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
2902488
remove pram
Louis-Dupont Aug 13, 2023
4d08223
wip
Louis-Dupont Aug 13, 2023
76262da
change
Louis-Dupont Aug 13, 2023
aeec8fa
change
Louis-Dupont Aug 13, 2023
a1d7fe7
add default and non default
Louis-Dupont Aug 13, 2023
311fb33
delete module
Louis-Dupont Aug 14, 2023
9d2479a
remove default
Louis-Dupont Aug 14, 2023
73636fa
add x to method
Louis-Dupont Aug 14, 2023
975e48c
remove default get_arch
Louis-Dupont Aug 14, 2023
ca0bb7a
wip
Louis-Dupont Aug 14, 2023
4fd8426
Merge branch 'master' into EXPLO/test_diff_with_master
Louis-Dupont Aug 14, 2023
04cad5c
improve display
Louis-Dupont Aug 14, 2023
fa039d6
minor update
Louis-Dupont Aug 14, 2023
38aa4a4
table content
Louis-Dupont Aug 14, 2023
ac44920
add
Louis-Dupont Aug 15, 2023
adcc88e
remove unwanted remaining
Louis-Dupont Aug 15, 2023
d008659
improve robustness
Louis-Dupont Aug 15, 2023
b2dd349
Merge branch 'master' into feature/SG-1057-add_breaking_change_tests
Louis-Dupont Aug 16, 2023
6f76464
adding test for class
Louis-Dupont Aug 16, 2023
b740a2e
add test to CI
Louis-Dupont Aug 16, 2023
e74ed45
cleanup
Louis-Dupont Aug 16, 2023
626527c
cleanup
Louis-Dupont Aug 16, 2023
ec1f1d4
add unittest
Louis-Dupont Aug 16, 2023
369bc31
divide code to modules and import git only when required
Louis-Dupont Aug 16, 2023
9553792
fix ci
Louis-Dupont Aug 16, 2023
a1dd898
add gitpython to dev
Louis-Dupont Aug 16, 2023
be3fc42
improve doc
Louis-Dupont Aug 16, 2023
6cd6c85
minor docstring change
Louis-Dupont Aug 16, 2023
7fc728b
install gitpython in CI
Louis-Dupont Aug 16, 2023
7db21b8
wip
Louis-Dupont Aug 20, 2023
06f5db2
set default value to false
Louis-Dupont Aug 20, 2023
001e2fe
fix test
Louis-Dupont Aug 20, 2023
d013a11
replace verbose by silent
Louis-Dupont Aug 20, 2023
1753be3
fix path
Louis-Dupont Aug 20, 2023
7f8ea29
Merge branch 'master' into feature/SG-1057-add_breaking_change_tests
Louis-Dupont Aug 20, 2023
0612555
clean how we define root_dir
Louis-Dupont Aug 20, 2023
c9bd613
update CI test
Louis-Dupont Aug 21, 2023
9e02eaa
try again
Louis-Dupont Aug 21, 2023
e5cf64a
try again
Louis-Dupont Aug 21, 2023
fddf71c
try again
Louis-Dupont Aug 21, 2023
642d9c0
rename breaking-change-test
Louis-Dupont Aug 21, 2023
42b989c
Merge branch 'master' into feature/SG-1057-add_breaking_change_tests
Louis-Dupont Aug 21, 2023
f0b37ee
add breaking chnage to base workflow
Louis-Dupont Aug 21, 2023
428ca3b
remove from releast
Louis-Dupont Aug 21, 2023
bbf0dc5
fix sg isntall in CI
Louis-Dupont Aug 21, 2023
5559d0d
rename the check name
Louis-Dupont Aug 21, 2023
121ca86
Merge branch 'master' into feature/SG-1057-add_breaking_change_tests
Louis-Dupont Aug 21, 2023
5050a51
move main to test
Louis-Dupont Aug 21, 2023
dcd031a
add tests as unittests
Louis-Dupont Aug 22, 2023
4a7976d
fix test
Louis-Dupont Aug 22, 2023
8664c88
Merge branch 'master' into feature/SG-1057-add_breaking_change_tests
Louis-Dupont Aug 22, 2023
ed628ba
Merge branch 'master' into feature/SG-1057-add_breaking_change_tests
Louis-Dupont Aug 22, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,36 @@ jobs:
- store_artifacts:
path: ~/sg_logs

breaking_change_check:
parameters:
py_version:
type: string
default: latest
docker:
- image: cimg/python:<< parameters.py_version >>
resource_class: small
steps:
- checkout
- run:
name: Detect breaking changes
no_output_timeout: 30m
command: |
python3 -m venv venv
. venv/bin/activate
python3 -m pip install pip==23.1.2
python3 -m pip install -e .
python3 -m pip install gitpython==3.1.0
coverage run --source=super_gradients -m unittest tests/breaking_change_tests/unit_test.py
coverage run --source=super_gradients -m unittest tests/breaking_change_tests/test_detect_breaking_change.py
- run:
name: Remove new environment when failed
command: "rm -r venv"
when: on_fail
- slack/notify:
channel: "sg-integration-tests"
event: fail
template: basic_fail_1 # see https://github.com/CircleCI-Public/slack-orb/wiki#templates.

change_rc_to_b:
description: "change rc in the tag to b"
docker:
Expand Down Expand Up @@ -906,6 +936,12 @@ workflows:
requires:
- deci-common/persist_version_info

- breaking_change_check:
name: "breaking-change-check"
py_version: << pipeline.parameters.build_py_version >>
requires:
- deci-common/persist_version_info

- deci-common/codeartifact_login:
repo_name: "deci-packages"
<<: *release_candidate_filter
Expand Down
1 change: 1 addition & 0 deletions requirements.dev.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
flake8==5.0.4
black==22.10.0
pre-commit==2.20.0
gitpython>=3.1.0
Empty file.
243 changes: 243 additions & 0 deletions tests/breaking_change_tests/breaking_changes_detection.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,243 @@
import ast
from abc import ABC
from pathlib import Path
from typing import List, Dict, Union

from termcolor import colored
from dataclasses import dataclass, field, asdict

from .code_parser import parse_functions_signatures, parse_imports


MODULE_PATH_COLOR = "yellow"
SOURCE_CODE_COLOR = "blue"
BREAKING_OBJECT_COLOR = "red"


@dataclass
class AbstractBreakingChange(ABC):
line_num: int

@property
def description(self) -> str:
raise NotImplementedError()

@property
def breaking_type_name(self) -> str:
raise NotImplementedError()


@dataclass
class ClassRemoved(AbstractBreakingChange):
class_name: str
line_num: int

@property
def description(self) -> str:
return f"{colored(self.class_name, SOURCE_CODE_COLOR)} -> {colored('X', BREAKING_OBJECT_COLOR)}"

@property
def breaking_type_name(self) -> str:
return "CLASS REMOVED"


@dataclass
class ImportRemoved(AbstractBreakingChange):
import_name: str
line_num: int

@property
def description(self) -> str:
return f"{colored(self.import_name, SOURCE_CODE_COLOR)} -> {colored('X', BREAKING_OBJECT_COLOR)}"

@property
def breaking_type_name(self) -> str:
return "IMPORT REMOVED"


@dataclass
class FunctionRemoved(AbstractBreakingChange):
function_name: str
line_num: int

@property
def description(self) -> str:
return f"{colored(self.function_name, SOURCE_CODE_COLOR)} -> {colored('X', BREAKING_OBJECT_COLOR)}"

@property
def breaking_type_name(self) -> str:
return "FUNCTION REMOVED"


@dataclass
class ParameterRemoved(AbstractBreakingChange):
parameter_name: str
function_name: str
line_num: int

@property
def description(self) -> str:
source_fn_colored = colored(self.function_name, SOURCE_CODE_COLOR)
current_fn_colored = colored(self.function_name, "yellow")
param_colored = colored(self.parameter_name, BREAKING_OBJECT_COLOR)
return f"{source_fn_colored}(..., {param_colored}) -> {current_fn_colored}(...)"

@property
def breaking_type_name(self) -> str:
return "FUNCTION PARAMETER REMOVED"


@dataclass
class RequiredParameterAdded(AbstractBreakingChange):
parameter_name: str
function_name: str
line_num: int

@property
def description(self) -> str:
source_fn_colored = colored(self.function_name, SOURCE_CODE_COLOR)
current_fn_colored = colored(self.function_name, "yellow")
param_colored = colored(self.parameter_name, BREAKING_OBJECT_COLOR)
return f"{source_fn_colored}(...) -> {current_fn_colored}(..., {param_colored})"

@property
def breaking_type_name(self) -> str:
return "FUNCTION PARAMETER ADDED"


@dataclass
class BreakingChanges:
module_path: str
classes_removed: List[ClassRemoved] = field(default_factory=list)
imports_removed: List[ImportRemoved] = field(default_factory=list)
functions_removed: List[FunctionRemoved] = field(default_factory=list)
params_removed: List[ParameterRemoved] = field(default_factory=list)
required_params_added: List[RequiredParameterAdded] = field(default_factory=list)

def __str__(self) -> str:
summary = ""
module_path_colored = colored(self.module_path, MODULE_PATH_COLOR)

breaking_changes: List[AbstractBreakingChange] = (
self.classes_removed + self.imports_removed + self.functions_removed + self.params_removed + self.required_params_added
)
for breaking_change in breaking_changes:

summary += "{:<70} {:<8} {:<30} {}\n".format(
module_path_colored, breaking_change.line_num, breaking_change.breaking_type_name, breaking_change.description
)

return summary

def json(self) -> Dict[str, List[str]]:
return asdict(self)

@property
def is_empty(self) -> bool:
return len(self.classes_removed + self.imports_removed + self.functions_removed + self.params_removed + self.required_params_added) == 0


def extract_code_breaking_changes(module_path: str, source_code: str, current_code: str) -> BreakingChanges:
"""Compares two versions of code to identify breaking changes.

:param module_path: The path to the module being compared.
:param source_code: The source version of the code.
:param current_code: The modified version of the code.
:return: A BreakingChanges object detailing the differences.
"""
breaking_changes = BreakingChanges(module_path=module_path)

source_classes = {node.name: node for node in ast.walk(ast.parse(source_code)) if isinstance(node, ast.ClassDef)}
current_classes = {node.name: node for node in ast.walk(ast.parse(current_code)) if isinstance(node, ast.ClassDef)}

# ClassRemoved
for class_name, source_class in source_classes.items():
if class_name not in current_classes:
breaking_changes.classes_removed.append(
ClassRemoved(
class_name=class_name,
line_num=source_class.lineno,
)
)

# FUNCTION SIGNATURES
source_functions_signatures = parse_functions_signatures(source_code)
current_functions_signatures = parse_functions_signatures(current_code)
for function_name, source_function_signature in source_functions_signatures.items():

if function_name in current_functions_signatures:
current_function_signature = current_functions_signatures[function_name]

# ParameterRemoved
for source_param in source_function_signature.params.all:
if source_param not in current_function_signature.params.all:
breaking_changes.params_removed.append(
ParameterRemoved(
function_name=function_name,
parameter_name=source_param,
line_num=current_function_signature.line_num,
)
)

# RequiredParameterAdded
for current_param in current_function_signature.params.required:
if current_param not in source_function_signature.params.required:
breaking_changes.required_params_added.append(
RequiredParameterAdded(
function_name=function_name,
parameter_name=current_param,
line_num=current_function_signature.line_num,
)
)

else:
# FunctionRemoved
breaking_changes.functions_removed.append(
FunctionRemoved(
function_name=function_name,
line_num=source_function_signature.line_num,
)
)

# Check import ONLY if __init__ file.
if module_path.endswith("__init__.py"):
source_imports = parse_imports(code=source_code)
current_imports = parse_imports(code=current_code)
breaking_changes.imports_removed = [
ImportRemoved(import_name=source_import, line_num=0) for source_import in source_imports if source_import not in current_imports
]
return breaking_changes


def analyze_breaking_changes(verbose: bool = 1) -> List[Dict[str, Union[str, List]]]:
"""Analyze changes between the current branch (HEAD) and the master branch.
:param verbose: If True, print the summary of breaking changes in a nicely formatted way
:return: List of changes, where each change is a dictionary listing each type of change for each module.
"""
# GitHelper requires `git` library which should NOT be required for the other functions
from .git_utils import GitHelper

root_dir = str(Path(__file__).resolve().parents[2])
git_explorer = GitHelper(git_path=root_dir)

summary = ""
breaking_changes_list = []
for module_path in git_explorer.diff_files(source_branch="master", current_branch="HEAD"):

master_code = git_explorer.load_branch_file(branch="master", file_path=module_path)
head_code = git_explorer.load_branch_file(branch="HEAD", file_path=module_path)
breaking_changes = extract_code_breaking_changes(module_path=module_path, source_code=master_code, current_code=head_code)

if not breaking_changes.is_empty:
breaking_changes_list.append(breaking_changes.json())
summary += str(breaking_changes)

if verbose:
if summary:
print("{:<60} {:<8} {:<30} {}\n".format("MODULE", "LINE NO", "BREAKING TYPE", "DESCRIPTION (Master -> HEAD)"))
print("-" * 175 + "\n")
print(summary)
else:
print(colored("NO BREAKING CHANGE DETECTED!", "green"))

return breaking_changes_list
Loading