def generate(cls, directory, control_hostname, num_nodes, cluster_id=None): """ Generate certificates in the given directory. :param FilePath directory: Directory to use for certificate authority. :param bytes control_hostname: The hostname of the control service. :param int num_nodes: Number of nodes in the cluster. :param UUID cluster_id: The unique identifier of the cluster for which the certificates are being generated. If not given, a random identifier will be generated. :return: ``Certificates`` instance. """ RootCredential.initialize( directory, b"acceptance-cluster", cluster_id=cluster_id, ) def run(*arguments): check_call([b"flocker-ca"] + list(arguments), cwd=directory.path) run(b"create-control-certificate", control_hostname) run(b"create-api-certificate", b"allison") # Rename to user.crt/user.key so we can use this folder directly # from flocker-deploy and other clients: directory.child(b"allison.crt").moveTo(directory.child(b"user.crt")) directory.child(b"allison.key").moveTo(directory.child(b"user.key")) for i in range(num_nodes): run(b"create-node-certificate") for i, child in enumerate( directory.globChildren(b"????????-????-*.crt")): sibling = FilePath(child.path[:-3] + b"key") child.moveTo(directory.child(b"node-%d.crt" % (i,))) sibling.moveTo(directory.child(b"node-%d.key" % (i,))) return cls(directory)
def generate(cls, directory, control_hostname, num_nodes): """ Generate certificates in the given directory. :param FilePath directory: Directory to use for ceritificate authority. :param bytes control_hostname: The hostname of the control service. :param int num_nodes: Number of nodes in the cluster. :return: ``Certificates`` instance. """ def run(*arguments): check_call([b"flocker-ca"] + list(arguments), cwd=directory.path) run(b"initialize", b"acceptance-cluster") run(b"create-control-certificate", control_hostname) run(b"create-api-certificate", b"allison") # Rename to user.crt/user.key so we can use this folder directly # from flocker-deploy and other clients: directory.child(b"allison.crt").moveTo(directory.child(b"user.crt")) directory.child(b"allison.key").moveTo(directory.child(b"user.key")) for i in range(num_nodes): run(b"create-node-certificate") for i, child in enumerate( directory.globChildren(b"????????-????-*.crt")): sibling = FilePath(child.path[:-3] + b"key") child.moveTo(directory.child(b"node-%di.crt" % i)) sibling.moveTo(directory.child(b"node-%di.key" % i)) return cls(directory)
def _backup_pcap(self, username, ip_addr): """ Backup existing pcap file. Used when restarting traffic capture for ACTIVE accounts and after the account expires. :param username (str): account username :param ip_addr (IPv4Address): IP address allocated for the account. """ log.debug("ACCOUNTS:: Backing up pcap for {} with IP {}.".format( username, str(ip_addr))) day_month_str = datetime.now().strftime("%m%d%H%M") cur_pcap_file = "{}_{}.pcap".format(username, str(ip_addr)) new_path = os.path.join(self.path['pcaps'], "{}_{}".format(username, str(ip_addr))) new_path_fp = FilePath(new_path) if not new_path_fp.isdir(): log.debug("ACCOUNTS:: Creating directory {}".format(new_path)) new_path_fp.createDirectory() new_pcap_file = "{}_{}_{}.pcap".format(username, str(ip_addr), day_month_str) cur_pcap_file = os.path.join(self.path['pcaps'], cur_pcap_file) new_pcap_file = os.path.join(new_path, new_pcap_file) log.debug("ACCOUNTS:: Current pcap file {}".format(cur_pcap_file)) log.debug("ACCOUNTS:: New pcap file {}".format(new_pcap_file)) fp = FilePath(cur_pcap_file) backup_fp = FilePath(new_pcap_file) fp.moveTo(backup_fp) backup_fp.chmod(0654)
def test_renamedSource(self): """ Warnings emitted by a function defined in a file which has been renamed since it was initially compiled can still be flushed. This is testing the code which specifically supports working around the unfortunate behavior of CPython to write a .py source file name into the .pyc files it generates and then trust that it is correct in various places. If source files are renamed, .pyc files may not be regenerated, but they will contain incorrect filenames. """ package = FilePath(self.mktemp().encode("utf-8")).child( b"twisted_private_helper" ) package.makedirs() package.child(b"__init__.py").setContent(b"") package.child(b"module.py").setContent( b""" import warnings def foo(): warnings.warn("oh no") """ ) pathEntry = package.parent().path.decode("utf-8") sys.path.insert(0, pathEntry) self.addCleanup(sys.path.remove, pathEntry) # Import it to cause pycs to be generated from twisted_private_helper import module # Clean up the state resulting from that import; we're not going to use # this module, so it should go away. del sys.modules["twisted_private_helper"] del sys.modules[module.__name__] # Some Python versions have extra state related to the just # imported/renamed package. Clean it up too. See also # http://bugs.python.org/issue15912 try: from importlib import invalidate_caches except ImportError: pass else: invalidate_caches() # Rename the source directory package.moveTo(package.sibling(b"twisted_renamed_helper")) # Import the newly renamed version from twisted_renamed_helper import module # type: ignore[import] self.addCleanup(sys.modules.pop, "twisted_renamed_helper") self.addCleanup(sys.modules.pop, module.__name__) # Generate the warning module.foo() # Flush it self.assertEqual(len(self.flushWarnings([module.foo])), 1)
def test_renamedSource(self): """ Warnings emitted by a function defined in a file which has been renamed since it was initially compiled can still be flushed. This is testing the code which specifically supports working around the unfortunate behavior of CPython to write a .py source file name into the .pyc files it generates and then trust that it is correct in various places. If source files are renamed, .pyc files may not be regenerated, but they will contain incorrect filenames. """ package = FilePath(self.mktemp().encode('utf-8')).child(b'twisted_private_helper') package.makedirs() package.child(b'__init__.py').setContent(b'') package.child(b'module.py').setContent(b''' import warnings def foo(): warnings.warn("oh no") ''') pathEntry = package.parent().path.decode('utf-8') sys.path.insert(0, pathEntry) self.addCleanup(sys.path.remove, pathEntry) # Import it to cause pycs to be generated from twisted_private_helper import module # Clean up the state resulting from that import; we're not going to use # this module, so it should go away. del sys.modules['twisted_private_helper'] del sys.modules[module.__name__] # Some Python versions have extra state related to the just # imported/renamed package. Clean it up too. See also # http://bugs.python.org/issue15912 try: from importlib import invalidate_caches except ImportError: pass else: invalidate_caches() # Rename the source directory package.moveTo(package.sibling(b'twisted_renamed_helper')) # Import the newly renamed version from twisted_renamed_helper import module self.addCleanup(sys.modules.pop, 'twisted_renamed_helper') self.addCleanup(sys.modules.pop, module.__name__) # Generate the warning module.foo() # Flush it self.assertEqual(len(self.flushWarnings([module.foo])), 1)
def buildPDF(self, bookPath, inputDirectory, outputPath): """ Build a PDF from the given a LaTeX book document. @type bookPath: L{FilePath} @param bookPath: The location of a LaTeX document defining a book. @type inputDirectory: L{FilePath} @param inputDirectory: The directory which the inputs of the book are relative to. @type outputPath: L{FilePath} @param outputPath: The location to which to write the resulting book. """ if not bookPath.basename().endswith(".tex"): raise ValueError("Book filename must end with .tex") workPath = FilePath(mkdtemp()) try: startDir = os.getcwd() try: os.chdir(inputDirectory.path) texToDVI = ( "latex -interaction=nonstopmode " "-output-directory=%s %s") % ( workPath.path, bookPath.path) # What I tell you three times is true! # The first two invocations of latex on the book file allows it # correctly create page numbers for in-text references. Why this is # the case, I could not tell you. -exarkun for i in range(3): self.run(texToDVI) bookBaseWithoutExtension = bookPath.basename()[:-4] dviPath = workPath.child(bookBaseWithoutExtension + ".dvi") psPath = workPath.child(bookBaseWithoutExtension + ".ps") pdfPath = workPath.child(bookBaseWithoutExtension + ".pdf") self.run( "dvips -o %(postscript)s -t letter -Ppdf %(dvi)s" % { 'postscript': psPath.path, 'dvi': dviPath.path}) self.run("ps2pdf13 %(postscript)s %(pdf)s" % { 'postscript': psPath.path, 'pdf': pdfPath.path}) pdfPath.moveTo(outputPath) workPath.remove() finally: os.chdir(startDir) except: workPath.moveTo(bookPath.parent().child(workPath.basename())) raise
def _wrap(path,**template): scratchfile=path.dirname()+"."+path.basename()+".tmp" fh=path.open('r') sfp=FilePath(scratchfile) sfh=sfp.open('w') seeklast=0 for buffer in fh.readlines(): for line in buffer: sfh.write(line.format(**template)) sfh.flush() sfh.close() fh.close() sfp.moveTo(path.realpath())
def _delete_ipfile(self, username): """ Create configuration file for static IP allocation. :param username (str): account username. """ log.info("ACCOUNTS:: Deleting IP file for {}.".format(username)) filename = os.path.join(self.path['client-ips'], username) log.debug("ACCOUNTS:: Moving file to {}.revoked.".format(filename)) # do not delete it, just rename it to `revoked` fp = FilePath(filename) revoked_fp = FilePath("{}.revoked".format(filename)) fp.moveTo(revoked_fp)
def test_renamedSource(self): """ Warnings emitted by a function defined in a file which has been renamed since it was initially compiled can still be flushed. This is testing the code which specifically supports working around the unfortunate behavior of CPython to write a .py source file name into the .pyc files it generates and then trust that it is correct in various places. If source files are renamed, .pyc files may not be regenerated, but they will contain incorrect filenames. """ package = FilePath(self.mktemp()).child('twisted_private_helper') package.makedirs() package.child('__init__.py').setContent('') package.child('module.py').setContent(''' import warnings def foo(): warnings.warn("oh no") ''') sys.path.insert(0, package.parent().path) self.addCleanup(sys.path.remove, package.parent().path) # Import it to cause pycs to be generated from twisted_private_helper import module # Clean up the state resulting from that import; we're not going to use # this module, so it should go away. del sys.modules['twisted_private_helper'] del sys.modules[module.__name__] # Rename the source directory package.moveTo(package.sibling('twisted_renamed_helper')) # Import the newly renamed version from twisted_renamed_helper import module self.addCleanup(sys.modules.pop, 'twisted_renamed_helper') self.addCleanup(sys.modules.pop, module.__name__) # Generate the warning module.foo() # Flush it self.assertEqual(len(self.flushWarnings([module.foo])), 1)
class DirectoryChangeListenerTestCase(TestCase): def test_delete(self): """ Verify directory deletions can be monitored """ self.tmpdir = FilePath(self.mktemp()) self.tmpdir.makedirs() def deleteAction(): self.tmpdir.remove() resource = KQueueReactorTestFixture(self, deleteAction) storageService = StubStorageService(resource.reactor) delegate = DataStoreMonitor(resource.reactor, storageService) dcl = DirectoryChangeListener(resource.reactor, self.tmpdir.path, delegate) dcl.startListening() resource.runReactor() self.assertTrue(storageService.stopCalled) self.assertEquals(delegate.methodCalled, "deleted") def test_rename(self): """ Verify directory renames can be monitored """ self.tmpdir = FilePath(self.mktemp()) self.tmpdir.makedirs() def renameAction(): self.tmpdir.moveTo(FilePath(self.mktemp())) resource = KQueueReactorTestFixture(self, renameAction) storageService = StubStorageService(resource.reactor) delegate = DataStoreMonitor(resource.reactor, storageService) dcl = DirectoryChangeListener(resource.reactor, self.tmpdir.path, delegate) dcl.startListening() resource.runReactor() self.assertTrue(storageService.stopCalled) self.assertEquals(delegate.methodCalled, "renamed")
def generate(cls, directory, control_hostname, num_nodes, cluster_name, cluster_id=None): """ Generate certificates in the given directory. :param FilePath directory: Directory to use for certificate authority. :param bytes control_hostname: The hostname of the control service. :param int num_nodes: Number of nodes in the cluster. :param UUID cluster_id: The unique identifier of the cluster for which the certificates are being generated. If not given, a random identifier will be generated. :return: ``Certificates`` instance. """ RootCredential.initialize( directory, cluster_name, cluster_id=cluster_id, ) def run(*arguments): check_call([b"flocker-ca"] + list(arguments), cwd=directory.path) run(b"create-control-certificate", control_hostname) run(b"create-api-certificate", b"allison") # Rename to user.crt/user.key so we can use this folder directly # from clients: directory.child(b"allison.crt").moveTo(directory.child(b"user.crt")) directory.child(b"allison.key").moveTo(directory.child(b"user.key")) for i in range(num_nodes): run(b"create-node-certificate") for i, child in enumerate( directory.globChildren(b"????????-????-*.crt")): sibling = FilePath(child.path[:-3] + b"key") child.moveTo(directory.child(b"node-%d.crt" % (i, ))) sibling.moveTo(directory.child(b"node-%d.key" % (i, ))) return cls(directory)
def add_node(self, index): """ Generate another node certificate. :rtype: CertAndKey :return: The newly created node certificate. """ def run(*arguments): check_call([b"flocker-ca"] + list(arguments), cwd=self.directory.path) run(b"create-node-certificate") tmp_crt = self.directory.globChildren(b"????????-????-*.crt")[0] tmp_key = FilePath(tmp_crt.path[:-3] + b"key") crt = self.directory.child(b"node-%d.crt" % (index, )) key = self.directory.child(b"node-%d.key" % (index, )) tmp_crt.moveTo(crt) tmp_key.moveTo(key) cert = CertAndKey(crt, key) self.nodes.append(cert) return cert
def add_node(self, index): """ Generate another node certificate. :rtype: CertAndKey :return: The newly created node certificate. """ def run(*arguments): check_call([b"flocker-ca"] + list(arguments), cwd=self.directory.path) run(b"create-node-certificate") tmp_crt = self.directory.globChildren(b"????????-????-*.crt")[0] tmp_key = FilePath(tmp_crt.path[:-3] + b"key") crt = self.directory.child(b"node-%d.crt" % (index,)) key = self.directory.child(b"node-%d.key" % (index,)) tmp_crt.moveTo(crt) tmp_key.moveTo(key) cert = CertAndKey(crt, key) self.nodes.append(cert) return cert
class WarnAboutFunctionTests(SynchronousTestCase): """ Tests for L{twisted.python.deprecate.warnAboutFunction} which allows the callers of a function to issue a C{DeprecationWarning} about that function. """ def setUp(self): """ Create a file that will have known line numbers when emitting warnings. """ self.package = FilePath(self.mktemp().encode("utf-8") ).child(b'twisted_private_helper') self.package.makedirs() self.package.child(b'__init__.py').setContent(b'') self.package.child(b'module.py').setContent(b''' "A module string" from twisted.python import deprecate def testFunction(): "A doc string" a = 1 + 2 return a def callTestFunction(): b = testFunction() if b == 3: deprecate.warnAboutFunction(testFunction, "A Warning String") ''') # Python 3 doesn't accept bytes in sys.path: packagePath = self.package.parent().path.decode("utf-8") sys.path.insert(0, packagePath) self.addCleanup(sys.path.remove, packagePath) modules = sys.modules.copy() self.addCleanup( lambda: (sys.modules.clear(), sys.modules.update(modules))) # On Windows on Python 3, most FilePath interactions produce # DeprecationWarnings, so flush them here so that they don't interfere # with the tests. if platform.isWindows() and _PY3: self.flushWarnings() def test_warning(self): """ L{deprecate.warnAboutFunction} emits a warning the file and line number of which point to the beginning of the implementation of the function passed to it. """ def aFunc(): pass deprecate.warnAboutFunction(aFunc, 'A Warning Message') warningsShown = self.flushWarnings() filename = __file__ if filename.lower().endswith('.pyc'): filename = filename[:-1] self.assertSamePath( FilePath(warningsShown[0]["filename"]), FilePath(filename)) self.assertEqual(warningsShown[0]["message"], "A Warning Message") def test_warningLineNumber(self): """ L{deprecate.warnAboutFunction} emits a C{DeprecationWarning} with the number of a line within the implementation of the function passed to it. """ from twisted_private_helper import module module.callTestFunction() warningsShown = self.flushWarnings() self.assertSamePath( FilePath(warningsShown[0]["filename"].encode("utf-8")), self.package.sibling(b'twisted_private_helper').child(b'module.py')) # Line number 9 is the last line in the testFunction in the helper # module. self.assertEqual(warningsShown[0]["lineno"], 9) self.assertEqual(warningsShown[0]["message"], "A Warning String") self.assertEqual(len(warningsShown), 1) def assertSamePath(self, first, second): """ Assert that the two paths are the same, considering case normalization appropriate for the current platform. @type first: L{FilePath} @type second: L{FilePath} @raise C{self.failureType}: If the paths are not the same. """ self.assertTrue( normcase(first.path) == normcase(second.path), "%r != %r" % (first, second)) def test_renamedFile(self): """ Even if the implementation of a deprecated function is moved around on the filesystem, the line number in the warning emitted by L{deprecate.warnAboutFunction} points to a line in the implementation of the deprecated function. """ from twisted_private_helper import module # Clean up the state resulting from that import; we're not going to use # this module, so it should go away. del sys.modules['twisted_private_helper'] del sys.modules[module.__name__] # Rename the source directory self.package.moveTo(self.package.sibling(b'twisted_renamed_helper')) # Make sure importlib notices we've changed importable packages: if invalidate_caches: invalidate_caches() # Import the newly renamed version from twisted_renamed_helper import module self.addCleanup(sys.modules.pop, 'twisted_renamed_helper') self.addCleanup(sys.modules.pop, module.__name__) module.callTestFunction() warningsShown = self.flushWarnings([module.testFunction]) warnedPath = FilePath(warningsShown[0]["filename"].encode("utf-8")) expectedPath = self.package.sibling( b'twisted_renamed_helper').child(b'module.py') self.assertSamePath(warnedPath, expectedPath) self.assertEqual(warningsShown[0]["lineno"], 9) self.assertEqual(warningsShown[0]["message"], "A Warning String") self.assertEqual(len(warningsShown), 1) def test_filteredWarning(self): """ L{deprecate.warnAboutFunction} emits a warning that will be filtered if L{warnings.filterwarning} is called with the module name of the deprecated function. """ # Clean up anything *else* that might spuriously filter out the warning, # such as the "always" simplefilter set up by unittest._collectWarnings. # We'll also rely on trial to restore the original filters afterwards. del warnings.filters[:] warnings.filterwarnings( action="ignore", module="twisted_private_helper") from twisted_private_helper import module module.callTestFunction() warningsShown = self.flushWarnings() self.assertEqual(len(warningsShown), 0) def test_filteredOnceWarning(self): """ L{deprecate.warnAboutFunction} emits a warning that will be filtered once if L{warnings.filterwarning} is called with the module name of the deprecated function and an action of once. """ # Clean up anything *else* that might spuriously filter out the warning, # such as the "always" simplefilter set up by unittest._collectWarnings. # We'll also rely on trial to restore the original filters afterwards. del warnings.filters[:] warnings.filterwarnings( action="module", module="twisted_private_helper") from twisted_private_helper import module module.callTestFunction() module.callTestFunction() warningsShown = self.flushWarnings() self.assertEqual(len(warningsShown), 1) message = warningsShown[0]['message'] category = warningsShown[0]['category'] filename = warningsShown[0]['filename'] lineno = warningsShown[0]['lineno'] msg = warnings.formatwarning(message, category, filename, lineno) self.assertTrue( msg.endswith("module.py:9: DeprecationWarning: A Warning String\n" " return a\n"), "Unexpected warning string: %r" % (msg,))
def connectionLost(self, reason): if self.persistent: self._historyFd.close() path = FilePath(self.historyFile + ('.%d' % self._historySession)) path.moveTo(FilePath(self.historyFile)) ConsoleManhole.connectionLost(self, reason)
class WarnAboutFunctionTests(SynchronousTestCase): """ Tests for L{twisted.python.deprecate.warnAboutFunction} which allows the callers of a function to issue a C{DeprecationWarning} about that function. """ def setUp(self): """ Create a file that will have known line numbers when emitting warnings. """ self.package = FilePath( self.mktemp().encode("utf-8")).child(b'twisted_private_helper') self.package.makedirs() self.package.child(b'__init__.py').setContent(b'') self.package.child(b'module.py').setContent(b''' "A module string" from twisted.python import deprecate def testFunction(): "A doc string" a = 1 + 2 return a def callTestFunction(): b = testFunction() if b == 3: deprecate.warnAboutFunction(testFunction, "A Warning String") ''') # Python 3 doesn't accept bytes in sys.path: packagePath = self.package.parent().path.decode("utf-8") sys.path.insert(0, packagePath) self.addCleanup(sys.path.remove, packagePath) modules = sys.modules.copy() self.addCleanup(lambda: (sys.modules.clear(), sys.modules.update(modules))) def test_warning(self): """ L{deprecate.warnAboutFunction} emits a warning the file and line number of which point to the beginning of the implementation of the function passed to it. """ def aFunc(): pass deprecate.warnAboutFunction(aFunc, 'A Warning Message') warningsShown = self.flushWarnings() filename = __file__ if filename.lower().endswith('.pyc'): filename = filename[:-1] self.assertSamePath(FilePath(warningsShown[0]["filename"]), FilePath(filename)) self.assertEqual(warningsShown[0]["message"], "A Warning Message") def test_warningLineNumber(self): """ L{deprecate.warnAboutFunction} emits a C{DeprecationWarning} with the number of a line within the implementation of the function passed to it. """ from twisted_private_helper import module module.callTestFunction() warningsShown = self.flushWarnings() self.assertSamePath( FilePath(warningsShown[0]["filename"].encode("utf-8")), self.package.sibling(b'twisted_private_helper').child( b'module.py')) # Line number 9 is the last line in the testFunction in the helper # module. self.assertEqual(warningsShown[0]["lineno"], 9) self.assertEqual(warningsShown[0]["message"], "A Warning String") self.assertEqual(len(warningsShown), 1) def assertSamePath(self, first, second): """ Assert that the two paths are the same, considering case normalization appropriate for the current platform. @type first: L{FilePath} @type second: L{FilePath} @raise C{self.failureType}: If the paths are not the same. """ self.assertTrue( normcase(first.path) == normcase(second.path), "%r != %r" % (first, second)) def test_renamedFile(self): """ Even if the implementation of a deprecated function is moved around on the filesystem, the line number in the warning emitted by L{deprecate.warnAboutFunction} points to a line in the implementation of the deprecated function. """ from twisted_private_helper import module # Clean up the state resulting from that import; we're not going to use # this module, so it should go away. del sys.modules['twisted_private_helper'] del sys.modules[module.__name__] # Rename the source directory self.package.moveTo(self.package.sibling(b'twisted_renamed_helper')) # Make sure importlib notices we've changed importable packages: if invalidate_caches: invalidate_caches() # Import the newly renamed version from twisted_renamed_helper import module self.addCleanup(sys.modules.pop, 'twisted_renamed_helper') self.addCleanup(sys.modules.pop, module.__name__) module.callTestFunction() warningsShown = self.flushWarnings() warnedPath = FilePath(warningsShown[0]["filename"].encode("utf-8")) expectedPath = self.package.sibling(b'twisted_renamed_helper').child( b'module.py') self.assertSamePath(warnedPath, expectedPath) self.assertEqual(warningsShown[0]["lineno"], 9) self.assertEqual(warningsShown[0]["message"], "A Warning String") self.assertEqual(len(warningsShown), 1) def test_filteredWarning(self): """ L{deprecate.warnAboutFunction} emits a warning that will be filtered if L{warnings.filterwarning} is called with the module name of the deprecated function. """ # Clean up anything *else* that might spuriously filter out the warning, # such as the "always" simplefilter set up by unittest._collectWarnings. # We'll also rely on trial to restore the original filters afterwards. del warnings.filters[:] warnings.filterwarnings(action="ignore", module="twisted_private_helper") from twisted_private_helper import module module.callTestFunction() warningsShown = self.flushWarnings() self.assertEqual(len(warningsShown), 0) def test_filteredOnceWarning(self): """ L{deprecate.warnAboutFunction} emits a warning that will be filtered once if L{warnings.filterwarning} is called with the module name of the deprecated function and an action of once. """ # Clean up anything *else* that might spuriously filter out the warning, # such as the "always" simplefilter set up by unittest._collectWarnings. # We'll also rely on trial to restore the original filters afterwards. del warnings.filters[:] warnings.filterwarnings(action="module", module="twisted_private_helper") from twisted_private_helper import module module.callTestFunction() module.callTestFunction() warningsShown = self.flushWarnings() self.assertEqual(len(warningsShown), 1) message = warningsShown[0]['message'] category = warningsShown[0]['category'] filename = warningsShown[0]['filename'] lineno = warningsShown[0]['lineno'] msg = warnings.formatwarning(message, category, filename, lineno) self.assertTrue( msg.endswith("module.py:9: DeprecationWarning: A Warning String\n" " return a\n"), "Unexpected warning string: %r" % (msg, ))