UPDATE:
When I comment out all the other tests, the test_add_two_same_interfaces(self,default_router,default_interface
works as expected.
When I run all the tests together, the last one fails but the rest pass. Is this a problem that pytest fixtures are not created to be independent of each other? Does this mean that I should yield a router and interface and then delete the object at the end?
@pytest.fixture(scope='module')
def default_router():
name = 'default'
return Router(name)
@pytest.fixture(scope='module')
def default_interface():
port = 'fa0/1'
cidr = '192.168.0.0/24'
ip_address = '192.168.1.1'
fa01 = Interface(port=port, cidr=cidr, ip_address=ip_address)
return fa01
class TestInit(object):
def test_valid_args(self,default_router):
router = default_router
name = router.hostname
hostname_message = f'Expected "default" as hostname, Actual: <{name}>'
assert router.hostname == 'default',hostname_message
expected_commands = ["en", "conf t", "no ip domain-lookup"]
actual_commands = router.commands
commands_message = f'Expected: {expected_commands}\n Actual: <{actual_commands}>'
assert actual_commands == expected_commands,commands_message
router_message = "Expected to get the default router"
assert Router._all_routers[0] == router,router_message
def test_invalid_args(self,default_router):
"""Test that if you try and make a second Router with the same name, it will fail."""
router = default_router
with pytest.raises(ValueError):
router1 = Router("default")
class TestAddInterface(object):
def test_valid_args(self,default_router,default_interface):
router = default_router
router.add_interface(default_interface)
expected_interface = 'fa 0/1,192.168.0.0/24,192.168.1.1'
actual_interface = str(router.interfaces[0])
interface_add_message = f'Expected: {expected_interface}\n Actual:'
assert actual_interface == expected_interface,interface_add_message
def test_add_two_same_interfaces(self,default_interface,default_router):
"""This test adds the same interface to a Router twice to test that it raises
a ValueError."""
print("current directory", os.getcwd())
router = default_router
interface = default_interface
router.add_interface(interface)
repeat_interface = 'fa0/1,192.168.0.0/24,192.168.1.1'.split(",")
print(repeat_interface)
with pytest.raises(ValueError):
new_copy = Interface(repeat_interface[0], repeat_interface[1], repeat_interface[2])
print([id(x) for x in router.interfaces])
print(id(new_copy))
router.add_interface(new_copy)
OLD:
I am trying to see whether or not a Router class will throw a ValueError if I try to add two of the same interfaces to it. I am using fixtures to create a Router and an Interface throughout the testing module called test_router.py. I am new to using the pytest API and am trying to use multiple fixtures on one test function that is within a test class. Is my call to @pytest.mark.usefixtures('default_router','default_interface')
wrong above the test ? I am getting the following message E AttributeError: 'function' object has no attribute 'add_interface'
Interface and Router class:
class Interface(object):
"""
Interface works with fast ethernet and gigabit ethernet ports.
TO DO: Provide support for serial interfaces.
"""
def __init__(self,port:str,cidr:str,ip_address:str):
# Make sure you can change the interface.
self.init_commands: [str] = [None, "no switchport"]
self.config_commands: [str] = [None]
# Stop the port interface from going down
# Get out of the interface
self.exit_commands = ["no shut", "exit"]
self._port: [str] = None
self._cidr: [str] = None
self._ip_address: [str] = None
# Example 255.255.255.0
self.netmask: str = None
self.port = port
self.cidr = cidr
self.ip_address = ip_address
self.init_setup()
self.config_setup()
@property
def commands(self) -> [str]:
commands = self.init_commands + self.config_commands + self.exit_commands
return commands
@property
def port(self) -> str:
# A list of valid regex for the first part of the port.
return ' '.join(self._port)
@property
def cidr(self) -> str:
return '/'.join(self._cidr)
@property
def ip_address(self) -> str:
return '.'.join(self._ip_address)
@port.setter
def port(self,port:str):
# todo: Add support for VLAN(You'll support Switches Later).
valid_port_types = ['^fastethernet$', '$f$', '^fa$', '^gigabitethernet$', '^g$', '^gb$']
# Split the port by spaces.
port_split = port.split()
# If the port is not split by spaces, then split it by letters and numbers separated by "/".
if len(port_split) != 2:
port_split = re.match("([a-zA-Z]+)([0-9]+/[0-9]+)",port_split[0])
if port_split == None:
raise ValueError(f"<{port}> is not a valid port name")
port_split = list(port_split.groups())
# If the first part of the port is not a valid port type, raise a ValueError.
if sum([bool(re.match(port_name, port_split[0])) for port_name in valid_port_types]) != 1:
raise ValueError(f'<{port_split[0]}> is not a valid port name')
# If the second part of the port does not contain '/', raise a ValueError.
if '/' not in port_split[1]:
raise ValueError(f'port number <{port_split[0]}> <{port_split[1]}> does not contain "/"')
self._port = port_split
self.init_setup()
@cidr.setter
def cidr(self,cidr):
cidr_split = re.split("/",cidr)
if len(cidr_split) != 2:
raise ValueError(f'cidr <{cidr}> does not contain "/" or is invalid')
cidr_split = [value.strip() for value in cidr_split]
valid_subnet_address = '\d{,3}.\d{,3}.\d{,3}.\d{,3}'
if not bool(re.match(valid_subnet_address,cidr_split[0])):
raise ValueError(f'<{cidr_split[0]}> is not a valid subnet address')
if '/' in cidr_split[1]:
cidr_split[1] = cidr_split[1].replace("/","")
prefix = int(cidr_split[1])
if prefix < 0 or prefix > 32:
raise ValueError(f"<{cidr_split[1]}> is not a valid subnet mask")
network,netmask = self.cidr_to_netmask('/'.join(cidr_split))
self.netmask = netmask
self._cidr = [network,str(prefix)]
if self._ip_address != None:
self.config_setup()
@ip_address.setter
def ip_address(self,ip_address):
# Remove spaces and split ip address by "."
split_ip_address = ip_address.replace(" ","").split(".")
valid_ip_address = '\d{,3}.\d{,3}.\d{,3}.\d{,3}'
# If the IP address does not match X.X.X.X, raise a ValueError.
if not bool(re.match(valid_ip_address, ip_address)):
raise ValueError(f'<{ip_address}> is not a valid IP address')
self._ip_address = split_ip_address
self.config_setup()
@staticmethod
# https://stackoverflow.com/questions/33750233/convert-cidr-to-subnet-mask-in-python
# "<<" and ">>" are bitwise operators that move the binary value over by the number of digits.
def cidr_to_netmask(cidr):
network, net_bits = cidr.split('/')
host_bits = 32 - int(net_bits)
# ntoa only supports 32 bit IPV4, use ntop to support ipv6
# todo support IPV6
netmask = socket.inet_ntoa(struct.pack('!I', (1 << 32) - (1 << host_bits)))
return network, netmask
def init_setup(self):
# Enter the interface.
self.init_commands[0] = "int %s" % self.port
def config_setup(self):
self.config_commands = ["ip address %s %s" % (self.ip_address, self.netmask)]
def __str__(self):
return ','.join([self.port,self.cidr,self.ip_address])
class Router(metaclass=IterRouter):
_all_routers = []
available_protocols = ['RIP1','RIP2','OSPF1','OSPF2']
def __init__(self,hostname):
# If the hostname already exists, then raise a ValueError
double_list = [hostname == router.hostname for router in self._all_routers]
if sum(double_list) != 0:
raise ValueError(f"Router hostname <{hostname}> already exists")
# Add new router to list of instances.
Router._all_routers.append(self)
# Set class instance attributes.
self.hostname = hostname
def add_interface(self,interface:Interface):
# If the interface already exists, raise a ValueError.
if len(self.interfaces) != 0:
if sum([str(interface) == str(current_interface) for current_interface in self.interfaces]) != 0:
raise ValueError('This interface already exists')
self.interfaces.append(interface)
Fixtures:
@pytest.fixture(scope='module')
def default_router():
name = 'default'
return Router(name)
@pytest.fixture(scope='module')
def default_interface():
port = 'fa0/1'
cidr = '192.168.0.0/24'
ip_address = '192.168.1.1'
fa01 = Interface(port=port, cidr=cidr, ip_address=ip_address)
return fa01
Code for testing:
class TestAddInterface(object):
@pytest.mark.usefixtures('default_router','default_interface')
def test_add_two_same_interfaces(self):
"""This test adds the same interface to a Router twice to test that it raises
a ValueError."""
router = default_router
router.add_interface(default_interface)
repeat_interface = 'fa0/1,192.168.0.0/24,192.168.1.1'.split(",")
print(repeat_interface)
with pytest.raises(ValueError):
new_copy = Interface(repeat_interface[0], repeat_interface[1], repeat_interface[2])
print([id(x) for x in router.interfaces])
print(id(new_copy))
router.add_interface(new_copy)
Error:
================================== FAILURES ===================================
________________ TestAddInterface.test_add_two_same_interfaces ________________
self = <test_router.TestAddInterface object at 0x000001B437FDE520>
@pytest.mark.usefixtures('default_router','default_interface')
def test_add_two_same_interfaces(self):
"""This test adds the same interface to a Router twice to test that it raises
a ValueError."""
router = default_router
> router.add_interface(default_interface)
E AttributeError: 'function' object has no attribute 'add_interface'