Initial Commit
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
"""
|
||||
Unit tests for L{twisted.python}.
|
||||
"""
|
||||
@@ -0,0 +1,23 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
A module that is deprecated, used by L{twisted.python.test.test_deprecate} for
|
||||
testing purposes.
|
||||
"""
|
||||
|
||||
|
||||
from incremental import Version
|
||||
|
||||
from twisted.python.deprecate import deprecatedModuleAttribute
|
||||
|
||||
# Known module-level attributes.
|
||||
DEPRECATED_ATTRIBUTE = 42
|
||||
ANOTHER_ATTRIBUTE = "hello"
|
||||
|
||||
|
||||
version = Version("Twisted", 8, 0, 0)
|
||||
message = "Oh noes!"
|
||||
|
||||
|
||||
deprecatedModuleAttribute(version, message, __name__, "DEPRECATED_ATTRIBUTE")
|
||||
@@ -0,0 +1,57 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Facilities for helping test code which interacts with Python's module system
|
||||
to load code.
|
||||
"""
|
||||
|
||||
import sys
|
||||
from types import ModuleType
|
||||
from typing import Iterable, List, Tuple
|
||||
|
||||
from twisted.python.filepath import FilePath
|
||||
|
||||
|
||||
class TwistedModulesMixin:
|
||||
"""
|
||||
A mixin for C{twisted.trial.unittest.SynchronousTestCase} providing useful
|
||||
methods for manipulating Python's module system.
|
||||
"""
|
||||
|
||||
def replaceSysPath(self, sysPath: List[str]) -> None:
|
||||
"""
|
||||
Replace sys.path, for the duration of the test, with the given value.
|
||||
"""
|
||||
originalSysPath = sys.path[:]
|
||||
|
||||
def cleanUpSysPath() -> None:
|
||||
sys.path[:] = originalSysPath
|
||||
|
||||
self.addCleanup(cleanUpSysPath) # type: ignore[attr-defined]
|
||||
sys.path[:] = sysPath
|
||||
|
||||
def replaceSysModules(self, sysModules: Iterable[Tuple[str, ModuleType]]) -> None:
|
||||
"""
|
||||
Replace sys.modules, for the duration of the test, with the given value.
|
||||
"""
|
||||
originalSysModules = sys.modules.copy()
|
||||
|
||||
def cleanUpSysModules() -> None:
|
||||
sys.modules.clear()
|
||||
sys.modules.update(originalSysModules)
|
||||
|
||||
self.addCleanup(cleanUpSysModules) # type: ignore[attr-defined]
|
||||
sys.modules.clear()
|
||||
sys.modules.update(sysModules)
|
||||
|
||||
def pathEntryWithOnePackage(self, pkgname: str = "test_package") -> FilePath:
|
||||
"""
|
||||
Generate a L{FilePath} with one package, named C{pkgname}, on it, and
|
||||
return the L{FilePath} of the path entry.
|
||||
"""
|
||||
entry = FilePath(self.mktemp()) # type: ignore[attr-defined]
|
||||
pkg = entry.child("test_package")
|
||||
pkg.makedirs()
|
||||
pkg.child("__init__.py").setContent(b"")
|
||||
return entry
|
||||
@@ -0,0 +1,40 @@
|
||||
# -*- test-case-name: twisted.python.test.test_sendmsg -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
import os
|
||||
import socket
|
||||
import sys
|
||||
from struct import unpack
|
||||
from typing import Tuple
|
||||
|
||||
from twisted.python.sendmsg import recvmsg
|
||||
|
||||
|
||||
def recvfd(socketfd: int) -> Tuple[int, bytes]:
|
||||
"""
|
||||
Receive a file descriptor from a L{sendmsg} message on the given C{AF_UNIX}
|
||||
socket.
|
||||
|
||||
@param socketfd: An C{AF_UNIX} socket, attached to another process waiting
|
||||
to send sockets via the ancillary data mechanism in L{send1msg}.
|
||||
|
||||
@param fd: C{int}
|
||||
|
||||
@return: a 2-tuple of (new file descriptor, description).
|
||||
@rtype: 2-tuple of (C{int}, C{bytes})
|
||||
"""
|
||||
ourSocket = socket.fromfd(socketfd, socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
data, ancillary, flags = recvmsg(ourSocket)
|
||||
[(cmsgLevel, cmsgType, packedFD)] = ancillary
|
||||
# cmsgLevel and cmsgType really need to be SOL_SOCKET / SCM_RIGHTS, but
|
||||
# since those are the *only* standard values, there's not much point in
|
||||
# checking.
|
||||
[unpackedFD] = unpack("i", packedFD)
|
||||
return (unpackedFD, data)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
fd, description = recvfd(int(sys.argv[1]))
|
||||
os.write(fd, b"Test fixture data: " + description + b".\n")
|
||||
os.close(fd)
|
||||
@@ -0,0 +1,33 @@
|
||||
# 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
|
||||
|
||||
|
||||
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.
|
||||
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=("Cc",),
|
||||
# And this excludes the separator.
|
||||
blacklist_characters=(":",),
|
||||
),
|
||||
)
|
||||
@@ -0,0 +1,40 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Tests for the data directory support.
|
||||
"""
|
||||
|
||||
|
||||
try:
|
||||
from twisted.python import _appdirs
|
||||
except ImportError:
|
||||
_appdirs = None # type: ignore[assignment]
|
||||
|
||||
from twisted.trial import unittest
|
||||
|
||||
|
||||
class AppdirsTests(unittest.TestCase):
|
||||
"""
|
||||
Tests for L{_appdirs}.
|
||||
"""
|
||||
|
||||
if not _appdirs:
|
||||
skip = "appdirs package not installed"
|
||||
|
||||
def test_moduleName(self) -> None:
|
||||
"""
|
||||
Calling L{appdirs.getDataDirectory} will return a user data directory
|
||||
in the system convention, with the module of the caller as the
|
||||
subdirectory.
|
||||
"""
|
||||
res = _appdirs.getDataDirectory()
|
||||
self.assertTrue(res.endswith("twisted.python.test.test_appdirs"))
|
||||
|
||||
def test_manual(self) -> None:
|
||||
"""
|
||||
Calling L{appdirs.getDataDirectory} with a C{moduleName} argument will
|
||||
make a data directory with that name instead.
|
||||
"""
|
||||
res = _appdirs.getDataDirectory("foo.bar.baz")
|
||||
self.assertTrue(res.endswith("foo.bar.baz"))
|
||||
@@ -0,0 +1,878 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
|
||||
"""
|
||||
Test cases for Twisted component architecture.
|
||||
"""
|
||||
|
||||
|
||||
from functools import wraps
|
||||
|
||||
from zope.interface import Attribute, Interface, implementer
|
||||
from zope.interface.adapter import AdapterRegistry
|
||||
|
||||
from twisted.python import components
|
||||
from twisted.python.compat import cmp, comparable
|
||||
from twisted.python.components import _addHook, _removeHook, proxyForInterface
|
||||
from twisted.trial import unittest
|
||||
|
||||
|
||||
class Compo(components.Componentized):
|
||||
num = 0
|
||||
|
||||
def inc(self):
|
||||
self.num = self.num + 1
|
||||
return self.num
|
||||
|
||||
|
||||
class IAdept(Interface):
|
||||
def adaptorFunc():
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
class IElapsed(Interface):
|
||||
def elapsedFunc():
|
||||
"""
|
||||
1!
|
||||
"""
|
||||
|
||||
|
||||
@implementer(IAdept)
|
||||
class Adept(components.Adapter):
|
||||
def __init__(self, orig):
|
||||
self.original = orig
|
||||
self.num = 0
|
||||
|
||||
def adaptorFunc(self):
|
||||
self.num = self.num + 1
|
||||
return self.num, self.original.inc()
|
||||
|
||||
|
||||
@implementer(IElapsed)
|
||||
class Elapsed(components.Adapter):
|
||||
def elapsedFunc(self):
|
||||
return 1
|
||||
|
||||
|
||||
class AComp(components.Componentized):
|
||||
pass
|
||||
|
||||
|
||||
class BComp(AComp):
|
||||
pass
|
||||
|
||||
|
||||
class CComp(BComp):
|
||||
pass
|
||||
|
||||
|
||||
class ITest(Interface):
|
||||
pass
|
||||
|
||||
|
||||
class ITest2(Interface):
|
||||
pass
|
||||
|
||||
|
||||
class ITest3(Interface):
|
||||
pass
|
||||
|
||||
|
||||
class ITest4(Interface):
|
||||
pass
|
||||
|
||||
|
||||
@implementer(ITest, ITest3, ITest4)
|
||||
class Test(components.Adapter):
|
||||
def __init__(self, orig):
|
||||
pass
|
||||
|
||||
|
||||
@implementer(ITest2)
|
||||
class Test2:
|
||||
temporaryAdapter = 1
|
||||
|
||||
def __init__(self, orig):
|
||||
pass
|
||||
|
||||
|
||||
class RegistryUsingMixin:
|
||||
"""
|
||||
Mixin for test cases which modify the global registry somehow.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
Configure L{twisted.python.components.registerAdapter} to mutate an
|
||||
alternate registry to improve test isolation.
|
||||
"""
|
||||
# Create a brand new, empty registry and put it onto the components
|
||||
# module where registerAdapter will use it. Also ensure that it goes
|
||||
# away at the end of the test.
|
||||
scratchRegistry = AdapterRegistry()
|
||||
self.patch(components, "globalRegistry", scratchRegistry)
|
||||
# Hook the new registry up to the adapter lookup system and ensure that
|
||||
# association is also discarded after the test.
|
||||
hook = _addHook(scratchRegistry)
|
||||
self.addCleanup(_removeHook, hook)
|
||||
|
||||
|
||||
class ComponentizedTests(unittest.SynchronousTestCase, RegistryUsingMixin):
|
||||
"""
|
||||
Simple test case for caching in Componentized.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
RegistryUsingMixin.setUp(self)
|
||||
|
||||
components.registerAdapter(Test, AComp, ITest)
|
||||
components.registerAdapter(Test, AComp, ITest3)
|
||||
components.registerAdapter(Test2, AComp, ITest2)
|
||||
|
||||
def testComponentized(self):
|
||||
components.registerAdapter(Adept, Compo, IAdept)
|
||||
components.registerAdapter(Elapsed, Compo, IElapsed)
|
||||
|
||||
c = Compo()
|
||||
assert c.getComponent(IAdept).adaptorFunc() == (1, 1)
|
||||
assert c.getComponent(IAdept).adaptorFunc() == (2, 2)
|
||||
assert IElapsed(IAdept(c)).elapsedFunc() == 1
|
||||
|
||||
def testInheritanceAdaptation(self):
|
||||
c = CComp()
|
||||
co1 = c.getComponent(ITest)
|
||||
co2 = c.getComponent(ITest)
|
||||
co3 = c.getComponent(ITest2)
|
||||
co4 = c.getComponent(ITest2)
|
||||
assert co1 is co2
|
||||
assert co3 is not co4
|
||||
c.removeComponent(co1)
|
||||
co5 = c.getComponent(ITest)
|
||||
co6 = c.getComponent(ITest)
|
||||
assert co5 is co6
|
||||
assert co1 is not co5
|
||||
|
||||
def testMultiAdapter(self):
|
||||
c = CComp()
|
||||
co1 = c.getComponent(ITest)
|
||||
co3 = c.getComponent(ITest3)
|
||||
co4 = c.getComponent(ITest4)
|
||||
self.assertIsNone(co4)
|
||||
self.assertIs(co1, co3)
|
||||
|
||||
def test_getComponentDefaults(self):
|
||||
"""
|
||||
Test that a default value specified to Componentized.getComponent if
|
||||
there is no component for the requested interface.
|
||||
"""
|
||||
componentized = components.Componentized()
|
||||
default = object()
|
||||
self.assertIs(componentized.getComponent(ITest, default), default)
|
||||
self.assertIs(componentized.getComponent(ITest, default=default), default)
|
||||
self.assertIs(componentized.getComponent(ITest), None)
|
||||
|
||||
def test_setAdapter(self):
|
||||
"""
|
||||
C{Componentized.setAdapter} sets a component for an interface by
|
||||
wrapping the instance with the given adapter class.
|
||||
"""
|
||||
componentized = components.Componentized()
|
||||
componentized.setAdapter(IAdept, Adept)
|
||||
component = componentized.getComponent(IAdept)
|
||||
self.assertEqual(component.original, componentized)
|
||||
self.assertIsInstance(component, Adept)
|
||||
|
||||
def test_addAdapter(self):
|
||||
"""
|
||||
C{Componentized.setAdapter} adapts the instance by wrapping it with
|
||||
given adapter class, then stores it using C{addComponent}.
|
||||
"""
|
||||
componentized = components.Componentized()
|
||||
componentized.addAdapter(Adept, ignoreClass=True)
|
||||
component = componentized.getComponent(IAdept)
|
||||
self.assertEqual(component.original, componentized)
|
||||
self.assertIsInstance(component, Adept)
|
||||
|
||||
def test_setComponent(self):
|
||||
"""
|
||||
C{Componentized.setComponent} stores the given component using the
|
||||
given interface as the key.
|
||||
"""
|
||||
componentized = components.Componentized()
|
||||
obj = object()
|
||||
componentized.setComponent(ITest, obj)
|
||||
self.assertIs(componentized.getComponent(ITest), obj)
|
||||
|
||||
def test_unsetComponent(self):
|
||||
"""
|
||||
C{Componentized.setComponent} removes the cached component for the
|
||||
given interface.
|
||||
"""
|
||||
componentized = components.Componentized()
|
||||
obj = object()
|
||||
componentized.setComponent(ITest, obj)
|
||||
componentized.unsetComponent(ITest)
|
||||
self.assertIsNone(componentized.getComponent(ITest))
|
||||
|
||||
def test_reprableComponentized(self):
|
||||
"""
|
||||
C{ReprableComponentized} has a C{__repr__} that lists its cache.
|
||||
"""
|
||||
rc = components.ReprableComponentized()
|
||||
rc.setComponent(ITest, "hello")
|
||||
result = repr(rc)
|
||||
self.assertIn("ITest", result)
|
||||
self.assertIn("hello", result)
|
||||
|
||||
|
||||
class AdapterTests(unittest.SynchronousTestCase):
|
||||
"""Test adapters."""
|
||||
|
||||
def testAdapterGetComponent(self):
|
||||
o = object()
|
||||
a = Adept(o)
|
||||
self.assertRaises(components.CannotAdapt, ITest, a)
|
||||
self.assertIsNone(ITest(a, None))
|
||||
|
||||
|
||||
class IMeta(Interface):
|
||||
pass
|
||||
|
||||
|
||||
@implementer(IMeta)
|
||||
class MetaAdder(components.Adapter):
|
||||
def add(self, num):
|
||||
return self.original.num + num
|
||||
|
||||
|
||||
@implementer(IMeta)
|
||||
class BackwardsAdder(components.Adapter):
|
||||
def add(self, num):
|
||||
return self.original.num - num
|
||||
|
||||
|
||||
class MetaNumber:
|
||||
"""
|
||||
Integer wrapper for Interface adaptation tests.
|
||||
"""
|
||||
|
||||
def __init__(self, num):
|
||||
self.num = num
|
||||
|
||||
|
||||
class ComponentNumber(components.Componentized):
|
||||
def __init__(self):
|
||||
self.num = 0
|
||||
components.Componentized.__init__(self)
|
||||
|
||||
|
||||
@implementer(IMeta)
|
||||
class ComponentAdder(components.Adapter):
|
||||
"""
|
||||
Adder for componentized adapter tests.
|
||||
"""
|
||||
|
||||
def __init__(self, original):
|
||||
components.Adapter.__init__(self, original)
|
||||
self.num = self.original.num
|
||||
|
||||
def add(self, num):
|
||||
self.num += num
|
||||
return self.num
|
||||
|
||||
|
||||
class IAttrX(Interface):
|
||||
"""
|
||||
Base interface for test of adapter with C{__cmp__}.
|
||||
"""
|
||||
|
||||
def x():
|
||||
"""
|
||||
Return a value.
|
||||
"""
|
||||
|
||||
|
||||
class IAttrXX(Interface):
|
||||
"""
|
||||
Adapted interface for test of adapter with C{__cmp__}.
|
||||
"""
|
||||
|
||||
def xx():
|
||||
"""
|
||||
Return a tuple of values.
|
||||
"""
|
||||
|
||||
|
||||
@implementer(IAttrX)
|
||||
class Xcellent:
|
||||
"""
|
||||
L{IAttrX} implementation for test of adapter with C{__cmp__}.
|
||||
"""
|
||||
|
||||
def x(self):
|
||||
"""
|
||||
Return a value.
|
||||
|
||||
@return: a value
|
||||
"""
|
||||
return "x!"
|
||||
|
||||
|
||||
@comparable
|
||||
class DoubleXAdapter:
|
||||
"""
|
||||
Adapter with __cmp__.
|
||||
"""
|
||||
|
||||
num = 42
|
||||
|
||||
def __init__(self, original):
|
||||
self.original = original
|
||||
|
||||
def xx(self):
|
||||
return (self.original.x(), self.original.x())
|
||||
|
||||
def __cmp__(self, other):
|
||||
return cmp(self.num, other.num)
|
||||
|
||||
|
||||
class MetaInterfaceTests(RegistryUsingMixin, unittest.SynchronousTestCase):
|
||||
def test_basic(self):
|
||||
"""
|
||||
Registered adapters can be used to adapt classes to an interface.
|
||||
"""
|
||||
components.registerAdapter(MetaAdder, MetaNumber, IMeta)
|
||||
n = MetaNumber(1)
|
||||
self.assertEqual(IMeta(n).add(1), 2)
|
||||
|
||||
def testComponentizedInteraction(self):
|
||||
components.registerAdapter(ComponentAdder, ComponentNumber, IMeta)
|
||||
c = ComponentNumber()
|
||||
IMeta(c).add(1)
|
||||
IMeta(c).add(1)
|
||||
self.assertEqual(IMeta(c).add(1), 3)
|
||||
|
||||
def testAdapterWithCmp(self):
|
||||
# Make sure that a __cmp__ on an adapter doesn't break anything
|
||||
components.registerAdapter(DoubleXAdapter, IAttrX, IAttrXX)
|
||||
xx = IAttrXX(Xcellent())
|
||||
self.assertEqual(("x!", "x!"), xx.xx())
|
||||
|
||||
|
||||
class RegistrationTests(RegistryUsingMixin, unittest.SynchronousTestCase):
|
||||
"""
|
||||
Tests for adapter registration.
|
||||
"""
|
||||
|
||||
def _registerAdapterForClassOrInterface(self, original):
|
||||
"""
|
||||
Register an adapter with L{components.registerAdapter} for the given
|
||||
class or interface and verify that the adapter can be looked up with
|
||||
L{components.getAdapterFactory}.
|
||||
"""
|
||||
adapter = lambda o: None
|
||||
components.registerAdapter(adapter, original, ITest)
|
||||
self.assertIs(components.getAdapterFactory(original, ITest, None), adapter)
|
||||
|
||||
def test_registerAdapterForClass(self):
|
||||
"""
|
||||
Test that an adapter from a class can be registered and then looked
|
||||
up.
|
||||
"""
|
||||
|
||||
class TheOriginal:
|
||||
pass
|
||||
|
||||
return self._registerAdapterForClassOrInterface(TheOriginal)
|
||||
|
||||
def test_registerAdapterForInterface(self):
|
||||
"""
|
||||
Test that an adapter from an interface can be registered and then
|
||||
looked up.
|
||||
"""
|
||||
return self._registerAdapterForClassOrInterface(ITest2)
|
||||
|
||||
def _duplicateAdapterForClassOrInterface(self, original):
|
||||
"""
|
||||
Verify that L{components.registerAdapter} raises L{ValueError} if the
|
||||
from-type/interface and to-interface pair is not unique.
|
||||
"""
|
||||
firstAdapter = lambda o: False
|
||||
secondAdapter = lambda o: True
|
||||
components.registerAdapter(firstAdapter, original, ITest)
|
||||
self.assertRaises(
|
||||
ValueError, components.registerAdapter, secondAdapter, original, ITest
|
||||
)
|
||||
# Make sure that the original adapter is still around as well
|
||||
self.assertIs(components.getAdapterFactory(original, ITest, None), firstAdapter)
|
||||
|
||||
def test_duplicateAdapterForClass(self):
|
||||
"""
|
||||
Test that attempting to register a second adapter from a class
|
||||
raises the appropriate exception.
|
||||
"""
|
||||
|
||||
class TheOriginal:
|
||||
pass
|
||||
|
||||
return self._duplicateAdapterForClassOrInterface(TheOriginal)
|
||||
|
||||
def test_duplicateAdapterForInterface(self):
|
||||
"""
|
||||
Test that attempting to register a second adapter from an interface
|
||||
raises the appropriate exception.
|
||||
"""
|
||||
return self._duplicateAdapterForClassOrInterface(ITest2)
|
||||
|
||||
def _duplicateAdapterForClassOrInterfaceAllowed(self, original):
|
||||
"""
|
||||
Verify that when C{components.ALLOW_DUPLICATES} is set to C{True}, new
|
||||
adapter registrations for a particular from-type/interface and
|
||||
to-interface pair replace older registrations.
|
||||
"""
|
||||
firstAdapter = lambda o: False
|
||||
secondAdapter = lambda o: True
|
||||
|
||||
class TheInterface(Interface):
|
||||
pass
|
||||
|
||||
components.registerAdapter(firstAdapter, original, TheInterface)
|
||||
components.ALLOW_DUPLICATES = True
|
||||
try:
|
||||
components.registerAdapter(secondAdapter, original, TheInterface)
|
||||
self.assertIs(
|
||||
components.getAdapterFactory(original, TheInterface, None),
|
||||
secondAdapter,
|
||||
)
|
||||
finally:
|
||||
components.ALLOW_DUPLICATES = False
|
||||
|
||||
# It should be rejected again at this point
|
||||
self.assertRaises(
|
||||
ValueError, components.registerAdapter, firstAdapter, original, TheInterface
|
||||
)
|
||||
|
||||
self.assertIs(
|
||||
components.getAdapterFactory(original, TheInterface, None), secondAdapter
|
||||
)
|
||||
|
||||
def test_duplicateAdapterForClassAllowed(self):
|
||||
"""
|
||||
Test that when L{components.ALLOW_DUPLICATES} is set to a true
|
||||
value, duplicate registrations from classes are allowed to override
|
||||
the original registration.
|
||||
"""
|
||||
|
||||
class TheOriginal:
|
||||
pass
|
||||
|
||||
return self._duplicateAdapterForClassOrInterfaceAllowed(TheOriginal)
|
||||
|
||||
def test_duplicateAdapterForInterfaceAllowed(self):
|
||||
"""
|
||||
Test that when L{components.ALLOW_DUPLICATES} is set to a true
|
||||
value, duplicate registrations from interfaces are allowed to
|
||||
override the original registration.
|
||||
"""
|
||||
|
||||
class TheOriginal(Interface):
|
||||
pass
|
||||
|
||||
return self._duplicateAdapterForClassOrInterfaceAllowed(TheOriginal)
|
||||
|
||||
def _multipleInterfacesForClassOrInterface(self, original):
|
||||
"""
|
||||
Verify that an adapter can be registered for multiple to-interfaces at a
|
||||
time.
|
||||
"""
|
||||
adapter = lambda o: None
|
||||
components.registerAdapter(adapter, original, ITest, ITest2)
|
||||
self.assertIs(components.getAdapterFactory(original, ITest, None), adapter)
|
||||
self.assertIs(components.getAdapterFactory(original, ITest2, None), adapter)
|
||||
|
||||
def test_multipleInterfacesForClass(self):
|
||||
"""
|
||||
Test the registration of an adapter from a class to several
|
||||
interfaces at once.
|
||||
"""
|
||||
|
||||
class TheOriginal:
|
||||
pass
|
||||
|
||||
return self._multipleInterfacesForClassOrInterface(TheOriginal)
|
||||
|
||||
def test_multipleInterfacesForInterface(self):
|
||||
"""
|
||||
Test the registration of an adapter from an interface to several
|
||||
interfaces at once.
|
||||
"""
|
||||
return self._multipleInterfacesForClassOrInterface(ITest3)
|
||||
|
||||
def _subclassAdapterRegistrationForClassOrInterface(self, original):
|
||||
"""
|
||||
Verify that a new adapter can be registered for a particular
|
||||
to-interface from a subclass of a type or interface which already has an
|
||||
adapter registered to that interface and that the subclass adapter takes
|
||||
precedence over the base class adapter.
|
||||
"""
|
||||
firstAdapter = lambda o: True
|
||||
secondAdapter = lambda o: False
|
||||
|
||||
class TheSubclass(original):
|
||||
pass
|
||||
|
||||
components.registerAdapter(firstAdapter, original, ITest)
|
||||
components.registerAdapter(secondAdapter, TheSubclass, ITest)
|
||||
self.assertIs(components.getAdapterFactory(original, ITest, None), firstAdapter)
|
||||
self.assertIs(
|
||||
components.getAdapterFactory(TheSubclass, ITest, None), secondAdapter
|
||||
)
|
||||
|
||||
def test_subclassAdapterRegistrationForClass(self):
|
||||
"""
|
||||
Test that an adapter to a particular interface can be registered
|
||||
from both a class and its subclass.
|
||||
"""
|
||||
|
||||
class TheOriginal:
|
||||
pass
|
||||
|
||||
return self._subclassAdapterRegistrationForClassOrInterface(TheOriginal)
|
||||
|
||||
def test_subclassAdapterRegistrationForInterface(self):
|
||||
"""
|
||||
Test that an adapter to a particular interface can be registered
|
||||
from both an interface and its subclass.
|
||||
"""
|
||||
return self._subclassAdapterRegistrationForClassOrInterface(ITest2)
|
||||
|
||||
|
||||
class IProxiedInterface(Interface):
|
||||
"""
|
||||
An interface class for use by L{proxyForInterface}.
|
||||
"""
|
||||
|
||||
ifaceAttribute = Attribute(
|
||||
"""
|
||||
An example declared attribute, which should be proxied."""
|
||||
)
|
||||
|
||||
def yay(*a, **kw):
|
||||
"""
|
||||
A sample method which should be proxied.
|
||||
"""
|
||||
|
||||
|
||||
class IProxiedSubInterface(IProxiedInterface):
|
||||
"""
|
||||
An interface that derives from another for use with L{proxyForInterface}.
|
||||
"""
|
||||
|
||||
def boo():
|
||||
"""
|
||||
A different sample method which should be proxied.
|
||||
"""
|
||||
|
||||
|
||||
@implementer(IProxiedInterface)
|
||||
class Yayable: # type: ignore[misc]
|
||||
# class does not implement Attribute ifaceAttribute
|
||||
# so we need to turn off mypy warning
|
||||
"""
|
||||
A provider of L{IProxiedInterface} which increments a counter for
|
||||
every call to C{yay}.
|
||||
|
||||
@ivar yays: The number of times C{yay} has been called.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.yays = 0
|
||||
self.yayArgs = []
|
||||
|
||||
def yay(self, *a, **kw):
|
||||
"""
|
||||
Increment C{self.yays}.
|
||||
"""
|
||||
self.yays += 1
|
||||
self.yayArgs.append((a, kw))
|
||||
return self.yays
|
||||
|
||||
|
||||
@implementer(IProxiedSubInterface)
|
||||
class Booable: # type: ignore[misc]
|
||||
# class does not implement Attribute ifaceAttribute
|
||||
# so we need to turn off mypy warning
|
||||
"""
|
||||
An implementation of IProxiedSubInterface
|
||||
"""
|
||||
|
||||
yayed = False
|
||||
booed = False
|
||||
|
||||
def yay(self, *a, **kw):
|
||||
"""
|
||||
Mark the fact that 'yay' has been called.
|
||||
"""
|
||||
self.yayed = True
|
||||
|
||||
def boo(self):
|
||||
"""
|
||||
Mark the fact that 'boo' has been called.1
|
||||
"""
|
||||
self.booed = True
|
||||
|
||||
|
||||
class IMultipleMethods(Interface):
|
||||
"""
|
||||
An interface with multiple methods.
|
||||
"""
|
||||
|
||||
def methodOne():
|
||||
"""
|
||||
The first method. Should return 1.
|
||||
"""
|
||||
|
||||
def methodTwo():
|
||||
"""
|
||||
The second method. Should return 2.
|
||||
"""
|
||||
|
||||
|
||||
class MultipleMethodImplementor:
|
||||
"""
|
||||
A precise implementation of L{IMultipleMethods}.
|
||||
"""
|
||||
|
||||
def methodOne(self):
|
||||
"""
|
||||
@return: 1
|
||||
"""
|
||||
return 1
|
||||
|
||||
def methodTwo(self):
|
||||
"""
|
||||
@return: 2
|
||||
"""
|
||||
return 2
|
||||
|
||||
|
||||
class ProxyForInterfaceTests(unittest.SynchronousTestCase):
|
||||
"""
|
||||
Tests for L{proxyForInterface}.
|
||||
"""
|
||||
|
||||
def test_original(self):
|
||||
"""
|
||||
Proxy objects should have an C{original} attribute which refers to the
|
||||
original object passed to the constructor.
|
||||
"""
|
||||
original = object()
|
||||
proxy = proxyForInterface(IProxiedInterface)(original)
|
||||
self.assertIs(proxy.original, original)
|
||||
|
||||
def test_proxyMethod(self):
|
||||
"""
|
||||
The class created from L{proxyForInterface} passes methods on an
|
||||
interface to the object which is passed to its constructor.
|
||||
"""
|
||||
klass = proxyForInterface(IProxiedInterface)
|
||||
yayable = Yayable()
|
||||
proxy = klass(yayable)
|
||||
proxy.yay()
|
||||
self.assertEqual(proxy.yay(), 2)
|
||||
self.assertEqual(yayable.yays, 2)
|
||||
|
||||
def test_decoratedProxyMethod(self):
|
||||
"""
|
||||
Methods of the class created from L{proxyForInterface} can be used with
|
||||
the decorator-helper L{functools.wraps}.
|
||||
"""
|
||||
base = proxyForInterface(IProxiedInterface)
|
||||
|
||||
class klass(base):
|
||||
@wraps(base.yay)
|
||||
def yay(self):
|
||||
self.original.yays += 1
|
||||
return base.yay(self)
|
||||
|
||||
original = Yayable()
|
||||
yayable = klass(original)
|
||||
yayable.yay()
|
||||
self.assertEqual(2, original.yays)
|
||||
|
||||
def test_proxyAttribute(self):
|
||||
"""
|
||||
Proxy objects should proxy declared attributes, but not other
|
||||
attributes.
|
||||
"""
|
||||
yayable = Yayable()
|
||||
yayable.ifaceAttribute = object()
|
||||
proxy = proxyForInterface(IProxiedInterface)(yayable)
|
||||
self.assertIs(proxy.ifaceAttribute, yayable.ifaceAttribute)
|
||||
self.assertRaises(AttributeError, lambda: proxy.yays)
|
||||
|
||||
def test_proxySetAttribute(self):
|
||||
"""
|
||||
The attributes that proxy objects proxy should be assignable and affect
|
||||
the original object.
|
||||
"""
|
||||
yayable = Yayable()
|
||||
proxy = proxyForInterface(IProxiedInterface)(yayable)
|
||||
thingy = object()
|
||||
proxy.ifaceAttribute = thingy
|
||||
self.assertIs(yayable.ifaceAttribute, thingy)
|
||||
|
||||
def test_proxyDeleteAttribute(self):
|
||||
"""
|
||||
The attributes that proxy objects proxy should be deletable and affect
|
||||
the original object.
|
||||
"""
|
||||
yayable = Yayable()
|
||||
yayable.ifaceAttribute = None
|
||||
proxy = proxyForInterface(IProxiedInterface)(yayable)
|
||||
del proxy.ifaceAttribute
|
||||
self.assertFalse(hasattr(yayable, "ifaceAttribute"))
|
||||
|
||||
def test_multipleMethods(self):
|
||||
"""
|
||||
[Regression test] The proxy should send its method calls to the correct
|
||||
method, not the incorrect one.
|
||||
"""
|
||||
multi = MultipleMethodImplementor()
|
||||
proxy = proxyForInterface(IMultipleMethods)(multi)
|
||||
self.assertEqual(proxy.methodOne(), 1)
|
||||
self.assertEqual(proxy.methodTwo(), 2)
|
||||
|
||||
def test_subclassing(self):
|
||||
"""
|
||||
It is possible to subclass the result of L{proxyForInterface}.
|
||||
"""
|
||||
|
||||
class SpecializedProxy(proxyForInterface(IProxiedInterface)):
|
||||
"""
|
||||
A specialized proxy which can decrement the number of yays.
|
||||
"""
|
||||
|
||||
def boo(self):
|
||||
"""
|
||||
Decrement the number of yays.
|
||||
"""
|
||||
self.original.yays -= 1
|
||||
|
||||
yayable = Yayable()
|
||||
special = SpecializedProxy(yayable)
|
||||
self.assertEqual(yayable.yays, 0)
|
||||
special.boo()
|
||||
self.assertEqual(yayable.yays, -1)
|
||||
|
||||
def test_proxyName(self):
|
||||
"""
|
||||
The name of a proxy class indicates which interface it proxies.
|
||||
"""
|
||||
proxy = proxyForInterface(IProxiedInterface)
|
||||
self.assertEqual(
|
||||
proxy.__name__,
|
||||
"(Proxy for " "twisted.python.test.test_components.IProxiedInterface)",
|
||||
)
|
||||
|
||||
def test_implements(self):
|
||||
"""
|
||||
The resulting proxy implements the interface that it proxies.
|
||||
"""
|
||||
proxy = proxyForInterface(IProxiedInterface)
|
||||
self.assertTrue(IProxiedInterface.implementedBy(proxy))
|
||||
|
||||
def test_proxyDescriptorGet(self):
|
||||
"""
|
||||
_ProxyDescriptor's __get__ method should return the appropriate
|
||||
attribute of its argument's 'original' attribute if it is invoked with
|
||||
an object. If it is invoked with None, it should return a false
|
||||
class-method emulator instead.
|
||||
|
||||
For some reason, Python's documentation recommends to define
|
||||
descriptors' __get__ methods with the 'type' parameter as optional,
|
||||
despite the fact that Python itself never actually calls the descriptor
|
||||
that way. This is probably do to support 'foo.__get__(bar)' as an
|
||||
idiom. Let's make sure that the behavior is correct. Since we don't
|
||||
actually use the 'type' argument at all, this test calls it the
|
||||
idiomatic way to ensure that signature works; test_proxyInheritance
|
||||
verifies the how-Python-actually-calls-it signature.
|
||||
"""
|
||||
|
||||
class Sample:
|
||||
called = False
|
||||
|
||||
def hello(self):
|
||||
self.called = True
|
||||
|
||||
fakeProxy = Sample()
|
||||
testObject = Sample()
|
||||
fakeProxy.original = testObject
|
||||
pd = components._ProxyDescriptor("hello", "original")
|
||||
self.assertEqual(pd.__get__(fakeProxy), testObject.hello)
|
||||
fakeClassMethod = pd.__get__(None)
|
||||
fakeClassMethod(fakeProxy)
|
||||
self.assertTrue(testObject.called)
|
||||
|
||||
def test_proxyInheritance(self):
|
||||
"""
|
||||
Subclasses of the class returned from L{proxyForInterface} should be
|
||||
able to upcall methods by reference to their superclass, as any normal
|
||||
Python class can.
|
||||
"""
|
||||
|
||||
class YayableWrapper(proxyForInterface(IProxiedInterface)):
|
||||
"""
|
||||
This class does not override any functionality.
|
||||
"""
|
||||
|
||||
class EnhancedWrapper(YayableWrapper):
|
||||
"""
|
||||
This class overrides the 'yay' method.
|
||||
"""
|
||||
|
||||
wrappedYays = 1
|
||||
|
||||
def yay(self, *a, **k):
|
||||
self.wrappedYays += 1
|
||||
return YayableWrapper.yay(self, *a, **k) + 7
|
||||
|
||||
yayable = Yayable()
|
||||
wrapper = EnhancedWrapper(yayable)
|
||||
self.assertEqual(wrapper.yay(3, 4, x=5, y=6), 8)
|
||||
self.assertEqual(yayable.yayArgs, [((3, 4), dict(x=5, y=6))])
|
||||
|
||||
def test_interfaceInheritance(self):
|
||||
"""
|
||||
Proxies of subinterfaces generated with proxyForInterface should allow
|
||||
access to attributes of both the child and the base interfaces.
|
||||
"""
|
||||
proxyClass = proxyForInterface(IProxiedSubInterface)
|
||||
booable = Booable()
|
||||
proxy = proxyClass(booable)
|
||||
proxy.yay()
|
||||
proxy.boo()
|
||||
self.assertTrue(booable.yayed)
|
||||
self.assertTrue(booable.booed)
|
||||
|
||||
def test_attributeCustomization(self):
|
||||
"""
|
||||
The original attribute name can be customized via the
|
||||
C{originalAttribute} argument of L{proxyForInterface}: the attribute
|
||||
should change, but the methods of the original object should still be
|
||||
callable, and the attributes still accessible.
|
||||
"""
|
||||
yayable = Yayable()
|
||||
yayable.ifaceAttribute = object()
|
||||
proxy = proxyForInterface(IProxiedInterface, originalAttribute="foo")(yayable)
|
||||
self.assertIs(proxy.foo, yayable)
|
||||
|
||||
# Check the behavior
|
||||
self.assertEqual(proxy.yay(), 1)
|
||||
self.assertIs(proxy.ifaceAttribute, yayable.ifaceAttribute)
|
||||
thingy = object()
|
||||
proxy.ifaceAttribute = thingy
|
||||
self.assertIs(yayable.ifaceAttribute, thingy)
|
||||
del proxy.ifaceAttribute
|
||||
self.assertFalse(hasattr(yayable, "ifaceAttribute"))
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,453 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Tests for L{twisted.python.fakepwd}.
|
||||
"""
|
||||
|
||||
try:
|
||||
import pwd as _pwd
|
||||
except ImportError:
|
||||
pwd = None
|
||||
else:
|
||||
pwd = _pwd
|
||||
|
||||
try:
|
||||
import spwd as _spwd
|
||||
except ImportError:
|
||||
spwd = None
|
||||
else:
|
||||
spwd = _spwd
|
||||
|
||||
import os
|
||||
from operator import getitem
|
||||
|
||||
from twisted.python.compat import _PYPY
|
||||
from twisted.python.fakepwd import ShadowDatabase, UserDatabase
|
||||
from twisted.trial.unittest import TestCase
|
||||
|
||||
SYSTEM_UID_MAX = 999
|
||||
|
||||
|
||||
def findInvalidUID():
|
||||
"""
|
||||
By convention, UIDs less than 1000 are reserved for the system. A system
|
||||
which allocated every single one of those UIDs would likely have practical
|
||||
problems with allocating new ones, so let's assume that we'll be able to
|
||||
find one. (If we don't, this will wrap around to negative values and
|
||||
I{eventually} find something.)
|
||||
|
||||
@return: a user ID which does not exist on the local system. Or, on
|
||||
systems without a L{pwd} module, return C{SYSTEM_UID_MAX}.
|
||||
"""
|
||||
guess = SYSTEM_UID_MAX
|
||||
if pwd is not None:
|
||||
while True:
|
||||
try:
|
||||
pwd.getpwuid(guess)
|
||||
except KeyError:
|
||||
break
|
||||
else:
|
||||
guess -= 1
|
||||
return guess
|
||||
|
||||
|
||||
INVALID_UID = findInvalidUID()
|
||||
|
||||
|
||||
class UserDatabaseTestsMixin:
|
||||
"""
|
||||
L{UserDatabaseTestsMixin} defines tests which apply to any user database
|
||||
implementation. Subclasses should mix it in, implement C{setUp} to create
|
||||
C{self.database} bound to a user database instance, and implement
|
||||
C{getExistingUserInfo} to return information about a user (such information
|
||||
should be unique per test method).
|
||||
"""
|
||||
|
||||
def test_getpwuid(self):
|
||||
"""
|
||||
I{getpwuid} accepts a uid and returns the user record associated with
|
||||
it.
|
||||
"""
|
||||
for i in range(2):
|
||||
# Get some user which exists in the database.
|
||||
username, password, uid, gid, gecos, dir, shell = self.getExistingUserInfo()
|
||||
|
||||
# Now try to look it up and make sure the result is correct.
|
||||
entry = self.database.getpwuid(uid)
|
||||
self.assertEqual(entry.pw_name, username)
|
||||
self.assertEqual(entry.pw_passwd, password)
|
||||
self.assertEqual(entry.pw_uid, uid)
|
||||
self.assertEqual(entry.pw_gid, gid)
|
||||
self.assertEqual(entry.pw_gecos, gecos)
|
||||
self.assertEqual(entry.pw_dir, dir)
|
||||
self.assertEqual(entry.pw_shell, shell)
|
||||
|
||||
def test_noSuchUID(self):
|
||||
"""
|
||||
I{getpwuid} raises L{KeyError} when passed a uid which does not exist
|
||||
in the user database.
|
||||
"""
|
||||
self.assertRaises(KeyError, self.database.getpwuid, INVALID_UID)
|
||||
|
||||
def test_getpwnam(self):
|
||||
"""
|
||||
I{getpwnam} accepts a username and returns the user record associated
|
||||
with it.
|
||||
"""
|
||||
for i in range(2):
|
||||
# Get some user which exists in the database.
|
||||
username, password, uid, gid, gecos, dir, shell = self.getExistingUserInfo()
|
||||
|
||||
# Now try to look it up and make sure the result is correct.
|
||||
entry = self.database.getpwnam(username)
|
||||
self.assertEqual(entry.pw_name, username)
|
||||
self.assertEqual(entry.pw_passwd, password)
|
||||
self.assertEqual(entry.pw_uid, uid)
|
||||
self.assertEqual(entry.pw_gid, gid)
|
||||
self.assertEqual(entry.pw_gecos, gecos)
|
||||
self.assertEqual(entry.pw_dir, dir)
|
||||
self.assertEqual(entry.pw_shell, shell)
|
||||
|
||||
def test_getpwnamRejectsBytes(self):
|
||||
"""
|
||||
L{getpwnam} rejects a non-L{str} username with an exception.
|
||||
"""
|
||||
exc_type = TypeError
|
||||
if _PYPY:
|
||||
# PyPy raises KeyError instead of TypeError. See
|
||||
# https://foss.heptapod.net/pypy/pypy/-/issues/3624
|
||||
exc_type = Exception
|
||||
self.assertRaises(exc_type, self.database.getpwnam, b"i-am-bytes")
|
||||
|
||||
def test_noSuchName(self):
|
||||
"""
|
||||
I{getpwnam} raises L{KeyError} when passed a username which does not
|
||||
exist in the user database.
|
||||
"""
|
||||
self.assertRaises(
|
||||
KeyError,
|
||||
self.database.getpwnam,
|
||||
"no"
|
||||
"such"
|
||||
"user"
|
||||
"exists"
|
||||
"the"
|
||||
"name"
|
||||
"is"
|
||||
"too"
|
||||
"long"
|
||||
"and"
|
||||
"has"
|
||||
"\1"
|
||||
"in"
|
||||
"it"
|
||||
"too",
|
||||
)
|
||||
|
||||
def test_recordLength(self):
|
||||
"""
|
||||
The user record returned by I{getpwuid}, I{getpwnam}, and I{getpwall}
|
||||
has a length.
|
||||
"""
|
||||
db = self.database
|
||||
username, password, uid, gid, gecos, dir, shell = self.getExistingUserInfo()
|
||||
for entry in [db.getpwuid(uid), db.getpwnam(username), db.getpwall()[0]]:
|
||||
self.assertIsInstance(len(entry), int)
|
||||
self.assertEqual(len(entry), 7)
|
||||
|
||||
def test_recordIndexable(self):
|
||||
"""
|
||||
The user record returned by I{getpwuid}, I{getpwnam}, and I{getpwall}
|
||||
is indexable, with successive indexes starting from 0 corresponding to
|
||||
the values of the C{pw_name}, C{pw_passwd}, C{pw_uid}, C{pw_gid},
|
||||
C{pw_gecos}, C{pw_dir}, and C{pw_shell} attributes, respectively.
|
||||
"""
|
||||
db = self.database
|
||||
username, password, uid, gid, gecos, dir, shell = self.getExistingUserInfo()
|
||||
for entry in [db.getpwuid(uid), db.getpwnam(username), db.getpwall()[0]]:
|
||||
self.assertEqual(entry[0], username)
|
||||
self.assertEqual(entry[1], password)
|
||||
self.assertEqual(entry[2], uid)
|
||||
self.assertEqual(entry[3], gid)
|
||||
self.assertEqual(entry[4], gecos)
|
||||
self.assertEqual(entry[5], dir)
|
||||
self.assertEqual(entry[6], shell)
|
||||
|
||||
self.assertEqual(len(entry), len(list(entry)))
|
||||
self.assertRaises(IndexError, getitem, entry, 7)
|
||||
|
||||
|
||||
class UserDatabaseTests(TestCase, UserDatabaseTestsMixin):
|
||||
"""
|
||||
Tests for L{UserDatabase}.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
Create a L{UserDatabase} with no user data in it.
|
||||
"""
|
||||
self.database = UserDatabase()
|
||||
self._counter = SYSTEM_UID_MAX + 1
|
||||
|
||||
def getExistingUserInfo(self):
|
||||
"""
|
||||
Add a new user to C{self.database} and return its information.
|
||||
"""
|
||||
self._counter += 1
|
||||
suffix = "_" + str(self._counter)
|
||||
username = "username" + suffix
|
||||
password = "password" + suffix
|
||||
uid = self._counter
|
||||
gid = self._counter + 1000
|
||||
gecos = "gecos" + suffix
|
||||
dir = "dir" + suffix
|
||||
shell = "shell" + suffix
|
||||
|
||||
self.database.addUser(username, password, uid, gid, gecos, dir, shell)
|
||||
return (username, password, uid, gid, gecos, dir, shell)
|
||||
|
||||
def test_addUser(self):
|
||||
"""
|
||||
L{UserDatabase.addUser} accepts seven arguments, one for each field of
|
||||
a L{pwd.struct_passwd}, and makes the new record available via
|
||||
L{UserDatabase.getpwuid}, L{UserDatabase.getpwnam}, and
|
||||
L{UserDatabase.getpwall}.
|
||||
"""
|
||||
username = "alice"
|
||||
password = "secr3t"
|
||||
uid = 123
|
||||
gid = 456
|
||||
gecos = "Alice,,,"
|
||||
home = "/users/alice"
|
||||
shell = "/usr/bin/foosh"
|
||||
|
||||
db = self.database
|
||||
db.addUser(username, password, uid, gid, gecos, home, shell)
|
||||
|
||||
for [entry] in [[db.getpwuid(uid)], [db.getpwnam(username)], db.getpwall()]:
|
||||
self.assertEqual(entry.pw_name, username)
|
||||
self.assertEqual(entry.pw_passwd, password)
|
||||
self.assertEqual(entry.pw_uid, uid)
|
||||
self.assertEqual(entry.pw_gid, gid)
|
||||
self.assertEqual(entry.pw_gecos, gecos)
|
||||
self.assertEqual(entry.pw_dir, home)
|
||||
self.assertEqual(entry.pw_shell, shell)
|
||||
|
||||
|
||||
class PwdModuleTests(TestCase, UserDatabaseTestsMixin):
|
||||
"""
|
||||
L{PwdModuleTests} runs the tests defined by L{UserDatabaseTestsMixin}
|
||||
against the built-in C{pwd} module. This serves to verify that
|
||||
L{UserDatabase} is really a fake of that API.
|
||||
"""
|
||||
|
||||
if pwd is None:
|
||||
skip = "Cannot verify UserDatabase against pwd without pwd"
|
||||
else:
|
||||
database = pwd
|
||||
|
||||
def setUp(self):
|
||||
self._users = iter(self.database.getpwall())
|
||||
self._uids = set()
|
||||
|
||||
def getExistingUserInfo(self):
|
||||
"""
|
||||
Read and return the next record from C{self._users}, filtering out
|
||||
any records with previously seen uid values (as these cannot be
|
||||
found with C{getpwuid} and only cause trouble).
|
||||
"""
|
||||
while True:
|
||||
entry = next(self._users)
|
||||
uid = entry.pw_uid
|
||||
if uid not in self._uids:
|
||||
self._uids.add(uid)
|
||||
return entry
|
||||
|
||||
|
||||
class ShadowDatabaseTestsMixin:
|
||||
"""
|
||||
L{ShadowDatabaseTestsMixin} defines tests which apply to any shadow user
|
||||
database implementation. Subclasses should mix it in, implement C{setUp} to
|
||||
create C{self.database} bound to a shadow user database instance, and
|
||||
implement C{getExistingUserInfo} to return information about a user (such
|
||||
information should be unique per test method).
|
||||
"""
|
||||
|
||||
def test_getspnam(self):
|
||||
"""
|
||||
L{getspnam} accepts a username and returns the user record associated
|
||||
with it.
|
||||
"""
|
||||
for i in range(2):
|
||||
# Get some user which exists in the database.
|
||||
(
|
||||
username,
|
||||
password,
|
||||
lastChange,
|
||||
min,
|
||||
max,
|
||||
warn,
|
||||
inact,
|
||||
expire,
|
||||
flag,
|
||||
) = self.getExistingUserInfo()
|
||||
|
||||
entry = self.database.getspnam(username)
|
||||
self.assertEqual(entry.sp_nam, username)
|
||||
self.assertEqual(entry.sp_pwd, password)
|
||||
self.assertEqual(entry.sp_lstchg, lastChange)
|
||||
self.assertEqual(entry.sp_min, min)
|
||||
self.assertEqual(entry.sp_max, max)
|
||||
self.assertEqual(entry.sp_warn, warn)
|
||||
self.assertEqual(entry.sp_inact, inact)
|
||||
self.assertEqual(entry.sp_expire, expire)
|
||||
self.assertEqual(entry.sp_flag, flag)
|
||||
|
||||
def test_noSuchName(self):
|
||||
"""
|
||||
I{getspnam} raises L{KeyError} when passed a username which does not
|
||||
exist in the user database.
|
||||
"""
|
||||
self.assertRaises(KeyError, self.database.getspnam, "alice")
|
||||
|
||||
def test_getspnamBytes(self):
|
||||
"""
|
||||
I{getspnam} raises L{TypeError} when passed a L{bytes}, just like
|
||||
L{spwd.getspnam}.
|
||||
"""
|
||||
self.assertRaises(TypeError, self.database.getspnam, b"i-am-bytes")
|
||||
|
||||
def test_recordLength(self):
|
||||
"""
|
||||
The shadow user record returned by I{getspnam} and I{getspall} has a
|
||||
length.
|
||||
"""
|
||||
db = self.database
|
||||
username = self.getExistingUserInfo()[0]
|
||||
for entry in [db.getspnam(username), db.getspall()[0]]:
|
||||
self.assertIsInstance(len(entry), int)
|
||||
self.assertEqual(len(entry), 9)
|
||||
|
||||
def test_recordIndexable(self):
|
||||
"""
|
||||
The shadow user record returned by I{getpwnam} and I{getspall} is
|
||||
indexable, with successive indexes starting from 0 corresponding to the
|
||||
values of the C{sp_nam}, C{sp_pwd}, C{sp_lstchg}, C{sp_min}, C{sp_max},
|
||||
C{sp_warn}, C{sp_inact}, C{sp_expire}, and C{sp_flag} attributes,
|
||||
respectively.
|
||||
"""
|
||||
db = self.database
|
||||
(
|
||||
username,
|
||||
password,
|
||||
lastChange,
|
||||
min,
|
||||
max,
|
||||
warn,
|
||||
inact,
|
||||
expire,
|
||||
flag,
|
||||
) = self.getExistingUserInfo()
|
||||
for entry in [db.getspnam(username), db.getspall()[0]]:
|
||||
self.assertEqual(entry[0], username)
|
||||
self.assertEqual(entry[1], password)
|
||||
self.assertEqual(entry[2], lastChange)
|
||||
self.assertEqual(entry[3], min)
|
||||
self.assertEqual(entry[4], max)
|
||||
self.assertEqual(entry[5], warn)
|
||||
self.assertEqual(entry[6], inact)
|
||||
self.assertEqual(entry[7], expire)
|
||||
self.assertEqual(entry[8], flag)
|
||||
|
||||
self.assertEqual(len(entry), len(list(entry)))
|
||||
self.assertRaises(IndexError, getitem, entry, 9)
|
||||
|
||||
|
||||
class ShadowDatabaseTests(TestCase, ShadowDatabaseTestsMixin):
|
||||
"""
|
||||
Tests for L{ShadowDatabase}.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
Create a L{ShadowDatabase} with no user data in it.
|
||||
"""
|
||||
self.database = ShadowDatabase()
|
||||
self._counter = 0
|
||||
|
||||
def getExistingUserInfo(self):
|
||||
"""
|
||||
Add a new user to C{self.database} and return its information.
|
||||
"""
|
||||
self._counter += 1
|
||||
suffix = "_" + str(self._counter)
|
||||
username = "username" + suffix
|
||||
password = "password" + suffix
|
||||
lastChange = self._counter + 1
|
||||
min = self._counter + 2
|
||||
max = self._counter + 3
|
||||
warn = self._counter + 4
|
||||
inact = self._counter + 5
|
||||
expire = self._counter + 6
|
||||
flag = self._counter + 7
|
||||
|
||||
self.database.addUser(
|
||||
username, password, lastChange, min, max, warn, inact, expire, flag
|
||||
)
|
||||
return (username, password, lastChange, min, max, warn, inact, expire, flag)
|
||||
|
||||
def test_addUser(self):
|
||||
"""
|
||||
L{UserDatabase.addUser} accepts seven arguments, one for each field of
|
||||
a L{pwd.struct_passwd}, and makes the new record available via
|
||||
L{UserDatabase.getpwuid}, L{UserDatabase.getpwnam}, and
|
||||
L{UserDatabase.getpwall}.
|
||||
"""
|
||||
username = "alice"
|
||||
password = "secr3t"
|
||||
lastChange = 17
|
||||
min = 42
|
||||
max = 105
|
||||
warn = 12
|
||||
inact = 3
|
||||
expire = 400
|
||||
flag = 3
|
||||
|
||||
db = self.database
|
||||
db.addUser(username, password, lastChange, min, max, warn, inact, expire, flag)
|
||||
|
||||
for [entry] in [[db.getspnam(username)], db.getspall()]:
|
||||
self.assertEqual(entry.sp_nam, username)
|
||||
self.assertEqual(entry.sp_pwd, password)
|
||||
self.assertEqual(entry.sp_lstchg, lastChange)
|
||||
self.assertEqual(entry.sp_min, min)
|
||||
self.assertEqual(entry.sp_max, max)
|
||||
self.assertEqual(entry.sp_warn, warn)
|
||||
self.assertEqual(entry.sp_inact, inact)
|
||||
self.assertEqual(entry.sp_expire, expire)
|
||||
self.assertEqual(entry.sp_flag, flag)
|
||||
|
||||
|
||||
class SPwdModuleTests(TestCase, ShadowDatabaseTestsMixin):
|
||||
"""
|
||||
L{SPwdModuleTests} runs the tests defined by L{ShadowDatabaseTestsMixin}
|
||||
against the built-in C{spwd} module. This serves to verify that
|
||||
L{ShadowDatabase} is really a fake of that API.
|
||||
"""
|
||||
|
||||
if spwd is None:
|
||||
skip = "Cannot verify ShadowDatabase against spwd without spwd"
|
||||
elif os.getuid() != 0:
|
||||
skip = "Cannot access shadow user database except as root"
|
||||
else:
|
||||
database = spwd
|
||||
|
||||
def setUp(self):
|
||||
self._users = iter(self.database.getspall())
|
||||
|
||||
def getExistingUserInfo(self):
|
||||
"""
|
||||
Read and return the next record from C{self._users}.
|
||||
"""
|
||||
return next(self._users)
|
||||
@@ -0,0 +1,45 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Tests for L{twisted.python.htmlizer}.
|
||||
"""
|
||||
|
||||
from io import BytesIO
|
||||
|
||||
from twisted.python.htmlizer import filter
|
||||
from twisted.trial.unittest import TestCase
|
||||
|
||||
|
||||
class FilterTests(TestCase):
|
||||
"""
|
||||
Tests for L{twisted.python.htmlizer.filter}.
|
||||
"""
|
||||
|
||||
def test_empty(self):
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
If passed an input file containing a variable access, L{filter} writes
|
||||
a I{pre} tag containing a I{py-src-variable} span containing the
|
||||
variable.
|
||||
"""
|
||||
input = BytesIO(b"foo\n")
|
||||
output = BytesIO()
|
||||
filter(input, output)
|
||||
self.assertEqual(
|
||||
output.getvalue(),
|
||||
b'<pre><span class="py-src-variable">foo</span>'
|
||||
b'<span class="py-src-newline">\n'
|
||||
b'</span><span class="py-src-endmarker"></span></pre>\n',
|
||||
)
|
||||
@@ -0,0 +1,138 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Tests for L{twisted.python._inotify}.
|
||||
"""
|
||||
|
||||
from twisted.python.filepath import FilePath
|
||||
from twisted.python.runtime import platform
|
||||
from twisted.trial.unittest import TestCase
|
||||
|
||||
try:
|
||||
from twisted.python import _inotify
|
||||
except ImportError:
|
||||
inotify = None
|
||||
else:
|
||||
inotify = _inotify
|
||||
|
||||
if inotify and platform.supportsINotify():
|
||||
from ctypes import c_char_p, c_int, c_uint32
|
||||
|
||||
from twisted.python._inotify import INotifyError, add, init, initializeModule
|
||||
else:
|
||||
inotify = None
|
||||
|
||||
|
||||
class INotifyTests(TestCase):
|
||||
"""
|
||||
Tests for L{twisted.python._inotify}.
|
||||
"""
|
||||
|
||||
if inotify is None:
|
||||
skip = "This platform doesn't support INotify."
|
||||
|
||||
def test_missingInit(self):
|
||||
"""
|
||||
If the I{libc} object passed to L{initializeModule} has no
|
||||
C{inotify_init} attribute, L{ImportError} is raised.
|
||||
"""
|
||||
|
||||
class libc:
|
||||
def inotify_add_watch(self):
|
||||
pass
|
||||
|
||||
def inotify_rm_watch(self):
|
||||
pass
|
||||
|
||||
self.assertRaises(ImportError, initializeModule, libc())
|
||||
|
||||
def test_missingAdd(self):
|
||||
"""
|
||||
If the I{libc} object passed to L{initializeModule} has no
|
||||
C{inotify_add_watch} attribute, L{ImportError} is raised.
|
||||
"""
|
||||
|
||||
class libc:
|
||||
def inotify_init(self):
|
||||
pass
|
||||
|
||||
def inotify_rm_watch(self):
|
||||
pass
|
||||
|
||||
self.assertRaises(ImportError, initializeModule, libc())
|
||||
|
||||
def test_missingRemove(self):
|
||||
"""
|
||||
If the I{libc} object passed to L{initializeModule} has no
|
||||
C{inotify_rm_watch} attribute, L{ImportError} is raised.
|
||||
"""
|
||||
|
||||
class libc:
|
||||
def inotify_init(self):
|
||||
pass
|
||||
|
||||
def inotify_add_watch(self):
|
||||
pass
|
||||
|
||||
self.assertRaises(ImportError, initializeModule, libc())
|
||||
|
||||
def test_setTypes(self):
|
||||
"""
|
||||
If the I{libc} object passed to L{initializeModule} has all of the
|
||||
necessary attributes, it sets the C{argtypes} and C{restype} attributes
|
||||
of the three ctypes methods used from libc.
|
||||
"""
|
||||
|
||||
class libc:
|
||||
def inotify_init(self):
|
||||
pass
|
||||
|
||||
inotify_init = staticmethod(inotify_init)
|
||||
|
||||
def inotify_rm_watch(self):
|
||||
pass
|
||||
|
||||
inotify_rm_watch = staticmethod(inotify_rm_watch)
|
||||
|
||||
def inotify_add_watch(self):
|
||||
pass
|
||||
|
||||
inotify_add_watch = staticmethod(inotify_add_watch)
|
||||
|
||||
c = libc()
|
||||
initializeModule(c)
|
||||
self.assertEqual(c.inotify_init.argtypes, [])
|
||||
self.assertEqual(c.inotify_init.restype, c_int)
|
||||
|
||||
self.assertEqual(c.inotify_rm_watch.argtypes, [c_int, c_int])
|
||||
self.assertEqual(c.inotify_rm_watch.restype, c_int)
|
||||
|
||||
self.assertEqual(c.inotify_add_watch.argtypes, [c_int, c_char_p, c_uint32])
|
||||
self.assertEqual(c.inotify_add_watch.restype, c_int)
|
||||
|
||||
def test_failedInit(self):
|
||||
"""
|
||||
If C{inotify_init} returns a negative number, L{init} raises
|
||||
L{INotifyError}.
|
||||
"""
|
||||
|
||||
class libc:
|
||||
def inotify_init(self):
|
||||
return -1
|
||||
|
||||
self.patch(inotify, "libc", libc())
|
||||
self.assertRaises(INotifyError, init)
|
||||
|
||||
def test_failedAddWatch(self):
|
||||
"""
|
||||
If C{inotify_add_watch} returns a negative number, L{add}
|
||||
raises L{INotifyError}.
|
||||
"""
|
||||
|
||||
class libc:
|
||||
def inotify_add_watch(self, fd, path, mask):
|
||||
return -1
|
||||
|
||||
self.patch(inotify, "libc", libc())
|
||||
self.assertRaises(INotifyError, add, 3, FilePath("/foo"), 0)
|
||||
@@ -0,0 +1,797 @@
|
||||
# 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 (
|
||||
CheckNewsfragmentScript,
|
||||
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))
|
||||
|
||||
|
||||
class CheckNewsfragmentScriptTests(ExternalTempdirTestCase):
|
||||
"""
|
||||
L{CheckNewsfragmentScript}.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
self.origin = FilePath(self.mktemp())
|
||||
_gitInit(self.origin)
|
||||
runCommand(["git", "checkout", "-b", "trunk"], cwd=self.origin.path)
|
||||
self.origin.child("test").setContent(b"test!")
|
||||
runCommand(["git", "add", self.origin.child("test").path], cwd=self.origin.path)
|
||||
runCommand(["git", "commit", "-m", "initial"], cwd=self.origin.path)
|
||||
|
||||
self.repo = FilePath(self.mktemp())
|
||||
|
||||
runCommand(["git", "clone", self.origin.path, self.repo.path])
|
||||
_gitConfig(self.repo)
|
||||
# Inject a rigged environment to have full control over the test execution.
|
||||
self.patch(os, "environ", GENERIC_CI_ENVIRON)
|
||||
|
||||
def test_noArgs(self):
|
||||
"""
|
||||
Too few arguments returns a failure.
|
||||
"""
|
||||
logs = []
|
||||
|
||||
with self.assertRaises(SystemExit) as e:
|
||||
CheckNewsfragmentScript(logs.append).main([])
|
||||
|
||||
self.assertEqual(
|
||||
e.exception.args, ("Must specify one argument: the Twisted checkout",)
|
||||
)
|
||||
|
||||
def test_diffFromTrunkNoNewsfragments(self):
|
||||
"""
|
||||
If there are changes from trunk, then there should also be a
|
||||
newsfragment.
|
||||
"""
|
||||
runCommand(["git", "checkout", "-b", "mypatch"], cwd=self.repo.path)
|
||||
somefile = self.repo.child("somefile")
|
||||
somefile.setContent(b"change")
|
||||
|
||||
runCommand(["git", "add", somefile.path, somefile.path], cwd=self.repo.path)
|
||||
runCommand(["git", "commit", "-m", "some file"], cwd=self.repo.path)
|
||||
|
||||
logs = []
|
||||
|
||||
with self.assertRaises(SystemExit) as e:
|
||||
CheckNewsfragmentScript(logs.append).main([self.repo.path])
|
||||
|
||||
self.assertEqual(e.exception.args, (1,))
|
||||
self.assertEqual(logs[-1], "No newsfragment found. Have you committed it?")
|
||||
|
||||
def test_noChangeFromTrunk(self):
|
||||
"""
|
||||
If there are no changes from trunk, then no need to check the
|
||||
newsfragments
|
||||
"""
|
||||
runCommand(["git", "checkout", "-b", "mypatch"], cwd=self.repo.path)
|
||||
|
||||
logs = []
|
||||
|
||||
with self.assertRaises(SystemExit) as e:
|
||||
CheckNewsfragmentScript(logs.append).main([self.repo.path])
|
||||
|
||||
self.assertEqual(e.exception.args, (0,))
|
||||
self.assertEqual(
|
||||
logs[-1], "On trunk or no diffs from trunk; no need to look at this."
|
||||
)
|
||||
|
||||
def test_trunk(self):
|
||||
"""
|
||||
Running it on trunk always gives green.
|
||||
"""
|
||||
logs = []
|
||||
|
||||
with self.assertRaises(SystemExit) as e:
|
||||
CheckNewsfragmentScript(logs.append).main([self.repo.path])
|
||||
|
||||
self.assertEqual(e.exception.args, (0,))
|
||||
self.assertEqual(
|
||||
logs[-1], "On trunk or no diffs from trunk; no need to look at this."
|
||||
)
|
||||
|
||||
def test_release(self):
|
||||
"""
|
||||
Running it on a release branch returns green if there is no
|
||||
newsfragments even if there are changes.
|
||||
"""
|
||||
runCommand(
|
||||
["git", "checkout", "-b", "release-16.11111-9001"], cwd=self.repo.path
|
||||
)
|
||||
|
||||
somefile = self.repo.child("somefile")
|
||||
somefile.setContent(b"change")
|
||||
|
||||
runCommand(["git", "add", somefile.path, somefile.path], cwd=self.repo.path)
|
||||
runCommand(["git", "commit", "-m", "some file"], cwd=self.repo.path)
|
||||
|
||||
logs = []
|
||||
|
||||
with self.assertRaises(SystemExit) as e:
|
||||
CheckNewsfragmentScript(logs.append).main([self.repo.path])
|
||||
|
||||
self.assertEqual(e.exception.args, (0,))
|
||||
self.assertEqual(logs[-1], "Release branch with no newsfragments, all good.")
|
||||
|
||||
def test_releaseWithNewsfragments(self):
|
||||
"""
|
||||
Running it on a release branch returns red if there are new
|
||||
newsfragments.
|
||||
"""
|
||||
runCommand(
|
||||
["git", "checkout", "-b", "release-16.11111-9001"], cwd=self.repo.path
|
||||
)
|
||||
|
||||
newsfragments = self.repo.child("twisted").child("newsfragments")
|
||||
newsfragments.makedirs()
|
||||
fragment = newsfragments.child("1234.misc")
|
||||
fragment.setContent(b"")
|
||||
|
||||
unrelated = self.repo.child("somefile")
|
||||
unrelated.setContent(b"Boo")
|
||||
|
||||
runCommand(["git", "add", fragment.path, unrelated.path], cwd=self.repo.path)
|
||||
runCommand(["git", "commit", "-m", "fragment"], cwd=self.repo.path)
|
||||
|
||||
logs = []
|
||||
|
||||
with self.assertRaises(SystemExit) as e:
|
||||
CheckNewsfragmentScript(logs.append).main([self.repo.path])
|
||||
|
||||
self.assertEqual(e.exception.args, (1,))
|
||||
self.assertEqual(logs[-1], "No newsfragments should be on the release branch.")
|
||||
|
||||
def test_preCommitAutoupdate(self):
|
||||
"""
|
||||
Running it on the autoupdate branch returns green if there is no
|
||||
newsfragments even if there are changes.
|
||||
"""
|
||||
runCommand(
|
||||
["git", "checkout", "-b", "pre-commit-ci-update-config"], cwd=self.repo.path
|
||||
)
|
||||
|
||||
somefile = self.repo.child("somefile")
|
||||
somefile.setContent(b"change")
|
||||
|
||||
runCommand(["git", "add", somefile.path, somefile.path], cwd=self.repo.path)
|
||||
runCommand(["git", "commit", "-m", "some file"], cwd=self.repo.path)
|
||||
|
||||
logs = []
|
||||
self.patch(os, "environ", PRECOMMIT_CI_ENVIRON)
|
||||
|
||||
with self.assertRaises(SystemExit) as e:
|
||||
CheckNewsfragmentScript(logs.append).main([self.repo.path])
|
||||
|
||||
self.assertEqual(e.exception.args, (0,))
|
||||
self.assertEqual(
|
||||
logs[-1], "Autoupdated branch with no newsfragments, all good."
|
||||
)
|
||||
|
||||
def test_preCommitAutoupdateWithNewsfragments(self):
|
||||
"""
|
||||
Running it on the autoupdate branch returns red if there are new
|
||||
newsfragments.
|
||||
"""
|
||||
runCommand(
|
||||
["git", "checkout", "-b", "pre-commit-ci-update-config"], cwd=self.repo.path
|
||||
)
|
||||
|
||||
newsfragments = self.repo.child("twisted").child("newsfragments")
|
||||
newsfragments.makedirs()
|
||||
fragment = newsfragments.child("1234.misc")
|
||||
fragment.setContent(b"")
|
||||
|
||||
unrelated = self.repo.child("somefile")
|
||||
unrelated.setContent(b"Boo")
|
||||
|
||||
runCommand(["git", "add", fragment.path, unrelated.path], cwd=self.repo.path)
|
||||
runCommand(["git", "commit", "-m", "fragment"], cwd=self.repo.path)
|
||||
|
||||
logs = []
|
||||
self.patch(os, "environ", PRECOMMIT_CI_ENVIRON)
|
||||
|
||||
with self.assertRaises(SystemExit) as e:
|
||||
CheckNewsfragmentScript(logs.append).main([self.repo.path])
|
||||
|
||||
self.assertEqual(e.exception.args, (1,))
|
||||
self.assertEqual(
|
||||
logs[-1], "No newsfragments should be present on an autoupdated branch."
|
||||
)
|
||||
|
||||
def test_onlyQuotes(self):
|
||||
"""
|
||||
Running it on a branch with only a quotefile change gives green.
|
||||
"""
|
||||
runCommand(["git", "checkout", "-b", "quotefile"], cwd=self.repo.path)
|
||||
|
||||
fun = self.repo.child("docs").child("fun")
|
||||
fun.makedirs()
|
||||
quotes = fun.child("Twisted.Quotes")
|
||||
quotes.setContent(b"Beep boop")
|
||||
|
||||
runCommand(["git", "add", quotes.path], cwd=self.repo.path)
|
||||
runCommand(["git", "commit", "-m", "quotes"], cwd=self.repo.path)
|
||||
|
||||
logs = []
|
||||
|
||||
with self.assertRaises(SystemExit) as e:
|
||||
CheckNewsfragmentScript(logs.append).main([self.repo.path])
|
||||
|
||||
self.assertEqual(e.exception.args, (0,))
|
||||
self.assertEqual(logs[-1], "Quotes change only; no newsfragment needed.")
|
||||
|
||||
def test_newsfragmentAdded(self):
|
||||
"""
|
||||
Running it on a branch with a fragment in the newsfragments dir added
|
||||
returns green.
|
||||
"""
|
||||
runCommand(["git", "checkout", "-b", "quotefile"], cwd=self.repo.path)
|
||||
|
||||
newsfragments = self.repo.child("twisted").child("newsfragments")
|
||||
newsfragments.makedirs()
|
||||
fragment = newsfragments.child("1234.misc")
|
||||
fragment.setContent(b"")
|
||||
|
||||
unrelated = self.repo.child("somefile")
|
||||
unrelated.setContent(b"Boo")
|
||||
|
||||
runCommand(["git", "add", fragment.path, unrelated.path], cwd=self.repo.path)
|
||||
runCommand(["git", "commit", "-m", "newsfragment"], cwd=self.repo.path)
|
||||
|
||||
logs = []
|
||||
|
||||
with self.assertRaises(SystemExit) as e:
|
||||
CheckNewsfragmentScript(logs.append).main([self.repo.path])
|
||||
|
||||
self.assertEqual(e.exception.args, (0,))
|
||||
self.assertEqual(logs[-1], "Found twisted/newsfragments/1234.misc")
|
||||
|
||||
def test_topfileButNotFragmentAdded(self):
|
||||
"""
|
||||
Running it on a branch with a non-fragment in the topfiles dir does not
|
||||
return green.
|
||||
"""
|
||||
runCommand(["git", "checkout", "-b", "quotefile"], cwd=self.repo.path)
|
||||
|
||||
topfiles = self.repo.child("twisted").child("topfiles")
|
||||
topfiles.makedirs()
|
||||
notFragment = topfiles.child("1234.txt")
|
||||
notFragment.setContent(b"")
|
||||
|
||||
unrelated = self.repo.child("somefile")
|
||||
unrelated.setContent(b"Boo")
|
||||
|
||||
runCommand(["git", "add", notFragment.path, unrelated.path], cwd=self.repo.path)
|
||||
runCommand(["git", "commit", "-m", "not topfile"], cwd=self.repo.path)
|
||||
|
||||
logs = []
|
||||
|
||||
with self.assertRaises(SystemExit) as e:
|
||||
CheckNewsfragmentScript(logs.append).main([self.repo.path])
|
||||
|
||||
self.assertEqual(e.exception.args, (1,))
|
||||
self.assertEqual(logs[-1], "No newsfragment found. Have you committed it?")
|
||||
|
||||
def test_newsfragmentAddedButWithOtherNewsfragments(self):
|
||||
"""
|
||||
Running it on a branch with a fragment in the topfiles dir added
|
||||
returns green, even if there are other files in the topfiles dir.
|
||||
"""
|
||||
runCommand(["git", "checkout", "-b", "quotefile"], cwd=self.repo.path)
|
||||
|
||||
newsfragments = self.repo.child("twisted").child("newsfragments")
|
||||
newsfragments.makedirs()
|
||||
fragment = newsfragments.child("1234.misc")
|
||||
fragment.setContent(b"")
|
||||
|
||||
unrelated = newsfragments.child("somefile")
|
||||
unrelated.setContent(b"Boo")
|
||||
|
||||
runCommand(["git", "add", fragment.path, unrelated.path], cwd=self.repo.path)
|
||||
runCommand(["git", "commit", "-m", "newsfragment"], cwd=self.repo.path)
|
||||
|
||||
logs = []
|
||||
|
||||
with self.assertRaises(SystemExit) as e:
|
||||
CheckNewsfragmentScript(logs.append).main([self.repo.path])
|
||||
|
||||
self.assertEqual(e.exception.args, (0,))
|
||||
self.assertEqual(logs[-1], "Found twisted/newsfragments/1234.misc")
|
||||
@@ -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):
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
If the platform is not Linux, C{isDocker()} always returns L{False}.
|
||||
"""
|
||||
platform = Platform(None, "win32")
|
||||
self.assertFalse(platform.isDocker())
|
||||
|
||||
def test_noCGroups(self):
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
If the platform is Linux, and the cgroups file (faked out here) exists,
|
||||
and none of the paths starts with C{/docker/}, C{isDocker()} returns
|
||||
C{False}.
|
||||
"""
|
||||
cgroupsFile = self.mktemp()
|
||||
with open(cgroupsFile, "wb") as f:
|
||||
# real cgroups file from a Fedora 17 system
|
||||
f.write(
|
||||
b"""9:perf_event:/
|
||||
8:blkio:/
|
||||
7:net_cls:/
|
||||
6:freezer:/
|
||||
5:devices:/
|
||||
4:memory:/
|
||||
3:cpuacct,cpu:/
|
||||
2:cpuset:/
|
||||
1:name=systemd:/system"""
|
||||
)
|
||||
|
||||
platform = Platform(None, "linux")
|
||||
self.assertFalse(platform.isDocker(_initCGroupLocation=cgroupsFile))
|
||||
@@ -0,0 +1,337 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Tests for L{twisted.python.sendmsg}.
|
||||
"""
|
||||
|
||||
import errno
|
||||
import os
|
||||
import sys
|
||||
import warnings
|
||||
from os import close, pathsep, pipe, read
|
||||
from socket import AF_INET, AF_INET6, SOL_SOCKET, error, socket
|
||||
from struct import pack
|
||||
|
||||
try:
|
||||
from socket import AF_UNIX, socketpair
|
||||
except ImportError:
|
||||
nonUNIXSkip = True
|
||||
else:
|
||||
nonUNIXSkip = False
|
||||
|
||||
from unittest import skipIf
|
||||
|
||||
from twisted.internet import reactor
|
||||
from twisted.internet.defer import Deferred, inlineCallbacks
|
||||
from twisted.internet.error import ProcessDone
|
||||
from twisted.internet.protocol import ProcessProtocol
|
||||
from twisted.python.filepath import FilePath
|
||||
from twisted.python.runtime import platform
|
||||
from twisted.trial.unittest import TestCase
|
||||
|
||||
if platform.isLinux():
|
||||
from socket import MSG_DONTWAIT
|
||||
|
||||
dontWaitSkip = False
|
||||
else:
|
||||
# It would be nice to be able to test flags on more platforms, but finding
|
||||
# a flag that works *at all* is somewhat challenging.
|
||||
dontWaitSkip = True
|
||||
|
||||
|
||||
try:
|
||||
from twisted.python.sendmsg import SCM_RIGHTS, getSocketFamily, recvmsg, sendmsg
|
||||
except ImportError:
|
||||
doImportSkip = True
|
||||
importSkipReason = "Platform doesn't support sendmsg."
|
||||
else:
|
||||
doImportSkip = False
|
||||
importSkipReason = ""
|
||||
|
||||
|
||||
class _FDHolder:
|
||||
"""
|
||||
A wrapper around a FD that will remember if it has been closed or not.
|
||||
"""
|
||||
|
||||
def __init__(self, fd):
|
||||
self._fd = fd
|
||||
|
||||
def fileno(self):
|
||||
"""
|
||||
Return the fileno of this FD.
|
||||
"""
|
||||
return self._fd
|
||||
|
||||
def close(self):
|
||||
"""
|
||||
Close the FD. If it's already been closed, do nothing.
|
||||
"""
|
||||
if self._fd:
|
||||
close(self._fd)
|
||||
self._fd = None
|
||||
|
||||
def __del__(self):
|
||||
"""
|
||||
If C{self._fd} is unclosed, raise a warning.
|
||||
"""
|
||||
if self._fd:
|
||||
warnings.warn(f"FD {self._fd} was not closed!", ResourceWarning)
|
||||
self.close()
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_value, traceback):
|
||||
self.close()
|
||||
|
||||
|
||||
def _makePipe():
|
||||
"""
|
||||
Create a pipe, and return the two FDs wrapped in L{_FDHolders}.
|
||||
"""
|
||||
r, w = pipe()
|
||||
return (_FDHolder(r), _FDHolder(w))
|
||||
|
||||
|
||||
class ExitedWithStderr(Exception):
|
||||
"""
|
||||
A process exited with some stderr.
|
||||
"""
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""
|
||||
Dump the errors in a pretty way in the event of a subprocess traceback.
|
||||
"""
|
||||
result = b"\n".join([b""] + list(self.args))
|
||||
return repr(result)
|
||||
|
||||
|
||||
class StartStopProcessProtocol(ProcessProtocol):
|
||||
"""
|
||||
An L{IProcessProtocol} with a Deferred for events where the subprocess
|
||||
starts and stops.
|
||||
|
||||
@ivar started: A L{Deferred} which fires with this protocol's
|
||||
L{IProcessTransport} provider when it is connected to one.
|
||||
|
||||
@ivar stopped: A L{Deferred} which fires with the process output or a
|
||||
failure if the process produces output on standard error.
|
||||
|
||||
@ivar output: A C{str} used to accumulate standard output.
|
||||
|
||||
@ivar errors: A C{str} used to accumulate standard error.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.started = Deferred()
|
||||
self.stopped = Deferred()
|
||||
self.output = b""
|
||||
self.errors = b""
|
||||
|
||||
def connectionMade(self):
|
||||
self.started.callback(self.transport)
|
||||
|
||||
def outReceived(self, data):
|
||||
self.output += data
|
||||
|
||||
def errReceived(self, data):
|
||||
self.errors += data
|
||||
|
||||
def processEnded(self, reason):
|
||||
if reason.check(ProcessDone):
|
||||
self.stopped.callback(self.output)
|
||||
else:
|
||||
self.stopped.errback(ExitedWithStderr(self.errors, self.output))
|
||||
|
||||
|
||||
def _spawn(script, outputFD):
|
||||
"""
|
||||
Start a script that is a peer of this test as a subprocess.
|
||||
|
||||
@param script: the module name of the script in this directory (no
|
||||
package prefix, no '.py')
|
||||
@type script: C{str}
|
||||
|
||||
@rtype: L{StartStopProcessProtocol}
|
||||
"""
|
||||
pyExe = FilePath(sys.executable).asTextMode().path
|
||||
env = dict(os.environ)
|
||||
env["PYTHONPATH"] = FilePath(pathsep.join(sys.path)).asTextMode().path
|
||||
sspp = StartStopProcessProtocol()
|
||||
reactor.spawnProcess(
|
||||
sspp,
|
||||
pyExe,
|
||||
[
|
||||
pyExe,
|
||||
FilePath(__file__).sibling(script + ".py").asTextMode().path,
|
||||
b"%d" % (outputFD,),
|
||||
],
|
||||
env=env,
|
||||
childFDs={0: "w", 1: "r", 2: "r", outputFD: outputFD},
|
||||
)
|
||||
return sspp
|
||||
|
||||
|
||||
@skipIf(doImportSkip, importSkipReason)
|
||||
class SendmsgTests(TestCase):
|
||||
"""
|
||||
Tests for the Python2/3 compatible L{sendmsg} interface.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
Create a pair of UNIX sockets.
|
||||
"""
|
||||
self.input, self.output = socketpair(AF_UNIX)
|
||||
|
||||
def tearDown(self):
|
||||
"""
|
||||
Close the sockets opened by setUp.
|
||||
"""
|
||||
self.input.close()
|
||||
self.output.close()
|
||||
|
||||
def test_syscallError(self):
|
||||
"""
|
||||
If the underlying C{sendmsg} call fails, L{send1msg} raises
|
||||
L{socket.error} with its errno set to the underlying errno value.
|
||||
"""
|
||||
self.input.close()
|
||||
exc = self.assertRaises(error, sendmsg, self.input, b"hello, world")
|
||||
self.assertEqual(exc.args[0], errno.EBADF)
|
||||
|
||||
def test_syscallErrorWithControlMessage(self):
|
||||
"""
|
||||
The behavior when the underlying C{sendmsg} call fails is the same
|
||||
whether L{sendmsg} is passed ancillary data or not.
|
||||
"""
|
||||
self.input.close()
|
||||
exc = self.assertRaises(
|
||||
error, sendmsg, self.input, b"hello, world", [(0, 0, b"0123")], 0
|
||||
)
|
||||
self.assertEqual(exc.args[0], errno.EBADF)
|
||||
|
||||
def test_roundtrip(self):
|
||||
"""
|
||||
L{recvmsg} will retrieve a message sent via L{sendmsg}.
|
||||
"""
|
||||
message = b"hello, world!"
|
||||
self.assertEqual(len(message), sendmsg(self.input, message))
|
||||
|
||||
result = recvmsg(self.output)
|
||||
self.assertEqual(result.data, b"hello, world!")
|
||||
self.assertEqual(result.flags, 0)
|
||||
self.assertEqual(result.ancillary, [])
|
||||
|
||||
def test_shortsend(self):
|
||||
"""
|
||||
L{sendmsg} returns the number of bytes which it was able to send.
|
||||
"""
|
||||
message = b"x" * 1024 * 1024 * 16
|
||||
self.input.setblocking(False)
|
||||
sent = sendmsg(self.input, message)
|
||||
# Sanity check - make sure the amount of data we sent was less than the
|
||||
# message, but not the whole message, as we should have filled the send
|
||||
# buffer. This won't work if the send buffer is large enough for
|
||||
# message, though.
|
||||
self.assertTrue(sent < len(message))
|
||||
received = recvmsg(self.output, len(message))
|
||||
self.assertEqual(len(received[0]), sent)
|
||||
|
||||
def test_roundtripEmptyAncillary(self):
|
||||
"""
|
||||
L{sendmsg} treats an empty ancillary data list the same way it treats
|
||||
receiving no argument for the ancillary parameter at all.
|
||||
"""
|
||||
sendmsg(self.input, b"hello, world!", [], 0)
|
||||
|
||||
result = recvmsg(self.output)
|
||||
self.assertEqual(result, (b"hello, world!", [], 0))
|
||||
|
||||
@skipIf(dontWaitSkip, "MSG_DONTWAIT is only known to work as intended on Linux")
|
||||
def test_flags(self):
|
||||
"""
|
||||
The C{flags} argument to L{sendmsg} is passed on to the underlying
|
||||
C{sendmsg} call, to affect it in whatever way is defined by those
|
||||
flags.
|
||||
"""
|
||||
# Just exercise one flag with simple, well-known behavior. MSG_DONTWAIT
|
||||
# makes the send a non-blocking call, even if the socket is in blocking
|
||||
# mode. See also test_flags in RecvmsgTests
|
||||
for i in range(8 * 1024):
|
||||
try:
|
||||
sendmsg(self.input, b"x" * 1024, flags=MSG_DONTWAIT)
|
||||
except OSError as e:
|
||||
self.assertEqual(e.args[0], errno.EAGAIN)
|
||||
break
|
||||
else:
|
||||
self.fail(
|
||||
"Failed to fill up the send buffer, "
|
||||
"or maybe send1msg blocked for a while"
|
||||
)
|
||||
|
||||
@inlineCallbacks
|
||||
def test_sendSubProcessFD(self):
|
||||
"""
|
||||
Calling L{sendmsg} with SOL_SOCKET, SCM_RIGHTS, and a platform-endian
|
||||
packed file descriptor number should send that file descriptor to a
|
||||
different process, where it can be retrieved by using L{recv1msg}.
|
||||
"""
|
||||
sspp = _spawn("pullpipe", self.output.fileno())
|
||||
yield sspp.started
|
||||
pipeOut, pipeIn = _makePipe()
|
||||
self.addCleanup(pipeOut.close)
|
||||
self.addCleanup(pipeIn.close)
|
||||
|
||||
with pipeIn:
|
||||
sendmsg(
|
||||
self.input,
|
||||
b"blonk",
|
||||
[(SOL_SOCKET, SCM_RIGHTS, pack("i", pipeIn.fileno()))],
|
||||
)
|
||||
|
||||
yield sspp.stopped
|
||||
self.assertEqual(read(pipeOut.fileno(), 1024), b"Test fixture data: blonk.\n")
|
||||
# Make sure that the pipe is actually closed now.
|
||||
self.assertEqual(read(pipeOut.fileno(), 1024), b"")
|
||||
|
||||
|
||||
@skipIf(doImportSkip, importSkipReason)
|
||||
class GetSocketFamilyTests(TestCase):
|
||||
"""
|
||||
Tests for L{getSocketFamily}.
|
||||
"""
|
||||
|
||||
def _socket(self, addressFamily):
|
||||
"""
|
||||
Create a new socket using the given address family and return that
|
||||
socket's file descriptor. The socket will automatically be closed when
|
||||
the test is torn down.
|
||||
"""
|
||||
s = socket(addressFamily)
|
||||
self.addCleanup(s.close)
|
||||
return s
|
||||
|
||||
def test_inet(self):
|
||||
"""
|
||||
When passed the file descriptor of a socket created with the C{AF_INET}
|
||||
address family, L{getSocketFamily} returns C{AF_INET}.
|
||||
"""
|
||||
self.assertEqual(AF_INET, getSocketFamily(self._socket(AF_INET)))
|
||||
|
||||
def test_inet6(self):
|
||||
"""
|
||||
When passed the file descriptor of a socket created with the
|
||||
C{AF_INET6} address family, L{getSocketFamily} returns C{AF_INET6}.
|
||||
"""
|
||||
self.assertEqual(AF_INET6, getSocketFamily(self._socket(AF_INET6)))
|
||||
|
||||
@skipIf(nonUNIXSkip, "Platform does not support AF_UNIX sockets")
|
||||
def test_unix(self):
|
||||
"""
|
||||
When passed the file descriptor of a socket created with the C{AF_UNIX}
|
||||
address family, L{getSocketFamily} returns C{AF_UNIX}.
|
||||
"""
|
||||
self.assertEqual(AF_UNIX, getSocketFamily(self._socket(AF_UNIX)))
|
||||
@@ -0,0 +1,635 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Test cases for twisted.python._shellcomp
|
||||
"""
|
||||
|
||||
|
||||
import sys
|
||||
from io import BytesIO
|
||||
from typing import List, Optional
|
||||
|
||||
from twisted.python import _shellcomp, reflect, usage
|
||||
from twisted.python.usage import CompleteFiles, CompleteList, Completer, Completions
|
||||
from twisted.trial import unittest
|
||||
|
||||
|
||||
class ZshScriptTestMeta(type):
|
||||
"""
|
||||
Metaclass of ZshScriptTestMixin.
|
||||
"""
|
||||
|
||||
def __new__(cls, name, bases, attrs):
|
||||
def makeTest(cmdName, optionsFQPN):
|
||||
def runTest(self):
|
||||
return test_genZshFunction(self, cmdName, optionsFQPN)
|
||||
|
||||
return runTest
|
||||
|
||||
# add test_ methods to the class for each script
|
||||
# we are testing.
|
||||
if "generateFor" in attrs:
|
||||
for cmdName, optionsFQPN in attrs["generateFor"]:
|
||||
test = makeTest(cmdName, optionsFQPN)
|
||||
attrs["test_genZshFunction_" + cmdName] = test
|
||||
|
||||
return type.__new__(cls, name, bases, attrs)
|
||||
|
||||
|
||||
class ZshScriptTestMixin(metaclass=ZshScriptTestMeta):
|
||||
"""
|
||||
Integration test helper to show that C{usage.Options} classes can have zsh
|
||||
completion functions generated for them without raising errors.
|
||||
|
||||
In your subclasses set a class variable like so::
|
||||
|
||||
# | cmd name | Fully Qualified Python Name of Options class |
|
||||
#
|
||||
generateFor = [('conch', 'twisted.conch.scripts.conch.ClientOptions'),
|
||||
('twistd', 'twisted.scripts.twistd.ServerOptions'),
|
||||
]
|
||||
|
||||
Each package that contains Twisted scripts should contain one TestCase
|
||||
subclass which also inherits from this mixin, and contains a C{generateFor}
|
||||
list appropriate for the scripts in that package.
|
||||
"""
|
||||
|
||||
|
||||
def test_genZshFunction(self, cmdName, optionsFQPN):
|
||||
"""
|
||||
Generate completion functions for given twisted command - no errors
|
||||
should be raised
|
||||
|
||||
@type cmdName: C{str}
|
||||
@param cmdName: The name of the command-line utility e.g. 'twistd'
|
||||
|
||||
@type optionsFQPN: C{str}
|
||||
@param optionsFQPN: The Fully Qualified Python Name of the C{Options}
|
||||
class to be tested.
|
||||
"""
|
||||
outputFile = BytesIO()
|
||||
self.patch(usage.Options, "_shellCompFile", outputFile)
|
||||
|
||||
# some scripts won't import or instantiate because of missing
|
||||
# dependencies (pyOpenSSL, etc) so we have to skip them.
|
||||
try:
|
||||
o = reflect.namedAny(optionsFQPN)()
|
||||
except Exception as e:
|
||||
raise unittest.SkipTest(
|
||||
"Couldn't import or instantiate " "Options class: %s" % (e,)
|
||||
)
|
||||
|
||||
try:
|
||||
o.parseOptions(["", "--_shell-completion", "zsh:2"])
|
||||
except ImportError as e:
|
||||
# this can happen for commands which don't have all
|
||||
# the necessary dependencies installed. skip test.
|
||||
# skip
|
||||
raise unittest.SkipTest("ImportError calling parseOptions(): %s", (e,))
|
||||
except SystemExit:
|
||||
pass # expected
|
||||
else:
|
||||
self.fail("SystemExit not raised")
|
||||
outputFile.seek(0)
|
||||
# test that we got some output
|
||||
self.assertEqual(1, len(outputFile.read(1)))
|
||||
outputFile.seek(0)
|
||||
outputFile.truncate()
|
||||
|
||||
# now, if it has sub commands, we have to test those too
|
||||
if hasattr(o, "subCommands"):
|
||||
for (cmd, short, parser, doc) in o.subCommands:
|
||||
try:
|
||||
o.parseOptions([cmd, "", "--_shell-completion", "zsh:3"])
|
||||
except ImportError as e:
|
||||
# this can happen for commands which don't have all
|
||||
# the necessary dependencies installed. skip test.
|
||||
raise unittest.SkipTest(
|
||||
"ImportError calling parseOptions() " "on subcommand: %s", (e,)
|
||||
)
|
||||
except SystemExit:
|
||||
pass # expected
|
||||
else:
|
||||
self.fail("SystemExit not raised")
|
||||
|
||||
outputFile.seek(0)
|
||||
# test that we got some output
|
||||
self.assertEqual(1, len(outputFile.read(1)))
|
||||
outputFile.seek(0)
|
||||
outputFile.truncate()
|
||||
|
||||
# flushed because we don't want DeprecationWarnings to be printed when
|
||||
# running these test cases.
|
||||
self.flushWarnings()
|
||||
|
||||
|
||||
class ZshTests(unittest.TestCase):
|
||||
"""
|
||||
Tests for zsh completion code
|
||||
"""
|
||||
|
||||
def test_accumulateMetadata(self):
|
||||
"""
|
||||
Are `compData' attributes you can place on Options classes
|
||||
picked up correctly?
|
||||
"""
|
||||
opts = FighterAceExtendedOptions()
|
||||
ag = _shellcomp.ZshArgumentsGenerator(opts, "ace", BytesIO())
|
||||
|
||||
descriptions = FighterAceOptions.compData.descriptions.copy()
|
||||
descriptions.update(FighterAceExtendedOptions.compData.descriptions)
|
||||
|
||||
self.assertEqual(ag.descriptions, descriptions)
|
||||
self.assertEqual(ag.multiUse, set(FighterAceOptions.compData.multiUse))
|
||||
self.assertEqual(
|
||||
ag.mutuallyExclusive, FighterAceOptions.compData.mutuallyExclusive
|
||||
)
|
||||
|
||||
optActions = FighterAceOptions.compData.optActions.copy()
|
||||
optActions.update(FighterAceExtendedOptions.compData.optActions)
|
||||
self.assertEqual(ag.optActions, optActions)
|
||||
|
||||
self.assertEqual(ag.extraActions, FighterAceOptions.compData.extraActions)
|
||||
|
||||
def test_mutuallyExclusiveCornerCase(self):
|
||||
"""
|
||||
Exercise a corner-case of ZshArgumentsGenerator.makeExcludesDict()
|
||||
where the long option name already exists in the `excludes` dict being
|
||||
built.
|
||||
"""
|
||||
|
||||
class OddFighterAceOptions(FighterAceExtendedOptions):
|
||||
# since "fokker", etc, are already defined as mutually-
|
||||
# exclusive on the super-class, defining them again here forces
|
||||
# the corner-case to be exercised.
|
||||
optFlags = [
|
||||
["anatra", None, "Select the Anatra DS as your dogfighter aircraft"]
|
||||
]
|
||||
compData = Completions(
|
||||
mutuallyExclusive=[["anatra", "fokker", "albatros", "spad", "bristol"]]
|
||||
)
|
||||
|
||||
opts = OddFighterAceOptions()
|
||||
ag = _shellcomp.ZshArgumentsGenerator(opts, "ace", BytesIO())
|
||||
|
||||
expected = {
|
||||
"albatros": {"anatra", "b", "bristol", "f", "fokker", "s", "spad"},
|
||||
"anatra": {"a", "albatros", "b", "bristol", "f", "fokker", "s", "spad"},
|
||||
"bristol": {"a", "albatros", "anatra", "f", "fokker", "s", "spad"},
|
||||
"fokker": {"a", "albatros", "anatra", "b", "bristol", "s", "spad"},
|
||||
"spad": {"a", "albatros", "anatra", "b", "bristol", "f", "fokker"},
|
||||
}
|
||||
|
||||
self.assertEqual(ag.excludes, expected)
|
||||
|
||||
def test_accumulateAdditionalOptions(self):
|
||||
"""
|
||||
We pick up options that are only defined by having an
|
||||
appropriately named method on your Options class,
|
||||
e.g. def opt_foo(self, foo)
|
||||
"""
|
||||
opts = FighterAceExtendedOptions()
|
||||
ag = _shellcomp.ZshArgumentsGenerator(opts, "ace", BytesIO())
|
||||
|
||||
self.assertIn("nocrash", ag.flagNameToDefinition)
|
||||
self.assertIn("nocrash", ag.allOptionsNameToDefinition)
|
||||
|
||||
self.assertIn("difficulty", ag.paramNameToDefinition)
|
||||
self.assertIn("difficulty", ag.allOptionsNameToDefinition)
|
||||
|
||||
def test_verifyZshNames(self):
|
||||
"""
|
||||
Using a parameter/flag name that doesn't exist
|
||||
will raise an error
|
||||
"""
|
||||
|
||||
class TmpOptions(FighterAceExtendedOptions):
|
||||
# Note typo of detail
|
||||
compData = Completions(optActions={"detaill": None})
|
||||
|
||||
self.assertRaises(
|
||||
ValueError, _shellcomp.ZshArgumentsGenerator, TmpOptions(), "ace", BytesIO()
|
||||
)
|
||||
|
||||
class TmpOptions2(FighterAceExtendedOptions):
|
||||
# Note that 'foo' and 'bar' are not real option
|
||||
# names defined in this class
|
||||
compData = Completions(mutuallyExclusive=[("foo", "bar")])
|
||||
|
||||
self.assertRaises(
|
||||
ValueError,
|
||||
_shellcomp.ZshArgumentsGenerator,
|
||||
TmpOptions2(),
|
||||
"ace",
|
||||
BytesIO(),
|
||||
)
|
||||
|
||||
def test_zshCode(self):
|
||||
"""
|
||||
Generate a completion function, and test the textual output
|
||||
against a known correct output
|
||||
"""
|
||||
outputFile = BytesIO()
|
||||
self.patch(usage.Options, "_shellCompFile", outputFile)
|
||||
self.patch(sys, "argv", ["silly", "", "--_shell-completion", "zsh:2"])
|
||||
opts = SimpleProgOptions()
|
||||
self.assertRaises(SystemExit, opts.parseOptions)
|
||||
self.assertEqual(testOutput1, outputFile.getvalue())
|
||||
|
||||
def test_zshCodeWithSubs(self):
|
||||
"""
|
||||
Generate a completion function with subcommands,
|
||||
and test the textual output against a known correct output
|
||||
"""
|
||||
outputFile = BytesIO()
|
||||
self.patch(usage.Options, "_shellCompFile", outputFile)
|
||||
self.patch(sys, "argv", ["silly2", "", "--_shell-completion", "zsh:2"])
|
||||
opts = SimpleProgWithSubcommands()
|
||||
self.assertRaises(SystemExit, opts.parseOptions)
|
||||
self.assertEqual(testOutput2, outputFile.getvalue())
|
||||
|
||||
def test_incompleteCommandLine(self):
|
||||
"""
|
||||
Completion still happens even if a command-line is given
|
||||
that would normally throw UsageError.
|
||||
"""
|
||||
outputFile = BytesIO()
|
||||
self.patch(usage.Options, "_shellCompFile", outputFile)
|
||||
opts = FighterAceOptions()
|
||||
|
||||
self.assertRaises(
|
||||
SystemExit,
|
||||
opts.parseOptions,
|
||||
[
|
||||
"--fokker",
|
||||
"server",
|
||||
"--unknown-option",
|
||||
"--unknown-option2",
|
||||
"--_shell-completion",
|
||||
"zsh:5",
|
||||
],
|
||||
)
|
||||
outputFile.seek(0)
|
||||
# test that we got some output
|
||||
self.assertEqual(1, len(outputFile.read(1)))
|
||||
|
||||
def test_incompleteCommandLine_case2(self):
|
||||
"""
|
||||
Completion still happens even if a command-line is given
|
||||
that would normally throw UsageError.
|
||||
|
||||
The existence of --unknown-option prior to the subcommand
|
||||
will break subcommand detection... but we complete anyway
|
||||
"""
|
||||
outputFile = BytesIO()
|
||||
self.patch(usage.Options, "_shellCompFile", outputFile)
|
||||
opts = FighterAceOptions()
|
||||
|
||||
self.assertRaises(
|
||||
SystemExit,
|
||||
opts.parseOptions,
|
||||
[
|
||||
"--fokker",
|
||||
"--unknown-option",
|
||||
"server",
|
||||
"--list-server",
|
||||
"--_shell-completion",
|
||||
"zsh:5",
|
||||
],
|
||||
)
|
||||
outputFile.seek(0)
|
||||
# test that we got some output
|
||||
self.assertEqual(1, len(outputFile.read(1)))
|
||||
|
||||
outputFile.seek(0)
|
||||
outputFile.truncate()
|
||||
|
||||
def test_incompleteCommandLine_case3(self):
|
||||
"""
|
||||
Completion still happens even if a command-line is given
|
||||
that would normally throw UsageError.
|
||||
|
||||
Break subcommand detection in a different way by providing
|
||||
an invalid subcommand name.
|
||||
"""
|
||||
outputFile = BytesIO()
|
||||
self.patch(usage.Options, "_shellCompFile", outputFile)
|
||||
opts = FighterAceOptions()
|
||||
|
||||
self.assertRaises(
|
||||
SystemExit,
|
||||
opts.parseOptions,
|
||||
[
|
||||
"--fokker",
|
||||
"unknown-subcommand",
|
||||
"--list-server",
|
||||
"--_shell-completion",
|
||||
"zsh:4",
|
||||
],
|
||||
)
|
||||
outputFile.seek(0)
|
||||
# test that we got some output
|
||||
self.assertEqual(1, len(outputFile.read(1)))
|
||||
|
||||
def test_skipSubcommandList(self):
|
||||
"""
|
||||
Ensure the optimization which skips building the subcommand list
|
||||
under certain conditions isn't broken.
|
||||
"""
|
||||
outputFile = BytesIO()
|
||||
self.patch(usage.Options, "_shellCompFile", outputFile)
|
||||
opts = FighterAceOptions()
|
||||
|
||||
self.assertRaises(
|
||||
SystemExit, opts.parseOptions, ["--alba", "--_shell-completion", "zsh:2"]
|
||||
)
|
||||
outputFile.seek(0)
|
||||
# test that we got some output
|
||||
self.assertEqual(1, len(outputFile.read(1)))
|
||||
|
||||
def test_poorlyDescribedOptMethod(self):
|
||||
"""
|
||||
Test corner case fetching an option description from a method docstring
|
||||
"""
|
||||
opts = FighterAceOptions()
|
||||
argGen = _shellcomp.ZshArgumentsGenerator(opts, "ace", None)
|
||||
|
||||
descr = argGen.getDescription("silly")
|
||||
|
||||
# docstring for opt_silly is useless so it should just use the
|
||||
# option name as the description
|
||||
self.assertEqual(descr, "silly")
|
||||
|
||||
def test_brokenActions(self):
|
||||
"""
|
||||
A C{Completer} with repeat=True may only be used as the
|
||||
last item in the extraActions list.
|
||||
"""
|
||||
|
||||
class BrokenActions(usage.Options):
|
||||
compData = usage.Completions(
|
||||
extraActions=[usage.Completer(repeat=True), usage.Completer()]
|
||||
)
|
||||
|
||||
outputFile = BytesIO()
|
||||
opts = BrokenActions()
|
||||
self.patch(opts, "_shellCompFile", outputFile)
|
||||
self.assertRaises(
|
||||
ValueError, opts.parseOptions, ["", "--_shell-completion", "zsh:2"]
|
||||
)
|
||||
|
||||
def test_optMethodsDontOverride(self):
|
||||
"""
|
||||
opt_* methods on Options classes should not override the
|
||||
data provided in optFlags or optParameters.
|
||||
"""
|
||||
|
||||
class Options(usage.Options):
|
||||
optFlags = [["flag", "f", "A flag"]]
|
||||
optParameters = [["param", "p", None, "A param"]]
|
||||
|
||||
def opt_flag(self):
|
||||
"""junk description"""
|
||||
|
||||
def opt_param(self, param):
|
||||
"""junk description"""
|
||||
|
||||
opts = Options()
|
||||
argGen = _shellcomp.ZshArgumentsGenerator(opts, "ace", None)
|
||||
|
||||
self.assertEqual(argGen.getDescription("flag"), "A flag")
|
||||
self.assertEqual(argGen.getDescription("param"), "A param")
|
||||
|
||||
|
||||
class EscapeTests(unittest.TestCase):
|
||||
def test_escape(self):
|
||||
"""
|
||||
Verify _shellcomp.escape() function
|
||||
"""
|
||||
esc = _shellcomp.escape
|
||||
|
||||
test = "$"
|
||||
self.assertEqual(esc(test), "'$'")
|
||||
|
||||
test = "A--'$\"\\`--B"
|
||||
self.assertEqual(esc(test), '"A--\'\\$\\"\\\\\\`--B"')
|
||||
|
||||
|
||||
class CompleterNotImplementedTests(unittest.TestCase):
|
||||
"""
|
||||
Test that using an unknown shell constant with SubcommandAction
|
||||
raises NotImplementedError
|
||||
|
||||
The other Completer() subclasses are tested in test_usage.py
|
||||
"""
|
||||
|
||||
def test_unknownShell(self):
|
||||
"""
|
||||
Using an unknown shellType should raise NotImplementedError
|
||||
"""
|
||||
action = _shellcomp.SubcommandAction()
|
||||
|
||||
self.assertRaises(
|
||||
NotImplementedError, action._shellCode, None, "bad_shell_type"
|
||||
)
|
||||
|
||||
|
||||
class FighterAceServerOptions(usage.Options):
|
||||
"""
|
||||
Options for FighterAce 'server' subcommand
|
||||
"""
|
||||
|
||||
optFlags = [
|
||||
["list-server", None, "List this server with the online FighterAce network"]
|
||||
]
|
||||
optParameters = [
|
||||
[
|
||||
"packets-per-second",
|
||||
None,
|
||||
"Number of update packets to send per second",
|
||||
"20",
|
||||
]
|
||||
]
|
||||
|
||||
|
||||
class FighterAceOptions(usage.Options):
|
||||
"""
|
||||
Command-line options for an imaginary `Fighter Ace` game
|
||||
"""
|
||||
|
||||
optFlags: List[List[Optional[str]]] = [
|
||||
["fokker", "f", "Select the Fokker Dr.I as your dogfighter aircraft"],
|
||||
["albatros", "a", "Select the Albatros D-III as your dogfighter aircraft"],
|
||||
["spad", "s", "Select the SPAD S.VII as your dogfighter aircraft"],
|
||||
["bristol", "b", "Select the Bristol Scout as your dogfighter aircraft"],
|
||||
["physics", "p", "Enable secret Twisted physics engine"],
|
||||
["jam", "j", "Enable a small chance that your machine guns will jam!"],
|
||||
["verbose", "v", "Verbose logging (may be specified more than once)"],
|
||||
]
|
||||
|
||||
optParameters: List[List[Optional[str]]] = [
|
||||
["pilot-name", None, "What's your name, Ace?", "Manfred von Richthofen"],
|
||||
["detail", "d", "Select the level of rendering detail (1-5)", "3"],
|
||||
]
|
||||
|
||||
subCommands = [
|
||||
["server", None, FighterAceServerOptions, "Start FighterAce game-server."],
|
||||
]
|
||||
|
||||
compData = Completions(
|
||||
descriptions={"physics": "Twisted-Physics", "detail": "Rendering detail level"},
|
||||
multiUse=["verbose"],
|
||||
mutuallyExclusive=[["fokker", "albatros", "spad", "bristol"]],
|
||||
optActions={"detail": CompleteList(["1" "2" "3" "4" "5"])},
|
||||
extraActions=[CompleteFiles(descr="saved game file to load")],
|
||||
)
|
||||
|
||||
def opt_silly(self):
|
||||
# A silly option which nobody can explain
|
||||
""" """
|
||||
|
||||
|
||||
class FighterAceExtendedOptions(FighterAceOptions):
|
||||
"""
|
||||
Extend the options and zsh metadata provided by FighterAceOptions.
|
||||
_shellcomp must accumulate options and metadata from all classes in the
|
||||
hiearchy so this is important to test.
|
||||
"""
|
||||
|
||||
optFlags = [["no-stalls", None, "Turn off the ability to stall your aircraft"]]
|
||||
optParameters = [
|
||||
["reality-level", None, "Select the level of physics reality (1-5)", "5"]
|
||||
]
|
||||
|
||||
compData = Completions(
|
||||
descriptions={"no-stalls": "Can't stall your plane"},
|
||||
optActions={"reality-level": Completer(descr="Physics reality level")},
|
||||
)
|
||||
|
||||
def opt_nocrash(self):
|
||||
"""
|
||||
Select that you can't crash your plane
|
||||
"""
|
||||
|
||||
def opt_difficulty(self, difficulty):
|
||||
"""
|
||||
How tough are you? (1-10)
|
||||
"""
|
||||
|
||||
|
||||
def _accuracyAction():
|
||||
# add tick marks just to exercise quoting
|
||||
return CompleteList(["1", "2", "3"], descr="Accuracy'`?")
|
||||
|
||||
|
||||
class SimpleProgOptions(usage.Options):
|
||||
"""
|
||||
Command-line options for a `Silly` imaginary program
|
||||
"""
|
||||
|
||||
optFlags = [
|
||||
["color", "c", "Turn on color output"],
|
||||
["gray", "g", "Turn on gray-scale output"],
|
||||
["verbose", "v", "Verbose logging (may be specified more than once)"],
|
||||
]
|
||||
|
||||
optParameters = [
|
||||
["optimization", None, "5", "Select the level of optimization (1-5)"],
|
||||
["accuracy", "a", "3", "Select the level of accuracy (1-3)"],
|
||||
]
|
||||
|
||||
compData = Completions(
|
||||
descriptions={"color": "Color on", "optimization": "Optimization level"},
|
||||
multiUse=["verbose"],
|
||||
mutuallyExclusive=[["color", "gray"]],
|
||||
optActions={
|
||||
"optimization": CompleteList(
|
||||
["1", "2", "3", "4", "5"], descr="Optimization?"
|
||||
),
|
||||
"accuracy": _accuracyAction,
|
||||
},
|
||||
extraActions=[CompleteFiles(descr="output file")],
|
||||
)
|
||||
|
||||
def opt_X(self):
|
||||
"""
|
||||
usage.Options does not recognize single-letter opt_ methods
|
||||
"""
|
||||
|
||||
|
||||
class SimpleProgSub1(usage.Options):
|
||||
optFlags = [["sub-opt", "s", "Sub Opt One"]]
|
||||
|
||||
|
||||
class SimpleProgSub2(usage.Options):
|
||||
optFlags = [["sub-opt", "s", "Sub Opt Two"]]
|
||||
|
||||
|
||||
class SimpleProgWithSubcommands(SimpleProgOptions):
|
||||
optFlags = [["some-option"], ["other-option", "o"]]
|
||||
|
||||
optParameters = [
|
||||
["some-param"],
|
||||
["other-param", "p"],
|
||||
["another-param", "P", "Yet Another Param"],
|
||||
]
|
||||
|
||||
subCommands = [
|
||||
["sub1", None, SimpleProgSub1, "Sub Command 1"],
|
||||
["sub2", None, SimpleProgSub2, "Sub Command 2"],
|
||||
]
|
||||
|
||||
|
||||
testOutput1 = b"""#compdef silly
|
||||
|
||||
_arguments -s -A "-*" \\
|
||||
':output file (*):_files -g "*"' \\
|
||||
"(--accuracy)-a[Select the level of accuracy (1-3)]:Accuracy'\\`?:(1 2 3)" \\
|
||||
"(-a)--accuracy=[Select the level of accuracy (1-3)]:Accuracy'\\`?:(1 2 3)" \\
|
||||
'(--color --gray -g)-c[Color on]' \\
|
||||
'(--gray -c -g)--color[Color on]' \\
|
||||
'(--color --gray -c)-g[Turn on gray-scale output]' \\
|
||||
'(--color -c -g)--gray[Turn on gray-scale output]' \\
|
||||
'--help[Display this help and exit.]' \\
|
||||
'--optimization=[Optimization level]:Optimization?:(1 2 3 4 5)' \\
|
||||
'*-v[Verbose logging (may be specified more than once)]' \\
|
||||
'*--verbose[Verbose logging (may be specified more than once)]' \\
|
||||
'--version[Display Twisted version and exit.]' \\
|
||||
&& return 0
|
||||
"""
|
||||
|
||||
# with sub-commands
|
||||
testOutput2 = b"""#compdef silly2
|
||||
|
||||
_arguments -s -A "-*" \\
|
||||
'*::subcmd:->subcmd' \\
|
||||
':output file (*):_files -g "*"' \\
|
||||
"(--accuracy)-a[Select the level of accuracy (1-3)]:Accuracy'\\`?:(1 2 3)" \\
|
||||
"(-a)--accuracy=[Select the level of accuracy (1-3)]:Accuracy'\\`?:(1 2 3)" \\
|
||||
'(--another-param)-P[another-param]:another-param:_files' \\
|
||||
'(-P)--another-param=[another-param]:another-param:_files' \\
|
||||
'(--color --gray -g)-c[Color on]' \\
|
||||
'(--gray -c -g)--color[Color on]' \\
|
||||
'(--color --gray -c)-g[Turn on gray-scale output]' \\
|
||||
'(--color -c -g)--gray[Turn on gray-scale output]' \\
|
||||
'--help[Display this help and exit.]' \\
|
||||
'--optimization=[Optimization level]:Optimization?:(1 2 3 4 5)' \\
|
||||
'(--other-option)-o[other-option]' \\
|
||||
'(-o)--other-option[other-option]' \\
|
||||
'(--other-param)-p[other-param]:other-param:_files' \\
|
||||
'(-p)--other-param=[other-param]:other-param:_files' \\
|
||||
'--some-option[some-option]' \\
|
||||
'--some-param=[some-param]:some-param:_files' \\
|
||||
'*-v[Verbose logging (may be specified more than once)]' \\
|
||||
'*--verbose[Verbose logging (may be specified more than once)]' \\
|
||||
'--version[Display Twisted version and exit.]' \\
|
||||
&& return 0
|
||||
local _zsh_subcmds_array
|
||||
_zsh_subcmds_array=(
|
||||
"sub1:Sub Command 1"
|
||||
"sub2:Sub Command 2"
|
||||
)
|
||||
|
||||
_describe "sub-command" _zsh_subcmds_array
|
||||
"""
|
||||
@@ -0,0 +1,158 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
from twisted.python.failure import Failure
|
||||
from twisted.trial.unittest import TestCase
|
||||
|
||||
try:
|
||||
import syslog as _stdsyslog
|
||||
except ImportError:
|
||||
stdsyslog = None
|
||||
else:
|
||||
stdsyslog = _stdsyslog
|
||||
from twisted.python import syslog
|
||||
|
||||
|
||||
class SyslogObserverTests(TestCase):
|
||||
"""
|
||||
Tests for L{SyslogObserver} which sends Twisted log events to the syslog.
|
||||
"""
|
||||
|
||||
events = None
|
||||
|
||||
if stdsyslog is None:
|
||||
skip = "syslog is not supported on this platform"
|
||||
|
||||
def setUp(self):
|
||||
self.patch(syslog.SyslogObserver, "openlog", self.openlog)
|
||||
self.patch(syslog.SyslogObserver, "syslog", self.syslog)
|
||||
self.observer = syslog.SyslogObserver("SyslogObserverTests")
|
||||
|
||||
def openlog(self, prefix, options, facility):
|
||||
self.logOpened = (prefix, options, facility)
|
||||
self.events = []
|
||||
|
||||
def syslog(self, options, message):
|
||||
self.events.append((options, message))
|
||||
|
||||
def test_emitWithoutMessage(self):
|
||||
"""
|
||||
L{SyslogObserver.emit} ignores events with an empty value for the
|
||||
C{'message'} key.
|
||||
"""
|
||||
self.observer.emit({"message": (), "isError": False, "system": "-"})
|
||||
self.assertEqual(self.events, [])
|
||||
|
||||
def test_emitCustomPriority(self):
|
||||
"""
|
||||
L{SyslogObserver.emit} uses the value of the C{'syslogPriority'} as the
|
||||
syslog priority, if that key is present in the event dictionary.
|
||||
"""
|
||||
self.observer.emit(
|
||||
{
|
||||
"message": ("hello, world",),
|
||||
"isError": False,
|
||||
"system": "-",
|
||||
"syslogPriority": stdsyslog.LOG_DEBUG,
|
||||
}
|
||||
)
|
||||
self.assertEqual(self.events, [(stdsyslog.LOG_DEBUG, "[-] hello, world")])
|
||||
|
||||
def test_emitErrorPriority(self):
|
||||
"""
|
||||
L{SyslogObserver.emit} uses C{LOG_ALERT} if the event represents an
|
||||
error.
|
||||
"""
|
||||
self.observer.emit(
|
||||
{
|
||||
"message": ("hello, world",),
|
||||
"isError": True,
|
||||
"system": "-",
|
||||
"failure": Failure(Exception("foo")),
|
||||
}
|
||||
)
|
||||
self.assertEqual(self.events, [(stdsyslog.LOG_ALERT, "[-] hello, world")])
|
||||
|
||||
def test_emitCustomPriorityOverridesError(self):
|
||||
"""
|
||||
L{SyslogObserver.emit} uses the value of the C{'syslogPriority'} key if
|
||||
it is specified even if the event dictionary represents an error.
|
||||
"""
|
||||
self.observer.emit(
|
||||
{
|
||||
"message": ("hello, world",),
|
||||
"isError": True,
|
||||
"system": "-",
|
||||
"syslogPriority": stdsyslog.LOG_NOTICE,
|
||||
"failure": Failure(Exception("bar")),
|
||||
}
|
||||
)
|
||||
self.assertEqual(self.events, [(stdsyslog.LOG_NOTICE, "[-] hello, world")])
|
||||
|
||||
def test_emitCustomFacility(self):
|
||||
"""
|
||||
L{SyslogObserver.emit} uses the value of the C{'syslogPriority'} as the
|
||||
syslog priority, if that key is present in the event dictionary.
|
||||
"""
|
||||
self.observer.emit(
|
||||
{
|
||||
"message": ("hello, world",),
|
||||
"isError": False,
|
||||
"system": "-",
|
||||
"syslogFacility": stdsyslog.LOG_CRON,
|
||||
}
|
||||
)
|
||||
self.assertEqual(
|
||||
self.events, [(stdsyslog.LOG_INFO | stdsyslog.LOG_CRON, "[-] hello, world")]
|
||||
)
|
||||
|
||||
def test_emitCustomSystem(self):
|
||||
"""
|
||||
L{SyslogObserver.emit} uses the value of the C{'system'} key to prefix
|
||||
the logged message.
|
||||
"""
|
||||
self.observer.emit(
|
||||
{
|
||||
"message": ("hello, world",),
|
||||
"isError": False,
|
||||
"system": "nonDefaultSystem",
|
||||
}
|
||||
)
|
||||
self.assertEqual(
|
||||
self.events, [(stdsyslog.LOG_INFO, "[nonDefaultSystem] hello, world")]
|
||||
)
|
||||
|
||||
def test_emitMessage(self):
|
||||
"""
|
||||
L{SyslogObserver.emit} logs the value of the C{'message'} key of the
|
||||
event dictionary it is passed to the syslog.
|
||||
"""
|
||||
self.observer.emit(
|
||||
{"message": ("hello, world",), "isError": False, "system": "-"}
|
||||
)
|
||||
self.assertEqual(self.events, [(stdsyslog.LOG_INFO, "[-] hello, world")])
|
||||
|
||||
def test_emitMultilineMessage(self):
|
||||
"""
|
||||
Each line of a multiline message is emitted separately to the syslog.
|
||||
"""
|
||||
self.observer.emit(
|
||||
{"message": ("hello,\nworld",), "isError": False, "system": "-"}
|
||||
)
|
||||
self.assertEqual(
|
||||
self.events,
|
||||
[(stdsyslog.LOG_INFO, "[-] hello,"), (stdsyslog.LOG_INFO, "[-] \tworld")],
|
||||
)
|
||||
|
||||
def test_emitStripsTrailingEmptyLines(self):
|
||||
"""
|
||||
Trailing empty lines of a multiline message are omitted from the
|
||||
messages sent to the syslog.
|
||||
"""
|
||||
self.observer.emit(
|
||||
{"message": ("hello,\nworld\n\n",), "isError": False, "system": "-"}
|
||||
)
|
||||
self.assertEqual(
|
||||
self.events,
|
||||
[(stdsyslog.LOG_INFO, "[-] hello,"), (stdsyslog.LOG_INFO, "[-] \tworld")],
|
||||
)
|
||||
@@ -0,0 +1,180 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Tests for L{twisted.python.systemd}.
|
||||
"""
|
||||
|
||||
|
||||
import os
|
||||
from typing import Dict, Mapping, Sequence
|
||||
|
||||
from hamcrest import assert_that, equal_to, not_
|
||||
from hypothesis import given
|
||||
from hypothesis.strategies import dictionaries, integers, lists
|
||||
|
||||
from twisted.python.systemd import ListenFDs
|
||||
from twisted.trial.unittest import SynchronousTestCase
|
||||
from .strategies import systemdDescriptorNames
|
||||
|
||||
|
||||
def buildEnvironment(count: int, pid: object) -> Dict[str, str]:
|
||||
"""
|
||||
@param count: The number of file descriptors to indicate as inherited.
|
||||
|
||||
@param pid: The pid of the inheriting process to indicate.
|
||||
|
||||
@return: A copy of the current process environment with the I{systemd}
|
||||
file descriptor inheritance-related environment variables added to it.
|
||||
"""
|
||||
result = os.environ.copy()
|
||||
result["LISTEN_FDS"] = str(count)
|
||||
result["LISTEN_FDNAMES"] = ":".join([f"{n}.socket" for n in range(count)])
|
||||
result["LISTEN_PID"] = str(pid)
|
||||
return result
|
||||
|
||||
|
||||
class ListenFDsTests(SynchronousTestCase):
|
||||
"""
|
||||
Apply tests to L{ListenFDs}, constructed based on an environment dictionary.
|
||||
"""
|
||||
|
||||
@given(lists(systemdDescriptorNames(), min_size=0, max_size=10))
|
||||
def test_fromEnvironmentEquivalence(self, names: Sequence[str]) -> None:
|
||||
"""
|
||||
The L{ListenFDs} and L{ListenFDs.fromEnvironment} constructors are
|
||||
equivalent for their respective representations of the same
|
||||
information.
|
||||
|
||||
@param names: The names of the file descriptors to represent as
|
||||
inherited in the test environment given to the parser. The number
|
||||
of descriptors represented will equal the length of this list.
|
||||
"""
|
||||
numFDs = len(names)
|
||||
descriptors = list(range(ListenFDs._START, ListenFDs._START + numFDs))
|
||||
fds = ListenFDs.fromEnvironment(
|
||||
{
|
||||
"LISTEN_PID": str(os.getpid()),
|
||||
"LISTEN_FDS": str(numFDs),
|
||||
"LISTEN_FDNAMES": ":".join(names),
|
||||
}
|
||||
)
|
||||
assert_that(fds, equal_to(ListenFDs(descriptors, tuple(names))))
|
||||
|
||||
def test_defaultEnviron(self) -> None:
|
||||
"""
|
||||
If the process environment is not explicitly passed to
|
||||
L{ListenFDs.fromEnvironment}, the real process environment dictionary
|
||||
is used.
|
||||
"""
|
||||
self.patch(os, "environ", buildEnvironment(5, os.getpid()))
|
||||
sddaemon = ListenFDs.fromEnvironment()
|
||||
self.assertEqual(list(range(3, 3 + 5)), sddaemon.inheritedDescriptors())
|
||||
|
||||
def test_secondEnvironment(self) -> None:
|
||||
"""
|
||||
L{ListenFDs.fromEnvironment} removes information about the
|
||||
inherited file descriptors from the environment mapping so that the
|
||||
same inherited file descriptors cannot be handled repeatedly from
|
||||
multiple L{ListenFDs} instances.
|
||||
"""
|
||||
env = buildEnvironment(3, os.getpid())
|
||||
first = ListenFDs.fromEnvironment(environ=env)
|
||||
second = ListenFDs.fromEnvironment(environ=env)
|
||||
self.assertEqual(list(range(3, 6)), first.inheritedDescriptors())
|
||||
self.assertEqual([], second.inheritedDescriptors())
|
||||
|
||||
def test_mismatchedPID(self) -> None:
|
||||
"""
|
||||
If the current process PID does not match the PID in the
|
||||
environment then the systemd variables in the environment were set for
|
||||
a different process (perhaps our parent) and the inherited descriptors
|
||||
are not intended for this process so L{ListenFDs.inheritedDescriptors}
|
||||
returns an empty list.
|
||||
"""
|
||||
env = buildEnvironment(3, os.getpid() + 1)
|
||||
sddaemon = ListenFDs.fromEnvironment(environ=env)
|
||||
self.assertEqual([], sddaemon.inheritedDescriptors())
|
||||
|
||||
def test_missingPIDVariable(self) -> None:
|
||||
"""
|
||||
If the I{LISTEN_PID} environment variable is not present then
|
||||
there is no clear indication that any file descriptors were inherited
|
||||
by this process so L{ListenFDs.inheritedDescriptors} returns an empty
|
||||
list.
|
||||
"""
|
||||
env = buildEnvironment(3, os.getpid())
|
||||
del env["LISTEN_PID"]
|
||||
sddaemon = ListenFDs.fromEnvironment(environ=env)
|
||||
self.assertEqual([], sddaemon.inheritedDescriptors())
|
||||
|
||||
def test_nonIntegerPIDVariable(self) -> None:
|
||||
"""
|
||||
If the I{LISTEN_PID} environment variable is set to a string that cannot
|
||||
be parsed as an integer, no inherited descriptors are reported.
|
||||
"""
|
||||
env = buildEnvironment(3, "hello, world")
|
||||
sddaemon = ListenFDs.fromEnvironment(environ=env)
|
||||
self.assertEqual([], sddaemon.inheritedDescriptors())
|
||||
|
||||
def test_missingFDSVariable(self) -> None:
|
||||
"""
|
||||
If the I{LISTEN_FDS} and I{LISTEN_FDNAMES} environment variables
|
||||
are not present, no inherited descriptors are reported.
|
||||
"""
|
||||
env = buildEnvironment(3, os.getpid())
|
||||
del env["LISTEN_FDS"]
|
||||
del env["LISTEN_FDNAMES"]
|
||||
sddaemon = ListenFDs.fromEnvironment(environ=env)
|
||||
self.assertEqual([], sddaemon.inheritedDescriptors())
|
||||
|
||||
def test_nonIntegerFDSVariable(self) -> None:
|
||||
"""
|
||||
If the I{LISTEN_FDS} environment variable is set to a string that cannot
|
||||
be parsed as an integer, no inherited descriptors are reported.
|
||||
"""
|
||||
env = buildEnvironment(3, os.getpid())
|
||||
env["LISTEN_FDS"] = "hello, world"
|
||||
sddaemon = ListenFDs.fromEnvironment(environ=env)
|
||||
self.assertEqual([], sddaemon.inheritedDescriptors())
|
||||
|
||||
@given(lists(integers(min_value=0, max_value=10), unique=True))
|
||||
def test_inheritedDescriptors(self, descriptors: Sequence[int]) -> None:
|
||||
"""
|
||||
L{ListenFDs.inheritedDescriptors} returns a copy of the inherited
|
||||
descriptors list.
|
||||
"""
|
||||
names = tuple(map(str, descriptors))
|
||||
fds = ListenFDs(descriptors, names)
|
||||
fdsCopy = fds.inheritedDescriptors()
|
||||
assert_that(descriptors, equal_to(fdsCopy))
|
||||
fdsCopy.append(1)
|
||||
assert_that(descriptors, not_(equal_to(fdsCopy)))
|
||||
|
||||
@given(dictionaries(systemdDescriptorNames(), integers(min_value=0), max_size=10))
|
||||
def test_inheritedNamedDescriptors(self, expected: Mapping[str, int]) -> None:
|
||||
"""
|
||||
L{ListenFDs.inheritedNamedDescriptors} returns a mapping from the
|
||||
descriptor names to their integer values, with items formed by
|
||||
pairwise combination of the input descriptors and names.
|
||||
"""
|
||||
items = list(expected.items())
|
||||
names = [name for name, _ in items]
|
||||
descriptors = [fd for _, fd in items]
|
||||
fds = ListenFDs(descriptors, names)
|
||||
assert_that(fds.inheritedNamedDescriptors(), equal_to(expected))
|
||||
|
||||
@given(lists(integers(min_value=0, max_value=10), unique=True))
|
||||
def test_repeated(self, descriptors: Sequence[int]) -> None:
|
||||
"""
|
||||
Any subsequent calls to C{inheritedDescriptors} and
|
||||
C{inheritedNamedDescriptors} return the same list.
|
||||
"""
|
||||
names = tuple(map(str, descriptors))
|
||||
sddaemon = ListenFDs(descriptors, names)
|
||||
self.assertEqual(
|
||||
sddaemon.inheritedDescriptors(), sddaemon.inheritedDescriptors()
|
||||
)
|
||||
self.assertEqual(
|
||||
sddaemon.inheritedNamedDescriptors(), sddaemon.inheritedNamedDescriptors()
|
||||
)
|
||||
@@ -0,0 +1,23 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Tests for L{twisted.python.textattributes}.
|
||||
"""
|
||||
|
||||
from twisted.python._textattributes import DefaultFormattingState
|
||||
from twisted.trial import unittest
|
||||
|
||||
|
||||
class DefaultFormattingStateTests(unittest.TestCase):
|
||||
"""
|
||||
Tests for L{twisted.python._textattributes.DefaultFormattingState}.
|
||||
"""
|
||||
|
||||
def test_equality(self):
|
||||
"""
|
||||
L{DefaultFormattingState}s are always equal to other
|
||||
L{DefaultFormattingState}s.
|
||||
"""
|
||||
self.assertEqual(DefaultFormattingState(), DefaultFormattingState())
|
||||
self.assertNotEqual(DefaultFormattingState(), "hello")
|
||||
@@ -0,0 +1,139 @@
|
||||
# # Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Tests for L{twisted.python._tzhelper}.
|
||||
"""
|
||||
|
||||
from os import environ
|
||||
|
||||
try:
|
||||
from time import tzset as _tzset
|
||||
except ImportError:
|
||||
tzset = None
|
||||
else:
|
||||
tzset = _tzset
|
||||
|
||||
from datetime import 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):
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
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():
|
||||
setTZ(tzIn)
|
||||
|
||||
|
||||
class FixedOffsetTimeZoneTests(TestCase):
|
||||
"""
|
||||
Tests for L{FixedOffsetTimeZone}.
|
||||
"""
|
||||
|
||||
def test_tzinfo(self):
|
||||
"""
|
||||
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, expectedOffsetDST, expectedOffsetSTD):
|
||||
setTZ(name)
|
||||
|
||||
localDST = mktime((2006, 6, 30, 0, 0, 0, 4, 181, 1))
|
||||
localSTD = mktime((2007, 1, 31, 0, 0, 0, 2, 31, 0))
|
||||
|
||||
tzDST = FixedOffsetTimeZone.fromLocalTimeStamp(localDST)
|
||||
tzSTD = FixedOffsetTimeZone.fromLocalTimeStamp(localSTD)
|
||||
|
||||
self.assertEqual(tzDST.tzname(localDST), f"UTC{expectedOffsetDST}")
|
||||
self.assertEqual(tzSTD.tzname(localSTD), f"UTC{expectedOffsetSTD}")
|
||||
|
||||
self.assertEqual(tzDST.dst(localDST), timedelta(0))
|
||||
self.assertEqual(tzSTD.dst(localSTD), timedelta(0))
|
||||
|
||||
def timeDeltaFromOffset(offset):
|
||||
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(localDST), timeDeltaFromOffset(expectedOffsetDST)
|
||||
)
|
||||
self.assertEqual(
|
||||
tzSTD.utcoffset(localSTD), timeDeltaFromOffset(expectedOffsetSTD)
|
||||
)
|
||||
|
||||
addTZCleanup(self)
|
||||
|
||||
# UTC
|
||||
testForTimeZone("UTC+00", "+0000", "+0000")
|
||||
# West of UTC
|
||||
testForTimeZone("EST+05EDT,M4.1.0,M10.5.0", "-0400", "-0500")
|
||||
# East of UTC
|
||||
testForTimeZone("CEST-01CEDT,M4.1.0,M10.5.0", "+0200", "+0100")
|
||||
# No DST
|
||||
testForTimeZone("CST+06", "-0600", "-0600")
|
||||
@@ -0,0 +1,803 @@
|
||||
# -*- test-case-name: twisted.python.test.test_url -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Tests for L{twisted.python.url}.
|
||||
"""
|
||||
|
||||
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 TestURL(SynchronousTestCase):
|
||||
"""
|
||||
Tests for L{URL}.
|
||||
"""
|
||||
|
||||
def assertUnicoded(self, u):
|
||||
"""
|
||||
The given L{URL}'s components should be L{unicode}.
|
||||
|
||||
@param u: The L{URL} to test.
|
||||
"""
|
||||
self.assertTrue(isinstance(u.scheme, str) or u.scheme is None, repr(u))
|
||||
self.assertTrue(isinstance(u.host, str) or u.host is None, 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, scheme, host, path, query, fragment, port, userinfo=""):
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
L{URL} should have appropriate default values.
|
||||
"""
|
||||
|
||||
def check(u):
|
||||
self.assertUnicoded(u)
|
||||
self.assertURL(u, "http", "", [], [], "", 80, "")
|
||||
|
||||
check(URL("http", ""))
|
||||
check(URL("http", "", [], []))
|
||||
check(URL("http", "", [], [], ""))
|
||||
|
||||
def test_init(self):
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
An empty L{URL} should serialize as the empty string.
|
||||
"""
|
||||
self.assertEqual(URL().asText(), "")
|
||||
|
||||
def test_justQueryText(self):
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
L{URL} compares equal to itself.
|
||||
"""
|
||||
u = URL.fromText("http://localhost/")
|
||||
self.assertEqual(u, u)
|
||||
|
||||
def test_similarEqual(self):
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
When a L{URL} is created with a C{query} argument, the C{query}
|
||||
argument is converted into an N-tuple of 2-tuples.
|
||||
"""
|
||||
url = URL(query=[["alpha", "beta"]])
|
||||
self.assertEqual(url.query, (("alpha", "beta"),))
|
||||
|
||||
def test_pathIterable(self):
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
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, expectation, name):
|
||||
self.assertEqual(
|
||||
str(raised.exception),
|
||||
"expected {} for {}, got {}".format(expectation, name, "<unexpected>"),
|
||||
)
|
||||
|
||||
def check(param, expectation=defaultExpectation):
|
||||
with self.assertRaises(TypeError) as raised:
|
||||
URL(**{param: Unexpected()})
|
||||
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(),
|
||||
]
|
||||
)
|
||||
assertRaised(raised, defaultExpectation, "path segment")
|
||||
with self.assertRaises(TypeError) as raised:
|
||||
URL(
|
||||
query=[
|
||||
("name", Unexpected()),
|
||||
]
|
||||
)
|
||||
assertRaised(
|
||||
raised, defaultExpectation + " or NoneType", "query parameter value"
|
||||
)
|
||||
with self.assertRaises(TypeError) as raised:
|
||||
URL(
|
||||
query=[
|
||||
(Unexpected(), "value"),
|
||||
]
|
||||
)
|
||||
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()])
|
||||
with self.assertRaises(ValueError):
|
||||
URL(query=[("k", "v", "vv")])
|
||||
with self.assertRaises(ValueError):
|
||||
URL(query=[("k",)])
|
||||
|
||||
url = URL.fromText("https://valid.example.com/")
|
||||
with self.assertRaises(TypeError) as raised:
|
||||
url.child(Unexpected())
|
||||
assertRaised(raised, defaultExpectation, "path segment")
|
||||
with self.assertRaises(TypeError) as raised:
|
||||
url.sibling(Unexpected())
|
||||
assertRaised(raised, defaultExpectation, "path segment")
|
||||
with self.assertRaises(TypeError) as raised:
|
||||
url.click(Unexpected())
|
||||
assertRaised(raised, defaultExpectation, "relative URL")
|
||||
|
||||
def test_technicallyTextIsIterableBut(self):
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
L{twisted.python.url} is deprecated since Twisted 17.5.0.
|
||||
"""
|
||||
from twisted.python import url
|
||||
|
||||
url
|
||||
|
||||
warningsShown = self.flushWarnings([self.test_urlDeprecation])
|
||||
self.assertEqual(1, len(warningsShown))
|
||||
self.assertEqual(
|
||||
(
|
||||
"twisted.python.url was deprecated in Twisted 17.5.0:"
|
||||
" Please use hyperlink from PyPI instead."
|
||||
),
|
||||
warningsShown[0]["message"],
|
||||
)
|
||||
@@ -0,0 +1,289 @@
|
||||
# -*- test-case-name: twisted.python.test.test_urlpath -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Tests for L{twisted.python.urlpath}.
|
||||
"""
|
||||
|
||||
from twisted.python import urlpath
|
||||
from twisted.trial import unittest
|
||||
|
||||
|
||||
class _BaseURLPathTests:
|
||||
"""
|
||||
Tests for instantiated L{urlpath.URLPath}s.
|
||||
"""
|
||||
|
||||
def test_partsAreBytes(self):
|
||||
"""
|
||||
All of the attributes of L{urlpath.URLPath} should be L{bytes}.
|
||||
"""
|
||||
self.assertIsInstance(self.path.scheme, bytes)
|
||||
self.assertIsInstance(self.path.netloc, bytes)
|
||||
self.assertIsInstance(self.path.path, bytes)
|
||||
self.assertIsInstance(self.path.query, bytes)
|
||||
self.assertIsInstance(self.path.fragment, bytes)
|
||||
|
||||
def test_strReturnsStr(self):
|
||||
"""
|
||||
Calling C{str()} with a L{URLPath} will always return a L{str}.
|
||||
"""
|
||||
self.assertEqual(type(self.path.__str__()), str)
|
||||
|
||||
def test_mutabilityWithText(self, stringType=str):
|
||||
"""
|
||||
Setting attributes on L{urlpath.URLPath} should change the value
|
||||
returned by L{str}.
|
||||
|
||||
@param stringType: a callable to parameterize this test for different
|
||||
text types.
|
||||
@type stringType: 1-argument callable taking L{str} and returning
|
||||
L{str} or L{bytes}.
|
||||
"""
|
||||
self.path.scheme = stringType("https")
|
||||
self.assertEqual(
|
||||
str(self.path), "https://example.com/foo/bar?yes=no&no=yes#footer"
|
||||
)
|
||||
self.path.netloc = stringType("another.example.invalid")
|
||||
self.assertEqual(
|
||||
str(self.path),
|
||||
"https://another.example.invalid/foo/bar?yes=no&no=yes#footer",
|
||||
)
|
||||
self.path.path = stringType("/hello")
|
||||
self.assertEqual(
|
||||
str(self.path), "https://another.example.invalid/hello?yes=no&no=yes#footer"
|
||||
)
|
||||
self.path.query = stringType("alpha=omega&opposites=same")
|
||||
self.assertEqual(
|
||||
str(self.path),
|
||||
"https://another.example.invalid/hello?alpha=omega&opposites=same"
|
||||
"#footer",
|
||||
)
|
||||
self.path.fragment = stringType("header")
|
||||
self.assertEqual(
|
||||
str(self.path),
|
||||
"https://another.example.invalid/hello?alpha=omega&opposites=same"
|
||||
"#header",
|
||||
)
|
||||
|
||||
def test_mutabilityWithBytes(self):
|
||||
"""
|
||||
Same as L{test_mutabilityWithText} but for bytes.
|
||||
"""
|
||||
self.test_mutabilityWithText(lambda x: x.encode("ascii"))
|
||||
|
||||
def test_allAttributesAreBytes(self):
|
||||
"""
|
||||
A created L{URLPath} has bytes attributes.
|
||||
"""
|
||||
self.assertIsInstance(self.path.scheme, bytes)
|
||||
self.assertIsInstance(self.path.netloc, bytes)
|
||||
self.assertIsInstance(self.path.path, bytes)
|
||||
self.assertIsInstance(self.path.query, bytes)
|
||||
self.assertIsInstance(self.path.fragment, bytes)
|
||||
|
||||
def test_stringConversion(self):
|
||||
"""
|
||||
Calling C{str()} with a L{URLPath} will return the same URL that it was
|
||||
constructed with.
|
||||
"""
|
||||
self.assertEqual(
|
||||
str(self.path), "http://example.com/foo/bar?yes=no&no=yes#footer"
|
||||
)
|
||||
|
||||
def test_childString(self):
|
||||
"""
|
||||
Calling C{str()} with a C{URLPath.child()} will return a URL which is
|
||||
the child of the URL it was instantiated with.
|
||||
"""
|
||||
self.assertEqual(
|
||||
str(self.path.child(b"hello")), "http://example.com/foo/bar/hello"
|
||||
)
|
||||
self.assertEqual(
|
||||
str(self.path.child(b"hello").child(b"")),
|
||||
"http://example.com/foo/bar/hello/",
|
||||
)
|
||||
self.assertEqual(
|
||||
str(self.path.child(b"hello", keepQuery=True)),
|
||||
"http://example.com/foo/bar/hello?yes=no&no=yes",
|
||||
)
|
||||
|
||||
def test_siblingString(self):
|
||||
"""
|
||||
Calling C{str()} with a C{URLPath.sibling()} will return a URL which is
|
||||
the sibling of the URL it was instantiated with.
|
||||
"""
|
||||
self.assertEqual(str(self.path.sibling(b"baz")), "http://example.com/foo/baz")
|
||||
self.assertEqual(
|
||||
str(self.path.sibling(b"baz", keepQuery=True)),
|
||||
"http://example.com/foo/baz?yes=no&no=yes",
|
||||
)
|
||||
|
||||
# The sibling of http://example.com/foo/bar/
|
||||
# is http://example.comf/foo/bar/baz
|
||||
# because really we are constructing a sibling of
|
||||
# http://example.com/foo/bar/index.html
|
||||
self.assertEqual(
|
||||
str(self.path.child(b"").sibling(b"baz")), "http://example.com/foo/bar/baz"
|
||||
)
|
||||
|
||||
def test_parentString(self):
|
||||
"""
|
||||
Calling C{str()} with a C{URLPath.parent()} will return a URL which is
|
||||
the parent of the URL it was instantiated with.
|
||||
"""
|
||||
# .parent() should be equivalent to '..'
|
||||
# 'foo' is the current directory, '/' is the parent directory
|
||||
self.assertEqual(str(self.path.parent()), "http://example.com/")
|
||||
self.assertEqual(
|
||||
str(self.path.parent(keepQuery=True)), "http://example.com/?yes=no&no=yes"
|
||||
)
|
||||
self.assertEqual(str(self.path.child(b"").parent()), "http://example.com/foo/")
|
||||
self.assertEqual(
|
||||
str(self.path.child(b"baz").parent()), "http://example.com/foo/"
|
||||
)
|
||||
self.assertEqual(
|
||||
str(self.path.parent().parent().parent().parent().parent()),
|
||||
"http://example.com/",
|
||||
)
|
||||
|
||||
def test_hereString(self):
|
||||
"""
|
||||
Calling C{str()} with a C{URLPath.here()} will return a URL which is
|
||||
the URL that it was instantiated with, without any file, query, or
|
||||
fragment.
|
||||
"""
|
||||
# .here() should be equivalent to '.'
|
||||
self.assertEqual(str(self.path.here()), "http://example.com/foo/")
|
||||
self.assertEqual(
|
||||
str(self.path.here(keepQuery=True)), "http://example.com/foo/?yes=no&no=yes"
|
||||
)
|
||||
self.assertEqual(
|
||||
str(self.path.child(b"").here()), "http://example.com/foo/bar/"
|
||||
)
|
||||
|
||||
def test_doubleSlash(self):
|
||||
"""
|
||||
Calling L{urlpath.URLPath.click} on a L{urlpath.URLPath} with a
|
||||
trailing slash with a relative URL containing a leading slash will
|
||||
result in a URL with a single slash at the start of the path portion.
|
||||
"""
|
||||
self.assertEqual(
|
||||
str(self.path.click(b"/hello/world")).encode("ascii"),
|
||||
b"http://example.com/hello/world",
|
||||
)
|
||||
|
||||
def test_pathList(self):
|
||||
"""
|
||||
L{urlpath.URLPath.pathList} returns a L{list} of L{bytes}.
|
||||
"""
|
||||
self.assertEqual(
|
||||
self.path.child(b"%00%01%02").pathList(),
|
||||
[b"", b"foo", b"bar", b"%00%01%02"],
|
||||
)
|
||||
|
||||
# Just testing that the 'copy' argument exists for compatibility; it
|
||||
# was originally provided for performance reasons, and its behavioral
|
||||
# contract is kind of nonsense (where is the state shared? who with?)
|
||||
# so it doesn't actually *do* anything any more.
|
||||
self.assertEqual(
|
||||
self.path.child(b"%00%01%02").pathList(copy=False),
|
||||
[b"", b"foo", b"bar", b"%00%01%02"],
|
||||
)
|
||||
self.assertEqual(
|
||||
self.path.child(b"%00%01%02").pathList(unquote=True),
|
||||
[b"", b"foo", b"bar", b"\x00\x01\x02"],
|
||||
)
|
||||
|
||||
|
||||
class BytesURLPathTests(_BaseURLPathTests, unittest.TestCase):
|
||||
"""
|
||||
Tests for interacting with a L{URLPath} created with C{fromBytes}.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
self.path = urlpath.URLPath.fromBytes(
|
||||
b"http://example.com/foo/bar?yes=no&no=yes#footer"
|
||||
)
|
||||
|
||||
def test_mustBeBytes(self):
|
||||
"""
|
||||
L{URLPath.fromBytes} must take a L{bytes} argument.
|
||||
"""
|
||||
with self.assertRaises(ValueError):
|
||||
urlpath.URLPath.fromBytes(None)
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
urlpath.URLPath.fromBytes("someurl")
|
||||
|
||||
def test_withoutArguments(self):
|
||||
"""
|
||||
An instantiation with no arguments creates a usable L{URLPath} with
|
||||
default arguments.
|
||||
"""
|
||||
url = urlpath.URLPath()
|
||||
self.assertEqual(str(url), "http://localhost/")
|
||||
|
||||
def test_partialArguments(self):
|
||||
"""
|
||||
Leaving some optional arguments unfilled makes a L{URLPath} with those
|
||||
optional arguments filled with defaults.
|
||||
"""
|
||||
# Not a "full" URL given to fromBytes, no /
|
||||
# / is filled in
|
||||
url = urlpath.URLPath.fromBytes(b"http://google.com")
|
||||
self.assertEqual(url.scheme, b"http")
|
||||
self.assertEqual(url.netloc, b"google.com")
|
||||
self.assertEqual(url.path, b"/")
|
||||
self.assertEqual(url.fragment, b"")
|
||||
self.assertEqual(url.query, b"")
|
||||
self.assertEqual(str(url), "http://google.com/")
|
||||
|
||||
def test_nonASCIIBytes(self):
|
||||
"""
|
||||
L{URLPath.fromBytes} can interpret non-ASCII bytes as percent-encoded
|
||||
"""
|
||||
url = urlpath.URLPath.fromBytes(b"http://example.com/\xff\x00")
|
||||
self.assertEqual(str(url), "http://example.com/%FF%00")
|
||||
|
||||
|
||||
class StringURLPathTests(_BaseURLPathTests, unittest.TestCase):
|
||||
"""
|
||||
Tests for interacting with a L{URLPath} created with C{fromString} and a
|
||||
L{str} argument.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
self.path = urlpath.URLPath.fromString(
|
||||
"http://example.com/foo/bar?yes=no&no=yes#footer"
|
||||
)
|
||||
|
||||
def test_mustBeStr(self):
|
||||
"""
|
||||
C{URLPath.fromString} must take a L{str} or L{str} argument.
|
||||
"""
|
||||
with self.assertRaises(ValueError):
|
||||
urlpath.URLPath.fromString(None)
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
urlpath.URLPath.fromString(b"someurl")
|
||||
|
||||
|
||||
class UnicodeURLPathTests(_BaseURLPathTests, unittest.TestCase):
|
||||
"""
|
||||
Tests for interacting with a L{URLPath} created with C{fromString} and a
|
||||
L{str} argument.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
self.path = urlpath.URLPath.fromString(
|
||||
"http://example.com/foo/bar?yes=no&no=yes#footer"
|
||||
)
|
||||
|
||||
def test_nonASCIICharacters(self):
|
||||
"""
|
||||
L{URLPath.fromString} can load non-ASCII characters.
|
||||
"""
|
||||
url = urlpath.URLPath.fromString("http://example.com/\xff\x00")
|
||||
self.assertEqual(str(url), "http://example.com/%C3%BF%00")
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,160 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Tests for L{twisted.python.versions}.
|
||||
"""
|
||||
|
||||
|
||||
import operator
|
||||
|
||||
from incremental import _inf
|
||||
|
||||
from twisted.python.versions import IncomparableVersions, Version, getVersionString
|
||||
from twisted.trial.unittest import SynchronousTestCase as TestCase
|
||||
|
||||
|
||||
class VersionsTests(TestCase):
|
||||
def test_versionComparison(self):
|
||||
"""
|
||||
Versions can be compared for equality and order.
|
||||
"""
|
||||
va = Version("dummy", 1, 0, 0)
|
||||
vb = Version("dummy", 0, 1, 0)
|
||||
self.assertTrue(va > vb)
|
||||
self.assertTrue(vb < va)
|
||||
self.assertTrue(va >= vb)
|
||||
self.assertTrue(vb <= va)
|
||||
self.assertTrue(va != vb)
|
||||
self.assertTrue(vb == Version("dummy", 0, 1, 0))
|
||||
self.assertTrue(vb == vb)
|
||||
|
||||
def test_versionComparisonCaseInsensitive(self):
|
||||
"""
|
||||
Version packages are compared case insensitively.
|
||||
"""
|
||||
va = Version("twisted", 1, 0, 0)
|
||||
vb = Version("Twisted", 0, 1, 0)
|
||||
self.assertTrue(va > vb)
|
||||
self.assertTrue(vb < va)
|
||||
self.assertTrue(va >= vb)
|
||||
self.assertTrue(vb <= va)
|
||||
self.assertTrue(va != vb)
|
||||
self.assertTrue(vb == Version("twisted", 0, 1, 0))
|
||||
self.assertTrue(vb == Version("TWISted", 0, 1, 0))
|
||||
self.assertTrue(vb == vb)
|
||||
|
||||
def test_comparingPrereleasesWithReleases(self):
|
||||
"""
|
||||
Prereleases are always less than versions without prereleases.
|
||||
"""
|
||||
va = Version("whatever", 1, 0, 0, prerelease=1)
|
||||
vb = Version("whatever", 1, 0, 0)
|
||||
self.assertTrue(va < vb)
|
||||
self.assertFalse(va > vb)
|
||||
self.assertNotEqual(vb, va)
|
||||
|
||||
def test_comparingPrereleases(self):
|
||||
"""
|
||||
The value specified as the prerelease is used in version comparisons.
|
||||
"""
|
||||
va = Version("whatever", 1, 0, 0, prerelease=1)
|
||||
vb = Version("whatever", 1, 0, 0, prerelease=2)
|
||||
self.assertTrue(va < vb)
|
||||
self.assertTrue(vb > va)
|
||||
self.assertTrue(va <= vb)
|
||||
self.assertTrue(vb >= va)
|
||||
self.assertTrue(va != vb)
|
||||
self.assertTrue(vb == Version("whatever", 1, 0, 0, prerelease=2))
|
||||
self.assertTrue(va == va)
|
||||
|
||||
def test_infComparison(self):
|
||||
"""
|
||||
L{_inf} is equal to L{_inf}.
|
||||
|
||||
This is a regression test.
|
||||
"""
|
||||
self.assertEqual(_inf, _inf)
|
||||
|
||||
def test_disallowBuggyComparisons(self):
|
||||
"""
|
||||
The package names of the Version objects need to be the same,
|
||||
"""
|
||||
self.assertRaises(
|
||||
IncomparableVersions,
|
||||
operator.eq,
|
||||
Version("dummy", 1, 0, 0),
|
||||
Version("dumym", 1, 0, 0),
|
||||
)
|
||||
|
||||
def test_notImplementedComparisons(self):
|
||||
"""
|
||||
Comparing a L{Version} to some other object type results in
|
||||
C{NotImplemented}.
|
||||
"""
|
||||
va = Version("dummy", 1, 0, 0)
|
||||
vb = ("dummy", 1, 0, 0) # a tuple is not a Version object
|
||||
self.assertEqual(va.__cmp__(vb), NotImplemented) # type: ignore[operator]
|
||||
|
||||
def test_repr(self):
|
||||
"""
|
||||
Calling C{repr} on a version returns a human-readable string
|
||||
representation of the version.
|
||||
"""
|
||||
self.assertEqual(repr(Version("dummy", 1, 2, 3)), "Version('dummy', 1, 2, 3)")
|
||||
|
||||
def test_reprWithPrerelease(self):
|
||||
"""
|
||||
Calling C{repr} on a version with a prerelease returns a human-readable
|
||||
string representation of the version including the prerelease.
|
||||
"""
|
||||
self.assertEqual(
|
||||
repr(Version("dummy", 1, 2, 3, prerelease=4)),
|
||||
"Version('dummy', 1, 2, 3, release_candidate=4)",
|
||||
)
|
||||
|
||||
def test_str(self):
|
||||
"""
|
||||
Calling C{str} on a version returns a human-readable string
|
||||
representation of the version.
|
||||
"""
|
||||
self.assertEqual(str(Version("dummy", 1, 2, 3)), "[dummy, version 1.2.3]")
|
||||
|
||||
def test_strWithPrerelease(self):
|
||||
"""
|
||||
Calling C{str} on a version with a prerelease includes the prerelease.
|
||||
"""
|
||||
self.assertEqual(
|
||||
str(Version("dummy", 1, 0, 0, prerelease=1)), "[dummy, version 1.0.0.rc1]"
|
||||
)
|
||||
|
||||
def testShort(self):
|
||||
self.assertEqual(Version("dummy", 1, 2, 3).short(), "1.2.3")
|
||||
|
||||
def test_getVersionString(self):
|
||||
"""
|
||||
L{getVersionString} returns a string with the package name and the
|
||||
short version number.
|
||||
"""
|
||||
self.assertEqual("Twisted 8.0.0", getVersionString(Version("Twisted", 8, 0, 0)))
|
||||
|
||||
def test_getVersionStringWithPrerelease(self):
|
||||
"""
|
||||
L{getVersionString} includes the prerelease, if any.
|
||||
"""
|
||||
self.assertEqual(
|
||||
getVersionString(Version("whatever", 8, 0, 0, prerelease=1)),
|
||||
"whatever 8.0.0.rc1",
|
||||
)
|
||||
|
||||
def test_base(self):
|
||||
"""
|
||||
The L{base} method returns a very simple representation of the version.
|
||||
"""
|
||||
self.assertEqual(Version("foo", 1, 0, 0).base(), "1.0.0")
|
||||
|
||||
def test_baseWithPrerelease(self):
|
||||
"""
|
||||
The base version includes 'preX' for versions with prereleases.
|
||||
"""
|
||||
self.assertEqual(Version("foo", 1, 0, 0, prerelease=8).base(), "1.0.0.rc8")
|
||||
@@ -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):
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
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):
|
||||
"""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):
|
||||
"""Importing C{WindowsError} triggers a L{DeprecationWarning}."""
|
||||
|
||||
self.assertWarns(
|
||||
DeprecationWarning,
|
||||
"twisted.python.win32.WindowsError was deprecated in Twisted 21.2.0: "
|
||||
"Catch OSError and check presence of 'winerror' attribute.",
|
||||
reflect.__file__,
|
||||
lambda: reflect.namedAny("twisted.python.win32.WindowsError"),
|
||||
)
|
||||
@@ -0,0 +1,99 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Test cases covering L{twisted.python.zippath}.
|
||||
"""
|
||||
|
||||
|
||||
import os
|
||||
import zipfile
|
||||
|
||||
from twisted.python.filepath import _coerceToFilesystemEncoding
|
||||
from twisted.python.zippath import ZipArchive
|
||||
from twisted.test.test_paths import AbstractFilePathTests
|
||||
|
||||
|
||||
def zipit(dirname, zfname):
|
||||
"""
|
||||
Create a zipfile on zfname, containing the contents of dirname'
|
||||
"""
|
||||
dirname = _coerceToFilesystemEncoding("", dirname)
|
||||
zfname = _coerceToFilesystemEncoding("", zfname)
|
||||
|
||||
with zipfile.ZipFile(zfname, "w") as zf:
|
||||
for (
|
||||
root,
|
||||
ignored,
|
||||
files,
|
||||
) in os.walk(dirname):
|
||||
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}.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
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_zipPathRepr(self):
|
||||
"""
|
||||
Make sure that invoking ZipPath's repr prints the correct class name
|
||||
and an absolute path to the zip file.
|
||||
"""
|
||||
child = 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):
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
Make sure that invoking ZipArchive's repr prints the correct class
|
||||
name and an absolute path to the zip file.
|
||||
"""
|
||||
path = ZipArchive(self.nativecmn + ".zip")
|
||||
pathRepr = "ZipArchive({!r})".format(os.path.abspath(self.nativecmn + ".zip"))
|
||||
|
||||
# Check for an absolute path
|
||||
self.assertEqual(repr(path), pathRepr)
|
||||
|
||||
# Create a path to the file rooted in the current working directory
|
||||
relativeCommon = self.nativecmn.replace(os.getcwd() + os.sep, "", 1) + ".zip"
|
||||
relpath = ZipArchive(relativeCommon)
|
||||
|
||||
# Check using a path without the cwd prepended
|
||||
self.assertEqual(repr(relpath), pathRepr)
|
||||
@@ -0,0 +1,331 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Tests for L{twisted.python.zipstream}
|
||||
"""
|
||||
|
||||
import random
|
||||
import struct
|
||||
import zipfile
|
||||
from hashlib import md5
|
||||
|
||||
from twisted.python import filepath, zipstream
|
||||
from twisted.trial import unittest
|
||||
|
||||
|
||||
class FileEntryMixin:
|
||||
"""
|
||||
File entry classes should behave as file-like objects
|
||||
"""
|
||||
|
||||
def getFileEntry(self, contents):
|
||||
"""
|
||||
Return an appropriate zip file entry
|
||||
"""
|
||||
filename = self.mktemp()
|
||||
with zipfile.ZipFile(filename, "w", self.compression) as z:
|
||||
z.writestr("content", contents)
|
||||
z = zipstream.ChunkingZipFile(filename, "r")
|
||||
return z.readfile("content")
|
||||
|
||||
def test_isatty(self):
|
||||
"""
|
||||
zip files should not be ttys, so isatty() should be false
|
||||
"""
|
||||
with self.getFileEntry("") as fileEntry:
|
||||
self.assertFalse(fileEntry.isatty())
|
||||
|
||||
def test_closed(self):
|
||||
"""
|
||||
The C{closed} attribute should reflect whether C{close()} has been
|
||||
called.
|
||||
"""
|
||||
with self.getFileEntry("") as fileEntry:
|
||||
self.assertFalse(fileEntry.closed)
|
||||
self.assertTrue(fileEntry.closed)
|
||||
|
||||
def test_readline(self):
|
||||
"""
|
||||
C{readline()} should mirror L{file.readline} and return up to a single
|
||||
delimiter.
|
||||
"""
|
||||
with self.getFileEntry(b"hoho\nho") as fileEntry:
|
||||
self.assertEqual(fileEntry.readline(), b"hoho\n")
|
||||
self.assertEqual(fileEntry.readline(), b"ho")
|
||||
self.assertEqual(fileEntry.readline(), b"")
|
||||
|
||||
def test_next(self):
|
||||
"""
|
||||
Zip file entries should implement the iterator protocol as files do.
|
||||
"""
|
||||
with self.getFileEntry(b"ho\nhoho") as fileEntry:
|
||||
self.assertEqual(fileEntry.next(), b"ho\n")
|
||||
self.assertEqual(fileEntry.next(), b"hoho")
|
||||
self.assertRaises(StopIteration, fileEntry.next)
|
||||
|
||||
def test_readlines(self):
|
||||
"""
|
||||
C{readlines()} should return a list of all the lines.
|
||||
"""
|
||||
with self.getFileEntry(b"ho\nho\nho") as fileEntry:
|
||||
self.assertEqual(fileEntry.readlines(), [b"ho\n", b"ho\n", b"ho"])
|
||||
|
||||
def test_iteration(self):
|
||||
"""
|
||||
C{__iter__()} and C{xreadlines()} should return C{self}.
|
||||
"""
|
||||
with self.getFileEntry("") as fileEntry:
|
||||
self.assertIs(iter(fileEntry), fileEntry)
|
||||
self.assertIs(fileEntry.xreadlines(), fileEntry)
|
||||
|
||||
def test_readWhole(self):
|
||||
"""
|
||||
C{.read()} should read the entire file.
|
||||
"""
|
||||
contents = b"Hello, world!"
|
||||
with self.getFileEntry(contents) as entry:
|
||||
self.assertEqual(entry.read(), contents)
|
||||
|
||||
def test_readPartial(self):
|
||||
"""
|
||||
C{.read(num)} should read num bytes from the file.
|
||||
"""
|
||||
contents = "0123456789"
|
||||
with self.getFileEntry(contents) as entry:
|
||||
one = entry.read(4)
|
||||
two = entry.read(200)
|
||||
self.assertEqual(one, b"0123")
|
||||
self.assertEqual(two, b"456789")
|
||||
|
||||
def test_tell(self):
|
||||
"""
|
||||
C{.tell()} should return the number of bytes that have been read so
|
||||
far.
|
||||
"""
|
||||
contents = "x" * 100
|
||||
with self.getFileEntry(contents) as entry:
|
||||
entry.read(2)
|
||||
self.assertEqual(entry.tell(), 2)
|
||||
entry.read(4)
|
||||
self.assertEqual(entry.tell(), 6)
|
||||
|
||||
|
||||
class DeflatedZipFileEntryTests(FileEntryMixin, unittest.TestCase):
|
||||
"""
|
||||
DeflatedZipFileEntry should be file-like
|
||||
"""
|
||||
|
||||
compression = zipfile.ZIP_DEFLATED
|
||||
|
||||
|
||||
class ZipFileEntryTests(FileEntryMixin, unittest.TestCase):
|
||||
"""
|
||||
ZipFileEntry should be file-like
|
||||
"""
|
||||
|
||||
compression = zipfile.ZIP_STORED
|
||||
|
||||
|
||||
class ZipstreamTests(unittest.TestCase):
|
||||
"""
|
||||
Tests for twisted.python.zipstream
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
Creates junk data that can be compressed and a test directory for any
|
||||
files that will be created
|
||||
"""
|
||||
self.testdir = filepath.FilePath(self.mktemp())
|
||||
self.testdir.makedirs()
|
||||
self.unzipdir = self.testdir.child("unzipped")
|
||||
self.unzipdir.makedirs()
|
||||
|
||||
def makeZipFile(self, contents, directory=""):
|
||||
"""
|
||||
Makes a zip file archive containing len(contents) files. Contents
|
||||
should be a list of strings, each string being the content of one file.
|
||||
"""
|
||||
zpfilename = self.testdir.child("zipfile.zip").path
|
||||
with zipfile.ZipFile(zpfilename, "w") as zpfile:
|
||||
for i, content in enumerate(contents):
|
||||
filename = str(i)
|
||||
if directory:
|
||||
filename = directory + "/" + filename
|
||||
zpfile.writestr(filename, content)
|
||||
return zpfilename
|
||||
|
||||
def test_invalidMode(self):
|
||||
"""
|
||||
A ChunkingZipFile opened in write-mode should not allow .readfile(),
|
||||
and raise a RuntimeError instead.
|
||||
"""
|
||||
with zipstream.ChunkingZipFile(self.mktemp(), "w") as czf:
|
||||
self.assertRaises(RuntimeError, czf.readfile, "something")
|
||||
|
||||
def test_closedArchive(self):
|
||||
"""
|
||||
A closed ChunkingZipFile should raise a L{RuntimeError} when
|
||||
.readfile() is invoked.
|
||||
"""
|
||||
czf = zipstream.ChunkingZipFile(self.makeZipFile(["something"]), "r")
|
||||
czf.close()
|
||||
self.assertRaises(RuntimeError, czf.readfile, "something")
|
||||
|
||||
def test_invalidHeader(self):
|
||||
"""
|
||||
A zipfile entry with the wrong magic number should raise BadZipfile for
|
||||
readfile(), but that should not affect other files in the archive.
|
||||
"""
|
||||
fn = self.makeZipFile(["test contents", "more contents"])
|
||||
with zipfile.ZipFile(fn, "r") as zf:
|
||||
zeroOffset = zf.getinfo("0").header_offset
|
||||
# Zero out just the one header.
|
||||
with open(fn, "r+b") as scribble:
|
||||
scribble.seek(zeroOffset, 0)
|
||||
scribble.write(b"0" * 4)
|
||||
with zipstream.ChunkingZipFile(fn) as czf:
|
||||
self.assertRaises(zipfile.BadZipfile, czf.readfile, "0")
|
||||
with czf.readfile("1") as zfe:
|
||||
self.assertEqual(zfe.read(), b"more contents")
|
||||
|
||||
def test_filenameMismatch(self):
|
||||
"""
|
||||
A zipfile entry with a different filename than is found in the central
|
||||
directory should raise BadZipfile.
|
||||
"""
|
||||
fn = self.makeZipFile([b"test contents", b"more contents"])
|
||||
with zipfile.ZipFile(fn, "r") as zf:
|
||||
info = zf.getinfo("0")
|
||||
info.filename = "not zero"
|
||||
with open(fn, "r+b") as scribble:
|
||||
scribble.seek(info.header_offset, 0)
|
||||
scribble.write(info.FileHeader())
|
||||
|
||||
with zipstream.ChunkingZipFile(fn) as czf:
|
||||
self.assertRaises(zipfile.BadZipfile, czf.readfile, "0")
|
||||
with czf.readfile("1") as zfe:
|
||||
self.assertEqual(zfe.read(), b"more contents")
|
||||
|
||||
def test_unsupportedCompression(self):
|
||||
"""
|
||||
A zipfile which describes an unsupported compression mechanism should
|
||||
raise BadZipfile.
|
||||
"""
|
||||
fn = self.mktemp()
|
||||
with zipfile.ZipFile(fn, "w") as zf:
|
||||
zi = zipfile.ZipInfo("0")
|
||||
zf.writestr(zi, "some data")
|
||||
# Mangle its compression type in the central directory; can't do
|
||||
# this before the writestr call or zipfile will (correctly) tell us
|
||||
# not to pass bad compression types :)
|
||||
zi.compress_type = 1234
|
||||
|
||||
with zipstream.ChunkingZipFile(fn) as czf:
|
||||
self.assertRaises(zipfile.BadZipfile, czf.readfile, "0")
|
||||
|
||||
def test_extraData(self):
|
||||
"""
|
||||
readfile() should skip over 'extra' data present in the zip metadata.
|
||||
"""
|
||||
fn = self.mktemp()
|
||||
with zipfile.ZipFile(fn, "w") as zf:
|
||||
zi = zipfile.ZipInfo("0")
|
||||
extra_data = b"hello, extra"
|
||||
zi.extra = struct.pack("<hh", 42, len(extra_data)) + extra_data
|
||||
zf.writestr(zi, b"the real data")
|
||||
with zipstream.ChunkingZipFile(fn) as czf, czf.readfile("0") as zfe:
|
||||
self.assertEqual(zfe.read(), b"the real data")
|
||||
|
||||
def test_unzipIterChunky(self):
|
||||
"""
|
||||
L{twisted.python.zipstream.unzipIterChunky} returns an iterator which
|
||||
must be exhausted to completely unzip the input archive.
|
||||
"""
|
||||
numfiles = 10
|
||||
contents = ["This is test file %d!" % i for i in range(numfiles)]
|
||||
contents = [i.encode("ascii") for i in contents]
|
||||
zpfilename = self.makeZipFile(contents)
|
||||
list(zipstream.unzipIterChunky(zpfilename, self.unzipdir.path))
|
||||
self.assertEqual(set(self.unzipdir.listdir()), set(map(str, range(numfiles))))
|
||||
|
||||
for child in self.unzipdir.children():
|
||||
num = int(child.basename())
|
||||
self.assertEqual(child.getContent(), contents[num])
|
||||
|
||||
def test_unzipIterChunkyDirectory(self):
|
||||
"""
|
||||
The path to which a file is extracted by L{zipstream.unzipIterChunky}
|
||||
is determined by joining the C{directory} argument to C{unzip} with the
|
||||
path within the archive of the file being extracted.
|
||||
"""
|
||||
numfiles = 10
|
||||
contents = ["This is test file %d!" % i for i in range(numfiles)]
|
||||
contents = [i.encode("ascii") for i in contents]
|
||||
zpfilename = self.makeZipFile(contents, "foo")
|
||||
list(zipstream.unzipIterChunky(zpfilename, self.unzipdir.path))
|
||||
fileContents = {str(num).encode("ascii") for num in range(numfiles)}
|
||||
self.assertEqual(set(self.unzipdir.child(b"foo").listdir()), fileContents)
|
||||
|
||||
for child in self.unzipdir.child(b"foo").children():
|
||||
num = int(child.basename())
|
||||
self.assertEqual(child.getContent(), contents[num])
|
||||
|
||||
# XXX these tests are kind of gross and old, but I think unzipIterChunky is
|
||||
# kind of a gross function anyway. We should really write an abstract
|
||||
# copyTo/moveTo that operates on FilePath and make sure ZipPath can support
|
||||
# it, then just deprecate / remove this stuff.
|
||||
def _unzipIterChunkyTest(self, compression, chunksize, lower, upper):
|
||||
"""
|
||||
unzipIterChunky should unzip the given number of bytes per iteration.
|
||||
"""
|
||||
junk = b""
|
||||
for n in range(1000):
|
||||
num = round(random.random(), 12)
|
||||
numEncoded = str(num).encode("ascii")
|
||||
junk += b" " + numEncoded
|
||||
|
||||
junkmd5 = md5(junk).hexdigest()
|
||||
|
||||
tempdir = filepath.FilePath(self.mktemp())
|
||||
tempdir.makedirs()
|
||||
zfpath = tempdir.child("bigfile.zip").path
|
||||
self._makebigfile(zfpath, compression, junk)
|
||||
uziter = zipstream.unzipIterChunky(zfpath, tempdir.path, chunksize=chunksize)
|
||||
r = next(uziter)
|
||||
# test that the number of chunks is in the right ballpark;
|
||||
# this could theoretically be any number but statistically it
|
||||
# should always be in this range
|
||||
approx = lower < r < upper
|
||||
self.assertTrue(approx)
|
||||
for r in uziter:
|
||||
pass
|
||||
self.assertEqual(r, 0)
|
||||
with tempdir.child("zipstreamjunk").open() as f:
|
||||
newmd5 = md5(f.read()).hexdigest()
|
||||
self.assertEqual(newmd5, junkmd5)
|
||||
|
||||
def test_unzipIterChunkyStored(self):
|
||||
"""
|
||||
unzipIterChunky should unzip the given number of bytes per iteration on
|
||||
a stored archive.
|
||||
"""
|
||||
self._unzipIterChunkyTest(zipfile.ZIP_STORED, 500, 35, 45)
|
||||
|
||||
def test_chunkyDeflated(self):
|
||||
"""
|
||||
unzipIterChunky should unzip the given number of bytes per iteration on
|
||||
a deflated archive.
|
||||
"""
|
||||
self._unzipIterChunkyTest(zipfile.ZIP_DEFLATED, 972, 23, 27)
|
||||
|
||||
def _makebigfile(self, filename, compression, junk):
|
||||
"""
|
||||
Create a zip file with the given file name and compression scheme.
|
||||
"""
|
||||
with zipfile.ZipFile(filename, "w", compression) as zf:
|
||||
for i in range(10):
|
||||
fn = "zipstream%d" % i
|
||||
zf.writestr(fn, "")
|
||||
zf.writestr("zipstreamjunk", junk)
|
||||
Reference in New Issue
Block a user