import unittest from buildscripts.check_for_noexcept import ( AlertKind, FileBoundSnippet, SnippetKind, analyze_changes, analyze_text_diff, get_matching_blocks, get_move_operations, get_noexcepts, get_nonmatching_blocks, strip_noexcept, ) class TestGetMatchingBlocks(unittest.TestCase): def _check_blocks_cover_string(self, blocks: list[tuple[int, int]], s: str): lines = s.splitlines() if len(blocks) == 0: self.assertEqual(len(lines), 0) return prev_end = 0 for block_start, block_end in sorted(blocks): self.assertEqual(block_start, prev_end) prev_end = block_end self.assertEqual(prev_end, len(lines)) def test_matches_and_nonmatches_are_accurate(self): cases = ( ("abc\nbcd\ncde\ndef", "abc\nwxy\ncde\ndef"), ("abc\nbcd\ncde\ndef", "abc\ncde\ndef"), ("abc\nbcd\ncde\ndef", "abc\nbcd\ncde\ndef\nxyz"), ) for lhs, rhs in cases: matching = get_matching_blocks(lhs, rhs) nonmatching = get_nonmatching_blocks(lhs, rhs) self.assertGreater(len(matching), 0) self.assertGreater(len(nonmatching), 0) lhs_lines, rhs_lines = lhs.splitlines(), rhs.splitlines() for lhs_start, lhs_end, rhs_start, rhs_end in matching: block_lhs = "\n".join(lhs_lines[lhs_start:lhs_end]) block_rhs = "\n".join(rhs_lines[rhs_start:rhs_end]) self.assertEqual(block_lhs, block_rhs) lhs_blocks = [] rhs_blocks = [] for lhs_start, lhs_end, rhs_start, rhs_end in matching + nonmatching: lhs_blocks.append((lhs_start, lhs_end)) rhs_blocks.append((rhs_start, rhs_end)) self._check_blocks_cover_string(rhs_blocks, rhs) self._check_blocks_cover_string(lhs_blocks, lhs) def test_empty_strings(self): self.assertEqual(get_matching_blocks("", "nonempty"), []) self.assertEqual(get_matching_blocks("nonempty", ""), []) self.assertEqual(get_matching_blocks("", ""), []) self.assertEqual(get_nonmatching_blocks("", "nonempty"), [(0, 0, 0, 1)]) self.assertEqual(get_nonmatching_blocks("nonempty", ""), [(0, 1, 0, 0)]) self.assertEqual(get_nonmatching_blocks("", ""), []) def test_no_match(self): self.assertEqual(get_matching_blocks("a b c d", "e f g h"), []) self.assertEqual(get_nonmatching_blocks("a b c d", "e f g h"), [(0, 1, 0, 1)]) def test_equal_strings(self): s = "a b c d" self.assertEqual(get_matching_blocks(s, s), [(0, 1, 0, 1)]) self.assertEqual(get_nonmatching_blocks(s, s), []) def test_basic_multiline_edit(self): lhs = "a\nb\nc\nd" rhs = "a\nw\nx\nd" self.assertEqual(get_matching_blocks(lhs, rhs), [(0, 1, 0, 1), (3, 4, 3, 4)]) self.assertEqual(get_nonmatching_blocks(lhs, rhs), [(1, 3, 1, 3)]) def test_basic_multiline_delete(self): lhs = "a\nb\nc\nd" rhs = "a\nd" self.assertEqual(get_matching_blocks(lhs, rhs), [(0, 1, 0, 1), (3, 4, 1, 2)]) self.assertEqual(get_nonmatching_blocks(lhs, rhs), [(1, 3, 1, 1)]) def test_basic_multiline_insert(self): lhs = "a\nd" rhs = "a\nb\nc\nd" self.assertEqual(get_matching_blocks(lhs, rhs), [(0, 1, 0, 1), (1, 2, 3, 4)]) self.assertEqual(get_nonmatching_blocks(lhs, rhs), [(1, 1, 1, 3)]) # The basic cases below end with semicolons (generally). We can generate similar cases ending with { # or just without the semicolon to cover those cases. def _generate_additional_cases(cases: list[str]) -> tuple[str]: new_cases = list(cases) for case in cases: if case.endswith(";"): new_cases.append(case[:-1] + " {") new_cases.append(case[:-1]) return tuple(set(new_cases)) # The hardcoded cases all contain noexcept. We can generate non-noexcept cases by removing any # noexcept tokens. def _generate_non_noexcept_cases(cases: list[str]) -> tuple[str]: new_cases = [strip_noexcept(case) for case in cases] return tuple(set(new_cases)) _BASIC_NOEXCEPT_MOVE_CONSTRUCTORS = _generate_additional_cases( [ "TypeName(TypeName&& other) const noexcept;", "TypeName(TypeName&& other) const noexcept(std::is_nothrow_constructible_v);", "TypeName(TypeName&& other) noexcept;", "TypeName(TypeName&& other) noexcept const;", "TypeName(TypeName&& other) noexcept(std::is_nothrow_constructible_v) const;", "Typenoexcept(Typenoexcept&& other) const noexcept;", "Typenoexcept(Typenoexcept&& other) noexcept;", "Typenoexcept(Typenoexcept&& other) noexcept const;", ] ) _BASIC_NOEXCEPT_MOVE_ASSIGNMENTS = _generate_additional_cases( [ "TypeName& operator=(TypeName&&) noexcept;", "TypeName& operator=(TypeName&&) noexcept(std::is_nothrow_constructible_v);", "TypeName& operator=(TypeName&&) noexcept { return *this; }", "TypeName& operator=(TypeName&&) noexcept const;", "TypeName& operator=(TypeName&&) noexcept const { return *this; }", "TypeName& operator=(TypeName&&) noexcept(std::is_nothrow_constructible_v) const { return *this; }", "TypeName& operator=(TypeName&&) const noexcept;", "TypeName& operator=(TypeName&&) const noexcept { return *this; }", "Typenoexcept& operator=(Typenoexcept&&) noexcept { return *this; }", "Typenoexcept& operator=(Typenoexcept&&) noexcept(std::is_nothrow_constructible_v) { return *this; }", ] ) _BASIC_NON_NOEXCEPT_MOVE_CONSTRUCTORS = _generate_non_noexcept_cases( _BASIC_NOEXCEPT_MOVE_CONSTRUCTORS ) _BASIC_NON_NOEXCEPT_MOVE_ASSIGNMENTS = _generate_non_noexcept_cases( _BASIC_NOEXCEPT_MOVE_ASSIGNMENTS ) _BASIC_NOEXCEPT_MOVE_OPERATIONS = ( _BASIC_NOEXCEPT_MOVE_CONSTRUCTORS + _BASIC_NOEXCEPT_MOVE_ASSIGNMENTS ) _BASIC_NON_NOEXCEPT_MOVE_OPERATIONS = ( _BASIC_NON_NOEXCEPT_MOVE_CONSTRUCTORS + _BASIC_NON_NOEXCEPT_MOVE_ASSIGNMENTS ) # Tricky statements that look similar-ish to move operations but are not. _BASIC_NOEXCEPT_NON_MOVE_OPERATIONS = _generate_additional_cases( [ "functionName(TypeName&&) noexcept;", "functionName(TypeName&& other) noexcept;", "functionName(TypeName&&) const noexcept;", "functionName(TypeName&&, int) const noexcept;", "functionName(TypeName&& other, int otherParam) noexcept;", "functionName() noexcept;", "TypeName() noexcept;", "TypeName(const TypeName&) noexcept;", "TypeName(const TypeName&) noexcept(std::is_trivially_constructible_v);", "TypeName(TypeName) noexcept;", "functionName(TypeName val) noexcept;", "functionName(TypeName& val) noexcept;", "functionName(TypeName&& val) noexcept;", "auto lambda = [x = std::move(y)]() noexcept { return x; }", "auto lambda = [](TypeName&& x) noexcept { return x; }", "[](TypeName&& x) noexcept { return x; }", "([](TypeName&& x) noexcept { return x; })()", "TypeName(TypeName&&, int otherParam) noexcept;", "TypeName operator+(const TypeName& other) noexcept;", "TypeName operator+(const TypeName& other) const noexcept;", "TypeName& operator*() const noexcept;", ] ) _BASIC_NON_NOEXCEPT_NON_MOVE_OPERATIONS = _generate_non_noexcept_cases( _BASIC_NOEXCEPT_NON_MOVE_OPERATIONS ) def _noexcept_moves_only(snippets): return [s for s in snippets if s.kind == SnippetKind.NoexceptMove] def _nonnoexcept_moves_only(snippets): return [s for s in snippets if s.kind == SnippetKind.NonNoexceptMove] def _make_snippet( *, file: str, kind: SnippetKind, identifier: str | None = None, line: int | None = None, line_start: int | None = None, line_end: int | None = None, alert_line: int | None = None, ): if line is not None: line_start = line line_end = line else: line_start = line_start line_end = line_end if alert_line is None: alert_line = line_start return FileBoundSnippet( file=file, kind=kind, start=0, end=0, identifier=identifier, line_start=line_start, line_end=line_end, alert_line=alert_line, ) class TestCheckForNoexcept(unittest.TestCase): def test_strip_noexcept(self): no_noexcept_cases = _BASIC_NON_NOEXCEPT_MOVE_OPERATIONS for case in no_noexcept_cases: self.assertEqual(strip_noexcept(case), case) self.assertEqual( strip_noexcept( "int f(SomeType x) noexcept(std::is_nothrow_constructible_v>);" ), "int f(SomeType x);", ) self.assertEqual( strip_noexcept("int f(SomeType x) noexcept const;"), "int f(SomeType x) const;", ) self.assertEqual( strip_noexcept("int f(SomeType x) noexcept;"), "int f(SomeType x);", ) def test_no_text(self): pre, post = analyze_text_diff("", "") self.assertEqual(pre, []) self.assertEqual(post, []) def test_simple_noexcept_additions(self): pre = "int f(SomeType x) const {" posts = ( "int f(SomeType x) noexcept const {", "int f(SomeType x) noexcept {", "int f(SomeType x) noexcept(std::is_nothrow_constructible_v>) {", "int func(SomeType x) noexcept const {", "int f(SomeType x) noexcept const {", "int f(SomeType y) noexcept const {", "int f(SomeOtherType x) noexcept const {", ) for post in posts: before, after = analyze_text_diff(pre, post) self.assertEqual(len(before), 0) self.assertEqual(len(after), 1) snippet = after[0] self.assertFalse(snippet.is_move()) self.assertEqual(snippet.start, post.find("noexcept")) self.assertEqual(snippet.end, snippet.start + len("noexcept")) self.assertEqual(snippet.line_start, 1) self.assertEqual(snippet.line_end, 1) def test_simple_removals(self): pre = "int f(SomeType x) noexcept const {" posts = ( "int f(SomeType x) const {", "int f(SomeType x) {", "int func(SomeType x) const {", "int f(SomeType x) const {", "int f(SomeType y) const {", "int f(SomeOtherType x) const {", ) for post in posts: before, after = analyze_text_diff(pre, post) self.assertEqual(len(before), 1) self.assertEqual(len(after), 0) snippet = before[0] self.assertFalse(snippet.is_move()) self.assertEqual(snippet.start, pre.find("noexcept")) self.assertEqual(snippet.end, snippet.start + len("noexcept")) self.assertEqual(snippet.line_start, 1) self.assertEqual(snippet.line_end, 1) def test_definite_noexcept_addition_removal(self): with_noexcept = """ int f() { return x * y; } int g(int z) const noexcept(std::is_nothrow_constructible_v) { return z + 1; } void q() noexcept { // do nothing } """ without_noexcept = """ int f() { return x * y; } int g(int z) const { return z + 1; } void q() { // do nothing } """ # Case where noexcept was definitively removed. before, after = analyze_text_diff(with_noexcept, without_noexcept) self.assertEqual(len(before), 2) self.assertEqual(len(after), 0) for snippet in before: self.assertEqual(snippet.kind, SnippetKind.NoexceptRemoval) # Case where noexcept was definitively added. before, after = analyze_text_diff(without_noexcept, with_noexcept) self.assertEqual(len(before), 0) self.assertEqual(len(after), 2) for snippet in before: self.assertEqual(snippet.kind, SnippetKind.NoexceptRemoval) def test_simple_unchanged_with_noexcept(self): pre = "int f(SomeType x) noexcept {" post = "float f(SomeOtherType y) noexcept {" before, after = analyze_text_diff(pre, post) self.assertEqual(len(before), 1) self.assertEqual(before[0].kind, SnippetKind.Noexcept) self.assertEqual(len(after), 1) self.assertEqual(after[0].kind, SnippetKind.Noexcept) def test_simple_unrelated_changes(self): pre = "int f(SomeType x) noexcept const {" posts = ( "int f(SomeType x) noexcept {", "int func(SomeType x) noexcept const {", "int f(SomeType x) noexcept const {", "int f(SomeType y) noexcept const {", "int f(SomeOtherType x) noexcept const {", ) for post in posts: before, after = analyze_text_diff(pre, post) self.assertEqual(len(before), 1) self.assertEqual(before[0].kind, SnippetKind.Noexcept) self.assertEqual(len(after), 1) self.assertEqual(after[0].kind, SnippetKind.Noexcept) def test_simple_noexcept_move_addition(self): pre = "\n\nTypeName(TypeName&& other);" post = "\n\nTypeName(TypeName&& other)\nnoexcept;" before, after = analyze_text_diff(pre, post) self.assertEqual(len(before), 1) snippet = before[0] self.assertEqual(snippet.kind, SnippetKind.NonNoexceptMove) self.assertEqual(snippet.start, 2) self.assertEqual(snippet.end, pre.find(")") + 1) self.assertEqual(snippet.line_start, 3) self.assertEqual(snippet.line_end, 3) self.assertEqual(len(after), 1) snippet = after[0] self.assertEqual(snippet.kind, SnippetKind.NoexceptMove) self.assertEqual(snippet.start, 2) self.assertEqual(snippet.end, len(post) - 1) self.assertEqual(snippet.line_start, 3) self.assertEqual(snippet.line_end, 4) def test_simple_noexcept_move_removal(self): pre = "\n\nTypeName(TypeName&& other) noexcept {\n /* code */ }" post = "\n\nTypeName(TypeName&& other) {\n /* code */ }" before, after = analyze_text_diff(pre, post) self.assertEqual(len(before), 1) snippet = before[0] self.assertEqual(snippet.kind, SnippetKind.NoexceptMove) self.assertEqual(snippet.start, 2) self.assertEqual(snippet.end, pre.find("{")) self.assertEqual(snippet.line_start, 3) self.assertEqual(snippet.line_end, 3) self.assertEqual(len(after), 1) snippet = after[0] self.assertEqual(snippet.kind, SnippetKind.NonNoexceptMove) self.assertEqual(snippet.start, 2) self.assertEqual(snippet.end, post.find(")") + 1) self.assertEqual(snippet.line_start, 3) self.assertEqual(snippet.line_end, 3) def test_get_move_operations_noexcept_independent(self): # Basic cases with_move_operation = _BASIC_NOEXCEPT_MOVE_OPERATIONS + _BASIC_NON_NOEXCEPT_MOVE_OPERATIONS without_move_operation = ( _BASIC_NOEXCEPT_NON_MOVE_OPERATIONS + _BASIC_NON_NOEXCEPT_NON_MOVE_OPERATIONS ) # Trickier cases with_move_operation += ( """ class TypeName { TypeName(TypeName&&) noexcept; }; """, """ TypeName& operator=(TypeName&& other) noexcept { auto lambda = [x = std::move(y)]() noexcept { return x; };", z = lambda(); return *this; } """, "TypeName(TypeName&&) { /* noexcept */ }", "TypeName(TypeName&&); // noexcept", "Typenoexcept(Typenoexcept&& varnoexcept);", "TypeName(TypeName&&); TypeName() noexcept;", "Typenoexcept& operator=(Typenoexcept&&) { return *this; }", """ TypeName& operator=(TypeName&& other) { auto lambda = [x = std::move(y)]() noexcept { return x; };", z = lambda(); return *this; } """, ) without_move_operation += ( """ class TypeName { TypeName(const TypeName&); TypeName(TypeName); functionName(TypeName&&); }; """, ) for snippet in with_move_operation: all_moves = get_move_operations(snippet) self.assertEqual(len(all_moves), 1, msg=f"Expected move operation: {snippet}") for snippet in without_move_operation: all_moves = get_move_operations(snippet) self.assertEqual(len(all_moves), 0, msg=f"Expected no move operation in {snippet}") def test_get_move_operations_noexcept_matters(self): # Basic cases with_noexcept = _BASIC_NOEXCEPT_MOVE_OPERATIONS without_noexcept = _BASIC_NON_NOEXCEPT_MOVE_OPERATIONS # Trickier cases with_noexcept += ( "Typenoexcept& operator=(Typenoexcept&&) noexcept { return *this; }", """ TypeName& operator=(TypeName&& other) noexcept { auto lambda = [x = std::move(y)]() noexcept { return x; };", z = lambda(); return *this; } """, ) without_noexcept += ( "TypeName(TypeName&&) { /* noexcept */ }", "TypeName(TypeName&&); // noexcept", "Typenoexcept(Typenoexcept&& varnoexcept);", "TypeName(TypeName&&); TypeName() noexcept;", "Typenoexcept& operator=(Typenoexcept&&) { return *this; }", """ TypeName& operator=(TypeName&& other) { auto lambda = [x = std::move(y)]() noexcept { return x; };", z = lambda(); return *this; } """, ) for snippet in with_noexcept: all_moves = get_move_operations(snippet) noexcept = _noexcept_moves_only(all_moves) non_noexcept = _nonnoexcept_moves_only(all_moves) self.assertEqual(len(noexcept), 1, msg=f"Expected noexcept move operation: {snippet}") self.assertEqual( len(non_noexcept), 0, msg=f"Expected no non-noexcept move operation: {snippet}" ) for snippet in without_noexcept: all_moves = get_move_operations(snippet) noexcept = _noexcept_moves_only(all_moves) non_noexcept = _nonnoexcept_moves_only(all_moves) self.assertEqual( len(noexcept), 0, msg=f"Expected no noexcept move operation: {snippet}" ) self.assertEqual( len(non_noexcept), 1, msg=f"Expected non-noexcept move operation: {snippet}" ) def test_get_move_operations_negative_cases(self): negative_cases = ( _BASIC_NOEXCEPT_NON_MOVE_OPERATIONS + _BASIC_NON_NOEXCEPT_NON_MOVE_OPERATIONS ) for snippet in negative_cases: all_moves = get_move_operations(snippet) self.assertEqual(len(all_moves), 0, msg=f"Expected no move operation: {snippet}") def test_get_noexcepts(self): with_noexcept = _BASIC_NOEXCEPT_MOVE_OPERATIONS + _BASIC_NOEXCEPT_NON_MOVE_OPERATIONS without_noexcept = ( _BASIC_NON_NOEXCEPT_MOVE_OPERATIONS + _BASIC_NON_NOEXCEPT_NON_MOVE_OPERATIONS ) for snippet in with_noexcept: self.assertGreater( len(get_noexcepts(snippet)), 0, msg=f"Expected noexcept in snippet: {snippet}" ) for snippet in without_noexcept: self.assertEqual( len(get_noexcepts(snippet)), 0, msg=f"Expected no noexcept in snippet: {snippet}" ) def test_move_identifiers(self): templates = ( "{t}({t}&&){c}{n};", "{t}({t}&& {v}){c}{n};", "operator=({t}&&){c}{n};", "operator=({t}&& {v}){c}{n};", "{t}& operator=({t}&&){c}{n};", "{t}& operator=({t}&& {v}){c}{n};", "{t}({t}&&)\n{c}\n{n};", "operator=({t}&&)\n{c}{n};", ) for template in templates: for const in (" const", ""): for noexcept in (" noexcept", "noexcept(condition)", ""): for varname in ("other", "varnoexcept", ""): for typename in ("TypeName", "Typenoexcept"): case = template.format(t=typename, v=varname, c=const, n=noexcept) all_moves = get_move_operations(case) self.assertEqual( len(all_moves), 1, msg=f"Expected move operation: {case}" ) move = all_moves[0] if "operator=" in template: identifier = typename + "::operator=" else: identifier = typename + "::" + typename self.assertEqual(move.identifier, identifier) def test_analyze_changes_simple_addition_removal(self): # No alert on noexcept deletion. snippets = [ _make_snippet(file="file1.cpp", kind=SnippetKind.Noexcept, line_start=10, line_end=10), ] alerts = analyze_changes(snippets, []) self.assertEqual(len(alerts), 0) # Alert on noexcept addition. alerts = analyze_changes([], snippets) self.assertEqual(len(alerts), 1) alert = alerts[0] self.assertEqual(alert.file, "file1.cpp") self.assertEqual(alert.kind, AlertKind.Noexcept) self.assertEqual(alert.line, 10) def test_analyze_changes_non_noexcept_move(self): other_snippets = [ _make_snippet( file="file1.cpp", kind=SnippetKind.NoexceptMove, identifier="irrelevant", line=5 ), _make_snippet(file="file2.cpp", kind=SnippetKind.Noexcept, line=10), ] non_noexcept_move_snippet = [ _make_snippet( file="file3.cpp", kind=SnippetKind.NonNoexceptMove, identifier="TypeName::TypeName", line=15, ), ] # The non-noexcept move is present on both sides, no alert. alerts = analyze_changes(non_noexcept_move_snippet, non_noexcept_move_snippet) self.assertEqual(len(alerts), 0) alerts = analyze_changes( other_snippets + non_noexcept_move_snippet, other_snippets + non_noexcept_move_snippet ) self.assertEqual(len(alerts), 0) # Other stuff is changing, but the non-noexcept move is not changing, no alert. alerts = analyze_changes( other_snippets + non_noexcept_move_snippet, non_noexcept_move_snippet ) self.assertFalse(any(alert.kind == AlertKind.NonNoexceptMove for alert in alerts)) alerts = analyze_changes( non_noexcept_move_snippet, other_snippets + non_noexcept_move_snippet ) self.assertFalse(any(alert.kind == AlertKind.NonNoexceptMove for alert in alerts)) # The non-noexcept move is removed, this is fine, no alert. alerts = analyze_changes(other_snippets + non_noexcept_move_snippet, other_snippets) self.assertEqual(len(alerts), 0) # The non-noexcept move is added, this is an alert. alerts = analyze_changes(other_snippets, other_snippets + non_noexcept_move_snippet) self.assertEqual(len(alerts), 1) alert = alerts[0] self.assertEqual(alert.file, "file3.cpp") self.assertEqual(alert.line, 15) self.assertEqual(alert.identifier, "TypeName::TypeName") self.assertEqual(alert.kind, AlertKind.NonNoexceptMove) def test_analyze_changes_complex_case(self): # We have a non-noexcept move (TypeName) in both pre and post, so no alert for that. # We have a net addition of noexcept, so alert for that. # We have a new non-noexcept move (OtherType), so alert for that. pre_snippets = [ _make_snippet(file="file1.cpp", kind=SnippetKind.Noexcept, line=10), _make_snippet( file="file2.cpp", kind=SnippetKind.NonNoexceptMove, identifier="TypeName::TypeName", line=20, ), ] post_snippets = [ _make_snippet(file="file1.cpp", kind=SnippetKind.Noexcept, line=10), _make_snippet(file="file1.cpp", kind=SnippetKind.Noexcept, line=30), _make_snippet( file="file2.cpp", kind=SnippetKind.NoexceptMove, identifier="TypeName::TypeName", line=20, ), _make_snippet( file="file3.cpp", kind=SnippetKind.NonNoexceptMove, identifier="OtherType::OtherType", line=30, ), _make_snippet( file="file4.cpp", kind=SnippetKind.NonNoexceptMove, identifier="TypeName::TypeName", line=40, ), ] alerts = analyze_changes(pre_snippets, post_snippets) self.assertEqual(len(alerts), 2) noexcept_move_alerts = [ alert for alert in alerts if alert.kind == AlertKind.NonNoexceptMove ] self.assertEqual(len(noexcept_move_alerts), 1) noexcept_move_alert = noexcept_move_alerts[0] self.assertEqual(noexcept_move_alert.file, "file3.cpp") self.assertEqual(noexcept_move_alert.line, 30) self.assertEqual(noexcept_move_alert.identifier, "OtherType::OtherType") noexcept_change_alerts = [alert for alert in alerts if alert.kind == AlertKind.Noexcept] self.assertEqual(len(noexcept_change_alerts), 1) noexcept_change_alert = noexcept_change_alerts[0] self.assertEqual(noexcept_change_alert.file, "file1.cpp") def test_analyze_changes_definitive_additions(self): post_snippets = [ _make_snippet(file="file1.cpp", kind=SnippetKind.Noexcept, line=10), _make_snippet(file="file1.cpp", kind=SnippetKind.NoexceptAddition, line=20), _make_snippet(file="file1.cpp", kind=SnippetKind.NoexceptAddition, line=30), _make_snippet(file="file1.cpp", kind=SnippetKind.Noexcept, line=40), ] alerts = analyze_changes([], post_snippets) # Only alert once for a definitive addition, even if there are multiple snippets. # Also don't alert for the net additions of noexcept since it would be repetitive. self.assertEqual(len(alerts), 1) alert = alerts[0] self.assertEqual(alert.file, "file1.cpp") self.assertEqual(alert.kind, AlertKind.Noexcept) self.assertEqual(alert.line, 20) def test_analyze_changes_definitive_removals(self): pre_snippets = [ _make_snippet(file="file1.cpp", kind=SnippetKind.Noexcept, line=10, alert_line=510), _make_snippet( file="file1.cpp", kind=SnippetKind.NoexceptAddition, line=20, alert_line=520 ), _make_snippet( file="file1.cpp", kind=SnippetKind.NoexceptAddition, line=30, alert_line=530 ), _make_snippet(file="file1.cpp", kind=SnippetKind.Noexcept, line=40, alert_line=540), ] alerts = analyze_changes(pre_snippets, []) # Only alert once for a definitive removal, even if there are multiple snippets. self.assertEqual(len(alerts), 1) alert = alerts[0] self.assertEqual(alert.file, "file1.cpp") self.assertEqual(alert.kind, AlertKind.Noexcept) self.assertEqual(alert.line, 520) if __name__ == "__main__": unittest.main()