| #!/usr/bin/env vpython3 |
| # Copyright 2020 The Chromium Authors |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| import unittest |
| |
| import apply_edits |
| |
| |
| def _FindPHB(filepath): |
| return apply_edits._FindPrimaryHeaderBasename(filepath) |
| |
| |
| class FindPrimaryHeaderBasenameTest(unittest.TestCase): |
| def testNoOpOnHeader(self): |
| self.assertIsNone(_FindPHB('bar.h')) |
| self.assertIsNone(_FindPHB('foo/bar.h')) |
| |
| def testStripDirectories(self): |
| self.assertEqual('bar', _FindPHB('foo/bar.cc')) |
| |
| def testStripPlatformSuffix(self): |
| self.assertEqual('bar', _FindPHB('bar_posix.cc')) |
| self.assertEqual('bar', _FindPHB('bar_unittest.cc')) |
| |
| def testStripTestSuffix(self): |
| self.assertEqual('bar', _FindPHB('bar_browsertest.cc')) |
| self.assertEqual('bar', _FindPHB('bar_unittest.cc')) |
| |
| def testStripPlatformAndTestSuffix(self): |
| self.assertEqual('bar', _FindPHB('bar_uitest_aura.cc')) |
| self.assertEqual('bar', _FindPHB('bar_linux_unittest.cc')) |
| |
| def testNoSuffixStrippingWithoutUnderscore(self): |
| self.assertEqual('barunittest', _FindPHB('barunittest.cc')) |
| |
| |
| def _ApplyEdit(old_contents_string, |
| edit, |
| contents_filepath="some_file.cc", |
| last_edit=None): |
| if last_edit is not None: |
| assert (last_edit > edit) # Test or prod caller should ensure. |
| ba = bytearray() |
| ba.extend(old_contents_string.encode('utf-8')) |
| return apply_edits._ApplySingleEdit(contents_filepath, |
| old_contents_string.encode("utf-8"), edit, |
| last_edit).decode("utf-8") |
| |
| |
| def _InsertHeader(old_contents, |
| contents_filepath='foo/impl.cc', |
| new_header_path='new/header.h'): |
| edit = apply_edits.Edit("include-user-header", -1, -1, |
| new_header_path.encode("utf-8")) |
| return _ApplyEdit(old_contents, edit, contents_filepath) |
| |
| |
| class InsertIncludeHeaderTest(unittest.TestCase): |
| def _assertEqualContents(self, expected, actual): |
| if expected != actual: |
| print("####################### EXPECTED:") |
| print(expected) |
| print("####################### ACTUAL:") |
| print(actual) |
| print("####################### END.") |
| self.assertEqual(expected, actual) |
| |
| def testSkippingCppComments(self): |
| old_contents = ''' |
| // Copyright info here. |
| |
| #include "old/header.h" |
| ''' |
| expected_new_contents = ''' |
| // Copyright info here. |
| |
| #include "new/header.h" |
| #include "old/header.h" |
| ''' |
| new_header_line = '#include "new/header.h' |
| self._assertEqualContents(expected_new_contents, |
| _InsertHeader(old_contents)) |
| |
| def testSkippingCppComments_DocCommentForStruct(self): |
| """ This is a regression test for https://crbug.com/1175684 """ |
| old_contents = ''' |
| // Copyright blah blah... |
| |
| #ifndef SANDBOX_LINUX_SYSTEM_HEADERS_LINUX_FILTER_H_ |
| #define SANDBOX_LINUX_SYSTEM_HEADERS_LINUX_FILTER_H_ |
| |
| #include <stdint.h> |
| |
| // Doc comment for a struct. |
| // Multiline. |
| struct sock_filter { |
| uint16_t code; |
| }; |
| ''' |
| expected_new_contents = ''' |
| // Copyright blah blah... |
| |
| #ifndef SANDBOX_LINUX_SYSTEM_HEADERS_LINUX_FILTER_H_ |
| #define SANDBOX_LINUX_SYSTEM_HEADERS_LINUX_FILTER_H_ |
| |
| #include <stdint.h> |
| |
| #include "new/header.h" |
| |
| // Doc comment for a struct. |
| // Multiline. |
| struct sock_filter { |
| uint16_t code; |
| }; |
| ''' |
| new_header_line = '#include "new/header.h' |
| self._assertEqualContents(expected_new_contents, |
| _InsertHeader(old_contents)) |
| |
| def testSkippingCppComments_DocCommentForStruct2(self): |
| """ This is a regression test for https://crbug.com/1175684 """ |
| old_contents = ''' |
| // Copyright blah blah... |
| |
| // Doc comment for a struct. |
| struct sock_filter { |
| uint16_t code; |
| }; |
| ''' |
| expected_new_contents = ''' |
| // Copyright blah blah... |
| |
| #include "new/header.h" |
| |
| // Doc comment for a struct. |
| struct sock_filter { |
| uint16_t code; |
| }; |
| ''' |
| new_header_line = '#include "new/header.h' |
| self._assertEqualContents(expected_new_contents, |
| _InsertHeader(old_contents)) |
| |
| def testSkippingCppComments_DocCommentForStruct3(self): |
| """ This is a regression test for https://crbug.com/1175684 """ |
| old_contents = ''' |
| // Doc comment for a struct. |
| struct sock_filter { |
| uint16_t code; |
| }; |
| ''' |
| expected_new_contents = ''' |
| #include "new/header.h" |
| |
| // Doc comment for a struct. |
| struct sock_filter { |
| uint16_t code; |
| }; |
| ''' |
| new_header_line = '#include "new/header.h' |
| self._assertEqualContents(expected_new_contents, |
| _InsertHeader(old_contents)) |
| |
| def testSkippingCppComments_DocCommentForInclude(self): |
| """ This is a regression test for https://crbug.com/1175684 """ |
| old_contents = ''' |
| // Copyright blah blah... |
| |
| // System includes. |
| #include <stdint.h> |
| |
| // Doc comment for a struct. |
| struct sock_filter { |
| uint16_t code; |
| }; |
| ''' |
| expected_new_contents = ''' |
| // Copyright blah blah... |
| |
| // System includes. |
| #include <stdint.h> |
| |
| #include "new/header.h" |
| |
| // Doc comment for a struct. |
| struct sock_filter { |
| uint16_t code; |
| }; |
| ''' |
| new_header_line = '#include "new/header.h' |
| self._assertEqualContents(expected_new_contents, |
| _InsertHeader(old_contents)) |
| |
| def testSkippingCppComments_DocCommentForWholeFile(self): |
| """ This is a regression test for https://crbug.com/1175684 """ |
| old_contents = ''' |
| // Copyright blah blah... |
| |
| // Doc comment for the whole file. |
| |
| struct sock_filter { |
| uint16_t code; |
| }; |
| ''' |
| expected_new_contents = ''' |
| // Copyright blah blah... |
| |
| // Doc comment for the whole file. |
| |
| #include "new/header.h" |
| |
| struct sock_filter { |
| uint16_t code; |
| }; |
| ''' |
| new_header_line = '#include "new/header.h' |
| self._assertEqualContents(expected_new_contents, |
| _InsertHeader(old_contents)) |
| |
| def testSkippingOldStyleComments(self): |
| old_contents = ''' |
| /* Copyright |
| * info here. |
| */ |
| |
| #include "old/header.h" |
| ''' |
| expected_new_contents = ''' |
| /* Copyright |
| * info here. |
| */ |
| |
| #include "new/header.h" |
| #include "old/header.h" |
| ''' |
| self._assertEqualContents(expected_new_contents, |
| _InsertHeader(old_contents)) |
| |
| def testSkippingOldStyleComments_NoWhitespaceAtLineStart(self): |
| old_contents = ''' |
| /* Copyright |
| * info here. |
| */ |
| |
| #include "old/header.h" |
| ''' |
| expected_new_contents = ''' |
| /* Copyright |
| * info here. |
| */ |
| |
| #include "new/header.h" |
| #include "old/header.h" |
| ''' |
| self._assertEqualContents(expected_new_contents, |
| _InsertHeader(old_contents)) |
| |
| def testSkippingSystemHeaders(self): |
| old_contents = ''' |
| #include <string> |
| #include <vector> // blah |
| |
| #include "old/header.h" |
| ''' |
| expected_new_contents = ''' |
| #include <string> |
| #include <vector> // blah |
| |
| #include "new/header.h" |
| #include "old/header.h" |
| ''' |
| self._assertEqualContents(expected_new_contents, |
| _InsertHeader(old_contents)) |
| |
| def testSkippingPrimaryHeader(self): |
| old_contents = ''' |
| // Copyright info here. |
| |
| #include "foo/impl.h" |
| |
| #include "old/header.h" |
| ''' |
| expected_new_contents = ''' |
| // Copyright info here. |
| |
| #include "foo/impl.h" |
| |
| #include "new/header.h" |
| #include "old/header.h" |
| ''' |
| self._assertEqualContents(expected_new_contents, |
| _InsertHeader(old_contents)) |
| |
| def testSimilarNonPrimaryHeader_WithPrimaryHeader(self): |
| old_contents = ''' |
| // Copyright info here. |
| |
| #include "primary/impl.h" // This is the primary header. |
| |
| #include "unrelated/impl.h" // This is *not* the primary header. |
| #include "zzz/foo.h" |
| ''' |
| expected_new_contents = ''' |
| // Copyright info here. |
| |
| #include "primary/impl.h" // This is the primary header. |
| |
| #include "unrelated/impl.h" // This is *not* the primary header. |
| #include "new/header.h" |
| #include "zzz/foo.h" |
| ''' |
| self._assertEqualContents(expected_new_contents, |
| _InsertHeader(old_contents)) |
| |
| def testSimilarNonPrimaryHeader_NoPrimaryHeader(self): |
| old_contents = ''' |
| // Copyright info here. |
| |
| #include "unrelated/impl.h" // This is *not* the primary header. |
| #include "zzz/foo.h" |
| ''' |
| expected_new_contents = ''' |
| // Copyright info here. |
| |
| #include "unrelated/impl.h" // This is *not* the primary header. |
| #include "new/header.h" |
| #include "zzz/foo.h" |
| ''' |
| self._assertEqualContents(expected_new_contents, |
| _InsertHeader(old_contents)) |
| |
| def testSkippingIncludeGuards(self): |
| old_contents = ''' |
| #ifndef FOO_IMPL_H_ |
| #define FOO_IMPL_H_ |
| |
| #include "old/header.h" |
| |
| #endif FOO_IMPL_H_ |
| ''' |
| expected_new_contents = ''' |
| #ifndef FOO_IMPL_H_ |
| #define FOO_IMPL_H_ |
| |
| #include "new/header.h" |
| #include "old/header.h" |
| |
| #endif FOO_IMPL_H_ |
| ''' |
| self._assertEqualContents( |
| expected_new_contents, |
| _InsertHeader(old_contents, 'foo/impl.h', 'new/header.h')) |
| |
| def testSkippingIncludeGuards2(self): |
| # This test is based on base/third_party/valgrind/memcheck.h |
| old_contents = ''' |
| #ifndef __MEMCHECK_H |
| #define __MEMCHECK_H |
| |
| #include "old/header.h" |
| |
| #endif |
| ''' |
| expected_new_contents = ''' |
| #ifndef __MEMCHECK_H |
| #define __MEMCHECK_H |
| |
| #include "new/header.h" |
| #include "old/header.h" |
| |
| #endif |
| ''' |
| self._assertEqualContents(expected_new_contents, |
| _InsertHeader(old_contents)) |
| |
| def testSkippingIncludeGuards3(self): |
| # This test is based on base/third_party/xdg_mime/xdgmime.h |
| old_contents = ''' |
| #ifndef __XDG_MIME_H__ |
| #define __XDG_MIME_H__ |
| |
| #include "old/header.h" |
| |
| #ifdef __cplusplus |
| extern "C" { |
| #endif /* __cplusplus */ |
| |
| typedef void (*XdgMimeCallback) (void *user_data); |
| |
| #ifdef __cplusplus |
| } |
| #endif /* __cplusplus */ |
| #endif /* __XDG_MIME_H__ */ |
| ''' |
| expected_new_contents = ''' |
| #ifndef __XDG_MIME_H__ |
| #define __XDG_MIME_H__ |
| |
| #include "new/header.h" |
| #include "old/header.h" |
| |
| #ifdef __cplusplus |
| extern "C" { |
| #endif /* __cplusplus */ |
| |
| typedef void (*XdgMimeCallback) (void *user_data); |
| |
| #ifdef __cplusplus |
| } |
| #endif /* __cplusplus */ |
| #endif /* __XDG_MIME_H__ */ |
| ''' |
| self._assertEqualContents(expected_new_contents, |
| _InsertHeader(old_contents)) |
| |
| @unittest.skip( |
| "Failing test due to regex (in apply_edits.py) not working as expected, please fix." |
| ) |
| def testSkippingIncludeGuards4(self): |
| # This test is based on ash/first_run/desktop_cleaner.h and/or |
| # components/subresource_filter/core/common/scoped_timers.h and/or |
| # device/gamepad/abstract_haptic_gamepad.h |
| old_contents = ''' |
| #ifndef ASH_FIRST_RUN_DESKTOP_CLEANER_ |
| #define ASH_FIRST_RUN_DESKTOP_CLEANER_ |
| |
| #include "old/header.h" |
| |
| namespace ash { |
| } // namespace ash |
| |
| #endif // ASH_FIRST_RUN_DESKTOP_CLEANER_ |
| ''' |
| expected_new_contents = ''' |
| #ifndef ASH_FIRST_RUN_DESKTOP_CLEANER_ |
| #define ASH_FIRST_RUN_DESKTOP_CLEANER_ |
| |
| #include "new/header.h" |
| #include "old/header.h" |
| |
| namespace ash { |
| } // namespace ash |
| |
| #endif // ASH_FIRST_RUN_DESKTOP_CLEANER_ |
| ''' |
| self._assertEqualContents(expected_new_contents, |
| _InsertHeader(old_contents)) |
| |
| @unittest.skip( |
| "Failing test due to regex (in apply_edits.py) not working as expected, please fix." |
| ) |
| def testSkippingIncludeGuards5(self): |
| # This test is based on third_party/weston/include/GLES2/gl2.h (the |extern |
| # "C"| part has been removed to make the test trickier to handle right - |
| # otherwise it is easy to see that the header has to be included before the |
| # |extern "C"| part). |
| # |
| # The tricky parts below include: |
| # 1. upper + lower case characters allowed in the guard name |
| # 2. Having to recognize that GL_APIENTRYP is *not* a guard |
| old_contents = ''' |
| #ifndef __gles2_gl2_h_ |
| #define __gles2_gl2_h_ 1 |
| |
| #include <GLES2/gl2platform.h> |
| |
| #ifndef GL_APIENTRYP |
| #define GL_APIENTRYP GL_APIENTRY* |
| #endif |
| |
| #endif |
| ''' |
| expected_new_contents = ''' |
| #ifndef __gles2_gl2_h_ |
| #define __gles2_gl2_h_ 1 |
| |
| #include <GLES2/gl2platform.h> |
| |
| #include "new/header.h" |
| |
| #ifndef GL_APIENTRYP |
| #define GL_APIENTRYP GL_APIENTRY* |
| #endif |
| |
| #endif |
| ''' |
| self._assertEqualContents(expected_new_contents, |
| _InsertHeader(old_contents)) |
| |
| @unittest.skip( |
| "Failing test due to regex (in apply_edits.py) not working as expected, please fix." |
| ) |
| def testSkippingIncludeGuards6(self): |
| # This test is based on ios/third_party/blink/src/html_token.h |
| old_contents = ''' |
| #ifndef HTMLToken_h |
| #define HTMLToken_h |
| |
| #include <stddef.h> |
| #include <vector> |
| |
| // ... |
| |
| #endif |
| ''' |
| expected_new_contents = ''' |
| #ifndef HTMLToken_h |
| #define HTMLToken_h |
| |
| #include <stddef.h> |
| #include <vector> |
| |
| #include "new/header.h" |
| |
| // ... |
| |
| #endif |
| ''' |
| self._assertEqualContents(expected_new_contents, |
| _InsertHeader(old_contents)) |
| |
| def testNoOpIfAlreadyPresent(self): |
| # This tests that the new header won't be inserted (and duplicated) |
| # if it is already included. |
| old_contents = ''' |
| // Copyright info here. |
| |
| #include "old/header.h" |
| #include "new/header.h" |
| #include "new/header2.h" |
| ''' |
| expected_new_contents = ''' |
| // Copyright info here. |
| |
| #include "old/header.h" |
| #include "new/header.h" |
| #include "new/header2.h" |
| ''' |
| self._assertEqualContents(expected_new_contents, |
| _InsertHeader(old_contents)) |
| |
| def testNoOpIfAlreadyPresent_WithTrailingComment(self): |
| # This tests that the new header won't be inserted (and duplicated) |
| # if it is already included. |
| old_contents = ''' |
| // Copyright info here. |
| |
| #include "old/header.h" |
| #include "new/header.h" // blah |
| #include "new/header2.h" |
| ''' |
| expected_new_contents = ''' |
| // Copyright info here. |
| |
| #include "old/header.h" |
| #include "new/header.h" // blah |
| #include "new/header2.h" |
| ''' |
| self._assertEqualContents(expected_new_contents, |
| _InsertHeader(old_contents)) |
| |
| def testNoOldHeaders(self): |
| # This tests that an extra new line is inserted after the new header |
| # when there are no old headers immediately below. |
| old_contents = ''' |
| #include <vector> |
| |
| struct S {}; |
| ''' |
| expected_new_contents = ''' |
| #include <vector> |
| |
| #include "new/header.h" |
| |
| struct S {}; |
| ''' |
| self._assertEqualContents(expected_new_contents, |
| _InsertHeader(old_contents)) |
| |
| def testPlatformIfDefs(self): |
| # This test is based on |
| # //base/third_party/double_conversion/double-conversion/utils.h |
| # We need to insert the new header in a non-conditional part. |
| old_contents = ''' |
| #ifndef DOUBLE_CONVERSION_UTILS_H_ |
| #define DOUBLE_CONVERSION_UTILS_H_ |
| |
| #include <cstdlib> |
| #include <cstring> |
| |
| #ifndef DOUBLE_CONVERSION_UNREACHABLE |
| #ifdef _MSC_VER |
| void DOUBLE_CONVERSION_NO_RETURN abort_noreturn(); |
| inline void abort_noreturn() { abort(); } |
| #define DOUBLE_CONVERSION_UNREACHABLE() (abort_noreturn()) |
| #else |
| #define DOUBLE_CONVERSION_UNREACHABLE() (abort()) |
| #endif |
| #endif |
| |
| namespace double_conversion { |
| ''' |
| expected_new_contents = ''' |
| #ifndef DOUBLE_CONVERSION_UTILS_H_ |
| #define DOUBLE_CONVERSION_UTILS_H_ |
| |
| #include <cstdlib> |
| #include <cstring> |
| |
| #include "new/header.h" |
| |
| #ifndef DOUBLE_CONVERSION_UNREACHABLE |
| #ifdef _MSC_VER |
| void DOUBLE_CONVERSION_NO_RETURN abort_noreturn(); |
| inline void abort_noreturn() { abort(); } |
| #define DOUBLE_CONVERSION_UNREACHABLE() (abort_noreturn()) |
| #else |
| #define DOUBLE_CONVERSION_UNREACHABLE() (abort()) |
| #endif |
| #endif |
| |
| namespace double_conversion { |
| ''' |
| self._assertEqualContents(expected_new_contents, |
| _InsertHeader(old_contents)) |
| |
| def testNoOldIncludesAndIfDefs(self): |
| # Artificial test: no old #includes + some #ifdefs. The main focus of the |
| # test is ensuring that the new header will be inserted into the |
| # unconditional part of the file. |
| old_contents = ''' |
| #ifndef NDEBUG |
| #include "base/logging.h" |
| #endif |
| |
| void foo(); |
| ''' |
| expected_new_contents = ''' |
| #include "new/header.h" |
| |
| #ifndef NDEBUG |
| #include "base/logging.h" |
| #endif |
| |
| void foo(); |
| ''' |
| self._assertEqualContents(expected_new_contents, |
| _InsertHeader(old_contents)) |
| |
| def testNoOldIncludesAndIfDefs2(self): |
| # Artificial test: no old #includes + some #ifdefs. The main focus of the |
| # test is ensuring that the new header will be inserted into the |
| # unconditional part of the file. |
| old_contents = ''' |
| #if BUILDFLAG(IS_WIN) |
| #include "foo_win.h" |
| #endif |
| |
| void foo(); |
| ''' |
| expected_new_contents = ''' |
| #include "new/header.h" |
| |
| #if BUILDFLAG(IS_WIN) |
| #include "foo_win.h" |
| #endif |
| |
| void foo(); |
| ''' |
| self._assertEqualContents(expected_new_contents, |
| _InsertHeader(old_contents)) |
| |
| def testUtf8BomMarker(self): |
| # Test based on |
| # //chrome/browser/ui/views/payments/payment_sheet_view_controller.cc |
| # which at some point began as follows: |
| # 00000000: efbb bf2f 2f20 436f 7079 7269 6768 7420 ...// Copyright |
| # |
| # Previous versions of apply_edits.py would not skip the BOM marker when |
| # figuring out where to insert the new include header. |
| old_contents = u'''\ufeff// Copyright |
| |
| #include "old/header.h" |
| ''' |
| expected_new_contents = u'''\ufeff// Copyright |
| |
| #include "new/header.h" |
| #include "old/header.h" |
| ''' |
| actual = bytearray() |
| actual.extend(old_contents.encode('utf-8')) |
| expected = bytearray() |
| expected.extend(expected_new_contents.encode('utf-8')) |
| # Test sanity check (i.e. not an assertion about code under test). |
| utf8_bom = [0xef, 0xbb, 0xbf] |
| self._assertEqualContents(list(actual[0:3]), utf8_bom) |
| self._assertEqualContents(list(expected[0:3]), utf8_bom) |
| # Actual test. |
| edit = apply_edits.Edit('include-user-header', -1, -1, b"new/header.h") |
| actual = apply_edits._ApplySingleEdit("foo/impl.cc", actual, edit, None) |
| self._assertEqualContents(expected, actual) |
| |
| |
| def _CreateReplacement(content_string, old_substring, new_substring): |
| """ Test helper for creating an Edit object with the right offset, etc. """ |
| b_content_string = content_string.encode("utf-8") |
| b_old_string = old_substring.encode("utf-8") |
| b_new_string = new_substring.encode("utf-8") |
| offset = b_content_string.find(b_old_string) |
| return apply_edits.Edit('r', offset, len(b_old_string), b_new_string) |
| |
| |
| class ApplyReplacementTest(unittest.TestCase): |
| def testBasics(self): |
| old_text = "123 456 789" |
| r = _CreateReplacement(old_text, "456", "foo") |
| new_text = _ApplyEdit(old_text, r) |
| self.assertEqual("123 foo 789", new_text) |
| |
| def testMiddleListElementRemoval(self): |
| old_text = "(123, 456, 789) // foobar" |
| r = _CreateReplacement(old_text, "456", "") |
| new_text = _ApplyEdit(old_text, r) |
| self.assertEqual("(123, 789) // foobar", new_text) |
| |
| def testFinalElementRemoval(self): |
| old_text = "(123, 456, 789) // foobar" |
| r = _CreateReplacement(old_text, "789", "") |
| new_text = _ApplyEdit(old_text, r) |
| self.assertEqual("(123, 456) // foobar", new_text) |
| |
| def testConflictingReplacement(self): |
| old_text = "123 456 789" |
| last = _CreateReplacement(old_text, "456", "foo") |
| edit = _CreateReplacement(old_text, "456", "bar") |
| expected_msg_regex = 'Conflicting replacement text' |
| expected_msg_regex += '.*some_file.cc at offset 4, length 3' |
| expected_msg_regex += '.*"bar" != "foo"' |
| with self.assertRaisesRegex(ValueError, expected_msg_regex): |
| _ApplyEdit(old_text, edit, last_edit=last) |
| |
| def testUnrecognizedEditDirective(self): |
| old_text = "123 456 789" |
| edit = apply_edits.Edit('unknown_directive', 123, 456, "foo") |
| expected_msg_regex = 'Unrecognized edit directive "unknown_directive"' |
| expected_msg_regex += '.*some_file.cc' |
| with self.assertRaisesRegex(ValueError, expected_msg_regex): |
| _ApplyEdit(old_text, edit) |
| |
| def testOverlappingReplacement(self): |
| old_text = "123 456 789" |
| last = _CreateReplacement(old_text, "456 789", "foo") |
| edit = _CreateReplacement(old_text, "123 456", "bar") |
| expected_msg_regex = 'Overlapping replacements' |
| expected_msg_regex += '.*some_file.cc' |
| expected_msg_regex += '.*offset 0, length 7.*"bar"' |
| expected_msg_regex += '.*offset 4, length 7.*"foo"' |
| with self.assertRaisesRegex(ValueError, expected_msg_regex): |
| _ApplyEdit(old_text, edit, last_edit=last) |
| |
| |
| if __name__ == '__main__': |
| unittest.main() |