This commit is contained in:
2024-12-17 14:36:15 -08:00
parent b2dbf46d28
commit 06d106de53
17731 changed files with 3037186 additions and 144 deletions

View File

@@ -0,0 +1,3 @@
"""
Unit tests for L{twisted.python}.
"""

View File

@@ -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")

View File

@@ -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

View File

@@ -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)

View File

@@ -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=(":",),
),
)

View File

@@ -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"))

View File

@@ -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

View File

@@ -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)

View File

@@ -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',
)

View File

@@ -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)

View File

@@ -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))

View File

@@ -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))

View File

@@ -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)))

View File

@@ -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
"""

View File

@@ -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")],
)

View File

@@ -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()
)

View File

@@ -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")

View File

@@ -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")

View File

@@ -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"],
)

View File

@@ -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

View File

@@ -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"),
)

View File

@@ -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)

View File

@@ -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)