RAHHH
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
"""
|
||||
Unit tests for L{twisted.python}.
|
||||
"""
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,23 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
A module that is deprecated, used by L{twisted.python.test.test_deprecate} for
|
||||
testing purposes.
|
||||
"""
|
||||
|
||||
|
||||
from incremental import Version
|
||||
|
||||
from twisted.python.deprecate import deprecatedModuleAttribute
|
||||
|
||||
# Known module-level attributes.
|
||||
DEPRECATED_ATTRIBUTE = 42
|
||||
ANOTHER_ATTRIBUTE = "hello"
|
||||
|
||||
|
||||
version = Version("Twisted", 8, 0, 0)
|
||||
message = "Oh noes!"
|
||||
|
||||
|
||||
deprecatedModuleAttribute(version, message, __name__, "DEPRECATED_ATTRIBUTE")
|
||||
@@ -0,0 +1,57 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Facilities for helping test code which interacts with Python's module system
|
||||
to load code.
|
||||
"""
|
||||
|
||||
import sys
|
||||
from types import ModuleType
|
||||
from typing import Iterable, List, Tuple
|
||||
|
||||
from twisted.python.filepath import FilePath
|
||||
|
||||
|
||||
class TwistedModulesMixin:
|
||||
"""
|
||||
A mixin for C{twisted.trial.unittest.SynchronousTestCase} providing useful
|
||||
methods for manipulating Python's module system.
|
||||
"""
|
||||
|
||||
def replaceSysPath(self, sysPath: List[str]) -> None:
|
||||
"""
|
||||
Replace sys.path, for the duration of the test, with the given value.
|
||||
"""
|
||||
originalSysPath = sys.path[:]
|
||||
|
||||
def cleanUpSysPath() -> None:
|
||||
sys.path[:] = originalSysPath
|
||||
|
||||
self.addCleanup(cleanUpSysPath) # type: ignore[attr-defined]
|
||||
sys.path[:] = sysPath
|
||||
|
||||
def replaceSysModules(self, sysModules: Iterable[Tuple[str, ModuleType]]) -> None:
|
||||
"""
|
||||
Replace sys.modules, for the duration of the test, with the given value.
|
||||
"""
|
||||
originalSysModules = sys.modules.copy()
|
||||
|
||||
def cleanUpSysModules() -> None:
|
||||
sys.modules.clear()
|
||||
sys.modules.update(originalSysModules)
|
||||
|
||||
self.addCleanup(cleanUpSysModules) # type: ignore[attr-defined]
|
||||
sys.modules.clear()
|
||||
sys.modules.update(sysModules)
|
||||
|
||||
def pathEntryWithOnePackage(self, pkgname: str = "test_package") -> FilePath[str]:
|
||||
"""
|
||||
Generate a L{FilePath} with one package, named C{pkgname}, on it, and
|
||||
return the L{FilePath} of the path entry.
|
||||
"""
|
||||
entry = FilePath(self.mktemp()) # type: ignore[attr-defined]
|
||||
pkg = entry.child("test_package")
|
||||
pkg.makedirs()
|
||||
pkg.child("__init__.py").setContent(b"")
|
||||
return entry
|
||||
@@ -0,0 +1,40 @@
|
||||
# -*- test-case-name: twisted.python.test.test_sendmsg -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
import os
|
||||
import socket
|
||||
import sys
|
||||
from struct import unpack
|
||||
from typing import Tuple
|
||||
|
||||
from twisted.python.sendmsg import recvmsg
|
||||
|
||||
|
||||
def recvfd(socketfd: int) -> Tuple[int, bytes]:
|
||||
"""
|
||||
Receive a file descriptor from a L{sendmsg} message on the given C{AF_UNIX}
|
||||
socket.
|
||||
|
||||
@param socketfd: An C{AF_UNIX} socket, attached to another process waiting
|
||||
to send sockets via the ancillary data mechanism in L{send1msg}.
|
||||
|
||||
@param fd: C{int}
|
||||
|
||||
@return: a 2-tuple of (new file descriptor, description).
|
||||
@rtype: 2-tuple of (C{int}, C{bytes})
|
||||
"""
|
||||
ourSocket = socket.fromfd(socketfd, socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
data, ancillary, flags = recvmsg(ourSocket)
|
||||
[(cmsgLevel, cmsgType, packedFD)] = ancillary
|
||||
# cmsgLevel and cmsgType really need to be SOL_SOCKET / SCM_RIGHTS, but
|
||||
# since those are the *only* standard values, there's not much point in
|
||||
# checking.
|
||||
[unpackedFD] = unpack("i", packedFD)
|
||||
return (unpackedFD, data)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
fd, description = recvfd(int(sys.argv[1]))
|
||||
os.write(fd, b"Test fixture data: " + description + b".\n")
|
||||
os.close(fd)
|
||||
@@ -0,0 +1,35 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Hypothesis strategies for values related to L{twisted.python}.
|
||||
"""
|
||||
|
||||
from hypothesis.strategies import SearchStrategy, characters, text
|
||||
from typing_extensions import Literal
|
||||
|
||||
|
||||
def systemdDescriptorNames() -> SearchStrategy[str]:
|
||||
"""
|
||||
Build strings that are legal values for the systemd
|
||||
I{FileDescriptorName} field.
|
||||
"""
|
||||
# systemd.socket(5) says:
|
||||
#
|
||||
# > Names may contain any ASCII character, but must exclude control
|
||||
# > characters and ":", and must be at most 255 characters in length.
|
||||
control_characters: Literal["Cc"] = "Cc"
|
||||
return text(
|
||||
# The docs don't say there is a min size so I'm guessing...
|
||||
min_size=1,
|
||||
max_size=255,
|
||||
alphabet=characters(
|
||||
# These constraints restrict us to ASCII.
|
||||
min_codepoint=0,
|
||||
max_codepoint=127,
|
||||
# This one excludes control characters.
|
||||
blacklist_categories=(control_characters,),
|
||||
# And this excludes the separator.
|
||||
blacklist_characters=(":",),
|
||||
),
|
||||
)
|
||||
@@ -0,0 +1,40 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Tests for the data directory support.
|
||||
"""
|
||||
|
||||
|
||||
try:
|
||||
from twisted.python import _appdirs
|
||||
except ImportError:
|
||||
_appdirs = None # type: ignore[assignment]
|
||||
|
||||
from twisted.trial import unittest
|
||||
|
||||
|
||||
class AppdirsTests(unittest.TestCase):
|
||||
"""
|
||||
Tests for L{_appdirs}.
|
||||
"""
|
||||
|
||||
if not _appdirs:
|
||||
skip = "appdirs package not installed"
|
||||
|
||||
def test_moduleName(self) -> None:
|
||||
"""
|
||||
Calling L{appdirs.getDataDirectory} will return a user data directory
|
||||
in the system convention, with the module of the caller as the
|
||||
subdirectory.
|
||||
"""
|
||||
res = _appdirs.getDataDirectory()
|
||||
self.assertTrue(res.endswith("twisted.python.test.test_appdirs"))
|
||||
|
||||
def test_manual(self) -> None:
|
||||
"""
|
||||
Calling L{appdirs.getDataDirectory} with a C{moduleName} argument will
|
||||
make a data directory with that name instead.
|
||||
"""
|
||||
res = _appdirs.getDataDirectory("foo.bar.baz")
|
||||
self.assertTrue(res.endswith("foo.bar.baz"))
|
||||
@@ -0,0 +1,878 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
|
||||
"""
|
||||
Test cases for Twisted component architecture.
|
||||
"""
|
||||
|
||||
|
||||
from functools import wraps
|
||||
|
||||
from zope.interface import Attribute, Interface, implementer
|
||||
from zope.interface.adapter import AdapterRegistry
|
||||
|
||||
from twisted.python import components
|
||||
from twisted.python.compat import cmp, comparable
|
||||
from twisted.python.components import _addHook, _removeHook, proxyForInterface
|
||||
from twisted.trial import unittest
|
||||
|
||||
|
||||
class Compo(components.Componentized):
|
||||
num = 0
|
||||
|
||||
def inc(self):
|
||||
self.num = self.num + 1
|
||||
return self.num
|
||||
|
||||
|
||||
class IAdept(Interface):
|
||||
def adaptorFunc():
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
class IElapsed(Interface):
|
||||
def elapsedFunc():
|
||||
"""
|
||||
1!
|
||||
"""
|
||||
|
||||
|
||||
@implementer(IAdept)
|
||||
class Adept(components.Adapter):
|
||||
def __init__(self, orig):
|
||||
self.original = orig
|
||||
self.num = 0
|
||||
|
||||
def adaptorFunc(self):
|
||||
self.num = self.num + 1
|
||||
return self.num, self.original.inc()
|
||||
|
||||
|
||||
@implementer(IElapsed)
|
||||
class Elapsed(components.Adapter):
|
||||
def elapsedFunc(self):
|
||||
return 1
|
||||
|
||||
|
||||
class AComp(components.Componentized):
|
||||
pass
|
||||
|
||||
|
||||
class BComp(AComp):
|
||||
pass
|
||||
|
||||
|
||||
class CComp(BComp):
|
||||
pass
|
||||
|
||||
|
||||
class ITest(Interface):
|
||||
pass
|
||||
|
||||
|
||||
class ITest2(Interface):
|
||||
pass
|
||||
|
||||
|
||||
class ITest3(Interface):
|
||||
pass
|
||||
|
||||
|
||||
class ITest4(Interface):
|
||||
pass
|
||||
|
||||
|
||||
@implementer(ITest, ITest3, ITest4)
|
||||
class Test(components.Adapter):
|
||||
def __init__(self, orig):
|
||||
pass
|
||||
|
||||
|
||||
@implementer(ITest2)
|
||||
class Test2:
|
||||
temporaryAdapter = 1
|
||||
|
||||
def __init__(self, orig):
|
||||
pass
|
||||
|
||||
|
||||
class RegistryUsingMixin:
|
||||
"""
|
||||
Mixin for test cases which modify the global registry somehow.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
Configure L{twisted.python.components.registerAdapter} to mutate an
|
||||
alternate registry to improve test isolation.
|
||||
"""
|
||||
# Create a brand new, empty registry and put it onto the components
|
||||
# module where registerAdapter will use it. Also ensure that it goes
|
||||
# away at the end of the test.
|
||||
scratchRegistry = AdapterRegistry()
|
||||
self.patch(components, "globalRegistry", scratchRegistry)
|
||||
# Hook the new registry up to the adapter lookup system and ensure that
|
||||
# association is also discarded after the test.
|
||||
hook = _addHook(scratchRegistry)
|
||||
self.addCleanup(_removeHook, hook)
|
||||
|
||||
|
||||
class ComponentizedTests(unittest.SynchronousTestCase, RegistryUsingMixin):
|
||||
"""
|
||||
Simple test case for caching in Componentized.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
RegistryUsingMixin.setUp(self)
|
||||
|
||||
components.registerAdapter(Test, AComp, ITest)
|
||||
components.registerAdapter(Test, AComp, ITest3)
|
||||
components.registerAdapter(Test2, AComp, ITest2)
|
||||
|
||||
def testComponentized(self):
|
||||
components.registerAdapter(Adept, Compo, IAdept)
|
||||
components.registerAdapter(Elapsed, Compo, IElapsed)
|
||||
|
||||
c = Compo()
|
||||
assert c.getComponent(IAdept).adaptorFunc() == (1, 1)
|
||||
assert c.getComponent(IAdept).adaptorFunc() == (2, 2)
|
||||
assert IElapsed(IAdept(c)).elapsedFunc() == 1
|
||||
|
||||
def testInheritanceAdaptation(self):
|
||||
c = CComp()
|
||||
co1 = c.getComponent(ITest)
|
||||
co2 = c.getComponent(ITest)
|
||||
co3 = c.getComponent(ITest2)
|
||||
co4 = c.getComponent(ITest2)
|
||||
assert co1 is co2
|
||||
assert co3 is not co4
|
||||
c.removeComponent(co1)
|
||||
co5 = c.getComponent(ITest)
|
||||
co6 = c.getComponent(ITest)
|
||||
assert co5 is co6
|
||||
assert co1 is not co5
|
||||
|
||||
def testMultiAdapter(self):
|
||||
c = CComp()
|
||||
co1 = c.getComponent(ITest)
|
||||
co3 = c.getComponent(ITest3)
|
||||
co4 = c.getComponent(ITest4)
|
||||
self.assertIsNone(co4)
|
||||
self.assertIs(co1, co3)
|
||||
|
||||
def test_getComponentDefaults(self):
|
||||
"""
|
||||
Test that a default value specified to Componentized.getComponent if
|
||||
there is no component for the requested interface.
|
||||
"""
|
||||
componentized = components.Componentized()
|
||||
default = object()
|
||||
self.assertIs(componentized.getComponent(ITest, default), default)
|
||||
self.assertIs(componentized.getComponent(ITest, default=default), default)
|
||||
self.assertIs(componentized.getComponent(ITest), None)
|
||||
|
||||
def test_setAdapter(self):
|
||||
"""
|
||||
C{Componentized.setAdapter} sets a component for an interface by
|
||||
wrapping the instance with the given adapter class.
|
||||
"""
|
||||
componentized = components.Componentized()
|
||||
componentized.setAdapter(IAdept, Adept)
|
||||
component = componentized.getComponent(IAdept)
|
||||
self.assertEqual(component.original, componentized)
|
||||
self.assertIsInstance(component, Adept)
|
||||
|
||||
def test_addAdapter(self):
|
||||
"""
|
||||
C{Componentized.setAdapter} adapts the instance by wrapping it with
|
||||
given adapter class, then stores it using C{addComponent}.
|
||||
"""
|
||||
componentized = components.Componentized()
|
||||
componentized.addAdapter(Adept, ignoreClass=True)
|
||||
component = componentized.getComponent(IAdept)
|
||||
self.assertEqual(component.original, componentized)
|
||||
self.assertIsInstance(component, Adept)
|
||||
|
||||
def test_setComponent(self):
|
||||
"""
|
||||
C{Componentized.setComponent} stores the given component using the
|
||||
given interface as the key.
|
||||
"""
|
||||
componentized = components.Componentized()
|
||||
obj = object()
|
||||
componentized.setComponent(ITest, obj)
|
||||
self.assertIs(componentized.getComponent(ITest), obj)
|
||||
|
||||
def test_unsetComponent(self):
|
||||
"""
|
||||
C{Componentized.setComponent} removes the cached component for the
|
||||
given interface.
|
||||
"""
|
||||
componentized = components.Componentized()
|
||||
obj = object()
|
||||
componentized.setComponent(ITest, obj)
|
||||
componentized.unsetComponent(ITest)
|
||||
self.assertIsNone(componentized.getComponent(ITest))
|
||||
|
||||
def test_reprableComponentized(self):
|
||||
"""
|
||||
C{ReprableComponentized} has a C{__repr__} that lists its cache.
|
||||
"""
|
||||
rc = components.ReprableComponentized()
|
||||
rc.setComponent(ITest, "hello")
|
||||
result = repr(rc)
|
||||
self.assertIn("ITest", result)
|
||||
self.assertIn("hello", result)
|
||||
|
||||
|
||||
class AdapterTests(unittest.SynchronousTestCase):
|
||||
"""Test adapters."""
|
||||
|
||||
def testAdapterGetComponent(self):
|
||||
o = object()
|
||||
a = Adept(o)
|
||||
self.assertRaises(components.CannotAdapt, ITest, a)
|
||||
self.assertIsNone(ITest(a, None))
|
||||
|
||||
|
||||
class IMeta(Interface):
|
||||
pass
|
||||
|
||||
|
||||
@implementer(IMeta)
|
||||
class MetaAdder(components.Adapter):
|
||||
def add(self, num):
|
||||
return self.original.num + num
|
||||
|
||||
|
||||
@implementer(IMeta)
|
||||
class BackwardsAdder(components.Adapter):
|
||||
def add(self, num):
|
||||
return self.original.num - num
|
||||
|
||||
|
||||
class MetaNumber:
|
||||
"""
|
||||
Integer wrapper for Interface adaptation tests.
|
||||
"""
|
||||
|
||||
def __init__(self, num):
|
||||
self.num = num
|
||||
|
||||
|
||||
class ComponentNumber(components.Componentized):
|
||||
def __init__(self):
|
||||
self.num = 0
|
||||
components.Componentized.__init__(self)
|
||||
|
||||
|
||||
@implementer(IMeta)
|
||||
class ComponentAdder(components.Adapter):
|
||||
"""
|
||||
Adder for componentized adapter tests.
|
||||
"""
|
||||
|
||||
def __init__(self, original):
|
||||
components.Adapter.__init__(self, original)
|
||||
self.num = self.original.num
|
||||
|
||||
def add(self, num):
|
||||
self.num += num
|
||||
return self.num
|
||||
|
||||
|
||||
class IAttrX(Interface):
|
||||
"""
|
||||
Base interface for test of adapter with C{__cmp__}.
|
||||
"""
|
||||
|
||||
def x():
|
||||
"""
|
||||
Return a value.
|
||||
"""
|
||||
|
||||
|
||||
class IAttrXX(Interface):
|
||||
"""
|
||||
Adapted interface for test of adapter with C{__cmp__}.
|
||||
"""
|
||||
|
||||
def xx():
|
||||
"""
|
||||
Return a tuple of values.
|
||||
"""
|
||||
|
||||
|
||||
@implementer(IAttrX)
|
||||
class Xcellent:
|
||||
"""
|
||||
L{IAttrX} implementation for test of adapter with C{__cmp__}.
|
||||
"""
|
||||
|
||||
def x(self):
|
||||
"""
|
||||
Return a value.
|
||||
|
||||
@return: a value
|
||||
"""
|
||||
return "x!"
|
||||
|
||||
|
||||
@comparable
|
||||
class DoubleXAdapter:
|
||||
"""
|
||||
Adapter with __cmp__.
|
||||
"""
|
||||
|
||||
num = 42
|
||||
|
||||
def __init__(self, original):
|
||||
self.original = original
|
||||
|
||||
def xx(self):
|
||||
return (self.original.x(), self.original.x())
|
||||
|
||||
def __cmp__(self, other):
|
||||
return cmp(self.num, other.num)
|
||||
|
||||
|
||||
class MetaInterfaceTests(RegistryUsingMixin, unittest.SynchronousTestCase):
|
||||
def test_basic(self):
|
||||
"""
|
||||
Registered adapters can be used to adapt classes to an interface.
|
||||
"""
|
||||
components.registerAdapter(MetaAdder, MetaNumber, IMeta)
|
||||
n = MetaNumber(1)
|
||||
self.assertEqual(IMeta(n).add(1), 2)
|
||||
|
||||
def testComponentizedInteraction(self):
|
||||
components.registerAdapter(ComponentAdder, ComponentNumber, IMeta)
|
||||
c = ComponentNumber()
|
||||
IMeta(c).add(1)
|
||||
IMeta(c).add(1)
|
||||
self.assertEqual(IMeta(c).add(1), 3)
|
||||
|
||||
def testAdapterWithCmp(self):
|
||||
# Make sure that a __cmp__ on an adapter doesn't break anything
|
||||
components.registerAdapter(DoubleXAdapter, IAttrX, IAttrXX)
|
||||
xx = IAttrXX(Xcellent())
|
||||
self.assertEqual(("x!", "x!"), xx.xx())
|
||||
|
||||
|
||||
class RegistrationTests(RegistryUsingMixin, unittest.SynchronousTestCase):
|
||||
"""
|
||||
Tests for adapter registration.
|
||||
"""
|
||||
|
||||
def _registerAdapterForClassOrInterface(self, original):
|
||||
"""
|
||||
Register an adapter with L{components.registerAdapter} for the given
|
||||
class or interface and verify that the adapter can be looked up with
|
||||
L{components.getAdapterFactory}.
|
||||
"""
|
||||
adapter = lambda o: None
|
||||
components.registerAdapter(adapter, original, ITest)
|
||||
self.assertIs(components.getAdapterFactory(original, ITest, None), adapter)
|
||||
|
||||
def test_registerAdapterForClass(self):
|
||||
"""
|
||||
Test that an adapter from a class can be registered and then looked
|
||||
up.
|
||||
"""
|
||||
|
||||
class TheOriginal:
|
||||
pass
|
||||
|
||||
return self._registerAdapterForClassOrInterface(TheOriginal)
|
||||
|
||||
def test_registerAdapterForInterface(self):
|
||||
"""
|
||||
Test that an adapter from an interface can be registered and then
|
||||
looked up.
|
||||
"""
|
||||
return self._registerAdapterForClassOrInterface(ITest2)
|
||||
|
||||
def _duplicateAdapterForClassOrInterface(self, original):
|
||||
"""
|
||||
Verify that L{components.registerAdapter} raises L{ValueError} if the
|
||||
from-type/interface and to-interface pair is not unique.
|
||||
"""
|
||||
firstAdapter = lambda o: False
|
||||
secondAdapter = lambda o: True
|
||||
components.registerAdapter(firstAdapter, original, ITest)
|
||||
self.assertRaises(
|
||||
ValueError, components.registerAdapter, secondAdapter, original, ITest
|
||||
)
|
||||
# Make sure that the original adapter is still around as well
|
||||
self.assertIs(components.getAdapterFactory(original, ITest, None), firstAdapter)
|
||||
|
||||
def test_duplicateAdapterForClass(self):
|
||||
"""
|
||||
Test that attempting to register a second adapter from a class
|
||||
raises the appropriate exception.
|
||||
"""
|
||||
|
||||
class TheOriginal:
|
||||
pass
|
||||
|
||||
return self._duplicateAdapterForClassOrInterface(TheOriginal)
|
||||
|
||||
def test_duplicateAdapterForInterface(self):
|
||||
"""
|
||||
Test that attempting to register a second adapter from an interface
|
||||
raises the appropriate exception.
|
||||
"""
|
||||
return self._duplicateAdapterForClassOrInterface(ITest2)
|
||||
|
||||
def _duplicateAdapterForClassOrInterfaceAllowed(self, original):
|
||||
"""
|
||||
Verify that when C{components.ALLOW_DUPLICATES} is set to C{True}, new
|
||||
adapter registrations for a particular from-type/interface and
|
||||
to-interface pair replace older registrations.
|
||||
"""
|
||||
firstAdapter = lambda o: False
|
||||
secondAdapter = lambda o: True
|
||||
|
||||
class TheInterface(Interface):
|
||||
pass
|
||||
|
||||
components.registerAdapter(firstAdapter, original, TheInterface)
|
||||
components.ALLOW_DUPLICATES = True
|
||||
try:
|
||||
components.registerAdapter(secondAdapter, original, TheInterface)
|
||||
self.assertIs(
|
||||
components.getAdapterFactory(original, TheInterface, None),
|
||||
secondAdapter,
|
||||
)
|
||||
finally:
|
||||
components.ALLOW_DUPLICATES = False
|
||||
|
||||
# It should be rejected again at this point
|
||||
self.assertRaises(
|
||||
ValueError, components.registerAdapter, firstAdapter, original, TheInterface
|
||||
)
|
||||
|
||||
self.assertIs(
|
||||
components.getAdapterFactory(original, TheInterface, None), secondAdapter
|
||||
)
|
||||
|
||||
def test_duplicateAdapterForClassAllowed(self):
|
||||
"""
|
||||
Test that when L{components.ALLOW_DUPLICATES} is set to a true
|
||||
value, duplicate registrations from classes are allowed to override
|
||||
the original registration.
|
||||
"""
|
||||
|
||||
class TheOriginal:
|
||||
pass
|
||||
|
||||
return self._duplicateAdapterForClassOrInterfaceAllowed(TheOriginal)
|
||||
|
||||
def test_duplicateAdapterForInterfaceAllowed(self):
|
||||
"""
|
||||
Test that when L{components.ALLOW_DUPLICATES} is set to a true
|
||||
value, duplicate registrations from interfaces are allowed to
|
||||
override the original registration.
|
||||
"""
|
||||
|
||||
class TheOriginal(Interface):
|
||||
pass
|
||||
|
||||
return self._duplicateAdapterForClassOrInterfaceAllowed(TheOriginal)
|
||||
|
||||
def _multipleInterfacesForClassOrInterface(self, original):
|
||||
"""
|
||||
Verify that an adapter can be registered for multiple to-interfaces at a
|
||||
time.
|
||||
"""
|
||||
adapter = lambda o: None
|
||||
components.registerAdapter(adapter, original, ITest, ITest2)
|
||||
self.assertIs(components.getAdapterFactory(original, ITest, None), adapter)
|
||||
self.assertIs(components.getAdapterFactory(original, ITest2, None), adapter)
|
||||
|
||||
def test_multipleInterfacesForClass(self):
|
||||
"""
|
||||
Test the registration of an adapter from a class to several
|
||||
interfaces at once.
|
||||
"""
|
||||
|
||||
class TheOriginal:
|
||||
pass
|
||||
|
||||
return self._multipleInterfacesForClassOrInterface(TheOriginal)
|
||||
|
||||
def test_multipleInterfacesForInterface(self):
|
||||
"""
|
||||
Test the registration of an adapter from an interface to several
|
||||
interfaces at once.
|
||||
"""
|
||||
return self._multipleInterfacesForClassOrInterface(ITest3)
|
||||
|
||||
def _subclassAdapterRegistrationForClassOrInterface(self, original):
|
||||
"""
|
||||
Verify that a new adapter can be registered for a particular
|
||||
to-interface from a subclass of a type or interface which already has an
|
||||
adapter registered to that interface and that the subclass adapter takes
|
||||
precedence over the base class adapter.
|
||||
"""
|
||||
firstAdapter = lambda o: True
|
||||
secondAdapter = lambda o: False
|
||||
|
||||
class TheSubclass(original):
|
||||
pass
|
||||
|
||||
components.registerAdapter(firstAdapter, original, ITest)
|
||||
components.registerAdapter(secondAdapter, TheSubclass, ITest)
|
||||
self.assertIs(components.getAdapterFactory(original, ITest, None), firstAdapter)
|
||||
self.assertIs(
|
||||
components.getAdapterFactory(TheSubclass, ITest, None), secondAdapter
|
||||
)
|
||||
|
||||
def test_subclassAdapterRegistrationForClass(self):
|
||||
"""
|
||||
Test that an adapter to a particular interface can be registered
|
||||
from both a class and its subclass.
|
||||
"""
|
||||
|
||||
class TheOriginal:
|
||||
pass
|
||||
|
||||
return self._subclassAdapterRegistrationForClassOrInterface(TheOriginal)
|
||||
|
||||
def test_subclassAdapterRegistrationForInterface(self):
|
||||
"""
|
||||
Test that an adapter to a particular interface can be registered
|
||||
from both an interface and its subclass.
|
||||
"""
|
||||
return self._subclassAdapterRegistrationForClassOrInterface(ITest2)
|
||||
|
||||
|
||||
class IProxiedInterface(Interface):
|
||||
"""
|
||||
An interface class for use by L{proxyForInterface}.
|
||||
"""
|
||||
|
||||
ifaceAttribute = Attribute(
|
||||
"""
|
||||
An example declared attribute, which should be proxied."""
|
||||
)
|
||||
|
||||
def yay(*a, **kw):
|
||||
"""
|
||||
A sample method which should be proxied.
|
||||
"""
|
||||
|
||||
|
||||
class IProxiedSubInterface(IProxiedInterface):
|
||||
"""
|
||||
An interface that derives from another for use with L{proxyForInterface}.
|
||||
"""
|
||||
|
||||
def boo():
|
||||
"""
|
||||
A different sample method which should be proxied.
|
||||
"""
|
||||
|
||||
|
||||
@implementer(IProxiedInterface)
|
||||
class Yayable: # type: ignore[misc]
|
||||
# class does not implement Attribute ifaceAttribute
|
||||
# so we need to turn off mypy warning
|
||||
"""
|
||||
A provider of L{IProxiedInterface} which increments a counter for
|
||||
every call to C{yay}.
|
||||
|
||||
@ivar yays: The number of times C{yay} has been called.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.yays = 0
|
||||
self.yayArgs = []
|
||||
|
||||
def yay(self, *a, **kw):
|
||||
"""
|
||||
Increment C{self.yays}.
|
||||
"""
|
||||
self.yays += 1
|
||||
self.yayArgs.append((a, kw))
|
||||
return self.yays
|
||||
|
||||
|
||||
@implementer(IProxiedSubInterface)
|
||||
class Booable: # type: ignore[misc]
|
||||
# class does not implement Attribute ifaceAttribute
|
||||
# so we need to turn off mypy warning
|
||||
"""
|
||||
An implementation of IProxiedSubInterface
|
||||
"""
|
||||
|
||||
yayed = False
|
||||
booed = False
|
||||
|
||||
def yay(self, *a, **kw):
|
||||
"""
|
||||
Mark the fact that 'yay' has been called.
|
||||
"""
|
||||
self.yayed = True
|
||||
|
||||
def boo(self):
|
||||
"""
|
||||
Mark the fact that 'boo' has been called.1
|
||||
"""
|
||||
self.booed = True
|
||||
|
||||
|
||||
class IMultipleMethods(Interface):
|
||||
"""
|
||||
An interface with multiple methods.
|
||||
"""
|
||||
|
||||
def methodOne():
|
||||
"""
|
||||
The first method. Should return 1.
|
||||
"""
|
||||
|
||||
def methodTwo():
|
||||
"""
|
||||
The second method. Should return 2.
|
||||
"""
|
||||
|
||||
|
||||
class MultipleMethodImplementor:
|
||||
"""
|
||||
A precise implementation of L{IMultipleMethods}.
|
||||
"""
|
||||
|
||||
def methodOne(self):
|
||||
"""
|
||||
@return: 1
|
||||
"""
|
||||
return 1
|
||||
|
||||
def methodTwo(self):
|
||||
"""
|
||||
@return: 2
|
||||
"""
|
||||
return 2
|
||||
|
||||
|
||||
class ProxyForInterfaceTests(unittest.SynchronousTestCase):
|
||||
"""
|
||||
Tests for L{proxyForInterface}.
|
||||
"""
|
||||
|
||||
def test_original(self):
|
||||
"""
|
||||
Proxy objects should have an C{original} attribute which refers to the
|
||||
original object passed to the constructor.
|
||||
"""
|
||||
original = object()
|
||||
proxy = proxyForInterface(IProxiedInterface)(original)
|
||||
self.assertIs(proxy.original, original)
|
||||
|
||||
def test_proxyMethod(self):
|
||||
"""
|
||||
The class created from L{proxyForInterface} passes methods on an
|
||||
interface to the object which is passed to its constructor.
|
||||
"""
|
||||
klass = proxyForInterface(IProxiedInterface)
|
||||
yayable = Yayable()
|
||||
proxy = klass(yayable)
|
||||
proxy.yay()
|
||||
self.assertEqual(proxy.yay(), 2)
|
||||
self.assertEqual(yayable.yays, 2)
|
||||
|
||||
def test_decoratedProxyMethod(self):
|
||||
"""
|
||||
Methods of the class created from L{proxyForInterface} can be used with
|
||||
the decorator-helper L{functools.wraps}.
|
||||
"""
|
||||
base = proxyForInterface(IProxiedInterface)
|
||||
|
||||
class klass(base):
|
||||
@wraps(base.yay)
|
||||
def yay(self):
|
||||
self.original.yays += 1
|
||||
return base.yay(self)
|
||||
|
||||
original = Yayable()
|
||||
yayable = klass(original)
|
||||
yayable.yay()
|
||||
self.assertEqual(2, original.yays)
|
||||
|
||||
def test_proxyAttribute(self):
|
||||
"""
|
||||
Proxy objects should proxy declared attributes, but not other
|
||||
attributes.
|
||||
"""
|
||||
yayable = Yayable()
|
||||
yayable.ifaceAttribute = object()
|
||||
proxy = proxyForInterface(IProxiedInterface)(yayable)
|
||||
self.assertIs(proxy.ifaceAttribute, yayable.ifaceAttribute)
|
||||
self.assertRaises(AttributeError, lambda: proxy.yays)
|
||||
|
||||
def test_proxySetAttribute(self):
|
||||
"""
|
||||
The attributes that proxy objects proxy should be assignable and affect
|
||||
the original object.
|
||||
"""
|
||||
yayable = Yayable()
|
||||
proxy = proxyForInterface(IProxiedInterface)(yayable)
|
||||
thingy = object()
|
||||
proxy.ifaceAttribute = thingy
|
||||
self.assertIs(yayable.ifaceAttribute, thingy)
|
||||
|
||||
def test_proxyDeleteAttribute(self):
|
||||
"""
|
||||
The attributes that proxy objects proxy should be deletable and affect
|
||||
the original object.
|
||||
"""
|
||||
yayable = Yayable()
|
||||
yayable.ifaceAttribute = None
|
||||
proxy = proxyForInterface(IProxiedInterface)(yayable)
|
||||
del proxy.ifaceAttribute
|
||||
self.assertFalse(hasattr(yayable, "ifaceAttribute"))
|
||||
|
||||
def test_multipleMethods(self):
|
||||
"""
|
||||
[Regression test] The proxy should send its method calls to the correct
|
||||
method, not the incorrect one.
|
||||
"""
|
||||
multi = MultipleMethodImplementor()
|
||||
proxy = proxyForInterface(IMultipleMethods)(multi)
|
||||
self.assertEqual(proxy.methodOne(), 1)
|
||||
self.assertEqual(proxy.methodTwo(), 2)
|
||||
|
||||
def test_subclassing(self):
|
||||
"""
|
||||
It is possible to subclass the result of L{proxyForInterface}.
|
||||
"""
|
||||
|
||||
class SpecializedProxy(proxyForInterface(IProxiedInterface)):
|
||||
"""
|
||||
A specialized proxy which can decrement the number of yays.
|
||||
"""
|
||||
|
||||
def boo(self):
|
||||
"""
|
||||
Decrement the number of yays.
|
||||
"""
|
||||
self.original.yays -= 1
|
||||
|
||||
yayable = Yayable()
|
||||
special = SpecializedProxy(yayable)
|
||||
self.assertEqual(yayable.yays, 0)
|
||||
special.boo()
|
||||
self.assertEqual(yayable.yays, -1)
|
||||
|
||||
def test_proxyName(self):
|
||||
"""
|
||||
The name of a proxy class indicates which interface it proxies.
|
||||
"""
|
||||
proxy = proxyForInterface(IProxiedInterface)
|
||||
self.assertEqual(
|
||||
proxy.__name__,
|
||||
"(Proxy for " "twisted.python.test.test_components.IProxiedInterface)",
|
||||
)
|
||||
|
||||
def test_implements(self):
|
||||
"""
|
||||
The resulting proxy implements the interface that it proxies.
|
||||
"""
|
||||
proxy = proxyForInterface(IProxiedInterface)
|
||||
self.assertTrue(IProxiedInterface.implementedBy(proxy))
|
||||
|
||||
def test_proxyDescriptorGet(self):
|
||||
"""
|
||||
_ProxyDescriptor's __get__ method should return the appropriate
|
||||
attribute of its argument's 'original' attribute if it is invoked with
|
||||
an object. If it is invoked with None, it should return a false
|
||||
class-method emulator instead.
|
||||
|
||||
For some reason, Python's documentation recommends to define
|
||||
descriptors' __get__ methods with the 'type' parameter as optional,
|
||||
despite the fact that Python itself never actually calls the descriptor
|
||||
that way. This is probably do to support 'foo.__get__(bar)' as an
|
||||
idiom. Let's make sure that the behavior is correct. Since we don't
|
||||
actually use the 'type' argument at all, this test calls it the
|
||||
idiomatic way to ensure that signature works; test_proxyInheritance
|
||||
verifies the how-Python-actually-calls-it signature.
|
||||
"""
|
||||
|
||||
class Sample:
|
||||
called = False
|
||||
|
||||
def hello(self):
|
||||
self.called = True
|
||||
|
||||
fakeProxy = Sample()
|
||||
testObject = Sample()
|
||||
fakeProxy.original = testObject
|
||||
pd = components._ProxyDescriptor("hello", "original")
|
||||
self.assertEqual(pd.__get__(fakeProxy), testObject.hello)
|
||||
fakeClassMethod = pd.__get__(None)
|
||||
fakeClassMethod(fakeProxy)
|
||||
self.assertTrue(testObject.called)
|
||||
|
||||
def test_proxyInheritance(self):
|
||||
"""
|
||||
Subclasses of the class returned from L{proxyForInterface} should be
|
||||
able to upcall methods by reference to their superclass, as any normal
|
||||
Python class can.
|
||||
"""
|
||||
|
||||
class YayableWrapper(proxyForInterface(IProxiedInterface)):
|
||||
"""
|
||||
This class does not override any functionality.
|
||||
"""
|
||||
|
||||
class EnhancedWrapper(YayableWrapper):
|
||||
"""
|
||||
This class overrides the 'yay' method.
|
||||
"""
|
||||
|
||||
wrappedYays = 1
|
||||
|
||||
def yay(self, *a, **k):
|
||||
self.wrappedYays += 1
|
||||
return YayableWrapper.yay(self, *a, **k) + 7
|
||||
|
||||
yayable = Yayable()
|
||||
wrapper = EnhancedWrapper(yayable)
|
||||
self.assertEqual(wrapper.yay(3, 4, x=5, y=6), 8)
|
||||
self.assertEqual(yayable.yayArgs, [((3, 4), dict(x=5, y=6))])
|
||||
|
||||
def test_interfaceInheritance(self):
|
||||
"""
|
||||
Proxies of subinterfaces generated with proxyForInterface should allow
|
||||
access to attributes of both the child and the base interfaces.
|
||||
"""
|
||||
proxyClass = proxyForInterface(IProxiedSubInterface)
|
||||
booable = Booable()
|
||||
proxy = proxyClass(booable)
|
||||
proxy.yay()
|
||||
proxy.boo()
|
||||
self.assertTrue(booable.yayed)
|
||||
self.assertTrue(booable.booed)
|
||||
|
||||
def test_attributeCustomization(self):
|
||||
"""
|
||||
The original attribute name can be customized via the
|
||||
C{originalAttribute} argument of L{proxyForInterface}: the attribute
|
||||
should change, but the methods of the original object should still be
|
||||
callable, and the attributes still accessible.
|
||||
"""
|
||||
yayable = Yayable()
|
||||
yayable.ifaceAttribute = object()
|
||||
proxy = proxyForInterface(IProxiedInterface, originalAttribute="foo")(yayable)
|
||||
self.assertIs(proxy.foo, yayable)
|
||||
|
||||
# Check the behavior
|
||||
self.assertEqual(proxy.yay(), 1)
|
||||
self.assertIs(proxy.ifaceAttribute, yayable.ifaceAttribute)
|
||||
thingy = object()
|
||||
proxy.ifaceAttribute = thingy
|
||||
self.assertIs(yayable.ifaceAttribute, thingy)
|
||||
del proxy.ifaceAttribute
|
||||
self.assertFalse(hasattr(yayable, "ifaceAttribute"))
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,453 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Tests for L{twisted.python.fakepwd}.
|
||||
"""
|
||||
|
||||
try:
|
||||
import pwd as _pwd
|
||||
except ImportError:
|
||||
pwd = None
|
||||
else:
|
||||
pwd = _pwd
|
||||
|
||||
try:
|
||||
import spwd as _spwd
|
||||
except ImportError:
|
||||
spwd = None
|
||||
else:
|
||||
spwd = _spwd
|
||||
|
||||
import os
|
||||
from operator import getitem
|
||||
|
||||
from twisted.python.compat import _PYPY
|
||||
from twisted.python.fakepwd import ShadowDatabase, UserDatabase
|
||||
from twisted.trial.unittest import TestCase
|
||||
|
||||
SYSTEM_UID_MAX = 999
|
||||
|
||||
|
||||
def findInvalidUID():
|
||||
"""
|
||||
By convention, UIDs less than 1000 are reserved for the system. A system
|
||||
which allocated every single one of those UIDs would likely have practical
|
||||
problems with allocating new ones, so let's assume that we'll be able to
|
||||
find one. (If we don't, this will wrap around to negative values and
|
||||
I{eventually} find something.)
|
||||
|
||||
@return: a user ID which does not exist on the local system. Or, on
|
||||
systems without a L{pwd} module, return C{SYSTEM_UID_MAX}.
|
||||
"""
|
||||
guess = SYSTEM_UID_MAX
|
||||
if pwd is not None:
|
||||
while True:
|
||||
try:
|
||||
pwd.getpwuid(guess)
|
||||
except KeyError:
|
||||
break
|
||||
else:
|
||||
guess -= 1
|
||||
return guess
|
||||
|
||||
|
||||
INVALID_UID = findInvalidUID()
|
||||
|
||||
|
||||
class UserDatabaseTestsMixin:
|
||||
"""
|
||||
L{UserDatabaseTestsMixin} defines tests which apply to any user database
|
||||
implementation. Subclasses should mix it in, implement C{setUp} to create
|
||||
C{self.database} bound to a user database instance, and implement
|
||||
C{getExistingUserInfo} to return information about a user (such information
|
||||
should be unique per test method).
|
||||
"""
|
||||
|
||||
def test_getpwuid(self):
|
||||
"""
|
||||
I{getpwuid} accepts a uid and returns the user record associated with
|
||||
it.
|
||||
"""
|
||||
for i in range(2):
|
||||
# Get some user which exists in the database.
|
||||
username, password, uid, gid, gecos, dir, shell = self.getExistingUserInfo()
|
||||
|
||||
# Now try to look it up and make sure the result is correct.
|
||||
entry = self.database.getpwuid(uid)
|
||||
self.assertEqual(entry.pw_name, username)
|
||||
self.assertEqual(entry.pw_passwd, password)
|
||||
self.assertEqual(entry.pw_uid, uid)
|
||||
self.assertEqual(entry.pw_gid, gid)
|
||||
self.assertEqual(entry.pw_gecos, gecos)
|
||||
self.assertEqual(entry.pw_dir, dir)
|
||||
self.assertEqual(entry.pw_shell, shell)
|
||||
|
||||
def test_noSuchUID(self):
|
||||
"""
|
||||
I{getpwuid} raises L{KeyError} when passed a uid which does not exist
|
||||
in the user database.
|
||||
"""
|
||||
self.assertRaises(KeyError, self.database.getpwuid, INVALID_UID)
|
||||
|
||||
def test_getpwnam(self):
|
||||
"""
|
||||
I{getpwnam} accepts a username and returns the user record associated
|
||||
with it.
|
||||
"""
|
||||
for i in range(2):
|
||||
# Get some user which exists in the database.
|
||||
username, password, uid, gid, gecos, dir, shell = self.getExistingUserInfo()
|
||||
|
||||
# Now try to look it up and make sure the result is correct.
|
||||
entry = self.database.getpwnam(username)
|
||||
self.assertEqual(entry.pw_name, username)
|
||||
self.assertEqual(entry.pw_passwd, password)
|
||||
self.assertEqual(entry.pw_uid, uid)
|
||||
self.assertEqual(entry.pw_gid, gid)
|
||||
self.assertEqual(entry.pw_gecos, gecos)
|
||||
self.assertEqual(entry.pw_dir, dir)
|
||||
self.assertEqual(entry.pw_shell, shell)
|
||||
|
||||
def test_getpwnamRejectsBytes(self):
|
||||
"""
|
||||
L{getpwnam} rejects a non-L{str} username with an exception.
|
||||
"""
|
||||
exc_type = TypeError
|
||||
if _PYPY:
|
||||
# PyPy raises KeyError instead of TypeError. See
|
||||
# https://foss.heptapod.net/pypy/pypy/-/issues/3624
|
||||
exc_type = Exception
|
||||
self.assertRaises(exc_type, self.database.getpwnam, b"i-am-bytes")
|
||||
|
||||
def test_noSuchName(self):
|
||||
"""
|
||||
I{getpwnam} raises L{KeyError} when passed a username which does not
|
||||
exist in the user database.
|
||||
"""
|
||||
self.assertRaises(
|
||||
KeyError,
|
||||
self.database.getpwnam,
|
||||
"no"
|
||||
"such"
|
||||
"user"
|
||||
"exists"
|
||||
"the"
|
||||
"name"
|
||||
"is"
|
||||
"too"
|
||||
"long"
|
||||
"and"
|
||||
"has"
|
||||
"\1"
|
||||
"in"
|
||||
"it"
|
||||
"too",
|
||||
)
|
||||
|
||||
def test_recordLength(self):
|
||||
"""
|
||||
The user record returned by I{getpwuid}, I{getpwnam}, and I{getpwall}
|
||||
has a length.
|
||||
"""
|
||||
db = self.database
|
||||
username, password, uid, gid, gecos, dir, shell = self.getExistingUserInfo()
|
||||
for entry in [db.getpwuid(uid), db.getpwnam(username), db.getpwall()[0]]:
|
||||
self.assertIsInstance(len(entry), int)
|
||||
self.assertEqual(len(entry), 7)
|
||||
|
||||
def test_recordIndexable(self):
|
||||
"""
|
||||
The user record returned by I{getpwuid}, I{getpwnam}, and I{getpwall}
|
||||
is indexable, with successive indexes starting from 0 corresponding to
|
||||
the values of the C{pw_name}, C{pw_passwd}, C{pw_uid}, C{pw_gid},
|
||||
C{pw_gecos}, C{pw_dir}, and C{pw_shell} attributes, respectively.
|
||||
"""
|
||||
db = self.database
|
||||
username, password, uid, gid, gecos, dir, shell = self.getExistingUserInfo()
|
||||
for entry in [db.getpwuid(uid), db.getpwnam(username), db.getpwall()[0]]:
|
||||
self.assertEqual(entry[0], username)
|
||||
self.assertEqual(entry[1], password)
|
||||
self.assertEqual(entry[2], uid)
|
||||
self.assertEqual(entry[3], gid)
|
||||
self.assertEqual(entry[4], gecos)
|
||||
self.assertEqual(entry[5], dir)
|
||||
self.assertEqual(entry[6], shell)
|
||||
|
||||
self.assertEqual(len(entry), len(list(entry)))
|
||||
self.assertRaises(IndexError, getitem, entry, 7)
|
||||
|
||||
|
||||
class UserDatabaseTests(TestCase, UserDatabaseTestsMixin):
|
||||
"""
|
||||
Tests for L{UserDatabase}.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
Create a L{UserDatabase} with no user data in it.
|
||||
"""
|
||||
self.database = UserDatabase()
|
||||
self._counter = SYSTEM_UID_MAX + 1
|
||||
|
||||
def getExistingUserInfo(self):
|
||||
"""
|
||||
Add a new user to C{self.database} and return its information.
|
||||
"""
|
||||
self._counter += 1
|
||||
suffix = "_" + str(self._counter)
|
||||
username = "username" + suffix
|
||||
password = "password" + suffix
|
||||
uid = self._counter
|
||||
gid = self._counter + 1000
|
||||
gecos = "gecos" + suffix
|
||||
dir = "dir" + suffix
|
||||
shell = "shell" + suffix
|
||||
|
||||
self.database.addUser(username, password, uid, gid, gecos, dir, shell)
|
||||
return (username, password, uid, gid, gecos, dir, shell)
|
||||
|
||||
def test_addUser(self):
|
||||
"""
|
||||
L{UserDatabase.addUser} accepts seven arguments, one for each field of
|
||||
a L{pwd.struct_passwd}, and makes the new record available via
|
||||
L{UserDatabase.getpwuid}, L{UserDatabase.getpwnam}, and
|
||||
L{UserDatabase.getpwall}.
|
||||
"""
|
||||
username = "alice"
|
||||
password = "secr3t"
|
||||
uid = 123
|
||||
gid = 456
|
||||
gecos = "Alice,,,"
|
||||
home = "/users/alice"
|
||||
shell = "/usr/bin/foosh"
|
||||
|
||||
db = self.database
|
||||
db.addUser(username, password, uid, gid, gecos, home, shell)
|
||||
|
||||
for [entry] in [[db.getpwuid(uid)], [db.getpwnam(username)], db.getpwall()]:
|
||||
self.assertEqual(entry.pw_name, username)
|
||||
self.assertEqual(entry.pw_passwd, password)
|
||||
self.assertEqual(entry.pw_uid, uid)
|
||||
self.assertEqual(entry.pw_gid, gid)
|
||||
self.assertEqual(entry.pw_gecos, gecos)
|
||||
self.assertEqual(entry.pw_dir, home)
|
||||
self.assertEqual(entry.pw_shell, shell)
|
||||
|
||||
|
||||
class PwdModuleTests(TestCase, UserDatabaseTestsMixin):
|
||||
"""
|
||||
L{PwdModuleTests} runs the tests defined by L{UserDatabaseTestsMixin}
|
||||
against the built-in C{pwd} module. This serves to verify that
|
||||
L{UserDatabase} is really a fake of that API.
|
||||
"""
|
||||
|
||||
if pwd is None:
|
||||
skip = "Cannot verify UserDatabase against pwd without pwd"
|
||||
else:
|
||||
database = pwd
|
||||
|
||||
def setUp(self):
|
||||
self._users = iter(self.database.getpwall())
|
||||
self._uids = set()
|
||||
|
||||
def getExistingUserInfo(self):
|
||||
"""
|
||||
Read and return the next record from C{self._users}, filtering out
|
||||
any records with previously seen uid values (as these cannot be
|
||||
found with C{getpwuid} and only cause trouble).
|
||||
"""
|
||||
while True:
|
||||
entry = next(self._users)
|
||||
uid = entry.pw_uid
|
||||
if uid not in self._uids:
|
||||
self._uids.add(uid)
|
||||
return entry
|
||||
|
||||
|
||||
class ShadowDatabaseTestsMixin:
|
||||
"""
|
||||
L{ShadowDatabaseTestsMixin} defines tests which apply to any shadow user
|
||||
database implementation. Subclasses should mix it in, implement C{setUp} to
|
||||
create C{self.database} bound to a shadow user database instance, and
|
||||
implement C{getExistingUserInfo} to return information about a user (such
|
||||
information should be unique per test method).
|
||||
"""
|
||||
|
||||
def test_getspnam(self):
|
||||
"""
|
||||
L{getspnam} accepts a username and returns the user record associated
|
||||
with it.
|
||||
"""
|
||||
for i in range(2):
|
||||
# Get some user which exists in the database.
|
||||
(
|
||||
username,
|
||||
password,
|
||||
lastChange,
|
||||
min,
|
||||
max,
|
||||
warn,
|
||||
inact,
|
||||
expire,
|
||||
flag,
|
||||
) = self.getExistingUserInfo()
|
||||
|
||||
entry = self.database.getspnam(username)
|
||||
self.assertEqual(entry.sp_nam, username)
|
||||
self.assertEqual(entry.sp_pwd, password)
|
||||
self.assertEqual(entry.sp_lstchg, lastChange)
|
||||
self.assertEqual(entry.sp_min, min)
|
||||
self.assertEqual(entry.sp_max, max)
|
||||
self.assertEqual(entry.sp_warn, warn)
|
||||
self.assertEqual(entry.sp_inact, inact)
|
||||
self.assertEqual(entry.sp_expire, expire)
|
||||
self.assertEqual(entry.sp_flag, flag)
|
||||
|
||||
def test_noSuchName(self):
|
||||
"""
|
||||
I{getspnam} raises L{KeyError} when passed a username which does not
|
||||
exist in the user database.
|
||||
"""
|
||||
self.assertRaises(KeyError, self.database.getspnam, "alice")
|
||||
|
||||
def test_getspnamBytes(self):
|
||||
"""
|
||||
I{getspnam} raises L{TypeError} when passed a L{bytes}, just like
|
||||
L{spwd.getspnam}.
|
||||
"""
|
||||
self.assertRaises(TypeError, self.database.getspnam, b"i-am-bytes")
|
||||
|
||||
def test_recordLength(self):
|
||||
"""
|
||||
The shadow user record returned by I{getspnam} and I{getspall} has a
|
||||
length.
|
||||
"""
|
||||
db = self.database
|
||||
username = self.getExistingUserInfo()[0]
|
||||
for entry in [db.getspnam(username), db.getspall()[0]]:
|
||||
self.assertIsInstance(len(entry), int)
|
||||
self.assertEqual(len(entry), 9)
|
||||
|
||||
def test_recordIndexable(self):
|
||||
"""
|
||||
The shadow user record returned by I{getpwnam} and I{getspall} is
|
||||
indexable, with successive indexes starting from 0 corresponding to the
|
||||
values of the C{sp_nam}, C{sp_pwd}, C{sp_lstchg}, C{sp_min}, C{sp_max},
|
||||
C{sp_warn}, C{sp_inact}, C{sp_expire}, and C{sp_flag} attributes,
|
||||
respectively.
|
||||
"""
|
||||
db = self.database
|
||||
(
|
||||
username,
|
||||
password,
|
||||
lastChange,
|
||||
min,
|
||||
max,
|
||||
warn,
|
||||
inact,
|
||||
expire,
|
||||
flag,
|
||||
) = self.getExistingUserInfo()
|
||||
for entry in [db.getspnam(username), db.getspall()[0]]:
|
||||
self.assertEqual(entry[0], username)
|
||||
self.assertEqual(entry[1], password)
|
||||
self.assertEqual(entry[2], lastChange)
|
||||
self.assertEqual(entry[3], min)
|
||||
self.assertEqual(entry[4], max)
|
||||
self.assertEqual(entry[5], warn)
|
||||
self.assertEqual(entry[6], inact)
|
||||
self.assertEqual(entry[7], expire)
|
||||
self.assertEqual(entry[8], flag)
|
||||
|
||||
self.assertEqual(len(entry), len(list(entry)))
|
||||
self.assertRaises(IndexError, getitem, entry, 9)
|
||||
|
||||
|
||||
class ShadowDatabaseTests(TestCase, ShadowDatabaseTestsMixin):
|
||||
"""
|
||||
Tests for L{ShadowDatabase}.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
Create a L{ShadowDatabase} with no user data in it.
|
||||
"""
|
||||
self.database = ShadowDatabase()
|
||||
self._counter = 0
|
||||
|
||||
def getExistingUserInfo(self):
|
||||
"""
|
||||
Add a new user to C{self.database} and return its information.
|
||||
"""
|
||||
self._counter += 1
|
||||
suffix = "_" + str(self._counter)
|
||||
username = "username" + suffix
|
||||
password = "password" + suffix
|
||||
lastChange = self._counter + 1
|
||||
min = self._counter + 2
|
||||
max = self._counter + 3
|
||||
warn = self._counter + 4
|
||||
inact = self._counter + 5
|
||||
expire = self._counter + 6
|
||||
flag = self._counter + 7
|
||||
|
||||
self.database.addUser(
|
||||
username, password, lastChange, min, max, warn, inact, expire, flag
|
||||
)
|
||||
return (username, password, lastChange, min, max, warn, inact, expire, flag)
|
||||
|
||||
def test_addUser(self):
|
||||
"""
|
||||
L{UserDatabase.addUser} accepts seven arguments, one for each field of
|
||||
a L{pwd.struct_passwd}, and makes the new record available via
|
||||
L{UserDatabase.getpwuid}, L{UserDatabase.getpwnam}, and
|
||||
L{UserDatabase.getpwall}.
|
||||
"""
|
||||
username = "alice"
|
||||
password = "secr3t"
|
||||
lastChange = 17
|
||||
min = 42
|
||||
max = 105
|
||||
warn = 12
|
||||
inact = 3
|
||||
expire = 400
|
||||
flag = 3
|
||||
|
||||
db = self.database
|
||||
db.addUser(username, password, lastChange, min, max, warn, inact, expire, flag)
|
||||
|
||||
for [entry] in [[db.getspnam(username)], db.getspall()]:
|
||||
self.assertEqual(entry.sp_nam, username)
|
||||
self.assertEqual(entry.sp_pwd, password)
|
||||
self.assertEqual(entry.sp_lstchg, lastChange)
|
||||
self.assertEqual(entry.sp_min, min)
|
||||
self.assertEqual(entry.sp_max, max)
|
||||
self.assertEqual(entry.sp_warn, warn)
|
||||
self.assertEqual(entry.sp_inact, inact)
|
||||
self.assertEqual(entry.sp_expire, expire)
|
||||
self.assertEqual(entry.sp_flag, flag)
|
||||
|
||||
|
||||
class SPwdModuleTests(TestCase, ShadowDatabaseTestsMixin):
|
||||
"""
|
||||
L{SPwdModuleTests} runs the tests defined by L{ShadowDatabaseTestsMixin}
|
||||
against the built-in C{spwd} module. This serves to verify that
|
||||
L{ShadowDatabase} is really a fake of that API.
|
||||
"""
|
||||
|
||||
if spwd is None:
|
||||
skip = "Cannot verify ShadowDatabase against spwd without spwd"
|
||||
elif os.getuid() != 0:
|
||||
skip = "Cannot access shadow user database except as root"
|
||||
else:
|
||||
database = spwd
|
||||
|
||||
def setUp(self):
|
||||
self._users = iter(self.database.getspall())
|
||||
|
||||
def getExistingUserInfo(self):
|
||||
"""
|
||||
Read and return the next record from C{self._users}.
|
||||
"""
|
||||
return next(self._users)
|
||||
@@ -0,0 +1,45 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Tests for L{twisted.python.htmlizer}.
|
||||
"""
|
||||
|
||||
from io import BytesIO
|
||||
|
||||
from twisted.python.htmlizer import filter
|
||||
from twisted.trial.unittest import TestCase
|
||||
|
||||
|
||||
class FilterTests(TestCase):
|
||||
"""
|
||||
Tests for L{twisted.python.htmlizer.filter}.
|
||||
"""
|
||||
|
||||
def test_empty(self) -> None:
|
||||
"""
|
||||
If passed an empty input file, L{filter} writes a I{pre} tag containing
|
||||
only an end marker to the output file.
|
||||
"""
|
||||
input = BytesIO(b"")
|
||||
output = BytesIO()
|
||||
filter(input, output)
|
||||
self.assertEqual(
|
||||
output.getvalue(), b'<pre><span class="py-src-endmarker"></span></pre>\n'
|
||||
)
|
||||
|
||||
def test_variable(self) -> None:
|
||||
"""
|
||||
If passed an input file containing a variable access, L{filter} writes
|
||||
a I{pre} tag containing a I{py-src-variable} span containing the
|
||||
variable.
|
||||
"""
|
||||
input = BytesIO(b"foo\n")
|
||||
output = BytesIO()
|
||||
filter(input, output)
|
||||
self.assertEqual(
|
||||
output.getvalue(),
|
||||
b'<pre><span class="py-src-variable">foo</span>'
|
||||
b'<span class="py-src-newline">\n'
|
||||
b'</span><span class="py-src-endmarker"></span></pre>\n',
|
||||
)
|
||||
@@ -0,0 +1,138 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Tests for L{twisted.python._inotify}.
|
||||
"""
|
||||
|
||||
from twisted.python.filepath import FilePath
|
||||
from twisted.python.runtime import platform
|
||||
from twisted.trial.unittest import TestCase
|
||||
|
||||
try:
|
||||
from twisted.python import _inotify
|
||||
except ImportError:
|
||||
inotify = None
|
||||
else:
|
||||
inotify = _inotify
|
||||
|
||||
if inotify and platform.supportsINotify():
|
||||
from ctypes import c_char_p, c_int, c_uint32
|
||||
|
||||
from twisted.python._inotify import INotifyError, add, init, initializeModule
|
||||
else:
|
||||
inotify = None
|
||||
|
||||
|
||||
class INotifyTests(TestCase):
|
||||
"""
|
||||
Tests for L{twisted.python._inotify}.
|
||||
"""
|
||||
|
||||
if inotify is None:
|
||||
skip = "This platform doesn't support INotify."
|
||||
|
||||
def test_missingInit(self):
|
||||
"""
|
||||
If the I{libc} object passed to L{initializeModule} has no
|
||||
C{inotify_init} attribute, L{ImportError} is raised.
|
||||
"""
|
||||
|
||||
class libc:
|
||||
def inotify_add_watch(self):
|
||||
pass
|
||||
|
||||
def inotify_rm_watch(self):
|
||||
pass
|
||||
|
||||
self.assertRaises(ImportError, initializeModule, libc())
|
||||
|
||||
def test_missingAdd(self):
|
||||
"""
|
||||
If the I{libc} object passed to L{initializeModule} has no
|
||||
C{inotify_add_watch} attribute, L{ImportError} is raised.
|
||||
"""
|
||||
|
||||
class libc:
|
||||
def inotify_init(self):
|
||||
pass
|
||||
|
||||
def inotify_rm_watch(self):
|
||||
pass
|
||||
|
||||
self.assertRaises(ImportError, initializeModule, libc())
|
||||
|
||||
def test_missingRemove(self):
|
||||
"""
|
||||
If the I{libc} object passed to L{initializeModule} has no
|
||||
C{inotify_rm_watch} attribute, L{ImportError} is raised.
|
||||
"""
|
||||
|
||||
class libc:
|
||||
def inotify_init(self):
|
||||
pass
|
||||
|
||||
def inotify_add_watch(self):
|
||||
pass
|
||||
|
||||
self.assertRaises(ImportError, initializeModule, libc())
|
||||
|
||||
def test_setTypes(self):
|
||||
"""
|
||||
If the I{libc} object passed to L{initializeModule} has all of the
|
||||
necessary attributes, it sets the C{argtypes} and C{restype} attributes
|
||||
of the three ctypes methods used from libc.
|
||||
"""
|
||||
|
||||
class libc:
|
||||
def inotify_init(self):
|
||||
pass
|
||||
|
||||
inotify_init = staticmethod(inotify_init)
|
||||
|
||||
def inotify_rm_watch(self):
|
||||
pass
|
||||
|
||||
inotify_rm_watch = staticmethod(inotify_rm_watch)
|
||||
|
||||
def inotify_add_watch(self):
|
||||
pass
|
||||
|
||||
inotify_add_watch = staticmethod(inotify_add_watch)
|
||||
|
||||
c = libc()
|
||||
initializeModule(c)
|
||||
self.assertEqual(c.inotify_init.argtypes, [])
|
||||
self.assertEqual(c.inotify_init.restype, c_int)
|
||||
|
||||
self.assertEqual(c.inotify_rm_watch.argtypes, [c_int, c_int])
|
||||
self.assertEqual(c.inotify_rm_watch.restype, c_int)
|
||||
|
||||
self.assertEqual(c.inotify_add_watch.argtypes, [c_int, c_char_p, c_uint32])
|
||||
self.assertEqual(c.inotify_add_watch.restype, c_int)
|
||||
|
||||
def test_failedInit(self):
|
||||
"""
|
||||
If C{inotify_init} returns a negative number, L{init} raises
|
||||
L{INotifyError}.
|
||||
"""
|
||||
|
||||
class libc:
|
||||
def inotify_init(self):
|
||||
return -1
|
||||
|
||||
self.patch(inotify, "libc", libc())
|
||||
self.assertRaises(INotifyError, init)
|
||||
|
||||
def test_failedAddWatch(self):
|
||||
"""
|
||||
If C{inotify_add_watch} returns a negative number, L{add}
|
||||
raises L{INotifyError}.
|
||||
"""
|
||||
|
||||
class libc:
|
||||
def inotify_add_watch(self, fd, path, mask):
|
||||
return -1
|
||||
|
||||
self.patch(inotify, "libc", libc())
|
||||
self.assertRaises(INotifyError, add, 3, FilePath("/foo"), 0)
|
||||
@@ -0,0 +1,503 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Tests for L{twisted.python.release} and L{twisted.python._release}.
|
||||
|
||||
All of these tests are skipped on platforms other than Linux, as the release is
|
||||
only ever performed on Linux.
|
||||
"""
|
||||
import glob
|
||||
import operator
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
from incremental import Version
|
||||
|
||||
from twisted.python import release
|
||||
from twisted.python._release import (
|
||||
GitCommand,
|
||||
IVCSCommand,
|
||||
NotWorkingDirectory,
|
||||
Project,
|
||||
filePathDelta,
|
||||
findTwistedProjects,
|
||||
getRepositoryCommand,
|
||||
replaceInFile,
|
||||
runCommand,
|
||||
)
|
||||
from twisted.python.filepath import FilePath
|
||||
from twisted.trial.unittest import TestCase
|
||||
|
||||
if sys.platform != "win32":
|
||||
skip = None
|
||||
else:
|
||||
skip = "Release toolchain only supported on POSIX."
|
||||
|
||||
# This should match the GitHub Actions environment used by pre-commit.ci to push changes to the auto-updated branches.
|
||||
PRECOMMIT_CI_ENVIRON = {
|
||||
"GITHUB_HEAD_REF": "pre-commit-ci-update-config",
|
||||
"PATH": os.environ["PATH"],
|
||||
}
|
||||
# This should match the GHA environment for non pre-commit.ci PRs.
|
||||
GENERIC_CI_ENVIRON = {
|
||||
"GITHUB_HEAD_REF": "1234-some-branch-name",
|
||||
"PATH": os.environ["PATH"],
|
||||
}
|
||||
|
||||
|
||||
class ExternalTempdirTestCase(TestCase):
|
||||
"""
|
||||
A test case which has mkdir make directories outside of the usual spot, so
|
||||
that Git commands don't interfere with the Twisted checkout.
|
||||
"""
|
||||
|
||||
def mktemp(self):
|
||||
"""
|
||||
Make our own directory.
|
||||
"""
|
||||
newDir = tempfile.mkdtemp(dir=tempfile.gettempdir())
|
||||
self.addCleanup(shutil.rmtree, newDir)
|
||||
return newDir
|
||||
|
||||
|
||||
def _gitConfig(path):
|
||||
"""
|
||||
Set some config in the repo that Git requires to make commits. This isn't
|
||||
needed in real usage, just for tests.
|
||||
|
||||
@param path: The path to the Git repository.
|
||||
@type path: L{FilePath}
|
||||
"""
|
||||
runCommand(
|
||||
[
|
||||
"git",
|
||||
"config",
|
||||
"--file",
|
||||
path.child(".git").child("config").path,
|
||||
"user.name",
|
||||
'"someone"',
|
||||
]
|
||||
)
|
||||
runCommand(
|
||||
[
|
||||
"git",
|
||||
"config",
|
||||
"--file",
|
||||
path.child(".git").child("config").path,
|
||||
"user.email",
|
||||
'"someone@someplace.com"',
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def _gitInit(path):
|
||||
"""
|
||||
Run a git init, and set some config that git requires. This isn't needed in
|
||||
real usage.
|
||||
|
||||
@param path: The path to where the Git repo will be created.
|
||||
@type path: L{FilePath}
|
||||
"""
|
||||
runCommand(["git", "init", path.path])
|
||||
_gitConfig(path)
|
||||
|
||||
|
||||
def genVersion(*args, **kwargs):
|
||||
"""
|
||||
A convenience for generating _version.py data.
|
||||
|
||||
@param args: Arguments to pass to L{Version}.
|
||||
@param kwargs: Keyword arguments to pass to L{Version}.
|
||||
"""
|
||||
return "from incremental import Version\n__version__={!r}".format(
|
||||
Version(*args, **kwargs)
|
||||
)
|
||||
|
||||
|
||||
class StructureAssertingMixin:
|
||||
"""
|
||||
A mixin for L{TestCase} subclasses which provides some methods for
|
||||
asserting the structure and contents of directories and files on the
|
||||
filesystem.
|
||||
"""
|
||||
|
||||
def createStructure(self, root, dirDict):
|
||||
"""
|
||||
Create a set of directories and files given a dict defining their
|
||||
structure.
|
||||
|
||||
@param root: The directory in which to create the structure. It must
|
||||
already exist.
|
||||
@type root: L{FilePath}
|
||||
|
||||
@param dirDict: The dict defining the structure. Keys should be strings
|
||||
naming files, values should be strings describing file contents OR
|
||||
dicts describing subdirectories. All files are written in binary
|
||||
mode. Any string values are assumed to describe text files and
|
||||
will have their newlines replaced with the platform-native newline
|
||||
convention. For example::
|
||||
|
||||
{"foofile": "foocontents",
|
||||
"bardir": {"barfile": "bar\ncontents"}}
|
||||
@type dirDict: C{dict}
|
||||
"""
|
||||
for x in dirDict:
|
||||
child = root.child(x)
|
||||
if isinstance(dirDict[x], dict):
|
||||
child.createDirectory()
|
||||
self.createStructure(child, dirDict[x])
|
||||
else:
|
||||
child.setContent(dirDict[x].replace("\n", os.linesep).encode())
|
||||
|
||||
def assertStructure(self, root, dirDict):
|
||||
"""
|
||||
Assert that a directory is equivalent to one described by a dict.
|
||||
|
||||
@param root: The filesystem directory to compare.
|
||||
@type root: L{FilePath}
|
||||
@param dirDict: The dict that should describe the contents of the
|
||||
directory. It should be the same structure as the C{dirDict}
|
||||
parameter to L{createStructure}.
|
||||
@type dirDict: C{dict}
|
||||
"""
|
||||
children = [each.basename() for each in root.children()]
|
||||
for pathSegment, expectation in dirDict.items():
|
||||
child = root.child(pathSegment)
|
||||
if callable(expectation):
|
||||
self.assertTrue(expectation(child))
|
||||
elif isinstance(expectation, dict):
|
||||
self.assertTrue(child.isdir(), f"{child.path} is not a dir!")
|
||||
self.assertStructure(child, expectation)
|
||||
else:
|
||||
actual = child.getContent().decode().replace(os.linesep, "\n")
|
||||
self.assertEqual(actual, expectation)
|
||||
children.remove(pathSegment)
|
||||
if children:
|
||||
self.fail(f"There were extra children in {root.path}: {children}")
|
||||
|
||||
|
||||
class ProjectTests(ExternalTempdirTestCase):
|
||||
"""
|
||||
There is a first-class representation of a project.
|
||||
"""
|
||||
|
||||
def assertProjectsEqual(self, observedProjects, expectedProjects):
|
||||
"""
|
||||
Assert that two lists of L{Project}s are equal.
|
||||
"""
|
||||
self.assertEqual(len(observedProjects), len(expectedProjects))
|
||||
observedProjects = sorted(
|
||||
observedProjects, key=operator.attrgetter("directory")
|
||||
)
|
||||
expectedProjects = sorted(
|
||||
expectedProjects, key=operator.attrgetter("directory")
|
||||
)
|
||||
for observed, expected in zip(observedProjects, expectedProjects):
|
||||
self.assertEqual(observed.directory, expected.directory)
|
||||
|
||||
def makeProject(self, version, baseDirectory=None):
|
||||
"""
|
||||
Make a Twisted-style project in the given base directory.
|
||||
|
||||
@param baseDirectory: The directory to create files in
|
||||
(as a L{FilePath).
|
||||
@param version: The version information for the project.
|
||||
@return: L{Project} pointing to the created project.
|
||||
"""
|
||||
if baseDirectory is None:
|
||||
baseDirectory = FilePath(self.mktemp())
|
||||
segments = version[0].split(".")
|
||||
directory = baseDirectory
|
||||
for segment in segments:
|
||||
directory = directory.child(segment)
|
||||
if not directory.exists():
|
||||
directory.createDirectory()
|
||||
directory.child("__init__.py").setContent(b"")
|
||||
directory.child("newsfragments").createDirectory()
|
||||
directory.child("_version.py").setContent(genVersion(*version).encode())
|
||||
return Project(directory)
|
||||
|
||||
def makeProjects(self, *versions):
|
||||
"""
|
||||
Create a series of projects underneath a temporary base directory.
|
||||
|
||||
@return: A L{FilePath} for the base directory.
|
||||
"""
|
||||
baseDirectory = FilePath(self.mktemp())
|
||||
for version in versions:
|
||||
self.makeProject(version, baseDirectory)
|
||||
return baseDirectory
|
||||
|
||||
def test_getVersion(self):
|
||||
"""
|
||||
Project objects know their version.
|
||||
"""
|
||||
version = ("twisted", 2, 1, 0)
|
||||
project = self.makeProject(version)
|
||||
self.assertEqual(project.getVersion(), Version(*version))
|
||||
|
||||
def test_repr(self):
|
||||
"""
|
||||
The representation of a Project is Project(directory).
|
||||
"""
|
||||
foo = Project(FilePath("bar"))
|
||||
self.assertEqual(repr(foo), "Project(%r)" % (foo.directory))
|
||||
|
||||
def test_findTwistedStyleProjects(self):
|
||||
"""
|
||||
findTwistedStyleProjects finds all projects underneath a particular
|
||||
directory. A 'project' is defined by the existence of a 'newsfragments'
|
||||
directory and is returned as a Project object.
|
||||
"""
|
||||
baseDirectory = self.makeProjects(("foo", 2, 3, 0), ("foo.bar", 0, 7, 4))
|
||||
projects = findTwistedProjects(baseDirectory)
|
||||
self.assertProjectsEqual(
|
||||
projects,
|
||||
[
|
||||
Project(baseDirectory.child("foo")),
|
||||
Project(baseDirectory.child("foo").child("bar")),
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
class UtilityTests(ExternalTempdirTestCase):
|
||||
"""
|
||||
Tests for various utility functions for releasing.
|
||||
"""
|
||||
|
||||
def test_chdir(self):
|
||||
"""
|
||||
Test that the runChdirSafe is actually safe, i.e., it still
|
||||
changes back to the original directory even if an error is
|
||||
raised.
|
||||
"""
|
||||
cwd = os.getcwd()
|
||||
|
||||
def chAndBreak():
|
||||
os.mkdir("releaseCh")
|
||||
os.chdir("releaseCh")
|
||||
1 // 0
|
||||
|
||||
self.assertRaises(ZeroDivisionError, release.runChdirSafe, chAndBreak)
|
||||
self.assertEqual(cwd, os.getcwd())
|
||||
|
||||
def test_replaceInFile(self):
|
||||
"""
|
||||
L{replaceInFile} replaces data in a file based on a dict. A key from
|
||||
the dict that is found in the file is replaced with the corresponding
|
||||
value.
|
||||
"""
|
||||
content = "foo\nhey hey $VER\nbar\n"
|
||||
with open("release.replace", "w") as outf:
|
||||
outf.write(content)
|
||||
|
||||
expected = content.replace("$VER", "2.0.0")
|
||||
replaceInFile("release.replace", {"$VER": "2.0.0"})
|
||||
with open("release.replace") as f:
|
||||
self.assertEqual(f.read(), expected)
|
||||
|
||||
expected = expected.replace("2.0.0", "3.0.0")
|
||||
replaceInFile("release.replace", {"2.0.0": "3.0.0"})
|
||||
with open("release.replace") as f:
|
||||
self.assertEqual(f.read(), expected)
|
||||
|
||||
|
||||
class FilePathDeltaTests(TestCase):
|
||||
"""
|
||||
Tests for L{filePathDelta}.
|
||||
"""
|
||||
|
||||
def test_filePathDeltaSubdir(self):
|
||||
"""
|
||||
L{filePathDelta} can create a simple relative path to a child path.
|
||||
"""
|
||||
self.assertEqual(
|
||||
filePathDelta(FilePath("/foo/bar"), FilePath("/foo/bar/baz")), ["baz"]
|
||||
)
|
||||
|
||||
def test_filePathDeltaSiblingDir(self):
|
||||
"""
|
||||
L{filePathDelta} can traverse upwards to create relative paths to
|
||||
siblings.
|
||||
"""
|
||||
self.assertEqual(
|
||||
filePathDelta(FilePath("/foo/bar"), FilePath("/foo/baz")), ["..", "baz"]
|
||||
)
|
||||
|
||||
def test_filePathNoCommonElements(self):
|
||||
"""
|
||||
L{filePathDelta} can create relative paths to totally unrelated paths
|
||||
for maximum portability.
|
||||
"""
|
||||
self.assertEqual(
|
||||
filePathDelta(FilePath("/foo/bar"), FilePath("/baz/quux")),
|
||||
["..", "..", "baz", "quux"],
|
||||
)
|
||||
|
||||
def test_filePathDeltaSimilarEndElements(self):
|
||||
"""
|
||||
L{filePathDelta} doesn't take into account final elements when
|
||||
comparing 2 paths, but stops at the first difference.
|
||||
"""
|
||||
self.assertEqual(
|
||||
filePathDelta(FilePath("/foo/bar/bar/spam"), FilePath("/foo/bar/baz/spam")),
|
||||
["..", "..", "baz", "spam"],
|
||||
)
|
||||
|
||||
|
||||
class CommandsTestMixin(StructureAssertingMixin):
|
||||
"""
|
||||
Test mixin for the VCS commands used by the release scripts.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
self.tmpDir = FilePath(self.mktemp())
|
||||
|
||||
def test_ensureIsWorkingDirectoryWithWorkingDirectory(self):
|
||||
"""
|
||||
Calling the C{ensureIsWorkingDirectory} VCS command's method on a valid
|
||||
working directory doesn't produce any error.
|
||||
"""
|
||||
reposDir = self.makeRepository(self.tmpDir)
|
||||
self.assertIsNone(self.createCommand.ensureIsWorkingDirectory(reposDir))
|
||||
|
||||
def test_ensureIsWorkingDirectoryWithNonWorkingDirectory(self):
|
||||
"""
|
||||
Calling the C{ensureIsWorkingDirectory} VCS command's method on an
|
||||
invalid working directory raises a L{NotWorkingDirectory} exception.
|
||||
"""
|
||||
self.assertRaises(
|
||||
NotWorkingDirectory,
|
||||
self.createCommand.ensureIsWorkingDirectory,
|
||||
self.tmpDir,
|
||||
)
|
||||
|
||||
def test_statusClean(self):
|
||||
"""
|
||||
Calling the C{isStatusClean} VCS command's method on a repository with
|
||||
no pending modifications returns C{True}.
|
||||
"""
|
||||
reposDir = self.makeRepository(self.tmpDir)
|
||||
self.assertTrue(self.createCommand.isStatusClean(reposDir))
|
||||
|
||||
def test_statusNotClean(self):
|
||||
"""
|
||||
Calling the C{isStatusClean} VCS command's method on a repository with
|
||||
no pending modifications returns C{False}.
|
||||
"""
|
||||
reposDir = self.makeRepository(self.tmpDir)
|
||||
reposDir.child("some-file").setContent(b"something")
|
||||
self.assertFalse(self.createCommand.isStatusClean(reposDir))
|
||||
|
||||
def test_remove(self):
|
||||
"""
|
||||
Calling the C{remove} VCS command's method remove the specified path
|
||||
from the directory.
|
||||
"""
|
||||
reposDir = self.makeRepository(self.tmpDir)
|
||||
testFile = reposDir.child("some-file")
|
||||
testFile.setContent(b"something")
|
||||
self.commitRepository(reposDir)
|
||||
self.assertTrue(testFile.exists())
|
||||
|
||||
self.createCommand.remove(testFile)
|
||||
testFile.restat(False) # Refresh the file information
|
||||
self.assertFalse(testFile.exists(), "File still exists")
|
||||
|
||||
def test_export(self):
|
||||
"""
|
||||
The C{exportTo} VCS command's method export the content of the
|
||||
repository as identical in a specified directory.
|
||||
"""
|
||||
structure = {
|
||||
"README.rst": "Hi this is 1.0.0.",
|
||||
"twisted": {
|
||||
"newsfragments": {"README": "Hi this is 1.0.0"},
|
||||
"_version.py": genVersion("twisted", 1, 0, 0),
|
||||
"web": {
|
||||
"newsfragments": {"README": "Hi this is 1.0.0"},
|
||||
"_version.py": genVersion("twisted.web", 1, 0, 0),
|
||||
},
|
||||
},
|
||||
}
|
||||
reposDir = self.makeRepository(self.tmpDir)
|
||||
self.createStructure(reposDir, structure)
|
||||
self.commitRepository(reposDir)
|
||||
|
||||
exportDir = FilePath(self.mktemp()).child("export")
|
||||
self.createCommand.exportTo(reposDir, exportDir)
|
||||
self.assertStructure(exportDir, structure)
|
||||
|
||||
|
||||
class GitCommandTest(CommandsTestMixin, ExternalTempdirTestCase):
|
||||
"""
|
||||
Specific L{CommandsTestMixin} related to Git repositories through
|
||||
L{GitCommand}.
|
||||
"""
|
||||
|
||||
createCommand = GitCommand
|
||||
|
||||
def makeRepository(self, root):
|
||||
"""
|
||||
Create a Git repository in the specified path.
|
||||
|
||||
@type root: L{FilePath}
|
||||
@params root: The directory to create the Git repository into.
|
||||
|
||||
@return: The path to the repository just created.
|
||||
@rtype: L{FilePath}
|
||||
"""
|
||||
_gitInit(root)
|
||||
return root
|
||||
|
||||
def commitRepository(self, repository):
|
||||
"""
|
||||
Add and commit all the files from the Git repository specified.
|
||||
|
||||
@type repository: L{FilePath}
|
||||
@params repository: The Git repository to commit into.
|
||||
"""
|
||||
runCommand(
|
||||
["git", "-C", repository.path, "add"] + glob.glob(repository.path + "/*")
|
||||
)
|
||||
runCommand(["git", "-C", repository.path, "commit", "-m", "hop"])
|
||||
|
||||
|
||||
class RepositoryCommandDetectionTest(ExternalTempdirTestCase):
|
||||
"""
|
||||
Test the L{getRepositoryCommand} to access the right set of VCS commands
|
||||
depending on the repository manipulated.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
self.repos = FilePath(self.mktemp())
|
||||
|
||||
def test_git(self):
|
||||
"""
|
||||
L{getRepositoryCommand} from a Git repository returns L{GitCommand}.
|
||||
"""
|
||||
_gitInit(self.repos)
|
||||
cmd = getRepositoryCommand(self.repos)
|
||||
self.assertIs(cmd, GitCommand)
|
||||
|
||||
def test_unknownRepository(self):
|
||||
"""
|
||||
L{getRepositoryCommand} from a directory which doesn't look like a Git
|
||||
repository produces a L{NotWorkingDirectory} exception.
|
||||
"""
|
||||
self.assertRaises(NotWorkingDirectory, getRepositoryCommand, self.repos)
|
||||
|
||||
|
||||
class VCSCommandInterfaceTests(TestCase):
|
||||
"""
|
||||
Test that the VCS command classes implement their interface.
|
||||
"""
|
||||
|
||||
def test_git(self):
|
||||
"""
|
||||
L{GitCommand} implements L{IVCSCommand}.
|
||||
"""
|
||||
self.assertTrue(IVCSCommand.implementedBy(GitCommand))
|
||||
@@ -0,0 +1,228 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Tests for L{twisted.python.runtime}.
|
||||
"""
|
||||
|
||||
|
||||
import sys
|
||||
|
||||
from twisted.python.reflect import namedModule
|
||||
from twisted.python.runtime import Platform, shortPythonVersion
|
||||
from twisted.trial.unittest import SynchronousTestCase
|
||||
from twisted.trial.util import suppress as SUPRESS
|
||||
|
||||
|
||||
class PythonVersionTests(SynchronousTestCase):
|
||||
"""
|
||||
Tests the shortPythonVersion method.
|
||||
"""
|
||||
|
||||
def test_shortPythonVersion(self) -> None:
|
||||
"""
|
||||
Verify if the Python version is returned correctly.
|
||||
"""
|
||||
ver = shortPythonVersion().split(".")
|
||||
for i in range(3):
|
||||
self.assertEqual(int(ver[i]), sys.version_info[i])
|
||||
|
||||
|
||||
class PlatformTests(SynchronousTestCase):
|
||||
"""
|
||||
Tests for the default L{Platform} initializer.
|
||||
"""
|
||||
|
||||
isWinNTDeprecationMessage = (
|
||||
"twisted.python.runtime.Platform.isWinNT was "
|
||||
"deprecated in Twisted 13.0. Use Platform.isWindows instead."
|
||||
)
|
||||
|
||||
def test_isKnown(self) -> None:
|
||||
"""
|
||||
L{Platform.isKnown} returns a boolean indicating whether this is one of
|
||||
the L{runtime.knownPlatforms}.
|
||||
"""
|
||||
platform = Platform()
|
||||
self.assertTrue(platform.isKnown())
|
||||
|
||||
def test_isVistaConsistency(self) -> None:
|
||||
"""
|
||||
Verify consistency of L{Platform.isVista}: it can only be C{True} if
|
||||
L{Platform.isWinNT} and L{Platform.isWindows} are C{True}.
|
||||
"""
|
||||
platform = Platform()
|
||||
if platform.isVista():
|
||||
self.assertTrue(platform.isWinNT())
|
||||
self.assertTrue(platform.isWindows())
|
||||
self.assertFalse(platform.isMacOSX())
|
||||
|
||||
def test_isMacOSXConsistency(self) -> None:
|
||||
"""
|
||||
L{Platform.isMacOSX} can only return C{True} if L{Platform.getType}
|
||||
returns C{'posix'}.
|
||||
"""
|
||||
platform = Platform()
|
||||
if platform.isMacOSX():
|
||||
self.assertEqual(platform.getType(), "posix")
|
||||
|
||||
def test_isLinuxConsistency(self) -> None:
|
||||
"""
|
||||
L{Platform.isLinux} can only return C{True} if L{Platform.getType}
|
||||
returns C{'posix'} and L{sys.platform} starts with C{"linux"}.
|
||||
"""
|
||||
platform = Platform()
|
||||
if platform.isLinux():
|
||||
self.assertTrue(sys.platform.startswith("linux"))
|
||||
|
||||
def test_isWinNT(self) -> None:
|
||||
"""
|
||||
L{Platform.isWinNT} can return only C{False} or C{True} and can not
|
||||
return C{True} if L{Platform.getType} is not C{"win32"}.
|
||||
"""
|
||||
platform = Platform()
|
||||
isWinNT = platform.isWinNT()
|
||||
self.assertIn(isWinNT, (False, True))
|
||||
if platform.getType() != "win32":
|
||||
self.assertFalse(isWinNT)
|
||||
|
||||
test_isWinNT.suppress = [ # type: ignore[attr-defined]
|
||||
SUPRESS(
|
||||
category=DeprecationWarning,
|
||||
message=isWinNTDeprecationMessage,
|
||||
)
|
||||
]
|
||||
|
||||
def test_isWinNTDeprecated(self) -> None:
|
||||
"""
|
||||
L{Platform.isWinNT} is deprecated in favor of L{platform.isWindows}.
|
||||
"""
|
||||
platform = Platform()
|
||||
platform.isWinNT()
|
||||
warnings = self.flushWarnings([self.test_isWinNTDeprecated])
|
||||
self.assertEqual(len(warnings), 1)
|
||||
self.assertEqual(warnings[0]["message"], self.isWinNTDeprecationMessage)
|
||||
|
||||
def test_supportsThreads(self) -> None:
|
||||
"""
|
||||
L{Platform.supportsThreads} returns C{True} if threads can be created in
|
||||
this runtime, C{False} otherwise.
|
||||
"""
|
||||
# It's difficult to test both cases of this without faking the threading
|
||||
# module. Perhaps an adequate test is to just test the behavior with
|
||||
# the current runtime, whatever that happens to be.
|
||||
try:
|
||||
namedModule("threading")
|
||||
except ImportError:
|
||||
self.assertFalse(Platform().supportsThreads())
|
||||
else:
|
||||
self.assertTrue(Platform().supportsThreads())
|
||||
|
||||
|
||||
class ForeignPlatformTests(SynchronousTestCase):
|
||||
"""
|
||||
Tests for L{Platform} based overridden initializer values.
|
||||
"""
|
||||
|
||||
def test_getType(self) -> None:
|
||||
"""
|
||||
If an operating system name is supplied to L{Platform}'s initializer,
|
||||
L{Platform.getType} returns the platform type which corresponds to that
|
||||
name.
|
||||
"""
|
||||
self.assertEqual(Platform("nt").getType(), "win32")
|
||||
self.assertEqual(Platform("ce").getType(), "win32")
|
||||
self.assertEqual(Platform("posix").getType(), "posix")
|
||||
self.assertEqual(Platform("java").getType(), "java")
|
||||
|
||||
def test_isMacOSX(self) -> None:
|
||||
"""
|
||||
If a system platform name is supplied to L{Platform}'s initializer, it
|
||||
is used to determine the result of L{Platform.isMacOSX}, which returns
|
||||
C{True} for C{"darwin"}, C{False} otherwise.
|
||||
"""
|
||||
self.assertTrue(Platform(None, "darwin").isMacOSX())
|
||||
self.assertFalse(Platform(None, "linux2").isMacOSX())
|
||||
self.assertFalse(Platform(None, "win32").isMacOSX())
|
||||
|
||||
def test_isLinux(self) -> None:
|
||||
"""
|
||||
If a system platform name is supplied to L{Platform}'s initializer, it
|
||||
is used to determine the result of L{Platform.isLinux}, which returns
|
||||
C{True} for values beginning with C{"linux"}, C{False} otherwise.
|
||||
"""
|
||||
self.assertFalse(Platform(None, "darwin").isLinux())
|
||||
self.assertTrue(Platform(None, "linux").isLinux())
|
||||
self.assertTrue(Platform(None, "linux2").isLinux())
|
||||
self.assertTrue(Platform(None, "linux3").isLinux())
|
||||
self.assertFalse(Platform(None, "win32").isLinux())
|
||||
|
||||
|
||||
class DockerPlatformTests(SynchronousTestCase):
|
||||
"""
|
||||
Tests for L{twisted.python.runtime.Platform.isDocker}.
|
||||
"""
|
||||
|
||||
def test_noChecksOnLinux(self) -> None:
|
||||
"""
|
||||
If the platform is not Linux, C{isDocker()} always returns L{False}.
|
||||
"""
|
||||
platform = Platform(None, "win32")
|
||||
self.assertFalse(platform.isDocker())
|
||||
|
||||
def test_noCGroups(self) -> None:
|
||||
"""
|
||||
If the platform is Linux, and the cgroups file in C{/proc} does not
|
||||
exist, C{isDocker()} returns L{False}
|
||||
"""
|
||||
platform = Platform(None, "linux")
|
||||
self.assertFalse(platform.isDocker(_initCGroupLocation="fakepath"))
|
||||
|
||||
def test_cgroupsSuggestsDocker(self) -> None:
|
||||
"""
|
||||
If the platform is Linux, and the cgroups file (faked out here) exists,
|
||||
and one of the paths starts with C{/docker/}, C{isDocker()} returns
|
||||
C{True}.
|
||||
"""
|
||||
cgroupsFile = self.mktemp()
|
||||
with open(cgroupsFile, "wb") as f:
|
||||
# real cgroups file from inside a Debian 7 docker container
|
||||
f.write(
|
||||
b"""10:debug:/
|
||||
9:net_prio:/
|
||||
8:perf_event:/docker/104155a6453cb67590027e397dc90fc25a06a7508403c797bc89ea43adf8d35f
|
||||
7:net_cls:/
|
||||
6:freezer:/docker/104155a6453cb67590027e397dc90fc25a06a7508403c797bc89ea43adf8d35f
|
||||
5:devices:/docker/104155a6453cb67590027e397dc90fc25a06a7508403c797bc89ea43adf8d35f
|
||||
4:blkio:/docker/104155a6453cb67590027e397dc90fc25a06a7508403c797bc89ea43adf8d35f
|
||||
3:cpuacct:/docker/104155a6453cb67590027e397dc90fc25a06a7508403c797bc89ea43adf8d35f
|
||||
2:cpu:/docker/104155a6453cb67590027e397dc90fc25a06a7508403c797bc89ea43adf8d35f
|
||||
1:cpuset:/docker/104155a6453cb67590027e397dc90fc25a06a7508403c797bc89ea43adf8d35f"""
|
||||
)
|
||||
|
||||
platform = Platform(None, "linux")
|
||||
self.assertTrue(platform.isDocker(_initCGroupLocation=cgroupsFile))
|
||||
|
||||
def test_cgroupsSuggestsRealSystem(self) -> None:
|
||||
"""
|
||||
If the platform is Linux, and the cgroups file (faked out here) exists,
|
||||
and none of the paths starts with C{/docker/}, C{isDocker()} returns
|
||||
C{False}.
|
||||
"""
|
||||
cgroupsFile = self.mktemp()
|
||||
with open(cgroupsFile, "wb") as f:
|
||||
# real cgroups file from a Fedora 17 system
|
||||
f.write(
|
||||
b"""9:perf_event:/
|
||||
8:blkio:/
|
||||
7:net_cls:/
|
||||
6:freezer:/
|
||||
5:devices:/
|
||||
4:memory:/
|
||||
3:cpuacct,cpu:/
|
||||
2:cpuset:/
|
||||
1:name=systemd:/system"""
|
||||
)
|
||||
|
||||
platform = Platform(None, "linux")
|
||||
self.assertFalse(platform.isDocker(_initCGroupLocation=cgroupsFile))
|
||||
@@ -0,0 +1,337 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Tests for L{twisted.python.sendmsg}.
|
||||
"""
|
||||
|
||||
import errno
|
||||
import os
|
||||
import sys
|
||||
import warnings
|
||||
from os import close, pathsep, pipe, read
|
||||
from socket import AF_INET, AF_INET6, SOL_SOCKET, error, socket
|
||||
from struct import pack
|
||||
|
||||
try:
|
||||
from socket import AF_UNIX, socketpair
|
||||
except ImportError:
|
||||
nonUNIXSkip = True
|
||||
else:
|
||||
nonUNIXSkip = False
|
||||
|
||||
from unittest import skipIf
|
||||
|
||||
from twisted.internet import reactor
|
||||
from twisted.internet.defer import Deferred, inlineCallbacks
|
||||
from twisted.internet.error import ProcessDone
|
||||
from twisted.internet.protocol import ProcessProtocol
|
||||
from twisted.python.filepath import FilePath
|
||||
from twisted.python.runtime import platform
|
||||
from twisted.trial.unittest import TestCase
|
||||
|
||||
if platform.isLinux():
|
||||
from socket import MSG_DONTWAIT
|
||||
|
||||
dontWaitSkip = False
|
||||
else:
|
||||
# It would be nice to be able to test flags on more platforms, but finding
|
||||
# a flag that works *at all* is somewhat challenging.
|
||||
dontWaitSkip = True
|
||||
|
||||
|
||||
try:
|
||||
from twisted.python.sendmsg import SCM_RIGHTS, getSocketFamily, recvmsg, sendmsg
|
||||
except ImportError:
|
||||
doImportSkip = True
|
||||
importSkipReason = "Platform doesn't support sendmsg."
|
||||
else:
|
||||
doImportSkip = False
|
||||
importSkipReason = ""
|
||||
|
||||
|
||||
class _FDHolder:
|
||||
"""
|
||||
A wrapper around a FD that will remember if it has been closed or not.
|
||||
"""
|
||||
|
||||
def __init__(self, fd):
|
||||
self._fd = fd
|
||||
|
||||
def fileno(self):
|
||||
"""
|
||||
Return the fileno of this FD.
|
||||
"""
|
||||
return self._fd
|
||||
|
||||
def close(self):
|
||||
"""
|
||||
Close the FD. If it's already been closed, do nothing.
|
||||
"""
|
||||
if self._fd:
|
||||
close(self._fd)
|
||||
self._fd = None
|
||||
|
||||
def __del__(self):
|
||||
"""
|
||||
If C{self._fd} is unclosed, raise a warning.
|
||||
"""
|
||||
if self._fd:
|
||||
warnings.warn(f"FD {self._fd} was not closed!", ResourceWarning)
|
||||
self.close()
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_value, traceback):
|
||||
self.close()
|
||||
|
||||
|
||||
def _makePipe():
|
||||
"""
|
||||
Create a pipe, and return the two FDs wrapped in L{_FDHolders}.
|
||||
"""
|
||||
r, w = pipe()
|
||||
return (_FDHolder(r), _FDHolder(w))
|
||||
|
||||
|
||||
class ExitedWithStderr(Exception):
|
||||
"""
|
||||
A process exited with some stderr.
|
||||
"""
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""
|
||||
Dump the errors in a pretty way in the event of a subprocess traceback.
|
||||
"""
|
||||
result = b"\n".join([b""] + list(self.args))
|
||||
return repr(result)
|
||||
|
||||
|
||||
class StartStopProcessProtocol(ProcessProtocol):
|
||||
"""
|
||||
An L{IProcessProtocol} with a Deferred for events where the subprocess
|
||||
starts and stops.
|
||||
|
||||
@ivar started: A L{Deferred} which fires with this protocol's
|
||||
L{IProcessTransport} provider when it is connected to one.
|
||||
|
||||
@ivar stopped: A L{Deferred} which fires with the process output or a
|
||||
failure if the process produces output on standard error.
|
||||
|
||||
@ivar output: A C{str} used to accumulate standard output.
|
||||
|
||||
@ivar errors: A C{str} used to accumulate standard error.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.started = Deferred()
|
||||
self.stopped = Deferred()
|
||||
self.output = b""
|
||||
self.errors = b""
|
||||
|
||||
def connectionMade(self):
|
||||
self.started.callback(self.transport)
|
||||
|
||||
def outReceived(self, data):
|
||||
self.output += data
|
||||
|
||||
def errReceived(self, data):
|
||||
self.errors += data
|
||||
|
||||
def processEnded(self, reason):
|
||||
if reason.check(ProcessDone):
|
||||
self.stopped.callback(self.output)
|
||||
else:
|
||||
self.stopped.errback(ExitedWithStderr(self.errors, self.output))
|
||||
|
||||
|
||||
def _spawn(script, outputFD):
|
||||
"""
|
||||
Start a script that is a peer of this test as a subprocess.
|
||||
|
||||
@param script: the module name of the script in this directory (no
|
||||
package prefix, no '.py')
|
||||
@type script: C{str}
|
||||
|
||||
@rtype: L{StartStopProcessProtocol}
|
||||
"""
|
||||
pyExe = FilePath(sys.executable).asTextMode().path
|
||||
env = dict(os.environ)
|
||||
env["PYTHONPATH"] = FilePath(pathsep.join(sys.path)).asTextMode().path
|
||||
sspp = StartStopProcessProtocol()
|
||||
reactor.spawnProcess(
|
||||
sspp,
|
||||
pyExe,
|
||||
[
|
||||
pyExe,
|
||||
FilePath(__file__).sibling(script + ".py").asTextMode().path,
|
||||
b"17",
|
||||
],
|
||||
env=env,
|
||||
childFDs={0: "w", 1: "r", 2: "r", 17: outputFD},
|
||||
)
|
||||
return sspp
|
||||
|
||||
|
||||
@skipIf(doImportSkip, importSkipReason)
|
||||
class SendmsgTests(TestCase):
|
||||
"""
|
||||
Tests for the Python2/3 compatible L{sendmsg} interface.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
Create a pair of UNIX sockets.
|
||||
"""
|
||||
self.input, self.output = socketpair(AF_UNIX)
|
||||
|
||||
def tearDown(self):
|
||||
"""
|
||||
Close the sockets opened by setUp.
|
||||
"""
|
||||
self.input.close()
|
||||
self.output.close()
|
||||
|
||||
def test_syscallError(self):
|
||||
"""
|
||||
If the underlying C{sendmsg} call fails, L{send1msg} raises
|
||||
L{socket.error} with its errno set to the underlying errno value.
|
||||
"""
|
||||
self.input.close()
|
||||
exc = self.assertRaises(error, sendmsg, self.input, b"hello, world")
|
||||
self.assertEqual(exc.args[0], errno.EBADF)
|
||||
|
||||
def test_syscallErrorWithControlMessage(self):
|
||||
"""
|
||||
The behavior when the underlying C{sendmsg} call fails is the same
|
||||
whether L{sendmsg} is passed ancillary data or not.
|
||||
"""
|
||||
self.input.close()
|
||||
exc = self.assertRaises(
|
||||
error, sendmsg, self.input, b"hello, world", [(0, 0, b"0123")], 0
|
||||
)
|
||||
self.assertEqual(exc.args[0], errno.EBADF)
|
||||
|
||||
def test_roundtrip(self):
|
||||
"""
|
||||
L{recvmsg} will retrieve a message sent via L{sendmsg}.
|
||||
"""
|
||||
message = b"hello, world!"
|
||||
self.assertEqual(len(message), sendmsg(self.input, message))
|
||||
|
||||
result = recvmsg(self.output)
|
||||
self.assertEqual(result.data, b"hello, world!")
|
||||
self.assertEqual(result.flags, 0)
|
||||
self.assertEqual(result.ancillary, [])
|
||||
|
||||
def test_shortsend(self):
|
||||
"""
|
||||
L{sendmsg} returns the number of bytes which it was able to send.
|
||||
"""
|
||||
message = b"x" * 1024 * 1024 * 16
|
||||
self.input.setblocking(False)
|
||||
sent = sendmsg(self.input, message)
|
||||
# Sanity check - make sure the amount of data we sent was less than the
|
||||
# message, but not the whole message, as we should have filled the send
|
||||
# buffer. This won't work if the send buffer is large enough for
|
||||
# message, though.
|
||||
self.assertTrue(sent < len(message))
|
||||
received = recvmsg(self.output, len(message))
|
||||
self.assertEqual(len(received[0]), sent)
|
||||
|
||||
def test_roundtripEmptyAncillary(self):
|
||||
"""
|
||||
L{sendmsg} treats an empty ancillary data list the same way it treats
|
||||
receiving no argument for the ancillary parameter at all.
|
||||
"""
|
||||
sendmsg(self.input, b"hello, world!", [], 0)
|
||||
|
||||
result = recvmsg(self.output)
|
||||
self.assertEqual(result, (b"hello, world!", [], 0))
|
||||
|
||||
@skipIf(dontWaitSkip, "MSG_DONTWAIT is only known to work as intended on Linux")
|
||||
def test_flags(self):
|
||||
"""
|
||||
The C{flags} argument to L{sendmsg} is passed on to the underlying
|
||||
C{sendmsg} call, to affect it in whatever way is defined by those
|
||||
flags.
|
||||
"""
|
||||
# Just exercise one flag with simple, well-known behavior. MSG_DONTWAIT
|
||||
# makes the send a non-blocking call, even if the socket is in blocking
|
||||
# mode. See also test_flags in RecvmsgTests
|
||||
for i in range(8 * 1024):
|
||||
try:
|
||||
sendmsg(self.input, b"x" * 1024, flags=MSG_DONTWAIT)
|
||||
except OSError as e:
|
||||
self.assertEqual(e.args[0], errno.EAGAIN)
|
||||
break
|
||||
else:
|
||||
self.fail(
|
||||
"Failed to fill up the send buffer, "
|
||||
"or maybe send1msg blocked for a while"
|
||||
)
|
||||
|
||||
@inlineCallbacks
|
||||
def test_sendSubProcessFD(self):
|
||||
"""
|
||||
Calling L{sendmsg} with SOL_SOCKET, SCM_RIGHTS, and a platform-endian
|
||||
packed file descriptor number should send that file descriptor to a
|
||||
different process, where it can be retrieved by using L{recv1msg}.
|
||||
"""
|
||||
sspp = _spawn("pullpipe", self.output.fileno())
|
||||
yield sspp.started
|
||||
pipeOut, pipeIn = _makePipe()
|
||||
self.addCleanup(pipeOut.close)
|
||||
self.addCleanup(pipeIn.close)
|
||||
|
||||
with pipeIn:
|
||||
sendmsg(
|
||||
self.input,
|
||||
b"blonk",
|
||||
[(SOL_SOCKET, SCM_RIGHTS, pack("i", pipeIn.fileno()))],
|
||||
)
|
||||
|
||||
yield sspp.stopped
|
||||
self.assertEqual(read(pipeOut.fileno(), 1024), b"Test fixture data: blonk.\n")
|
||||
# Make sure that the pipe is actually closed now.
|
||||
self.assertEqual(read(pipeOut.fileno(), 1024), b"")
|
||||
|
||||
|
||||
@skipIf(doImportSkip, importSkipReason)
|
||||
class GetSocketFamilyTests(TestCase):
|
||||
"""
|
||||
Tests for L{getSocketFamily}.
|
||||
"""
|
||||
|
||||
def _socket(self, addressFamily):
|
||||
"""
|
||||
Create a new socket using the given address family and return that
|
||||
socket's file descriptor. The socket will automatically be closed when
|
||||
the test is torn down.
|
||||
"""
|
||||
s = socket(addressFamily)
|
||||
self.addCleanup(s.close)
|
||||
return s
|
||||
|
||||
def test_inet(self):
|
||||
"""
|
||||
When passed the file descriptor of a socket created with the C{AF_INET}
|
||||
address family, L{getSocketFamily} returns C{AF_INET}.
|
||||
"""
|
||||
self.assertEqual(AF_INET, getSocketFamily(self._socket(AF_INET)))
|
||||
|
||||
def test_inet6(self):
|
||||
"""
|
||||
When passed the file descriptor of a socket created with the
|
||||
C{AF_INET6} address family, L{getSocketFamily} returns C{AF_INET6}.
|
||||
"""
|
||||
self.assertEqual(AF_INET6, getSocketFamily(self._socket(AF_INET6)))
|
||||
|
||||
@skipIf(nonUNIXSkip, "Platform does not support AF_UNIX sockets")
|
||||
def test_unix(self):
|
||||
"""
|
||||
When passed the file descriptor of a socket created with the C{AF_UNIX}
|
||||
address family, L{getSocketFamily} returns C{AF_UNIX}.
|
||||
"""
|
||||
self.assertEqual(AF_UNIX, getSocketFamily(self._socket(AF_UNIX)))
|
||||
@@ -0,0 +1,635 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Test cases for twisted.python._shellcomp
|
||||
"""
|
||||
|
||||
|
||||
import sys
|
||||
from io import BytesIO
|
||||
from typing import List, Optional
|
||||
|
||||
from twisted.python import _shellcomp, reflect, usage
|
||||
from twisted.python.usage import CompleteFiles, CompleteList, Completer, Completions
|
||||
from twisted.trial import unittest
|
||||
|
||||
|
||||
class ZshScriptTestMeta(type):
|
||||
"""
|
||||
Metaclass of ZshScriptTestMixin.
|
||||
"""
|
||||
|
||||
def __new__(cls, name, bases, attrs):
|
||||
def makeTest(cmdName, optionsFQPN):
|
||||
def runTest(self):
|
||||
return test_genZshFunction(self, cmdName, optionsFQPN)
|
||||
|
||||
return runTest
|
||||
|
||||
# add test_ methods to the class for each script
|
||||
# we are testing.
|
||||
if "generateFor" in attrs:
|
||||
for cmdName, optionsFQPN in attrs["generateFor"]:
|
||||
test = makeTest(cmdName, optionsFQPN)
|
||||
attrs["test_genZshFunction_" + cmdName] = test
|
||||
|
||||
return type.__new__(cls, name, bases, attrs)
|
||||
|
||||
|
||||
class ZshScriptTestMixin(metaclass=ZshScriptTestMeta):
|
||||
"""
|
||||
Integration test helper to show that C{usage.Options} classes can have zsh
|
||||
completion functions generated for them without raising errors.
|
||||
|
||||
In your subclasses set a class variable like so::
|
||||
|
||||
# | cmd name | Fully Qualified Python Name of Options class |
|
||||
#
|
||||
generateFor = [('conch', 'twisted.conch.scripts.conch.ClientOptions'),
|
||||
('twistd', 'twisted.scripts.twistd.ServerOptions'),
|
||||
]
|
||||
|
||||
Each package that contains Twisted scripts should contain one TestCase
|
||||
subclass which also inherits from this mixin, and contains a C{generateFor}
|
||||
list appropriate for the scripts in that package.
|
||||
"""
|
||||
|
||||
|
||||
def test_genZshFunction(self, cmdName, optionsFQPN):
|
||||
"""
|
||||
Generate completion functions for given twisted command - no errors
|
||||
should be raised
|
||||
|
||||
@type cmdName: C{str}
|
||||
@param cmdName: The name of the command-line utility e.g. 'twistd'
|
||||
|
||||
@type optionsFQPN: C{str}
|
||||
@param optionsFQPN: The Fully Qualified Python Name of the C{Options}
|
||||
class to be tested.
|
||||
"""
|
||||
outputFile = BytesIO()
|
||||
self.patch(usage.Options, "_shellCompFile", outputFile)
|
||||
|
||||
# some scripts won't import or instantiate because of missing
|
||||
# dependencies (pyOpenSSL, etc) so we have to skip them.
|
||||
try:
|
||||
o = reflect.namedAny(optionsFQPN)()
|
||||
except Exception as e:
|
||||
raise unittest.SkipTest(
|
||||
"Couldn't import or instantiate " "Options class: %s" % (e,)
|
||||
)
|
||||
|
||||
try:
|
||||
o.parseOptions(["", "--_shell-completion", "zsh:2"])
|
||||
except ImportError as e:
|
||||
# this can happen for commands which don't have all
|
||||
# the necessary dependencies installed. skip test.
|
||||
# skip
|
||||
raise unittest.SkipTest("ImportError calling parseOptions(): %s", (e,))
|
||||
except SystemExit:
|
||||
pass # expected
|
||||
else:
|
||||
self.fail("SystemExit not raised")
|
||||
outputFile.seek(0)
|
||||
# test that we got some output
|
||||
self.assertEqual(1, len(outputFile.read(1)))
|
||||
outputFile.seek(0)
|
||||
outputFile.truncate()
|
||||
|
||||
# now, if it has sub commands, we have to test those too
|
||||
if hasattr(o, "subCommands"):
|
||||
for cmd, short, parser, doc in o.subCommands:
|
||||
try:
|
||||
o.parseOptions([cmd, "", "--_shell-completion", "zsh:3"])
|
||||
except ImportError as e:
|
||||
# this can happen for commands which don't have all
|
||||
# the necessary dependencies installed. skip test.
|
||||
raise unittest.SkipTest(
|
||||
"ImportError calling parseOptions() " "on subcommand: %s", (e,)
|
||||
)
|
||||
except SystemExit:
|
||||
pass # expected
|
||||
else:
|
||||
self.fail("SystemExit not raised")
|
||||
|
||||
outputFile.seek(0)
|
||||
# test that we got some output
|
||||
self.assertEqual(1, len(outputFile.read(1)))
|
||||
outputFile.seek(0)
|
||||
outputFile.truncate()
|
||||
|
||||
# flushed because we don't want DeprecationWarnings to be printed when
|
||||
# running these test cases.
|
||||
self.flushWarnings()
|
||||
|
||||
|
||||
class ZshTests(unittest.TestCase):
|
||||
"""
|
||||
Tests for zsh completion code
|
||||
"""
|
||||
|
||||
def test_accumulateMetadata(self):
|
||||
"""
|
||||
Are `compData' attributes you can place on Options classes
|
||||
picked up correctly?
|
||||
"""
|
||||
opts = FighterAceExtendedOptions()
|
||||
ag = _shellcomp.ZshArgumentsGenerator(opts, "ace", BytesIO())
|
||||
|
||||
descriptions = FighterAceOptions.compData.descriptions.copy()
|
||||
descriptions.update(FighterAceExtendedOptions.compData.descriptions)
|
||||
|
||||
self.assertEqual(ag.descriptions, descriptions)
|
||||
self.assertEqual(ag.multiUse, set(FighterAceOptions.compData.multiUse))
|
||||
self.assertEqual(
|
||||
ag.mutuallyExclusive, FighterAceOptions.compData.mutuallyExclusive
|
||||
)
|
||||
|
||||
optActions = FighterAceOptions.compData.optActions.copy()
|
||||
optActions.update(FighterAceExtendedOptions.compData.optActions)
|
||||
self.assertEqual(ag.optActions, optActions)
|
||||
|
||||
self.assertEqual(ag.extraActions, FighterAceOptions.compData.extraActions)
|
||||
|
||||
def test_mutuallyExclusiveCornerCase(self):
|
||||
"""
|
||||
Exercise a corner-case of ZshArgumentsGenerator.makeExcludesDict()
|
||||
where the long option name already exists in the `excludes` dict being
|
||||
built.
|
||||
"""
|
||||
|
||||
class OddFighterAceOptions(FighterAceExtendedOptions):
|
||||
# since "fokker", etc, are already defined as mutually-
|
||||
# exclusive on the super-class, defining them again here forces
|
||||
# the corner-case to be exercised.
|
||||
optFlags = [
|
||||
["anatra", None, "Select the Anatra DS as your dogfighter aircraft"]
|
||||
]
|
||||
compData = Completions(
|
||||
mutuallyExclusive=[["anatra", "fokker", "albatros", "spad", "bristol"]]
|
||||
)
|
||||
|
||||
opts = OddFighterAceOptions()
|
||||
ag = _shellcomp.ZshArgumentsGenerator(opts, "ace", BytesIO())
|
||||
|
||||
expected = {
|
||||
"albatros": {"anatra", "b", "bristol", "f", "fokker", "s", "spad"},
|
||||
"anatra": {"a", "albatros", "b", "bristol", "f", "fokker", "s", "spad"},
|
||||
"bristol": {"a", "albatros", "anatra", "f", "fokker", "s", "spad"},
|
||||
"fokker": {"a", "albatros", "anatra", "b", "bristol", "s", "spad"},
|
||||
"spad": {"a", "albatros", "anatra", "b", "bristol", "f", "fokker"},
|
||||
}
|
||||
|
||||
self.assertEqual(ag.excludes, expected)
|
||||
|
||||
def test_accumulateAdditionalOptions(self):
|
||||
"""
|
||||
We pick up options that are only defined by having an
|
||||
appropriately named method on your Options class,
|
||||
e.g. def opt_foo(self, foo)
|
||||
"""
|
||||
opts = FighterAceExtendedOptions()
|
||||
ag = _shellcomp.ZshArgumentsGenerator(opts, "ace", BytesIO())
|
||||
|
||||
self.assertIn("nocrash", ag.flagNameToDefinition)
|
||||
self.assertIn("nocrash", ag.allOptionsNameToDefinition)
|
||||
|
||||
self.assertIn("difficulty", ag.paramNameToDefinition)
|
||||
self.assertIn("difficulty", ag.allOptionsNameToDefinition)
|
||||
|
||||
def test_verifyZshNames(self):
|
||||
"""
|
||||
Using a parameter/flag name that doesn't exist
|
||||
will raise an error
|
||||
"""
|
||||
|
||||
class TmpOptions(FighterAceExtendedOptions):
|
||||
# Note typo of detail
|
||||
compData = Completions(optActions={"detaill": None})
|
||||
|
||||
self.assertRaises(
|
||||
ValueError, _shellcomp.ZshArgumentsGenerator, TmpOptions(), "ace", BytesIO()
|
||||
)
|
||||
|
||||
class TmpOptions2(FighterAceExtendedOptions):
|
||||
# Note that 'foo' and 'bar' are not real option
|
||||
# names defined in this class
|
||||
compData = Completions(mutuallyExclusive=[("foo", "bar")])
|
||||
|
||||
self.assertRaises(
|
||||
ValueError,
|
||||
_shellcomp.ZshArgumentsGenerator,
|
||||
TmpOptions2(),
|
||||
"ace",
|
||||
BytesIO(),
|
||||
)
|
||||
|
||||
def test_zshCode(self):
|
||||
"""
|
||||
Generate a completion function, and test the textual output
|
||||
against a known correct output
|
||||
"""
|
||||
outputFile = BytesIO()
|
||||
self.patch(usage.Options, "_shellCompFile", outputFile)
|
||||
self.patch(sys, "argv", ["silly", "", "--_shell-completion", "zsh:2"])
|
||||
opts = SimpleProgOptions()
|
||||
self.assertRaises(SystemExit, opts.parseOptions)
|
||||
self.assertEqual(testOutput1, outputFile.getvalue())
|
||||
|
||||
def test_zshCodeWithSubs(self):
|
||||
"""
|
||||
Generate a completion function with subcommands,
|
||||
and test the textual output against a known correct output
|
||||
"""
|
||||
outputFile = BytesIO()
|
||||
self.patch(usage.Options, "_shellCompFile", outputFile)
|
||||
self.patch(sys, "argv", ["silly2", "", "--_shell-completion", "zsh:2"])
|
||||
opts = SimpleProgWithSubcommands()
|
||||
self.assertRaises(SystemExit, opts.parseOptions)
|
||||
self.assertEqual(testOutput2, outputFile.getvalue())
|
||||
|
||||
def test_incompleteCommandLine(self):
|
||||
"""
|
||||
Completion still happens even if a command-line is given
|
||||
that would normally throw UsageError.
|
||||
"""
|
||||
outputFile = BytesIO()
|
||||
self.patch(usage.Options, "_shellCompFile", outputFile)
|
||||
opts = FighterAceOptions()
|
||||
|
||||
self.assertRaises(
|
||||
SystemExit,
|
||||
opts.parseOptions,
|
||||
[
|
||||
"--fokker",
|
||||
"server",
|
||||
"--unknown-option",
|
||||
"--unknown-option2",
|
||||
"--_shell-completion",
|
||||
"zsh:5",
|
||||
],
|
||||
)
|
||||
outputFile.seek(0)
|
||||
# test that we got some output
|
||||
self.assertEqual(1, len(outputFile.read(1)))
|
||||
|
||||
def test_incompleteCommandLine_case2(self):
|
||||
"""
|
||||
Completion still happens even if a command-line is given
|
||||
that would normally throw UsageError.
|
||||
|
||||
The existence of --unknown-option prior to the subcommand
|
||||
will break subcommand detection... but we complete anyway
|
||||
"""
|
||||
outputFile = BytesIO()
|
||||
self.patch(usage.Options, "_shellCompFile", outputFile)
|
||||
opts = FighterAceOptions()
|
||||
|
||||
self.assertRaises(
|
||||
SystemExit,
|
||||
opts.parseOptions,
|
||||
[
|
||||
"--fokker",
|
||||
"--unknown-option",
|
||||
"server",
|
||||
"--list-server",
|
||||
"--_shell-completion",
|
||||
"zsh:5",
|
||||
],
|
||||
)
|
||||
outputFile.seek(0)
|
||||
# test that we got some output
|
||||
self.assertEqual(1, len(outputFile.read(1)))
|
||||
|
||||
outputFile.seek(0)
|
||||
outputFile.truncate()
|
||||
|
||||
def test_incompleteCommandLine_case3(self):
|
||||
"""
|
||||
Completion still happens even if a command-line is given
|
||||
that would normally throw UsageError.
|
||||
|
||||
Break subcommand detection in a different way by providing
|
||||
an invalid subcommand name.
|
||||
"""
|
||||
outputFile = BytesIO()
|
||||
self.patch(usage.Options, "_shellCompFile", outputFile)
|
||||
opts = FighterAceOptions()
|
||||
|
||||
self.assertRaises(
|
||||
SystemExit,
|
||||
opts.parseOptions,
|
||||
[
|
||||
"--fokker",
|
||||
"unknown-subcommand",
|
||||
"--list-server",
|
||||
"--_shell-completion",
|
||||
"zsh:4",
|
||||
],
|
||||
)
|
||||
outputFile.seek(0)
|
||||
# test that we got some output
|
||||
self.assertEqual(1, len(outputFile.read(1)))
|
||||
|
||||
def test_skipSubcommandList(self):
|
||||
"""
|
||||
Ensure the optimization which skips building the subcommand list
|
||||
under certain conditions isn't broken.
|
||||
"""
|
||||
outputFile = BytesIO()
|
||||
self.patch(usage.Options, "_shellCompFile", outputFile)
|
||||
opts = FighterAceOptions()
|
||||
|
||||
self.assertRaises(
|
||||
SystemExit, opts.parseOptions, ["--alba", "--_shell-completion", "zsh:2"]
|
||||
)
|
||||
outputFile.seek(0)
|
||||
# test that we got some output
|
||||
self.assertEqual(1, len(outputFile.read(1)))
|
||||
|
||||
def test_poorlyDescribedOptMethod(self):
|
||||
"""
|
||||
Test corner case fetching an option description from a method docstring
|
||||
"""
|
||||
opts = FighterAceOptions()
|
||||
argGen = _shellcomp.ZshArgumentsGenerator(opts, "ace", None)
|
||||
|
||||
descr = argGen.getDescription("silly")
|
||||
|
||||
# docstring for opt_silly is useless so it should just use the
|
||||
# option name as the description
|
||||
self.assertEqual(descr, "silly")
|
||||
|
||||
def test_brokenActions(self):
|
||||
"""
|
||||
A C{Completer} with repeat=True may only be used as the
|
||||
last item in the extraActions list.
|
||||
"""
|
||||
|
||||
class BrokenActions(usage.Options):
|
||||
compData = usage.Completions(
|
||||
extraActions=[usage.Completer(repeat=True), usage.Completer()]
|
||||
)
|
||||
|
||||
outputFile = BytesIO()
|
||||
opts = BrokenActions()
|
||||
self.patch(opts, "_shellCompFile", outputFile)
|
||||
self.assertRaises(
|
||||
ValueError, opts.parseOptions, ["", "--_shell-completion", "zsh:2"]
|
||||
)
|
||||
|
||||
def test_optMethodsDontOverride(self):
|
||||
"""
|
||||
opt_* methods on Options classes should not override the
|
||||
data provided in optFlags or optParameters.
|
||||
"""
|
||||
|
||||
class Options(usage.Options):
|
||||
optFlags = [["flag", "f", "A flag"]]
|
||||
optParameters = [["param", "p", None, "A param"]]
|
||||
|
||||
def opt_flag(self):
|
||||
"""junk description"""
|
||||
|
||||
def opt_param(self, param):
|
||||
"""junk description"""
|
||||
|
||||
opts = Options()
|
||||
argGen = _shellcomp.ZshArgumentsGenerator(opts, "ace", None)
|
||||
|
||||
self.assertEqual(argGen.getDescription("flag"), "A flag")
|
||||
self.assertEqual(argGen.getDescription("param"), "A param")
|
||||
|
||||
|
||||
class EscapeTests(unittest.TestCase):
|
||||
def test_escape(self):
|
||||
"""
|
||||
Verify _shellcomp.escape() function
|
||||
"""
|
||||
esc = _shellcomp.escape
|
||||
|
||||
test = "$"
|
||||
self.assertEqual(esc(test), "'$'")
|
||||
|
||||
test = "A--'$\"\\`--B"
|
||||
self.assertEqual(esc(test), '"A--\'\\$\\"\\\\\\`--B"')
|
||||
|
||||
|
||||
class CompleterNotImplementedTests(unittest.TestCase):
|
||||
"""
|
||||
Test that using an unknown shell constant with SubcommandAction
|
||||
raises NotImplementedError
|
||||
|
||||
The other Completer() subclasses are tested in test_usage.py
|
||||
"""
|
||||
|
||||
def test_unknownShell(self):
|
||||
"""
|
||||
Using an unknown shellType should raise NotImplementedError
|
||||
"""
|
||||
action = _shellcomp.SubcommandAction()
|
||||
|
||||
self.assertRaises(
|
||||
NotImplementedError, action._shellCode, None, "bad_shell_type"
|
||||
)
|
||||
|
||||
|
||||
class FighterAceServerOptions(usage.Options):
|
||||
"""
|
||||
Options for FighterAce 'server' subcommand
|
||||
"""
|
||||
|
||||
optFlags = [
|
||||
["list-server", None, "List this server with the online FighterAce network"]
|
||||
]
|
||||
optParameters = [
|
||||
[
|
||||
"packets-per-second",
|
||||
None,
|
||||
"Number of update packets to send per second",
|
||||
"20",
|
||||
]
|
||||
]
|
||||
|
||||
|
||||
class FighterAceOptions(usage.Options):
|
||||
"""
|
||||
Command-line options for an imaginary `Fighter Ace` game
|
||||
"""
|
||||
|
||||
optFlags: List[List[Optional[str]]] = [
|
||||
["fokker", "f", "Select the Fokker Dr.I as your dogfighter aircraft"],
|
||||
["albatros", "a", "Select the Albatros D-III as your dogfighter aircraft"],
|
||||
["spad", "s", "Select the SPAD S.VII as your dogfighter aircraft"],
|
||||
["bristol", "b", "Select the Bristol Scout as your dogfighter aircraft"],
|
||||
["physics", "p", "Enable secret Twisted physics engine"],
|
||||
["jam", "j", "Enable a small chance that your machine guns will jam!"],
|
||||
["verbose", "v", "Verbose logging (may be specified more than once)"],
|
||||
]
|
||||
|
||||
optParameters: List[List[Optional[str]]] = [
|
||||
["pilot-name", None, "What's your name, Ace?", "Manfred von Richthofen"],
|
||||
["detail", "d", "Select the level of rendering detail (1-5)", "3"],
|
||||
]
|
||||
|
||||
subCommands = [
|
||||
["server", None, FighterAceServerOptions, "Start FighterAce game-server."],
|
||||
]
|
||||
|
||||
compData = Completions(
|
||||
descriptions={"physics": "Twisted-Physics", "detail": "Rendering detail level"},
|
||||
multiUse=["verbose"],
|
||||
mutuallyExclusive=[["fokker", "albatros", "spad", "bristol"]],
|
||||
optActions={"detail": CompleteList(["1" "2" "3" "4" "5"])},
|
||||
extraActions=[CompleteFiles(descr="saved game file to load")],
|
||||
)
|
||||
|
||||
def opt_silly(self):
|
||||
# A silly option which nobody can explain
|
||||
""" """
|
||||
|
||||
|
||||
class FighterAceExtendedOptions(FighterAceOptions):
|
||||
"""
|
||||
Extend the options and zsh metadata provided by FighterAceOptions.
|
||||
_shellcomp must accumulate options and metadata from all classes in the
|
||||
hiearchy so this is important to test.
|
||||
"""
|
||||
|
||||
optFlags = [["no-stalls", None, "Turn off the ability to stall your aircraft"]]
|
||||
optParameters = [
|
||||
["reality-level", None, "Select the level of physics reality (1-5)", "5"]
|
||||
]
|
||||
|
||||
compData = Completions(
|
||||
descriptions={"no-stalls": "Can't stall your plane"},
|
||||
optActions={"reality-level": Completer(descr="Physics reality level")},
|
||||
)
|
||||
|
||||
def opt_nocrash(self):
|
||||
"""
|
||||
Select that you can't crash your plane
|
||||
"""
|
||||
|
||||
def opt_difficulty(self, difficulty):
|
||||
"""
|
||||
How tough are you? (1-10)
|
||||
"""
|
||||
|
||||
|
||||
def _accuracyAction():
|
||||
# add tick marks just to exercise quoting
|
||||
return CompleteList(["1", "2", "3"], descr="Accuracy'`?")
|
||||
|
||||
|
||||
class SimpleProgOptions(usage.Options):
|
||||
"""
|
||||
Command-line options for a `Silly` imaginary program
|
||||
"""
|
||||
|
||||
optFlags = [
|
||||
["color", "c", "Turn on color output"],
|
||||
["gray", "g", "Turn on gray-scale output"],
|
||||
["verbose", "v", "Verbose logging (may be specified more than once)"],
|
||||
]
|
||||
|
||||
optParameters = [
|
||||
["optimization", None, "5", "Select the level of optimization (1-5)"],
|
||||
["accuracy", "a", "3", "Select the level of accuracy (1-3)"],
|
||||
]
|
||||
|
||||
compData = Completions(
|
||||
descriptions={"color": "Color on", "optimization": "Optimization level"},
|
||||
multiUse=["verbose"],
|
||||
mutuallyExclusive=[["color", "gray"]],
|
||||
optActions={
|
||||
"optimization": CompleteList(
|
||||
["1", "2", "3", "4", "5"], descr="Optimization?"
|
||||
),
|
||||
"accuracy": _accuracyAction,
|
||||
},
|
||||
extraActions=[CompleteFiles(descr="output file")],
|
||||
)
|
||||
|
||||
def opt_X(self):
|
||||
"""
|
||||
usage.Options does not recognize single-letter opt_ methods
|
||||
"""
|
||||
|
||||
|
||||
class SimpleProgSub1(usage.Options):
|
||||
optFlags = [["sub-opt", "s", "Sub Opt One"]]
|
||||
|
||||
|
||||
class SimpleProgSub2(usage.Options):
|
||||
optFlags = [["sub-opt", "s", "Sub Opt Two"]]
|
||||
|
||||
|
||||
class SimpleProgWithSubcommands(SimpleProgOptions):
|
||||
optFlags = [["some-option"], ["other-option", "o"]]
|
||||
|
||||
optParameters = [
|
||||
["some-param"],
|
||||
["other-param", "p"],
|
||||
["another-param", "P", "Yet Another Param"],
|
||||
]
|
||||
|
||||
subCommands = [
|
||||
["sub1", None, SimpleProgSub1, "Sub Command 1"],
|
||||
["sub2", None, SimpleProgSub2, "Sub Command 2"],
|
||||
]
|
||||
|
||||
|
||||
testOutput1 = b"""#compdef silly
|
||||
|
||||
_arguments -s -A "-*" \\
|
||||
':output file (*):_files -g "*"' \\
|
||||
"(--accuracy)-a[Select the level of accuracy (1-3)]:Accuracy'\\`?:(1 2 3)" \\
|
||||
"(-a)--accuracy=[Select the level of accuracy (1-3)]:Accuracy'\\`?:(1 2 3)" \\
|
||||
'(--color --gray -g)-c[Color on]' \\
|
||||
'(--gray -c -g)--color[Color on]' \\
|
||||
'(--color --gray -c)-g[Turn on gray-scale output]' \\
|
||||
'(--color -c -g)--gray[Turn on gray-scale output]' \\
|
||||
'--help[Display this help and exit.]' \\
|
||||
'--optimization=[Optimization level]:Optimization?:(1 2 3 4 5)' \\
|
||||
'*-v[Verbose logging (may be specified more than once)]' \\
|
||||
'*--verbose[Verbose logging (may be specified more than once)]' \\
|
||||
'--version[Display Twisted version and exit.]' \\
|
||||
&& return 0
|
||||
"""
|
||||
|
||||
# with sub-commands
|
||||
testOutput2 = b"""#compdef silly2
|
||||
|
||||
_arguments -s -A "-*" \\
|
||||
'*::subcmd:->subcmd' \\
|
||||
':output file (*):_files -g "*"' \\
|
||||
"(--accuracy)-a[Select the level of accuracy (1-3)]:Accuracy'\\`?:(1 2 3)" \\
|
||||
"(-a)--accuracy=[Select the level of accuracy (1-3)]:Accuracy'\\`?:(1 2 3)" \\
|
||||
'(--another-param)-P[another-param]:another-param:_files' \\
|
||||
'(-P)--another-param=[another-param]:another-param:_files' \\
|
||||
'(--color --gray -g)-c[Color on]' \\
|
||||
'(--gray -c -g)--color[Color on]' \\
|
||||
'(--color --gray -c)-g[Turn on gray-scale output]' \\
|
||||
'(--color -c -g)--gray[Turn on gray-scale output]' \\
|
||||
'--help[Display this help and exit.]' \\
|
||||
'--optimization=[Optimization level]:Optimization?:(1 2 3 4 5)' \\
|
||||
'(--other-option)-o[other-option]' \\
|
||||
'(-o)--other-option[other-option]' \\
|
||||
'(--other-param)-p[other-param]:other-param:_files' \\
|
||||
'(-p)--other-param=[other-param]:other-param:_files' \\
|
||||
'--some-option[some-option]' \\
|
||||
'--some-param=[some-param]:some-param:_files' \\
|
||||
'*-v[Verbose logging (may be specified more than once)]' \\
|
||||
'*--verbose[Verbose logging (may be specified more than once)]' \\
|
||||
'--version[Display Twisted version and exit.]' \\
|
||||
&& return 0
|
||||
local _zsh_subcmds_array
|
||||
_zsh_subcmds_array=(
|
||||
"sub1:Sub Command 1"
|
||||
"sub2:Sub Command 2"
|
||||
)
|
||||
|
||||
_describe "sub-command" _zsh_subcmds_array
|
||||
"""
|
||||
@@ -0,0 +1,158 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
from twisted.python.failure import Failure
|
||||
from twisted.trial.unittest import TestCase
|
||||
|
||||
try:
|
||||
import syslog as _stdsyslog
|
||||
except ImportError:
|
||||
stdsyslog = None
|
||||
else:
|
||||
stdsyslog = _stdsyslog
|
||||
from twisted.python import syslog
|
||||
|
||||
|
||||
class SyslogObserverTests(TestCase):
|
||||
"""
|
||||
Tests for L{SyslogObserver} which sends Twisted log events to the syslog.
|
||||
"""
|
||||
|
||||
events = None
|
||||
|
||||
if stdsyslog is None:
|
||||
skip = "syslog is not supported on this platform"
|
||||
|
||||
def setUp(self):
|
||||
self.patch(syslog.SyslogObserver, "openlog", self.openlog)
|
||||
self.patch(syslog.SyslogObserver, "syslog", self.syslog)
|
||||
self.observer = syslog.SyslogObserver("SyslogObserverTests")
|
||||
|
||||
def openlog(self, prefix, options, facility):
|
||||
self.logOpened = (prefix, options, facility)
|
||||
self.events = []
|
||||
|
||||
def syslog(self, options, message):
|
||||
self.events.append((options, message))
|
||||
|
||||
def test_emitWithoutMessage(self):
|
||||
"""
|
||||
L{SyslogObserver.emit} ignores events with an empty value for the
|
||||
C{'message'} key.
|
||||
"""
|
||||
self.observer.emit({"message": (), "isError": False, "system": "-"})
|
||||
self.assertEqual(self.events, [])
|
||||
|
||||
def test_emitCustomPriority(self):
|
||||
"""
|
||||
L{SyslogObserver.emit} uses the value of the C{'syslogPriority'} as the
|
||||
syslog priority, if that key is present in the event dictionary.
|
||||
"""
|
||||
self.observer.emit(
|
||||
{
|
||||
"message": ("hello, world",),
|
||||
"isError": False,
|
||||
"system": "-",
|
||||
"syslogPriority": stdsyslog.LOG_DEBUG,
|
||||
}
|
||||
)
|
||||
self.assertEqual(self.events, [(stdsyslog.LOG_DEBUG, "[-] hello, world")])
|
||||
|
||||
def test_emitErrorPriority(self):
|
||||
"""
|
||||
L{SyslogObserver.emit} uses C{LOG_ALERT} if the event represents an
|
||||
error.
|
||||
"""
|
||||
self.observer.emit(
|
||||
{
|
||||
"message": ("hello, world",),
|
||||
"isError": True,
|
||||
"system": "-",
|
||||
"failure": Failure(Exception("foo")),
|
||||
}
|
||||
)
|
||||
self.assertEqual(self.events, [(stdsyslog.LOG_ALERT, "[-] hello, world")])
|
||||
|
||||
def test_emitCustomPriorityOverridesError(self):
|
||||
"""
|
||||
L{SyslogObserver.emit} uses the value of the C{'syslogPriority'} key if
|
||||
it is specified even if the event dictionary represents an error.
|
||||
"""
|
||||
self.observer.emit(
|
||||
{
|
||||
"message": ("hello, world",),
|
||||
"isError": True,
|
||||
"system": "-",
|
||||
"syslogPriority": stdsyslog.LOG_NOTICE,
|
||||
"failure": Failure(Exception("bar")),
|
||||
}
|
||||
)
|
||||
self.assertEqual(self.events, [(stdsyslog.LOG_NOTICE, "[-] hello, world")])
|
||||
|
||||
def test_emitCustomFacility(self):
|
||||
"""
|
||||
L{SyslogObserver.emit} uses the value of the C{'syslogPriority'} as the
|
||||
syslog priority, if that key is present in the event dictionary.
|
||||
"""
|
||||
self.observer.emit(
|
||||
{
|
||||
"message": ("hello, world",),
|
||||
"isError": False,
|
||||
"system": "-",
|
||||
"syslogFacility": stdsyslog.LOG_CRON,
|
||||
}
|
||||
)
|
||||
self.assertEqual(
|
||||
self.events, [(stdsyslog.LOG_INFO | stdsyslog.LOG_CRON, "[-] hello, world")]
|
||||
)
|
||||
|
||||
def test_emitCustomSystem(self):
|
||||
"""
|
||||
L{SyslogObserver.emit} uses the value of the C{'system'} key to prefix
|
||||
the logged message.
|
||||
"""
|
||||
self.observer.emit(
|
||||
{
|
||||
"message": ("hello, world",),
|
||||
"isError": False,
|
||||
"system": "nonDefaultSystem",
|
||||
}
|
||||
)
|
||||
self.assertEqual(
|
||||
self.events, [(stdsyslog.LOG_INFO, "[nonDefaultSystem] hello, world")]
|
||||
)
|
||||
|
||||
def test_emitMessage(self):
|
||||
"""
|
||||
L{SyslogObserver.emit} logs the value of the C{'message'} key of the
|
||||
event dictionary it is passed to the syslog.
|
||||
"""
|
||||
self.observer.emit(
|
||||
{"message": ("hello, world",), "isError": False, "system": "-"}
|
||||
)
|
||||
self.assertEqual(self.events, [(stdsyslog.LOG_INFO, "[-] hello, world")])
|
||||
|
||||
def test_emitMultilineMessage(self):
|
||||
"""
|
||||
Each line of a multiline message is emitted separately to the syslog.
|
||||
"""
|
||||
self.observer.emit(
|
||||
{"message": ("hello,\nworld",), "isError": False, "system": "-"}
|
||||
)
|
||||
self.assertEqual(
|
||||
self.events,
|
||||
[(stdsyslog.LOG_INFO, "[-] hello,"), (stdsyslog.LOG_INFO, "[-] \tworld")],
|
||||
)
|
||||
|
||||
def test_emitStripsTrailingEmptyLines(self):
|
||||
"""
|
||||
Trailing empty lines of a multiline message are omitted from the
|
||||
messages sent to the syslog.
|
||||
"""
|
||||
self.observer.emit(
|
||||
{"message": ("hello,\nworld\n\n",), "isError": False, "system": "-"}
|
||||
)
|
||||
self.assertEqual(
|
||||
self.events,
|
||||
[(stdsyslog.LOG_INFO, "[-] hello,"), (stdsyslog.LOG_INFO, "[-] \tworld")],
|
||||
)
|
||||
@@ -0,0 +1,180 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Tests for L{twisted.python.systemd}.
|
||||
"""
|
||||
|
||||
|
||||
import os
|
||||
from typing import Dict, Mapping, Sequence
|
||||
|
||||
from hamcrest import assert_that, equal_to, not_
|
||||
from hypothesis import given
|
||||
from hypothesis.strategies import dictionaries, integers, lists
|
||||
|
||||
from twisted.python.systemd import ListenFDs
|
||||
from twisted.trial.unittest import SynchronousTestCase
|
||||
from .strategies import systemdDescriptorNames
|
||||
|
||||
|
||||
def buildEnvironment(count: int, pid: object) -> Dict[str, str]:
|
||||
"""
|
||||
@param count: The number of file descriptors to indicate as inherited.
|
||||
|
||||
@param pid: The pid of the inheriting process to indicate.
|
||||
|
||||
@return: A copy of the current process environment with the I{systemd}
|
||||
file descriptor inheritance-related environment variables added to it.
|
||||
"""
|
||||
result = os.environ.copy()
|
||||
result["LISTEN_FDS"] = str(count)
|
||||
result["LISTEN_FDNAMES"] = ":".join([f"{n}.socket" for n in range(count)])
|
||||
result["LISTEN_PID"] = str(pid)
|
||||
return result
|
||||
|
||||
|
||||
class ListenFDsTests(SynchronousTestCase):
|
||||
"""
|
||||
Apply tests to L{ListenFDs}, constructed based on an environment dictionary.
|
||||
"""
|
||||
|
||||
@given(lists(systemdDescriptorNames(), min_size=0, max_size=10))
|
||||
def test_fromEnvironmentEquivalence(self, names: Sequence[str]) -> None:
|
||||
"""
|
||||
The L{ListenFDs} and L{ListenFDs.fromEnvironment} constructors are
|
||||
equivalent for their respective representations of the same
|
||||
information.
|
||||
|
||||
@param names: The names of the file descriptors to represent as
|
||||
inherited in the test environment given to the parser. The number
|
||||
of descriptors represented will equal the length of this list.
|
||||
"""
|
||||
numFDs = len(names)
|
||||
descriptors = list(range(ListenFDs._START, ListenFDs._START + numFDs))
|
||||
fds = ListenFDs.fromEnvironment(
|
||||
{
|
||||
"LISTEN_PID": str(os.getpid()),
|
||||
"LISTEN_FDS": str(numFDs),
|
||||
"LISTEN_FDNAMES": ":".join(names),
|
||||
}
|
||||
)
|
||||
assert_that(fds, equal_to(ListenFDs(descriptors, tuple(names))))
|
||||
|
||||
def test_defaultEnviron(self) -> None:
|
||||
"""
|
||||
If the process environment is not explicitly passed to
|
||||
L{ListenFDs.fromEnvironment}, the real process environment dictionary
|
||||
is used.
|
||||
"""
|
||||
self.patch(os, "environ", buildEnvironment(5, os.getpid()))
|
||||
sddaemon = ListenFDs.fromEnvironment()
|
||||
self.assertEqual(list(range(3, 3 + 5)), sddaemon.inheritedDescriptors())
|
||||
|
||||
def test_secondEnvironment(self) -> None:
|
||||
"""
|
||||
L{ListenFDs.fromEnvironment} removes information about the
|
||||
inherited file descriptors from the environment mapping so that the
|
||||
same inherited file descriptors cannot be handled repeatedly from
|
||||
multiple L{ListenFDs} instances.
|
||||
"""
|
||||
env = buildEnvironment(3, os.getpid())
|
||||
first = ListenFDs.fromEnvironment(environ=env)
|
||||
second = ListenFDs.fromEnvironment(environ=env)
|
||||
self.assertEqual(list(range(3, 6)), first.inheritedDescriptors())
|
||||
self.assertEqual([], second.inheritedDescriptors())
|
||||
|
||||
def test_mismatchedPID(self) -> None:
|
||||
"""
|
||||
If the current process PID does not match the PID in the
|
||||
environment then the systemd variables in the environment were set for
|
||||
a different process (perhaps our parent) and the inherited descriptors
|
||||
are not intended for this process so L{ListenFDs.inheritedDescriptors}
|
||||
returns an empty list.
|
||||
"""
|
||||
env = buildEnvironment(3, os.getpid() + 1)
|
||||
sddaemon = ListenFDs.fromEnvironment(environ=env)
|
||||
self.assertEqual([], sddaemon.inheritedDescriptors())
|
||||
|
||||
def test_missingPIDVariable(self) -> None:
|
||||
"""
|
||||
If the I{LISTEN_PID} environment variable is not present then
|
||||
there is no clear indication that any file descriptors were inherited
|
||||
by this process so L{ListenFDs.inheritedDescriptors} returns an empty
|
||||
list.
|
||||
"""
|
||||
env = buildEnvironment(3, os.getpid())
|
||||
del env["LISTEN_PID"]
|
||||
sddaemon = ListenFDs.fromEnvironment(environ=env)
|
||||
self.assertEqual([], sddaemon.inheritedDescriptors())
|
||||
|
||||
def test_nonIntegerPIDVariable(self) -> None:
|
||||
"""
|
||||
If the I{LISTEN_PID} environment variable is set to a string that cannot
|
||||
be parsed as an integer, no inherited descriptors are reported.
|
||||
"""
|
||||
env = buildEnvironment(3, "hello, world")
|
||||
sddaemon = ListenFDs.fromEnvironment(environ=env)
|
||||
self.assertEqual([], sddaemon.inheritedDescriptors())
|
||||
|
||||
def test_missingFDSVariable(self) -> None:
|
||||
"""
|
||||
If the I{LISTEN_FDS} and I{LISTEN_FDNAMES} environment variables
|
||||
are not present, no inherited descriptors are reported.
|
||||
"""
|
||||
env = buildEnvironment(3, os.getpid())
|
||||
del env["LISTEN_FDS"]
|
||||
del env["LISTEN_FDNAMES"]
|
||||
sddaemon = ListenFDs.fromEnvironment(environ=env)
|
||||
self.assertEqual([], sddaemon.inheritedDescriptors())
|
||||
|
||||
def test_nonIntegerFDSVariable(self) -> None:
|
||||
"""
|
||||
If the I{LISTEN_FDS} environment variable is set to a string that cannot
|
||||
be parsed as an integer, no inherited descriptors are reported.
|
||||
"""
|
||||
env = buildEnvironment(3, os.getpid())
|
||||
env["LISTEN_FDS"] = "hello, world"
|
||||
sddaemon = ListenFDs.fromEnvironment(environ=env)
|
||||
self.assertEqual([], sddaemon.inheritedDescriptors())
|
||||
|
||||
@given(lists(integers(min_value=0, max_value=10), unique=True))
|
||||
def test_inheritedDescriptors(self, descriptors: Sequence[int]) -> None:
|
||||
"""
|
||||
L{ListenFDs.inheritedDescriptors} returns a copy of the inherited
|
||||
descriptors list.
|
||||
"""
|
||||
names = tuple(map(str, descriptors))
|
||||
fds = ListenFDs(descriptors, names)
|
||||
fdsCopy = fds.inheritedDescriptors()
|
||||
assert_that(descriptors, equal_to(fdsCopy))
|
||||
fdsCopy.append(1)
|
||||
assert_that(descriptors, not_(equal_to(fdsCopy)))
|
||||
|
||||
@given(dictionaries(systemdDescriptorNames(), integers(min_value=0), max_size=10))
|
||||
def test_inheritedNamedDescriptors(self, expected: Mapping[str, int]) -> None:
|
||||
"""
|
||||
L{ListenFDs.inheritedNamedDescriptors} returns a mapping from the
|
||||
descriptor names to their integer values, with items formed by
|
||||
pairwise combination of the input descriptors and names.
|
||||
"""
|
||||
items = list(expected.items())
|
||||
names = [name for name, _ in items]
|
||||
descriptors = [fd for _, fd in items]
|
||||
fds = ListenFDs(descriptors, names)
|
||||
assert_that(fds.inheritedNamedDescriptors(), equal_to(expected))
|
||||
|
||||
@given(lists(integers(min_value=0, max_value=10), unique=True))
|
||||
def test_repeated(self, descriptors: Sequence[int]) -> None:
|
||||
"""
|
||||
Any subsequent calls to C{inheritedDescriptors} and
|
||||
C{inheritedNamedDescriptors} return the same list.
|
||||
"""
|
||||
names = tuple(map(str, descriptors))
|
||||
sddaemon = ListenFDs(descriptors, names)
|
||||
self.assertEqual(
|
||||
sddaemon.inheritedDescriptors(), sddaemon.inheritedDescriptors()
|
||||
)
|
||||
self.assertEqual(
|
||||
sddaemon.inheritedNamedDescriptors(), sddaemon.inheritedNamedDescriptors()
|
||||
)
|
||||
@@ -0,0 +1,23 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Tests for L{twisted.python.textattributes}.
|
||||
"""
|
||||
|
||||
from twisted.python._textattributes import DefaultFormattingState
|
||||
from twisted.trial import unittest
|
||||
|
||||
|
||||
class DefaultFormattingStateTests(unittest.TestCase):
|
||||
"""
|
||||
Tests for L{twisted.python._textattributes.DefaultFormattingState}.
|
||||
"""
|
||||
|
||||
def test_equality(self) -> None:
|
||||
"""
|
||||
L{DefaultFormattingState}s are always equal to other
|
||||
L{DefaultFormattingState}s.
|
||||
"""
|
||||
self.assertEqual(DefaultFormattingState(), DefaultFormattingState())
|
||||
self.assertNotEqual(DefaultFormattingState(), "hello")
|
||||
@@ -0,0 +1,145 @@
|
||||
# # Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Tests for L{twisted.python._tzhelper}.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from os import environ
|
||||
|
||||
try:
|
||||
from time import tzset as _tzset
|
||||
except ImportError:
|
||||
tzset = None
|
||||
else:
|
||||
tzset = _tzset
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from time import mktime as mktime_real
|
||||
|
||||
from twisted.python._tzhelper import FixedOffsetTimeZone
|
||||
from twisted.trial.unittest import SkipTest, TestCase
|
||||
|
||||
# On some rare platforms (FreeBSD 8? I was not able to reproduce
|
||||
# on FreeBSD 9) 'mktime' seems to always fail once tzset() has been
|
||||
# called more than once in a process lifetime. I think this is
|
||||
# just a platform bug, so let's work around it. -glyph
|
||||
|
||||
|
||||
def mktime(t9: tuple[int, int, int, int, int, int, int, int, int]) -> float:
|
||||
"""
|
||||
Call L{mktime_real}, and if it raises L{OverflowError}, catch it and raise
|
||||
SkipTest instead.
|
||||
|
||||
@param t9: A time as a 9-item tuple.
|
||||
@type t9: L{tuple}
|
||||
|
||||
@return: A timestamp.
|
||||
@rtype: L{float}
|
||||
"""
|
||||
try:
|
||||
return mktime_real(t9)
|
||||
except OverflowError:
|
||||
raise SkipTest(f"Platform cannot construct time zone for {t9!r}")
|
||||
|
||||
|
||||
def setTZ(name: str | None) -> None:
|
||||
"""
|
||||
Set time zone.
|
||||
|
||||
@param name: a time zone name
|
||||
@type name: L{str}
|
||||
"""
|
||||
if tzset is None:
|
||||
return
|
||||
|
||||
if name is None:
|
||||
try:
|
||||
del environ["TZ"]
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
environ["TZ"] = name
|
||||
tzset()
|
||||
|
||||
|
||||
def addTZCleanup(testCase: TestCase) -> None:
|
||||
"""
|
||||
Add cleanup hooks to a test case to reset timezone to original value.
|
||||
|
||||
@param testCase: the test case to add the cleanup to.
|
||||
@type testCase: L{unittest.TestCase}
|
||||
"""
|
||||
tzIn = environ.get("TZ", None)
|
||||
|
||||
@testCase.addCleanup
|
||||
def resetTZ() -> None:
|
||||
setTZ(tzIn)
|
||||
|
||||
|
||||
class FixedOffsetTimeZoneTests(TestCase):
|
||||
"""
|
||||
Tests for L{FixedOffsetTimeZone}.
|
||||
"""
|
||||
|
||||
def test_tzinfo(self) -> None:
|
||||
"""
|
||||
Test that timezone attributes respect the timezone as set by the
|
||||
standard C{TZ} environment variable and L{tzset} API.
|
||||
"""
|
||||
if tzset is None:
|
||||
raise SkipTest("Platform cannot change timezone; unable to verify offsets.")
|
||||
|
||||
def testForTimeZone(
|
||||
name: str, expectedOffsetDST: str, expectedOffsetSTD: str
|
||||
) -> None:
|
||||
setTZ(name)
|
||||
|
||||
localDST = mktime((2006, 6, 30, 0, 0, 0, 4, 181, 1))
|
||||
localDSTdt = datetime.fromtimestamp(localDST)
|
||||
localSTD = mktime((2007, 1, 31, 0, 0, 0, 2, 31, 0))
|
||||
localSTDdt = datetime.fromtimestamp(localSTD)
|
||||
|
||||
tzDST = FixedOffsetTimeZone.fromLocalTimeStamp(localDST)
|
||||
tzSTD = FixedOffsetTimeZone.fromLocalTimeStamp(localSTD)
|
||||
|
||||
self.assertEqual(tzDST.tzname(localDSTdt), f"UTC{expectedOffsetDST}")
|
||||
self.assertEqual(tzSTD.tzname(localSTDdt), f"UTC{expectedOffsetSTD}")
|
||||
|
||||
self.assertEqual(tzDST.dst(localDSTdt), timedelta(0))
|
||||
self.assertEqual(tzSTD.dst(localSTDdt), timedelta(0))
|
||||
|
||||
def timeDeltaFromOffset(offset: str) -> timedelta:
|
||||
assert len(offset) == 5
|
||||
|
||||
sign = offset[0]
|
||||
hours = int(offset[1:3])
|
||||
minutes = int(offset[3:5])
|
||||
|
||||
if sign == "-":
|
||||
hours = -hours
|
||||
minutes = -minutes
|
||||
else:
|
||||
assert sign == "+"
|
||||
|
||||
return timedelta(hours=hours, minutes=minutes)
|
||||
|
||||
self.assertEqual(
|
||||
tzDST.utcoffset(localDSTdt), timeDeltaFromOffset(expectedOffsetDST)
|
||||
)
|
||||
self.assertEqual(
|
||||
tzSTD.utcoffset(localSTDdt), timeDeltaFromOffset(expectedOffsetSTD)
|
||||
)
|
||||
|
||||
addTZCleanup(self)
|
||||
|
||||
# UTC
|
||||
testForTimeZone("UTC+00", "+0000", "+0000")
|
||||
# West of UTC
|
||||
testForTimeZone("EST+05EDT,M4.1.0,M10.5.0", "-0400", "-0500")
|
||||
# East of UTC
|
||||
testForTimeZone("CEST-01CEDT,M4.1.0,M10.5.0", "+0200", "+0100")
|
||||
# No DST
|
||||
testForTimeZone("CST+06", "-0600", "-0600")
|
||||
@@ -0,0 +1,825 @@
|
||||
# -*- test-case-name: twisted.python.test.test_url -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Tests for L{twisted.python.url}.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Iterable
|
||||
|
||||
from typing_extensions import Protocol
|
||||
|
||||
from twisted.trial.unittest import SynchronousTestCase
|
||||
from ..url import URL
|
||||
|
||||
theurl = "http://www.foo.com/a/nice/path/?zot=23&zut"
|
||||
|
||||
# Examples from RFC 3986 section 5.4, Reference Resolution Examples
|
||||
relativeLinkBaseForRFC3986 = "http://a/b/c/d;p?q"
|
||||
relativeLinkTestsForRFC3986 = [
|
||||
# "Normal"
|
||||
# ('g:h', 'g:h'), # Not supported: scheme with relative path
|
||||
("g", "http://a/b/c/g"),
|
||||
("./g", "http://a/b/c/g"),
|
||||
("g/", "http://a/b/c/g/"),
|
||||
("/g", "http://a/g"),
|
||||
("//g", "http://g"),
|
||||
("?y", "http://a/b/c/d;p?y"),
|
||||
("g?y", "http://a/b/c/g?y"),
|
||||
("#s", "http://a/b/c/d;p?q#s"),
|
||||
("g#s", "http://a/b/c/g#s"),
|
||||
("g?y#s", "http://a/b/c/g?y#s"),
|
||||
(";x", "http://a/b/c/;x"),
|
||||
("g;x", "http://a/b/c/g;x"),
|
||||
("g;x?y#s", "http://a/b/c/g;x?y#s"),
|
||||
("", "http://a/b/c/d;p?q"),
|
||||
(".", "http://a/b/c/"),
|
||||
("./", "http://a/b/c/"),
|
||||
("..", "http://a/b/"),
|
||||
("../", "http://a/b/"),
|
||||
("../g", "http://a/b/g"),
|
||||
("../..", "http://a/"),
|
||||
("../../", "http://a/"),
|
||||
("../../g", "http://a/g"),
|
||||
# Abnormal examples
|
||||
# ".." cannot be used to change the authority component of a URI.
|
||||
("../../../g", "http://a/g"),
|
||||
("../../../../g", "http://a/g"),
|
||||
# Only include "." and ".." when they are only part of a larger segment,
|
||||
# not by themselves.
|
||||
("/./g", "http://a/g"),
|
||||
("/../g", "http://a/g"),
|
||||
("g.", "http://a/b/c/g."),
|
||||
(".g", "http://a/b/c/.g"),
|
||||
("g..", "http://a/b/c/g.."),
|
||||
("..g", "http://a/b/c/..g"),
|
||||
# Unnecessary or nonsensical forms of "." and "..".
|
||||
("./../g", "http://a/b/g"),
|
||||
("./g/.", "http://a/b/c/g/"),
|
||||
("g/./h", "http://a/b/c/g/h"),
|
||||
("g/../h", "http://a/b/c/h"),
|
||||
("g;x=1/./y", "http://a/b/c/g;x=1/y"),
|
||||
("g;x=1/../y", "http://a/b/c/y"),
|
||||
# Separating the reference's query and fragment components from the path.
|
||||
("g?y/./x", "http://a/b/c/g?y/./x"),
|
||||
("g?y/../x", "http://a/b/c/g?y/../x"),
|
||||
("g#s/./x", "http://a/b/c/g#s/./x"),
|
||||
("g#s/../x", "http://a/b/c/g#s/../x"),
|
||||
# Not supported: scheme with relative path
|
||||
# ("http:g", "http:g"), # strict
|
||||
# ("http:g", "http://a/b/c/g"), # non-strict
|
||||
]
|
||||
|
||||
|
||||
_percentenc = lambda s: "".join("%%%02X" % ord(c) for c in s)
|
||||
|
||||
|
||||
class _HasException(Protocol):
|
||||
@property
|
||||
def exception(self) -> BaseException:
|
||||
...
|
||||
|
||||
|
||||
class TestURL(SynchronousTestCase):
|
||||
"""
|
||||
Tests for L{URL}.
|
||||
"""
|
||||
|
||||
def assertUnicoded(self, u: URL) -> None:
|
||||
"""
|
||||
The given L{URL}'s components should be L{unicode}.
|
||||
|
||||
@param u: The L{URL} to test.
|
||||
"""
|
||||
self.assertIsInstance(u.scheme, str, repr(u))
|
||||
self.assertIsInstance(u.host, str, repr(u))
|
||||
for seg in u.path:
|
||||
self.assertIsInstance(seg, str, repr(u))
|
||||
for k, v in u.query:
|
||||
self.assertIsInstance(k, str, repr(u))
|
||||
self.assertTrue(v is None or isinstance(v, str), repr(u))
|
||||
self.assertIsInstance(u.fragment, str, repr(u))
|
||||
|
||||
def assertURL(
|
||||
self,
|
||||
u: URL,
|
||||
scheme: str,
|
||||
host: str,
|
||||
path: Iterable[str],
|
||||
query: Iterable[tuple[str, str | None]],
|
||||
fragment: str,
|
||||
port: int | None,
|
||||
userinfo: str = "",
|
||||
) -> None:
|
||||
"""
|
||||
The given L{URL} should have the given components.
|
||||
|
||||
@param u: The actual L{URL} to examine.
|
||||
|
||||
@param scheme: The expected scheme.
|
||||
|
||||
@param host: The expected host.
|
||||
|
||||
@param path: The expected path.
|
||||
|
||||
@param query: The expected query.
|
||||
|
||||
@param fragment: The expected fragment.
|
||||
|
||||
@param port: The expected port.
|
||||
|
||||
@param userinfo: The expected userinfo.
|
||||
"""
|
||||
actual = (u.scheme, u.host, u.path, u.query, u.fragment, u.port, u.userinfo)
|
||||
expected = (scheme, host, tuple(path), tuple(query), fragment, port, u.userinfo)
|
||||
self.assertEqual(actual, expected)
|
||||
|
||||
def test_initDefaults(self) -> None:
|
||||
"""
|
||||
L{URL} should have appropriate default values.
|
||||
"""
|
||||
|
||||
def check(u: URL) -> None:
|
||||
self.assertUnicoded(u)
|
||||
self.assertURL(u, "http", "", [], [], "", 80, "")
|
||||
|
||||
check(URL("http", ""))
|
||||
check(URL("http", "", [], []))
|
||||
check(URL("http", "", [], [], ""))
|
||||
|
||||
def test_init(self) -> None:
|
||||
"""
|
||||
L{URL} should accept L{unicode} parameters.
|
||||
"""
|
||||
u = URL("s", "h", ["p"], [("k", "v"), ("k", None)], "f")
|
||||
self.assertUnicoded(u)
|
||||
self.assertURL(u, "s", "h", ["p"], [("k", "v"), ("k", None)], "f", None)
|
||||
|
||||
self.assertURL(
|
||||
URL("http", "\xe0", ["\xe9"], [("\u03bb", "\u03c0")], "\u22a5"),
|
||||
"http",
|
||||
"\xe0",
|
||||
["\xe9"],
|
||||
[("\u03bb", "\u03c0")],
|
||||
"\u22a5",
|
||||
80,
|
||||
)
|
||||
|
||||
def test_initPercent(self) -> None:
|
||||
"""
|
||||
L{URL} should accept (and not interpret) percent characters.
|
||||
"""
|
||||
u = URL("s", "%68", ["%70"], [("%6B", "%76"), ("%6B", None)], "%66")
|
||||
self.assertUnicoded(u)
|
||||
self.assertURL(
|
||||
u, "s", "%68", ["%70"], [("%6B", "%76"), ("%6B", None)], "%66", None
|
||||
)
|
||||
|
||||
def test_repr(self) -> None:
|
||||
"""
|
||||
L{URL.__repr__} will display the canonical form of the URL, wrapped in
|
||||
a L{URL.fromText} invocation, so that it is C{eval}-able but still easy
|
||||
to read.
|
||||
"""
|
||||
self.assertEqual(
|
||||
repr(
|
||||
URL(
|
||||
scheme="http",
|
||||
host="foo",
|
||||
path=["bar"],
|
||||
query=[("baz", None), ("k", "v")],
|
||||
fragment="frob",
|
||||
)
|
||||
),
|
||||
"URL.from_text({})".format(repr("http://foo/bar?baz&k=v#frob")),
|
||||
)
|
||||
|
||||
def test_fromText(self) -> None:
|
||||
"""
|
||||
Round-tripping L{URL.fromText} with C{str} results in an equivalent
|
||||
URL.
|
||||
"""
|
||||
urlpath = URL.fromText(theurl)
|
||||
self.assertEqual(theurl, urlpath.asText())
|
||||
|
||||
def test_roundtrip(self) -> None:
|
||||
"""
|
||||
L{URL.asText} should invert L{URL.fromText}.
|
||||
"""
|
||||
tests = (
|
||||
"http://localhost",
|
||||
"http://localhost/",
|
||||
"http://localhost/foo",
|
||||
"http://localhost/foo/",
|
||||
"http://localhost/foo!!bar/",
|
||||
"http://localhost/foo%20bar/",
|
||||
"http://localhost/foo%2Fbar/",
|
||||
"http://localhost/foo?n",
|
||||
"http://localhost/foo?n=v",
|
||||
"http://localhost/foo?n=/a/b",
|
||||
"http://example.com/foo!@$bar?b!@z=123",
|
||||
"http://localhost/asd?a=asd%20sdf/345",
|
||||
"http://(%2525)/(%2525)?(%2525)&(%2525)=(%2525)#(%2525)",
|
||||
"http://(%C3%A9)/(%C3%A9)?(%C3%A9)&(%C3%A9)=(%C3%A9)#(%C3%A9)",
|
||||
)
|
||||
for test in tests:
|
||||
result = URL.fromText(test).asText()
|
||||
self.assertEqual(test, result)
|
||||
|
||||
def test_equality(self) -> None:
|
||||
"""
|
||||
Two URLs decoded using L{URL.fromText} will be equal (C{==}) if they
|
||||
decoded same URL string, and unequal (C{!=}) if they decoded different
|
||||
strings.
|
||||
"""
|
||||
urlpath = URL.fromText(theurl)
|
||||
self.assertEqual(urlpath, URL.fromText(theurl))
|
||||
self.assertNotEqual(
|
||||
urlpath,
|
||||
URL.fromText(
|
||||
"ftp://www.anotherinvaliddomain.com/" "foo/bar/baz/?zot=21&zut"
|
||||
),
|
||||
)
|
||||
|
||||
def test_fragmentEquality(self) -> None:
|
||||
"""
|
||||
An URL created with the empty string for a fragment compares equal
|
||||
to an URL created with an unspecified fragment.
|
||||
"""
|
||||
self.assertEqual(URL(fragment=""), URL())
|
||||
self.assertEqual(
|
||||
URL.fromText("http://localhost/#"), URL.fromText("http://localhost/")
|
||||
)
|
||||
|
||||
def test_child(self) -> None:
|
||||
"""
|
||||
L{URL.child} appends a new path segment, but does not affect the query
|
||||
or fragment.
|
||||
"""
|
||||
urlpath = URL.fromText(theurl)
|
||||
self.assertEqual(
|
||||
"http://www.foo.com/a/nice/path/gong?zot=23&zut",
|
||||
urlpath.child("gong").asText(),
|
||||
)
|
||||
self.assertEqual(
|
||||
"http://www.foo.com/a/nice/path/gong%2F?zot=23&zut",
|
||||
urlpath.child("gong/").asText(),
|
||||
)
|
||||
self.assertEqual(
|
||||
"http://www.foo.com/a/nice/path/gong%2Fdouble?zot=23&zut",
|
||||
urlpath.child("gong/double").asText(),
|
||||
)
|
||||
self.assertEqual(
|
||||
"http://www.foo.com/a/nice/path/gong%2Fdouble%2F?zot=23&zut",
|
||||
urlpath.child("gong/double/").asText(),
|
||||
)
|
||||
|
||||
def test_multiChild(self) -> None:
|
||||
"""
|
||||
L{URL.child} receives multiple segments as C{*args} and appends each in
|
||||
turn.
|
||||
"""
|
||||
self.assertEqual(
|
||||
URL.fromText("http://example.com/a/b").child("c", "d", "e").asText(),
|
||||
"http://example.com/a/b/c/d/e",
|
||||
)
|
||||
|
||||
def test_childInitRoot(self) -> None:
|
||||
"""
|
||||
L{URL.child} of a L{URL} without a path produces a L{URL} with a single
|
||||
path segment.
|
||||
"""
|
||||
childURL = URL(host="www.foo.com").child("c")
|
||||
self.assertTrue(childURL.rooted)
|
||||
self.assertEqual("http://www.foo.com/c", childURL.asText())
|
||||
|
||||
def test_sibling(self) -> None:
|
||||
"""
|
||||
L{URL.sibling} of a L{URL} replaces the last path segment, but does not
|
||||
affect the query or fragment.
|
||||
"""
|
||||
urlpath = URL.fromText(theurl)
|
||||
self.assertEqual(
|
||||
"http://www.foo.com/a/nice/path/sister?zot=23&zut",
|
||||
urlpath.sibling("sister").asText(),
|
||||
)
|
||||
# Use an url without trailing '/' to check child removal.
|
||||
theurl2 = "http://www.foo.com/a/nice/path?zot=23&zut"
|
||||
urlpath = URL.fromText(theurl2)
|
||||
self.assertEqual(
|
||||
"http://www.foo.com/a/nice/sister?zot=23&zut",
|
||||
urlpath.sibling("sister").asText(),
|
||||
)
|
||||
|
||||
def test_click(self) -> None:
|
||||
"""
|
||||
L{URL.click} interprets the given string as a relative URI-reference
|
||||
and returns a new L{URL} interpreting C{self} as the base absolute URI.
|
||||
"""
|
||||
urlpath = URL.fromText(theurl)
|
||||
# A null uri should be valid (return here).
|
||||
self.assertEqual(
|
||||
"http://www.foo.com/a/nice/path/?zot=23&zut", urlpath.click("").asText()
|
||||
)
|
||||
# A simple relative path remove the query.
|
||||
self.assertEqual(
|
||||
"http://www.foo.com/a/nice/path/click", urlpath.click("click").asText()
|
||||
)
|
||||
# An absolute path replace path and query.
|
||||
self.assertEqual("http://www.foo.com/click", urlpath.click("/click").asText())
|
||||
# Replace just the query.
|
||||
self.assertEqual(
|
||||
"http://www.foo.com/a/nice/path/?burp", urlpath.click("?burp").asText()
|
||||
)
|
||||
# One full url to another should not generate '//' between authority.
|
||||
# and path
|
||||
self.assertNotIn(
|
||||
"//foobar", urlpath.click("http://www.foo.com/foobar").asText()
|
||||
)
|
||||
|
||||
# From a url with no query clicking a url with a query, the query
|
||||
# should be handled properly.
|
||||
u = URL.fromText("http://www.foo.com/me/noquery")
|
||||
self.assertEqual(
|
||||
"http://www.foo.com/me/17?spam=158", u.click("/me/17?spam=158").asText()
|
||||
)
|
||||
|
||||
# Check that everything from the path onward is removed when the click
|
||||
# link has no path.
|
||||
u = URL.fromText("http://localhost/foo?abc=def")
|
||||
self.assertEqual(
|
||||
u.click("http://www.python.org").asText(), "http://www.python.org"
|
||||
)
|
||||
|
||||
def test_clickRFC3986(self) -> None:
|
||||
"""
|
||||
L{URL.click} should correctly resolve the examples in RFC 3986.
|
||||
"""
|
||||
base = URL.fromText(relativeLinkBaseForRFC3986)
|
||||
for ref, expected in relativeLinkTestsForRFC3986:
|
||||
self.assertEqual(base.click(ref).asText(), expected)
|
||||
|
||||
def test_clickSchemeRelPath(self) -> None:
|
||||
"""
|
||||
L{URL.click} should not accept schemes with relative paths.
|
||||
"""
|
||||
base = URL.fromText(relativeLinkBaseForRFC3986)
|
||||
self.assertRaises(NotImplementedError, base.click, "g:h")
|
||||
self.assertRaises(NotImplementedError, base.click, "http:h")
|
||||
|
||||
def test_cloneUnchanged(self) -> None:
|
||||
"""
|
||||
Verify that L{URL.replace} doesn't change any of the arguments it
|
||||
is passed.
|
||||
"""
|
||||
urlpath = URL.fromText("https://x:1/y?z=1#A")
|
||||
self.assertEqual(
|
||||
urlpath.replace(
|
||||
urlpath.scheme,
|
||||
urlpath.host,
|
||||
urlpath.path,
|
||||
urlpath.query,
|
||||
urlpath.fragment,
|
||||
urlpath.port,
|
||||
),
|
||||
urlpath,
|
||||
)
|
||||
self.assertEqual(urlpath.replace(), urlpath)
|
||||
|
||||
def test_clickCollapse(self) -> None:
|
||||
"""
|
||||
L{URL.click} collapses C{.} and C{..} according to RFC 3986 section
|
||||
5.2.4.
|
||||
"""
|
||||
tests = [
|
||||
["http://localhost/", ".", "http://localhost/"],
|
||||
["http://localhost/", "..", "http://localhost/"],
|
||||
["http://localhost/a/b/c", ".", "http://localhost/a/b/"],
|
||||
["http://localhost/a/b/c", "..", "http://localhost/a/"],
|
||||
["http://localhost/a/b/c", "./d/e", "http://localhost/a/b/d/e"],
|
||||
["http://localhost/a/b/c", "../d/e", "http://localhost/a/d/e"],
|
||||
["http://localhost/a/b/c", "/./d/e", "http://localhost/d/e"],
|
||||
["http://localhost/a/b/c", "/../d/e", "http://localhost/d/e"],
|
||||
["http://localhost/a/b/c/", "../../d/e/", "http://localhost/a/d/e/"],
|
||||
["http://localhost/a/./c", "../d/e", "http://localhost/d/e"],
|
||||
["http://localhost/a/./c/", "../d/e", "http://localhost/a/d/e"],
|
||||
["http://localhost/a/b/c/d", "./e/../f/../g", "http://localhost/a/b/c/g"],
|
||||
["http://localhost/a/b/c", "d//e", "http://localhost/a/b/d//e"],
|
||||
]
|
||||
for start, click, expected in tests:
|
||||
actual = URL.fromText(start).click(click).asText()
|
||||
self.assertEqual(
|
||||
actual,
|
||||
expected,
|
||||
"{start}.click({click}) => {actual} not {expected}".format(
|
||||
start=start,
|
||||
click=repr(click),
|
||||
actual=actual,
|
||||
expected=expected,
|
||||
),
|
||||
)
|
||||
|
||||
def test_queryAdd(self) -> None:
|
||||
"""
|
||||
L{URL.add} adds query parameters.
|
||||
"""
|
||||
self.assertEqual(
|
||||
"http://www.foo.com/a/nice/path/?foo=bar",
|
||||
URL.fromText("http://www.foo.com/a/nice/path/").add("foo", "bar").asText(),
|
||||
)
|
||||
self.assertEqual(
|
||||
"http://www.foo.com/?foo=bar",
|
||||
URL(host="www.foo.com").add("foo", "bar").asText(),
|
||||
)
|
||||
urlpath = URL.fromText(theurl)
|
||||
self.assertEqual(
|
||||
"http://www.foo.com/a/nice/path/?zot=23&zut&burp",
|
||||
urlpath.add("burp").asText(),
|
||||
)
|
||||
self.assertEqual(
|
||||
"http://www.foo.com/a/nice/path/?zot=23&zut&burp=xxx",
|
||||
urlpath.add("burp", "xxx").asText(),
|
||||
)
|
||||
self.assertEqual(
|
||||
"http://www.foo.com/a/nice/path/?zot=23&zut&burp=xxx&zing",
|
||||
urlpath.add("burp", "xxx").add("zing").asText(),
|
||||
)
|
||||
# Note the inversion!
|
||||
self.assertEqual(
|
||||
"http://www.foo.com/a/nice/path/?zot=23&zut&zing&burp=xxx",
|
||||
urlpath.add("zing").add("burp", "xxx").asText(),
|
||||
)
|
||||
# Note the two values for the same name.
|
||||
self.assertEqual(
|
||||
"http://www.foo.com/a/nice/path/?zot=23&zut&burp=xxx&zot=32",
|
||||
urlpath.add("burp", "xxx").add("zot", "32").asText(),
|
||||
)
|
||||
|
||||
def test_querySet(self) -> None:
|
||||
"""
|
||||
L{URL.set} replaces query parameters by name.
|
||||
"""
|
||||
urlpath = URL.fromText(theurl)
|
||||
self.assertEqual(
|
||||
"http://www.foo.com/a/nice/path/?zot=32&zut",
|
||||
urlpath.set("zot", "32").asText(),
|
||||
)
|
||||
# Replace name without value with name/value and vice-versa.
|
||||
self.assertEqual(
|
||||
"http://www.foo.com/a/nice/path/?zot&zut=itworked",
|
||||
urlpath.set("zot").set("zut", "itworked").asText(),
|
||||
)
|
||||
# Q: what happens when the query has two values and we replace?
|
||||
# A: we replace both values with a single one
|
||||
self.assertEqual(
|
||||
"http://www.foo.com/a/nice/path/?zot=32&zut",
|
||||
urlpath.add("zot", "xxx").set("zot", "32").asText(),
|
||||
)
|
||||
|
||||
def test_queryRemove(self) -> None:
|
||||
"""
|
||||
L{URL.remove} removes all instances of a query parameter.
|
||||
"""
|
||||
url = URL.fromText("https://example.com/a/b/?foo=1&bar=2&foo=3")
|
||||
self.assertEqual(
|
||||
url.remove("foo"), URL.fromText("https://example.com/a/b/?bar=2")
|
||||
)
|
||||
|
||||
def test_empty(self) -> None:
|
||||
"""
|
||||
An empty L{URL} should serialize as the empty string.
|
||||
"""
|
||||
self.assertEqual(URL().asText(), "")
|
||||
|
||||
def test_justQueryText(self) -> None:
|
||||
"""
|
||||
An L{URL} with query text should serialize as just query text.
|
||||
"""
|
||||
u = URL(query=[("hello", "world")])
|
||||
self.assertEqual(u.asText(), "?hello=world")
|
||||
|
||||
def test_identicalEqual(self) -> None:
|
||||
"""
|
||||
L{URL} compares equal to itself.
|
||||
"""
|
||||
u = URL.fromText("http://localhost/")
|
||||
self.assertEqual(u, u)
|
||||
|
||||
def test_similarEqual(self) -> None:
|
||||
"""
|
||||
URLs with equivalent components should compare equal.
|
||||
"""
|
||||
u1 = URL.fromText("http://localhost/")
|
||||
u2 = URL.fromText("http://localhost/")
|
||||
self.assertEqual(u1, u2)
|
||||
|
||||
def test_differentNotEqual(self) -> None:
|
||||
"""
|
||||
L{URL}s that refer to different resources are both unequal (C{!=}) and
|
||||
also not equal (not C{==}).
|
||||
"""
|
||||
u1 = URL.fromText("http://localhost/a")
|
||||
u2 = URL.fromText("http://localhost/b")
|
||||
self.assertFalse(u1 == u2, f"{u1!r} != {u2!r}")
|
||||
self.assertNotEqual(u1, u2)
|
||||
|
||||
def test_otherTypesNotEqual(self) -> None:
|
||||
"""
|
||||
L{URL} is not equal (C{==}) to other types.
|
||||
"""
|
||||
u = URL.fromText("http://localhost/")
|
||||
self.assertFalse(u == 42, "URL must not equal a number.")
|
||||
self.assertFalse(u == object(), "URL must not equal an object.")
|
||||
self.assertNotEqual(u, 42)
|
||||
self.assertNotEqual(u, object())
|
||||
|
||||
def test_identicalNotUnequal(self) -> None:
|
||||
"""
|
||||
Identical L{URL}s are not unequal (C{!=}) to each other.
|
||||
"""
|
||||
u = URL.fromText("http://localhost/")
|
||||
self.assertFalse(u != u, "%r == itself" % u)
|
||||
|
||||
def test_similarNotUnequal(self) -> None:
|
||||
"""
|
||||
Structurally similar L{URL}s are not unequal (C{!=}) to each other.
|
||||
"""
|
||||
u1 = URL.fromText("http://localhost/")
|
||||
u2 = URL.fromText("http://localhost/")
|
||||
self.assertFalse(u1 != u2, f"{u1!r} == {u2!r}")
|
||||
|
||||
def test_differentUnequal(self) -> None:
|
||||
"""
|
||||
Structurally different L{URL}s are unequal (C{!=}) to each other.
|
||||
"""
|
||||
u1 = URL.fromText("http://localhost/a")
|
||||
u2 = URL.fromText("http://localhost/b")
|
||||
self.assertTrue(u1 != u2, f"{u1!r} == {u2!r}")
|
||||
|
||||
def test_otherTypesUnequal(self) -> None:
|
||||
"""
|
||||
L{URL} is unequal (C{!=}) to other types.
|
||||
"""
|
||||
u = URL.fromText("http://localhost/")
|
||||
self.assertTrue(u != 42, "URL must differ from a number.")
|
||||
self.assertTrue(u != object(), "URL must be differ from an object.")
|
||||
|
||||
def test_asURI(self) -> None:
|
||||
"""
|
||||
L{URL.asURI} produces an URI which converts any URI unicode encoding
|
||||
into pure US-ASCII and returns a new L{URL}.
|
||||
"""
|
||||
unicodey = (
|
||||
"http://\N{LATIN SMALL LETTER E WITH ACUTE}.com/"
|
||||
"\N{LATIN SMALL LETTER E}\N{COMBINING ACUTE ACCENT}"
|
||||
"?\N{LATIN SMALL LETTER A}\N{COMBINING ACUTE ACCENT}="
|
||||
"\N{LATIN SMALL LETTER I}\N{COMBINING ACUTE ACCENT}"
|
||||
"#\N{LATIN SMALL LETTER U}\N{COMBINING ACUTE ACCENT}"
|
||||
)
|
||||
iri = URL.fromText(unicodey)
|
||||
uri = iri.asURI()
|
||||
self.assertEqual(iri.host, "\N{LATIN SMALL LETTER E WITH ACUTE}.com")
|
||||
self.assertEqual(
|
||||
iri.path[0], "\N{LATIN SMALL LETTER E}\N{COMBINING ACUTE ACCENT}"
|
||||
)
|
||||
self.assertEqual(iri.asText(), unicodey)
|
||||
expectedURI = "http://xn--9ca.com/%C3%A9?%C3%A1=%C3%AD#%C3%BA"
|
||||
actualURI = uri.asText()
|
||||
self.assertEqual(actualURI, expectedURI, f"{actualURI!r} != {expectedURI!r}")
|
||||
|
||||
def test_asIRI(self) -> None:
|
||||
"""
|
||||
L{URL.asIRI} decodes any percent-encoded text in the URI, making it
|
||||
more suitable for reading by humans, and returns a new L{URL}.
|
||||
"""
|
||||
asciiish = "http://xn--9ca.com/%C3%A9?%C3%A1=%C3%AD#%C3%BA"
|
||||
uri = URL.fromText(asciiish)
|
||||
iri = uri.asIRI()
|
||||
self.assertEqual(uri.host, "xn--9ca.com")
|
||||
self.assertEqual(uri.path[0], "%C3%A9")
|
||||
self.assertEqual(uri.asText(), asciiish)
|
||||
expectedIRI = (
|
||||
"http://\N{LATIN SMALL LETTER E WITH ACUTE}.com/"
|
||||
"\N{LATIN SMALL LETTER E WITH ACUTE}"
|
||||
"?\N{LATIN SMALL LETTER A WITH ACUTE}="
|
||||
"\N{LATIN SMALL LETTER I WITH ACUTE}"
|
||||
"#\N{LATIN SMALL LETTER U WITH ACUTE}"
|
||||
)
|
||||
actualIRI = iri.asText()
|
||||
self.assertEqual(actualIRI, expectedIRI, f"{actualIRI!r} != {expectedIRI!r}")
|
||||
|
||||
def test_badUTF8AsIRI(self) -> None:
|
||||
"""
|
||||
Bad UTF-8 in a path segment, query parameter, or fragment results in
|
||||
that portion of the URI remaining percent-encoded in the IRI.
|
||||
"""
|
||||
urlWithBinary = "http://xn--9ca.com/%00%FF/%C3%A9"
|
||||
uri = URL.fromText(urlWithBinary)
|
||||
iri = uri.asIRI()
|
||||
expectedIRI = (
|
||||
"http://\N{LATIN SMALL LETTER E WITH ACUTE}.com/"
|
||||
"%00%FF/"
|
||||
"\N{LATIN SMALL LETTER E WITH ACUTE}"
|
||||
)
|
||||
actualIRI = iri.asText()
|
||||
self.assertEqual(actualIRI, expectedIRI, f"{actualIRI!r} != {expectedIRI!r}")
|
||||
|
||||
def test_alreadyIRIAsIRI(self) -> None:
|
||||
"""
|
||||
A L{URL} composed of non-ASCII text will result in non-ASCII text.
|
||||
"""
|
||||
unicodey = (
|
||||
"http://\N{LATIN SMALL LETTER E WITH ACUTE}.com/"
|
||||
"\N{LATIN SMALL LETTER E}\N{COMBINING ACUTE ACCENT}"
|
||||
"?\N{LATIN SMALL LETTER A}\N{COMBINING ACUTE ACCENT}="
|
||||
"\N{LATIN SMALL LETTER I}\N{COMBINING ACUTE ACCENT}"
|
||||
"#\N{LATIN SMALL LETTER U}\N{COMBINING ACUTE ACCENT}"
|
||||
)
|
||||
iri = URL.fromText(unicodey)
|
||||
alsoIRI = iri.asIRI()
|
||||
self.assertEqual(alsoIRI.asText(), unicodey)
|
||||
|
||||
def test_alreadyURIAsURI(self) -> None:
|
||||
"""
|
||||
A L{URL} composed of encoded text will remain encoded.
|
||||
"""
|
||||
expectedURI = "http://xn--9ca.com/%C3%A9?%C3%A1=%C3%AD#%C3%BA"
|
||||
uri = URL.fromText(expectedURI)
|
||||
actualURI = uri.asURI().asText()
|
||||
self.assertEqual(actualURI, expectedURI)
|
||||
|
||||
def test_userinfo(self) -> None:
|
||||
"""
|
||||
L{URL.fromText} will parse the C{userinfo} portion of the URI
|
||||
separately from the host and port.
|
||||
"""
|
||||
url = URL.fromText(
|
||||
"http://someuser:somepassword@example.com/some-segment@ignore"
|
||||
)
|
||||
self.assertEqual(url.authority(True), "someuser:somepassword@example.com")
|
||||
self.assertEqual(url.authority(False), "someuser:@example.com")
|
||||
self.assertEqual(url.userinfo, "someuser:somepassword")
|
||||
self.assertEqual(url.user, "someuser")
|
||||
self.assertEqual(
|
||||
url.asText(), "http://someuser:@example.com/some-segment@ignore"
|
||||
)
|
||||
self.assertEqual(
|
||||
url.replace(userinfo="someuser").asText(),
|
||||
"http://someuser@example.com/some-segment@ignore",
|
||||
)
|
||||
|
||||
def test_portText(self) -> None:
|
||||
"""
|
||||
L{URL.fromText} parses custom port numbers as integers.
|
||||
"""
|
||||
portURL = URL.fromText("http://www.example.com:8080/")
|
||||
self.assertEqual(portURL.port, 8080)
|
||||
self.assertEqual(portURL.asText(), "http://www.example.com:8080/")
|
||||
|
||||
def test_mailto(self) -> None:
|
||||
"""
|
||||
Although L{URL} instances are mainly for dealing with HTTP, other
|
||||
schemes (such as C{mailto:}) should work as well. For example,
|
||||
L{URL.fromText}/L{URL.asText} round-trips cleanly for a C{mailto:} URL
|
||||
representing an email address.
|
||||
"""
|
||||
self.assertEqual(
|
||||
URL.fromText("mailto:user@example.com").asText(), "mailto:user@example.com"
|
||||
)
|
||||
|
||||
def test_queryIterable(self) -> None:
|
||||
"""
|
||||
When a L{URL} is created with a C{query} argument, the C{query}
|
||||
argument is converted into an N-tuple of 2-tuples.
|
||||
"""
|
||||
# note the type here is invalid as only 2-tuples are accepted
|
||||
url = URL(query=[["alpha", "beta"]]) # type: ignore[list-item]
|
||||
self.assertEqual(url.query, (("alpha", "beta"),))
|
||||
|
||||
def test_pathIterable(self) -> None:
|
||||
"""
|
||||
When a L{URL} is created with a C{path} argument, the C{path} is
|
||||
converted into a tuple.
|
||||
"""
|
||||
url = URL(path=["hello", "world"])
|
||||
self.assertEqual(url.path, ("hello", "world"))
|
||||
|
||||
def test_invalidArguments(self) -> None:
|
||||
"""
|
||||
Passing an argument of the wrong type to any of the constructor
|
||||
arguments of L{URL} will raise a descriptive L{TypeError}.
|
||||
|
||||
L{URL} typechecks very aggressively to ensure that its constitutent
|
||||
parts are all properly immutable and to prevent confusing errors when
|
||||
bad data crops up in a method call long after the code that called the
|
||||
constructor is off the stack.
|
||||
"""
|
||||
|
||||
class Unexpected:
|
||||
def __str__(self) -> str:
|
||||
return "wrong"
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return "<unexpected>"
|
||||
|
||||
defaultExpectation = "unicode" if bytes is str else "str"
|
||||
|
||||
def assertRaised(raised: _HasException, expectation: str, name: str) -> None:
|
||||
self.assertEqual(
|
||||
str(raised.exception),
|
||||
"expected {} for {}, got {}".format(expectation, name, "<unexpected>"),
|
||||
)
|
||||
|
||||
def check(param: str, expectation: str = defaultExpectation) -> None:
|
||||
with self.assertRaises(TypeError) as raised:
|
||||
URL(**{param: Unexpected()}) # type: ignore[arg-type]
|
||||
assertRaised(raised, expectation, param)
|
||||
|
||||
check("scheme")
|
||||
check("host")
|
||||
check("fragment")
|
||||
check("rooted", "bool")
|
||||
check("userinfo")
|
||||
check("port", "int or NoneType")
|
||||
|
||||
with self.assertRaises(TypeError) as raised:
|
||||
URL(
|
||||
path=[
|
||||
Unexpected(), # type: ignore[list-item]
|
||||
]
|
||||
)
|
||||
assertRaised(raised, defaultExpectation, "path segment")
|
||||
with self.assertRaises(TypeError) as raised:
|
||||
URL(
|
||||
query=[
|
||||
("name", Unexpected()), # type: ignore[list-item]
|
||||
]
|
||||
)
|
||||
assertRaised(
|
||||
raised, defaultExpectation + " or NoneType", "query parameter value"
|
||||
)
|
||||
with self.assertRaises(TypeError) as raised:
|
||||
URL(
|
||||
query=[
|
||||
(Unexpected(), "value"), # type: ignore[list-item]
|
||||
]
|
||||
)
|
||||
assertRaised(raised, defaultExpectation, "query parameter name")
|
||||
# No custom error message for this one, just want to make sure
|
||||
# non-2-tuples don't get through.
|
||||
with self.assertRaises(TypeError):
|
||||
URL(query=[Unexpected()]) # type: ignore[list-item]
|
||||
with self.assertRaises(ValueError):
|
||||
URL(query=[("k", "v", "vv")]) # type: ignore[list-item]
|
||||
with self.assertRaises(ValueError):
|
||||
URL(query=[("k",)]) # type: ignore[list-item]
|
||||
|
||||
url = URL.fromText("https://valid.example.com/")
|
||||
with self.assertRaises(TypeError) as raised:
|
||||
url.child(Unexpected()) # type: ignore[arg-type]
|
||||
assertRaised(raised, defaultExpectation, "path segment")
|
||||
with self.assertRaises(TypeError) as raised:
|
||||
url.sibling(Unexpected()) # type: ignore[arg-type]
|
||||
assertRaised(raised, defaultExpectation, "path segment")
|
||||
with self.assertRaises(TypeError) as raised:
|
||||
url.click(Unexpected()) # type: ignore[arg-type]
|
||||
assertRaised(raised, defaultExpectation, "relative URL")
|
||||
|
||||
def test_technicallyTextIsIterableBut(self) -> None:
|
||||
"""
|
||||
Technically, L{str} (or L{unicode}, as appropriate) is iterable, but
|
||||
C{URL(path="foo")} resulting in C{URL.fromText("f/o/o")} is never what
|
||||
you want.
|
||||
"""
|
||||
with self.assertRaises(TypeError) as raised:
|
||||
URL(path="foo")
|
||||
self.assertEqual(
|
||||
str(raised.exception),
|
||||
"expected iterable of text for path, not: {}".format(repr("foo")),
|
||||
)
|
||||
|
||||
|
||||
class URLDeprecationTests(SynchronousTestCase):
|
||||
"""
|
||||
L{twisted.python.url} is deprecated.
|
||||
"""
|
||||
|
||||
def test_urlDeprecation(self) -> None:
|
||||
"""
|
||||
L{twisted.python.url} is deprecated since Twisted 17.5.0.
|
||||
"""
|
||||
from twisted.python import url
|
||||
|
||||
url
|
||||
|
||||
warningsShown = self.flushWarnings([self.test_urlDeprecation])
|
||||
self.assertEqual(1, len(warningsShown))
|
||||
self.assertEqual(
|
||||
(
|
||||
"twisted.python.url was deprecated in Twisted 17.5.0:"
|
||||
" Please use hyperlink from PyPI instead."
|
||||
),
|
||||
warningsShown[0]["message"],
|
||||
)
|
||||
@@ -0,0 +1,289 @@
|
||||
# -*- test-case-name: twisted.python.test.test_urlpath -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Tests for L{twisted.python.urlpath}.
|
||||
"""
|
||||
|
||||
from twisted.python import urlpath
|
||||
from twisted.trial import unittest
|
||||
|
||||
|
||||
class _BaseURLPathTests:
|
||||
"""
|
||||
Tests for instantiated L{urlpath.URLPath}s.
|
||||
"""
|
||||
|
||||
def test_partsAreBytes(self):
|
||||
"""
|
||||
All of the attributes of L{urlpath.URLPath} should be L{bytes}.
|
||||
"""
|
||||
self.assertIsInstance(self.path.scheme, bytes)
|
||||
self.assertIsInstance(self.path.netloc, bytes)
|
||||
self.assertIsInstance(self.path.path, bytes)
|
||||
self.assertIsInstance(self.path.query, bytes)
|
||||
self.assertIsInstance(self.path.fragment, bytes)
|
||||
|
||||
def test_strReturnsStr(self):
|
||||
"""
|
||||
Calling C{str()} with a L{URLPath} will always return a L{str}.
|
||||
"""
|
||||
self.assertEqual(type(self.path.__str__()), str)
|
||||
|
||||
def test_mutabilityWithText(self, stringType=str):
|
||||
"""
|
||||
Setting attributes on L{urlpath.URLPath} should change the value
|
||||
returned by L{str}.
|
||||
|
||||
@param stringType: a callable to parameterize this test for different
|
||||
text types.
|
||||
@type stringType: 1-argument callable taking L{str} and returning
|
||||
L{str} or L{bytes}.
|
||||
"""
|
||||
self.path.scheme = stringType("https")
|
||||
self.assertEqual(
|
||||
str(self.path), "https://example.com/foo/bar?yes=no&no=yes#footer"
|
||||
)
|
||||
self.path.netloc = stringType("another.example.invalid")
|
||||
self.assertEqual(
|
||||
str(self.path),
|
||||
"https://another.example.invalid/foo/bar?yes=no&no=yes#footer",
|
||||
)
|
||||
self.path.path = stringType("/hello")
|
||||
self.assertEqual(
|
||||
str(self.path), "https://another.example.invalid/hello?yes=no&no=yes#footer"
|
||||
)
|
||||
self.path.query = stringType("alpha=omega&opposites=same")
|
||||
self.assertEqual(
|
||||
str(self.path),
|
||||
"https://another.example.invalid/hello?alpha=omega&opposites=same"
|
||||
"#footer",
|
||||
)
|
||||
self.path.fragment = stringType("header")
|
||||
self.assertEqual(
|
||||
str(self.path),
|
||||
"https://another.example.invalid/hello?alpha=omega&opposites=same"
|
||||
"#header",
|
||||
)
|
||||
|
||||
def test_mutabilityWithBytes(self):
|
||||
"""
|
||||
Same as L{test_mutabilityWithText} but for bytes.
|
||||
"""
|
||||
self.test_mutabilityWithText(lambda x: x.encode("ascii"))
|
||||
|
||||
def test_allAttributesAreBytes(self):
|
||||
"""
|
||||
A created L{URLPath} has bytes attributes.
|
||||
"""
|
||||
self.assertIsInstance(self.path.scheme, bytes)
|
||||
self.assertIsInstance(self.path.netloc, bytes)
|
||||
self.assertIsInstance(self.path.path, bytes)
|
||||
self.assertIsInstance(self.path.query, bytes)
|
||||
self.assertIsInstance(self.path.fragment, bytes)
|
||||
|
||||
def test_stringConversion(self):
|
||||
"""
|
||||
Calling C{str()} with a L{URLPath} will return the same URL that it was
|
||||
constructed with.
|
||||
"""
|
||||
self.assertEqual(
|
||||
str(self.path), "http://example.com/foo/bar?yes=no&no=yes#footer"
|
||||
)
|
||||
|
||||
def test_childString(self):
|
||||
"""
|
||||
Calling C{str()} with a C{URLPath.child()} will return a URL which is
|
||||
the child of the URL it was instantiated with.
|
||||
"""
|
||||
self.assertEqual(
|
||||
str(self.path.child(b"hello")), "http://example.com/foo/bar/hello"
|
||||
)
|
||||
self.assertEqual(
|
||||
str(self.path.child(b"hello").child(b"")),
|
||||
"http://example.com/foo/bar/hello/",
|
||||
)
|
||||
self.assertEqual(
|
||||
str(self.path.child(b"hello", keepQuery=True)),
|
||||
"http://example.com/foo/bar/hello?yes=no&no=yes",
|
||||
)
|
||||
|
||||
def test_siblingString(self):
|
||||
"""
|
||||
Calling C{str()} with a C{URLPath.sibling()} will return a URL which is
|
||||
the sibling of the URL it was instantiated with.
|
||||
"""
|
||||
self.assertEqual(str(self.path.sibling(b"baz")), "http://example.com/foo/baz")
|
||||
self.assertEqual(
|
||||
str(self.path.sibling(b"baz", keepQuery=True)),
|
||||
"http://example.com/foo/baz?yes=no&no=yes",
|
||||
)
|
||||
|
||||
# The sibling of http://example.com/foo/bar/
|
||||
# is http://example.comf/foo/bar/baz
|
||||
# because really we are constructing a sibling of
|
||||
# http://example.com/foo/bar/index.html
|
||||
self.assertEqual(
|
||||
str(self.path.child(b"").sibling(b"baz")), "http://example.com/foo/bar/baz"
|
||||
)
|
||||
|
||||
def test_parentString(self):
|
||||
"""
|
||||
Calling C{str()} with a C{URLPath.parent()} will return a URL which is
|
||||
the parent of the URL it was instantiated with.
|
||||
"""
|
||||
# .parent() should be equivalent to '..'
|
||||
# 'foo' is the current directory, '/' is the parent directory
|
||||
self.assertEqual(str(self.path.parent()), "http://example.com/")
|
||||
self.assertEqual(
|
||||
str(self.path.parent(keepQuery=True)), "http://example.com/?yes=no&no=yes"
|
||||
)
|
||||
self.assertEqual(str(self.path.child(b"").parent()), "http://example.com/foo/")
|
||||
self.assertEqual(
|
||||
str(self.path.child(b"baz").parent()), "http://example.com/foo/"
|
||||
)
|
||||
self.assertEqual(
|
||||
str(self.path.parent().parent().parent().parent().parent()),
|
||||
"http://example.com/",
|
||||
)
|
||||
|
||||
def test_hereString(self):
|
||||
"""
|
||||
Calling C{str()} with a C{URLPath.here()} will return a URL which is
|
||||
the URL that it was instantiated with, without any file, query, or
|
||||
fragment.
|
||||
"""
|
||||
# .here() should be equivalent to '.'
|
||||
self.assertEqual(str(self.path.here()), "http://example.com/foo/")
|
||||
self.assertEqual(
|
||||
str(self.path.here(keepQuery=True)), "http://example.com/foo/?yes=no&no=yes"
|
||||
)
|
||||
self.assertEqual(
|
||||
str(self.path.child(b"").here()), "http://example.com/foo/bar/"
|
||||
)
|
||||
|
||||
def test_doubleSlash(self):
|
||||
"""
|
||||
Calling L{urlpath.URLPath.click} on a L{urlpath.URLPath} with a
|
||||
trailing slash with a relative URL containing a leading slash will
|
||||
result in a URL with a single slash at the start of the path portion.
|
||||
"""
|
||||
self.assertEqual(
|
||||
str(self.path.click(b"/hello/world")).encode("ascii"),
|
||||
b"http://example.com/hello/world",
|
||||
)
|
||||
|
||||
def test_pathList(self):
|
||||
"""
|
||||
L{urlpath.URLPath.pathList} returns a L{list} of L{bytes}.
|
||||
"""
|
||||
self.assertEqual(
|
||||
self.path.child(b"%00%01%02").pathList(),
|
||||
[b"", b"foo", b"bar", b"%00%01%02"],
|
||||
)
|
||||
|
||||
# Just testing that the 'copy' argument exists for compatibility; it
|
||||
# was originally provided for performance reasons, and its behavioral
|
||||
# contract is kind of nonsense (where is the state shared? who with?)
|
||||
# so it doesn't actually *do* anything any more.
|
||||
self.assertEqual(
|
||||
self.path.child(b"%00%01%02").pathList(copy=False),
|
||||
[b"", b"foo", b"bar", b"%00%01%02"],
|
||||
)
|
||||
self.assertEqual(
|
||||
self.path.child(b"%00%01%02").pathList(unquote=True),
|
||||
[b"", b"foo", b"bar", b"\x00\x01\x02"],
|
||||
)
|
||||
|
||||
|
||||
class BytesURLPathTests(_BaseURLPathTests, unittest.TestCase):
|
||||
"""
|
||||
Tests for interacting with a L{URLPath} created with C{fromBytes}.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
self.path = urlpath.URLPath.fromBytes(
|
||||
b"http://example.com/foo/bar?yes=no&no=yes#footer"
|
||||
)
|
||||
|
||||
def test_mustBeBytes(self):
|
||||
"""
|
||||
L{URLPath.fromBytes} must take a L{bytes} argument.
|
||||
"""
|
||||
with self.assertRaises(ValueError):
|
||||
urlpath.URLPath.fromBytes(None)
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
urlpath.URLPath.fromBytes("someurl")
|
||||
|
||||
def test_withoutArguments(self):
|
||||
"""
|
||||
An instantiation with no arguments creates a usable L{URLPath} with
|
||||
default arguments.
|
||||
"""
|
||||
url = urlpath.URLPath()
|
||||
self.assertEqual(str(url), "http://localhost/")
|
||||
|
||||
def test_partialArguments(self):
|
||||
"""
|
||||
Leaving some optional arguments unfilled makes a L{URLPath} with those
|
||||
optional arguments filled with defaults.
|
||||
"""
|
||||
# Not a "full" URL given to fromBytes, no /
|
||||
# / is filled in
|
||||
url = urlpath.URLPath.fromBytes(b"http://google.com")
|
||||
self.assertEqual(url.scheme, b"http")
|
||||
self.assertEqual(url.netloc, b"google.com")
|
||||
self.assertEqual(url.path, b"/")
|
||||
self.assertEqual(url.fragment, b"")
|
||||
self.assertEqual(url.query, b"")
|
||||
self.assertEqual(str(url), "http://google.com/")
|
||||
|
||||
def test_nonASCIIBytes(self):
|
||||
"""
|
||||
L{URLPath.fromBytes} can interpret non-ASCII bytes as percent-encoded
|
||||
"""
|
||||
url = urlpath.URLPath.fromBytes(b"http://example.com/\xff\x00")
|
||||
self.assertEqual(str(url), "http://example.com/%FF%00")
|
||||
|
||||
|
||||
class StringURLPathTests(_BaseURLPathTests, unittest.TestCase):
|
||||
"""
|
||||
Tests for interacting with a L{URLPath} created with C{fromString} and a
|
||||
L{str} argument.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
self.path = urlpath.URLPath.fromString(
|
||||
"http://example.com/foo/bar?yes=no&no=yes#footer"
|
||||
)
|
||||
|
||||
def test_mustBeStr(self):
|
||||
"""
|
||||
C{URLPath.fromString} must take a L{str} or L{str} argument.
|
||||
"""
|
||||
with self.assertRaises(ValueError):
|
||||
urlpath.URLPath.fromString(None)
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
urlpath.URLPath.fromString(b"someurl")
|
||||
|
||||
|
||||
class UnicodeURLPathTests(_BaseURLPathTests, unittest.TestCase):
|
||||
"""
|
||||
Tests for interacting with a L{URLPath} created with C{fromString} and a
|
||||
L{str} argument.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
self.path = urlpath.URLPath.fromString(
|
||||
"http://example.com/foo/bar?yes=no&no=yes#footer"
|
||||
)
|
||||
|
||||
def test_nonASCIICharacters(self):
|
||||
"""
|
||||
L{URLPath.fromString} can load non-ASCII characters.
|
||||
"""
|
||||
url = urlpath.URLPath.fromString("http://example.com/\xff\x00")
|
||||
self.assertEqual(str(url), "http://example.com/%C3%BF%00")
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,64 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Tests for L{twisted.python.win32}.
|
||||
"""
|
||||
|
||||
from twisted.python import reflect, win32
|
||||
from twisted.trial import unittest
|
||||
|
||||
|
||||
class CommandLineQuotingTests(unittest.TestCase):
|
||||
"""
|
||||
Tests for L{cmdLineQuote}.
|
||||
"""
|
||||
|
||||
def test_argWithoutSpaces(self) -> None:
|
||||
"""
|
||||
Calling C{cmdLineQuote} with an argument with no spaces returns
|
||||
the argument unchanged.
|
||||
"""
|
||||
self.assertEqual(win32.cmdLineQuote("an_argument"), "an_argument")
|
||||
|
||||
def test_argWithSpaces(self) -> None:
|
||||
"""
|
||||
Calling C{cmdLineQuote} with an argument containing spaces returns
|
||||
the argument surrounded by quotes.
|
||||
"""
|
||||
self.assertEqual(win32.cmdLineQuote("An Argument"), '"An Argument"')
|
||||
|
||||
def test_emptyStringArg(self) -> None:
|
||||
"""
|
||||
Calling C{cmdLineQuote} with an empty string returns a quoted empty
|
||||
string.
|
||||
"""
|
||||
self.assertEqual(win32.cmdLineQuote(""), '""')
|
||||
|
||||
|
||||
class DeprecationTests(unittest.TestCase):
|
||||
"""
|
||||
Tests for deprecated (Fake)WindowsError.
|
||||
"""
|
||||
|
||||
def test_deprecation_FakeWindowsError(self) -> None:
|
||||
"""Importing C{FakeWindowsError} triggers a L{DeprecationWarning}."""
|
||||
|
||||
self.assertWarns(
|
||||
DeprecationWarning,
|
||||
"twisted.python.win32.FakeWindowsError was deprecated in Twisted 21.2.0: "
|
||||
"Catch OSError and check presence of 'winerror' attribute.",
|
||||
reflect.__file__,
|
||||
lambda: reflect.namedAny("twisted.python.win32.FakeWindowsError"),
|
||||
)
|
||||
|
||||
def test_deprecation_WindowsError(self) -> None:
|
||||
"""Importing C{WindowsError} triggers a L{DeprecationWarning}."""
|
||||
|
||||
self.assertWarns(
|
||||
DeprecationWarning,
|
||||
"twisted.python.win32.WindowsError was deprecated in Twisted 21.2.0: "
|
||||
"Catch OSError and check presence of 'winerror' attribute.",
|
||||
reflect.__file__,
|
||||
lambda: reflect.namedAny("twisted.python.win32.WindowsError"),
|
||||
)
|
||||
@@ -0,0 +1,109 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Test cases covering L{twisted.python.zippath}.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import zipfile
|
||||
from typing import Union
|
||||
|
||||
from twisted.python.filepath import _coerceToFilesystemEncoding
|
||||
from twisted.python.zippath import ZipArchive, ZipPath
|
||||
from twisted.test.test_paths import AbstractFilePathTests
|
||||
|
||||
|
||||
def zipit(dirname: str | bytes, zfname: str | bytes) -> None:
|
||||
"""
|
||||
Create a zipfile on zfname, containing the contents of dirname'
|
||||
"""
|
||||
coercedDirname = _coerceToFilesystemEncoding("", dirname)
|
||||
coercedZfname = _coerceToFilesystemEncoding("", zfname)
|
||||
|
||||
with zipfile.ZipFile(coercedZfname, "w") as zf:
|
||||
for (
|
||||
root,
|
||||
ignored,
|
||||
files,
|
||||
) in os.walk(coercedDirname):
|
||||
for fname in files:
|
||||
fspath = os.path.join(root, fname)
|
||||
arcpath = os.path.join(root, fname)[len(dirname) + 1 :]
|
||||
zf.write(fspath, arcpath)
|
||||
|
||||
|
||||
class ZipFilePathTests(AbstractFilePathTests):
|
||||
"""
|
||||
Test various L{ZipPath} path manipulations as well as reprs for L{ZipPath}
|
||||
and L{ZipArchive}.
|
||||
"""
|
||||
|
||||
path: ZipArchive[bytes] # type:ignore[assignment]
|
||||
root: ZipArchive[bytes] # type:ignore[assignment]
|
||||
|
||||
def setUp(self) -> None:
|
||||
AbstractFilePathTests.setUp(self)
|
||||
zipit(self.cmn, self.cmn + b".zip")
|
||||
self.nativecmn = _coerceToFilesystemEncoding("", self.cmn)
|
||||
self.path = ZipArchive(self.cmn + b".zip")
|
||||
self.root = self.path
|
||||
self.all = [x.replace(self.cmn, self.cmn + b".zip") for x in self.all]
|
||||
|
||||
def test_sibling(self) -> None:
|
||||
"""
|
||||
L{ZipPath.sibling} returns a path at the same level.
|
||||
"""
|
||||
self.assertEqual(self.path.child("one").sibling("two"), self.path.child("two"))
|
||||
|
||||
def test_zipPathRepr(self) -> None:
|
||||
"""
|
||||
Make sure that invoking ZipPath's repr prints the correct class name
|
||||
and an absolute path to the zip file.
|
||||
"""
|
||||
child: Union[ZipPath[str, bytes], ZipPath[str, str]] = self.path.child("foo")
|
||||
pathRepr = "ZipPath({!r})".format(
|
||||
os.path.abspath(self.nativecmn + ".zip" + os.sep + "foo"),
|
||||
)
|
||||
|
||||
# Check for an absolute path
|
||||
self.assertEqual(repr(child), pathRepr)
|
||||
|
||||
# Create a path to the file rooted in the current working directory
|
||||
relativeCommon = self.nativecmn.replace(os.getcwd() + os.sep, "", 1) + ".zip"
|
||||
relpath = ZipArchive(relativeCommon)
|
||||
child = relpath.child("foo")
|
||||
|
||||
# Check using a path without the cwd prepended
|
||||
self.assertEqual(repr(child), pathRepr)
|
||||
|
||||
def test_zipPathReprParentDirSegment(self) -> None:
|
||||
"""
|
||||
The repr of a ZipPath with C{".."} in the internal part of its path
|
||||
includes the C{".."} rather than applying the usual parent directory
|
||||
meaning.
|
||||
"""
|
||||
child = self.path.child("foo").child("..").child("bar")
|
||||
pathRepr = "ZipPath(%r)" % (
|
||||
self.nativecmn + ".zip" + os.sep.join(["", "foo", "..", "bar"])
|
||||
)
|
||||
self.assertEqual(repr(child), pathRepr)
|
||||
|
||||
def test_zipArchiveRepr(self) -> None:
|
||||
"""
|
||||
Make sure that invoking ZipArchive's repr prints the correct class
|
||||
name and an absolute path to the zip file.
|
||||
"""
|
||||
path = ZipArchive(self.nativecmn + ".zip")
|
||||
pathRepr = "ZipArchive({!r})".format(os.path.abspath(self.nativecmn + ".zip"))
|
||||
|
||||
# Check for an absolute path
|
||||
self.assertEqual(repr(path), pathRepr)
|
||||
|
||||
# Create a path to the file rooted in the current working directory
|
||||
relativeCommon = self.nativecmn.replace(os.getcwd() + os.sep, "", 1) + ".zip"
|
||||
relpath = ZipArchive(relativeCommon)
|
||||
|
||||
# Check using a path without the cwd prepended
|
||||
self.assertEqual(repr(relpath), pathRepr)
|
||||
@@ -0,0 +1,331 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Tests for L{twisted.python.zipstream}
|
||||
"""
|
||||
|
||||
import random
|
||||
import struct
|
||||
import zipfile
|
||||
from hashlib import md5
|
||||
|
||||
from twisted.python import filepath, zipstream
|
||||
from twisted.trial import unittest
|
||||
|
||||
|
||||
class FileEntryMixin:
|
||||
"""
|
||||
File entry classes should behave as file-like objects
|
||||
"""
|
||||
|
||||
def getFileEntry(self, contents):
|
||||
"""
|
||||
Return an appropriate zip file entry
|
||||
"""
|
||||
filename = self.mktemp()
|
||||
with zipfile.ZipFile(filename, "w", self.compression) as z:
|
||||
z.writestr("content", contents)
|
||||
z = zipstream.ChunkingZipFile(filename, "r")
|
||||
return z.readfile("content")
|
||||
|
||||
def test_isatty(self):
|
||||
"""
|
||||
zip files should not be ttys, so isatty() should be false
|
||||
"""
|
||||
with self.getFileEntry("") as fileEntry:
|
||||
self.assertFalse(fileEntry.isatty())
|
||||
|
||||
def test_closed(self):
|
||||
"""
|
||||
The C{closed} attribute should reflect whether C{close()} has been
|
||||
called.
|
||||
"""
|
||||
with self.getFileEntry("") as fileEntry:
|
||||
self.assertFalse(fileEntry.closed)
|
||||
self.assertTrue(fileEntry.closed)
|
||||
|
||||
def test_readline(self):
|
||||
"""
|
||||
C{readline()} should mirror L{file.readline} and return up to a single
|
||||
delimiter.
|
||||
"""
|
||||
with self.getFileEntry(b"hoho\nho") as fileEntry:
|
||||
self.assertEqual(fileEntry.readline(), b"hoho\n")
|
||||
self.assertEqual(fileEntry.readline(), b"ho")
|
||||
self.assertEqual(fileEntry.readline(), b"")
|
||||
|
||||
def test_next(self):
|
||||
"""
|
||||
Zip file entries should implement the iterator protocol as files do.
|
||||
"""
|
||||
with self.getFileEntry(b"ho\nhoho") as fileEntry:
|
||||
self.assertEqual(fileEntry.next(), b"ho\n")
|
||||
self.assertEqual(fileEntry.next(), b"hoho")
|
||||
self.assertRaises(StopIteration, fileEntry.next)
|
||||
|
||||
def test_readlines(self):
|
||||
"""
|
||||
C{readlines()} should return a list of all the lines.
|
||||
"""
|
||||
with self.getFileEntry(b"ho\nho\nho") as fileEntry:
|
||||
self.assertEqual(fileEntry.readlines(), [b"ho\n", b"ho\n", b"ho"])
|
||||
|
||||
def test_iteration(self):
|
||||
"""
|
||||
C{__iter__()} and C{xreadlines()} should return C{self}.
|
||||
"""
|
||||
with self.getFileEntry("") as fileEntry:
|
||||
self.assertIs(iter(fileEntry), fileEntry)
|
||||
self.assertIs(fileEntry.xreadlines(), fileEntry)
|
||||
|
||||
def test_readWhole(self):
|
||||
"""
|
||||
C{.read()} should read the entire file.
|
||||
"""
|
||||
contents = b"Hello, world!"
|
||||
with self.getFileEntry(contents) as entry:
|
||||
self.assertEqual(entry.read(), contents)
|
||||
|
||||
def test_readPartial(self):
|
||||
"""
|
||||
C{.read(num)} should read num bytes from the file.
|
||||
"""
|
||||
contents = "0123456789"
|
||||
with self.getFileEntry(contents) as entry:
|
||||
one = entry.read(4)
|
||||
two = entry.read(200)
|
||||
self.assertEqual(one, b"0123")
|
||||
self.assertEqual(two, b"456789")
|
||||
|
||||
def test_tell(self):
|
||||
"""
|
||||
C{.tell()} should return the number of bytes that have been read so
|
||||
far.
|
||||
"""
|
||||
contents = "x" * 100
|
||||
with self.getFileEntry(contents) as entry:
|
||||
entry.read(2)
|
||||
self.assertEqual(entry.tell(), 2)
|
||||
entry.read(4)
|
||||
self.assertEqual(entry.tell(), 6)
|
||||
|
||||
|
||||
class DeflatedZipFileEntryTests(FileEntryMixin, unittest.TestCase):
|
||||
"""
|
||||
DeflatedZipFileEntry should be file-like
|
||||
"""
|
||||
|
||||
compression = zipfile.ZIP_DEFLATED
|
||||
|
||||
|
||||
class ZipFileEntryTests(FileEntryMixin, unittest.TestCase):
|
||||
"""
|
||||
ZipFileEntry should be file-like
|
||||
"""
|
||||
|
||||
compression = zipfile.ZIP_STORED
|
||||
|
||||
|
||||
class ZipstreamTests(unittest.TestCase):
|
||||
"""
|
||||
Tests for twisted.python.zipstream
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
Creates junk data that can be compressed and a test directory for any
|
||||
files that will be created
|
||||
"""
|
||||
self.testdir = filepath.FilePath(self.mktemp())
|
||||
self.testdir.makedirs()
|
||||
self.unzipdir = self.testdir.child("unzipped")
|
||||
self.unzipdir.makedirs()
|
||||
|
||||
def makeZipFile(self, contents, directory=""):
|
||||
"""
|
||||
Makes a zip file archive containing len(contents) files. Contents
|
||||
should be a list of strings, each string being the content of one file.
|
||||
"""
|
||||
zpfilename = self.testdir.child("zipfile.zip").path
|
||||
with zipfile.ZipFile(zpfilename, "w") as zpfile:
|
||||
for i, content in enumerate(contents):
|
||||
filename = str(i)
|
||||
if directory:
|
||||
filename = directory + "/" + filename
|
||||
zpfile.writestr(filename, content)
|
||||
return zpfilename
|
||||
|
||||
def test_invalidMode(self):
|
||||
"""
|
||||
A ChunkingZipFile opened in write-mode should not allow .readfile(),
|
||||
and raise a RuntimeError instead.
|
||||
"""
|
||||
with zipstream.ChunkingZipFile(self.mktemp(), "w") as czf:
|
||||
self.assertRaises(RuntimeError, czf.readfile, "something")
|
||||
|
||||
def test_closedArchive(self):
|
||||
"""
|
||||
A closed ChunkingZipFile should raise a L{RuntimeError} when
|
||||
.readfile() is invoked.
|
||||
"""
|
||||
czf = zipstream.ChunkingZipFile(self.makeZipFile(["something"]), "r")
|
||||
czf.close()
|
||||
self.assertRaises(RuntimeError, czf.readfile, "something")
|
||||
|
||||
def test_invalidHeader(self):
|
||||
"""
|
||||
A zipfile entry with the wrong magic number should raise BadZipFile for
|
||||
readfile(), but that should not affect other files in the archive.
|
||||
"""
|
||||
fn = self.makeZipFile(["test contents", "more contents"])
|
||||
with zipfile.ZipFile(fn, "r") as zf:
|
||||
zeroOffset = zf.getinfo("0").header_offset
|
||||
# Zero out just the one header.
|
||||
with open(fn, "r+b") as scribble:
|
||||
scribble.seek(zeroOffset, 0)
|
||||
scribble.write(b"0" * 4)
|
||||
with zipstream.ChunkingZipFile(fn) as czf:
|
||||
self.assertRaises(zipfile.BadZipFile, czf.readfile, "0")
|
||||
with czf.readfile("1") as zfe:
|
||||
self.assertEqual(zfe.read(), b"more contents")
|
||||
|
||||
def test_filenameMismatch(self):
|
||||
"""
|
||||
A zipfile entry with a different filename than is found in the central
|
||||
directory should raise BadZipFile.
|
||||
"""
|
||||
fn = self.makeZipFile([b"test contents", b"more contents"])
|
||||
with zipfile.ZipFile(fn, "r") as zf:
|
||||
info = zf.getinfo("0")
|
||||
info.filename = "not zero"
|
||||
with open(fn, "r+b") as scribble:
|
||||
scribble.seek(info.header_offset, 0)
|
||||
scribble.write(info.FileHeader())
|
||||
|
||||
with zipstream.ChunkingZipFile(fn) as czf:
|
||||
self.assertRaises(zipfile.BadZipFile, czf.readfile, "0")
|
||||
with czf.readfile("1") as zfe:
|
||||
self.assertEqual(zfe.read(), b"more contents")
|
||||
|
||||
def test_unsupportedCompression(self):
|
||||
"""
|
||||
A zipfile which describes an unsupported compression mechanism should
|
||||
raise BadZipFile.
|
||||
"""
|
||||
fn = self.mktemp()
|
||||
with zipfile.ZipFile(fn, "w") as zf:
|
||||
zi = zipfile.ZipInfo("0")
|
||||
zf.writestr(zi, "some data")
|
||||
# Mangle its compression type in the central directory; can't do
|
||||
# this before the writestr call or zipfile will (correctly) tell us
|
||||
# not to pass bad compression types :)
|
||||
zi.compress_type = 1234
|
||||
|
||||
with zipstream.ChunkingZipFile(fn) as czf:
|
||||
self.assertRaises(zipfile.BadZipFile, czf.readfile, "0")
|
||||
|
||||
def test_extraData(self):
|
||||
"""
|
||||
readfile() should skip over 'extra' data present in the zip metadata.
|
||||
"""
|
||||
fn = self.mktemp()
|
||||
with zipfile.ZipFile(fn, "w") as zf:
|
||||
zi = zipfile.ZipInfo("0")
|
||||
extra_data = b"hello, extra"
|
||||
zi.extra = struct.pack("<hh", 42, len(extra_data)) + extra_data
|
||||
zf.writestr(zi, b"the real data")
|
||||
with zipstream.ChunkingZipFile(fn) as czf, czf.readfile("0") as zfe:
|
||||
self.assertEqual(zfe.read(), b"the real data")
|
||||
|
||||
def test_unzipIterChunky(self):
|
||||
"""
|
||||
L{twisted.python.zipstream.unzipIterChunky} returns an iterator which
|
||||
must be exhausted to completely unzip the input archive.
|
||||
"""
|
||||
numfiles = 10
|
||||
contents = ["This is test file %d!" % i for i in range(numfiles)]
|
||||
contents = [i.encode("ascii") for i in contents]
|
||||
zpfilename = self.makeZipFile(contents)
|
||||
list(zipstream.unzipIterChunky(zpfilename, self.unzipdir.path))
|
||||
self.assertEqual(set(self.unzipdir.listdir()), set(map(str, range(numfiles))))
|
||||
|
||||
for child in self.unzipdir.children():
|
||||
num = int(child.basename())
|
||||
self.assertEqual(child.getContent(), contents[num])
|
||||
|
||||
def test_unzipIterChunkyDirectory(self):
|
||||
"""
|
||||
The path to which a file is extracted by L{zipstream.unzipIterChunky}
|
||||
is determined by joining the C{directory} argument to C{unzip} with the
|
||||
path within the archive of the file being extracted.
|
||||
"""
|
||||
numfiles = 10
|
||||
contents = ["This is test file %d!" % i for i in range(numfiles)]
|
||||
contents = [i.encode("ascii") for i in contents]
|
||||
zpfilename = self.makeZipFile(contents, "foo")
|
||||
list(zipstream.unzipIterChunky(zpfilename, self.unzipdir.path))
|
||||
fileContents = {str(num).encode("ascii") for num in range(numfiles)}
|
||||
self.assertEqual(set(self.unzipdir.child(b"foo").listdir()), fileContents)
|
||||
|
||||
for child in self.unzipdir.child(b"foo").children():
|
||||
num = int(child.basename())
|
||||
self.assertEqual(child.getContent(), contents[num])
|
||||
|
||||
# XXX these tests are kind of gross and old, but I think unzipIterChunky is
|
||||
# kind of a gross function anyway. We should really write an abstract
|
||||
# copyTo/moveTo that operates on FilePath and make sure ZipPath can support
|
||||
# it, then just deprecate / remove this stuff.
|
||||
def _unzipIterChunkyTest(self, compression, chunksize, lower, upper):
|
||||
"""
|
||||
unzipIterChunky should unzip the given number of bytes per iteration.
|
||||
"""
|
||||
junk = b""
|
||||
for n in range(1000):
|
||||
num = round(random.random(), 12)
|
||||
numEncoded = str(num).encode("ascii")
|
||||
junk += b" " + numEncoded
|
||||
|
||||
junkmd5 = md5(junk).hexdigest()
|
||||
|
||||
tempdir = filepath.FilePath(self.mktemp())
|
||||
tempdir.makedirs()
|
||||
zfpath = tempdir.child("bigfile.zip").path
|
||||
self._makebigfile(zfpath, compression, junk)
|
||||
uziter = zipstream.unzipIterChunky(zfpath, tempdir.path, chunksize=chunksize)
|
||||
r = next(uziter)
|
||||
# test that the number of chunks is in the right ballpark;
|
||||
# this could theoretically be any number but statistically it
|
||||
# should always be in this range
|
||||
approx = lower < r < upper
|
||||
self.assertTrue(approx)
|
||||
for r in uziter:
|
||||
pass
|
||||
self.assertEqual(r, 0)
|
||||
with tempdir.child("zipstreamjunk").open() as f:
|
||||
newmd5 = md5(f.read()).hexdigest()
|
||||
self.assertEqual(newmd5, junkmd5)
|
||||
|
||||
def test_unzipIterChunkyStored(self):
|
||||
"""
|
||||
unzipIterChunky should unzip the given number of bytes per iteration on
|
||||
a stored archive.
|
||||
"""
|
||||
self._unzipIterChunkyTest(zipfile.ZIP_STORED, 500, 35, 45)
|
||||
|
||||
def test_chunkyDeflated(self):
|
||||
"""
|
||||
unzipIterChunky should unzip the given number of bytes per iteration on
|
||||
a deflated archive.
|
||||
"""
|
||||
self._unzipIterChunkyTest(zipfile.ZIP_DEFLATED, 972, 23, 27)
|
||||
|
||||
def _makebigfile(self, filename, compression, junk):
|
||||
"""
|
||||
Create a zip file with the given file name and compression scheme.
|
||||
"""
|
||||
with zipfile.ZipFile(filename, "w", compression) as zf:
|
||||
for i in range(10):
|
||||
fn = "zipstream%d" % i
|
||||
zf.writestr(fn, "")
|
||||
zf.writestr("zipstreamjunk", junk)
|
||||
Reference in New Issue
Block a user