Merge lp:~jtv/maas/unify-get_name_and_vlan_from_cluster_interface into lp:~maas-committers/maas/trunk

Proposed by Jeroen T. Vermeulen
Status: Merged
Approved by: Jeroen T. Vermeulen
Approved revision: no longer in the source branch.
Merged at revision: 3374
Proposed branch: lp:~jtv/maas/unify-get_name_and_vlan_from_cluster_interface
Merge into: lp:~maas-committers/maas/trunk
Diff against target: 535 lines (+164/-142)
12 files modified
src/maasserver/forms.py (+6/-3)
src/maasserver/migrations/0099_convert_cluster_interfaces_to_networks.py (+7/-18)
src/maasserver/models/network.py (+0/-16)
src/maasserver/models/tests/test_network.py (+0/-39)
src/maasserver/testing/factory.py (+0/-23)
src/maasserver/testing/tests/test_factory.py (+2/-39)
src/maasserver/tests/test_forms_nodegroupinterface.py (+5/-2)
src/maasserver/utils/interfaces.py (+19/-0)
src/maasserver/utils/tests/test_interfaces.py (+59/-1)
src/maastesting/factory.py (+22/-0)
src/maastesting/tests/test_factory.py (+20/-0)
src/maastesting/utils.py (+24/-1)
To merge this branch: bzr merge lp:~jtv/maas/unify-get_name_and_vlan_from_cluster_interface
Reviewer Review Type Date Requested Status
Gavin Panella (community) Approve
Review via email: mp+241738@code.launchpad.net

Commit message

Unify our two implementations of get_name_and_vlan_from_cluster_interface, so that we can then fix bug 1391139 in one place, with tests.

Description of the change

This was harder than I expected: there were some other helpers that needed moving. Fixing the actual bug should be easy by comparison.

Along the way I made the function independent of the model, otherwise it wouldn't be advisable to use it in migration code. It was a simple matter of externalising retrieval of two fields, and making those the parameters. But otherwise, nothing substantial changes.

Jeroen

To post a comment you must log in.
Revision history for this message
Gavin Panella (allenap) wrote :

Looks fine. I don't like FakeRandInt much, but I don't have a better suggestion offhand, and you've only moved it anyway.

review: Approve
Revision history for this message
Jeroen T. Vermeulen (jtv) wrote :

Thanks. Made some changes "inspired by" your notes, as they say in Hollywood nowadays. :-)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'src/maasserver/forms.py'
--- src/maasserver/forms.py 2014-11-13 02:17:06 +0000
+++ src/maasserver/forms.py 2014-11-14 14:24:50 +0000
@@ -125,7 +125,6 @@
125 Tag,125 Tag,
126 Zone,126 Zone,
127 )127 )
128from maasserver.models.network import get_name_and_vlan_from_cluster_interface
129from maasserver.models.node import (128from maasserver.models.node import (
130 fqdn_is_duplicate,129 fqdn_is_duplicate,
131 nodegroup_fqdn,130 nodegroup_fqdn,
@@ -138,7 +137,10 @@
138 )137 )
139from maasserver.utils import strip_domain138from maasserver.utils import strip_domain
140from maasserver.utils.forms import compose_invalid_choice_text139from maasserver.utils.forms import compose_invalid_choice_text
141from maasserver.utils.interfaces import make_name_from_interface140from maasserver.utils.interfaces import (
141 get_name_and_vlan_from_cluster_interface,
142 make_name_from_interface,
143 )
142from maasserver.utils.orm import get_one144from maasserver.utils.orm import get_one
143from maasserver.utils.osystems import (145from maasserver.utils.osystems import (
144 get_distro_series_initial,146 get_distro_series_initial,
@@ -1409,7 +1411,8 @@
1409 # Can be None or empty string, do nothing if so.1411 # Can be None or empty string, do nothing if so.
1410 return1412 return
14111413
1412 name, vlan_tag = get_name_and_vlan_from_cluster_interface(interface)1414 name, vlan_tag = get_name_and_vlan_from_cluster_interface(
1415 interface.name, interface.interface)
1413 ipnetwork = make_network(interface.ip, interface.subnet_mask)1416 ipnetwork = make_network(interface.ip, interface.subnet_mask)
1414 network = Network(1417 network = Network(
1415 name=name,1418 name=name,
14161419
=== modified file 'src/maasserver/migrations/0099_convert_cluster_interfaces_to_networks.py'
--- src/maasserver/migrations/0099_convert_cluster_interfaces_to_networks.py 2014-08-25 02:47:21 +0000
+++ src/maasserver/migrations/0099_convert_cluster_interfaces_to_networks.py 2014-11-14 14:24:50 +0000
@@ -5,28 +5,15 @@
5 transaction,5 transaction,
6 )6 )
7from django.db.utils import IntegrityError7from django.db.utils import IntegrityError
8from maasserver.utils.interfaces import (
9 get_name_and_vlan_from_cluster_interface,
10 )
8from netaddr import IPNetwork11from netaddr import IPNetwork
9from south.db import db12from south.db import db
10from south.utils import datetime_utils as datetime13from south.utils import datetime_utils as datetime
11from south.v2 import DataMigration14from south.v2 import DataMigration
1215
1316
14def get_name_and_vlan_from_cluster_interface(cluster_interface):
15 """Given a `NodeGroupInterface`, return a name suitable for a `Network`.
16
17 :return: a tuple of the new name and vlan tag. vlan tag may be None
18 """
19 name = cluster_interface.interface
20 nodegroup_name = cluster_interface.nodegroup.name
21 vlan_tag = None
22 if '.' in name:
23 _, vlan_tag = name.split('.', 1)
24 name = name.replace('.', '-')
25 name = name.replace(':', '-')
26 network_name = "-".join((nodegroup_name, name))
27 return network_name, vlan_tag
28
29
30class Migration(DataMigration):17class Migration(DataMigration):
3118
32 def forwards(self, orm):19 def forwards(self, orm):
@@ -36,8 +23,10 @@
36 # Can be None or empty string, do nothing if so.23 # Can be None or empty string, do nothing if so.
37 continue24 continue
3825
39 name, vlan_tag = get_name_and_vlan_from_cluster_interface(interface)26 name, vlan_tag = get_name_and_vlan_from_cluster_interface(
40 ipnetwork = IPNetwork("%s/%s" % (interface.ip, interface.subnet_mask))27 interface.nodegroup.name, interface.interface)
28 ipnetwork = IPNetwork(
29 "%s/%s" % (interface.ip, interface.subnet_mask))
41 network = orm['maasserver.Network'](30 network = orm['maasserver.Network'](
42 name=name,31 name=name,
43 ip=unicode(ipnetwork.network),32 ip=unicode(ipnetwork.network),
4433
=== modified file 'src/maasserver/models/network.py'
--- src/maasserver/models/network.py 2014-09-17 09:20:05 +0000
+++ src/maasserver/models/network.py 2014-11-14 14:24:50 +0000
@@ -186,22 +186,6 @@
186 return specifier_class(spec)186 return specifier_class(spec)
187187
188188
189def get_name_and_vlan_from_cluster_interface(cluster_interface):
190 """Given a `NodeGroupInterface`, return a name suitable for a `Network`.
191
192 :return: a tuple of the new name and vlan tag. vlan tag may be None
193 """
194 name = cluster_interface.interface
195 nodegroup_name = cluster_interface.nodegroup.name
196 vlan_tag = None
197 if '.' in name:
198 _, vlan_tag = name.split('.', 1)
199 name = name.replace('.', '-')
200 name = name.replace(':', '-')
201 network_name = "-".join((nodegroup_name, name))
202 return network_name, vlan_tag
203
204
205class NetworkManager(Manager):189class NetworkManager(Manager):
206 """Manager for :class:`Network` model class.190 """Manager for :class:`Network` model class.
207191
208192
=== modified file 'src/maasserver/models/tests/test_network.py'
--- src/maasserver/models/tests/test_network.py 2014-09-19 03:12:47 +0000
+++ src/maasserver/models/tests/test_network.py 2014-11-14 14:24:50 +0000
@@ -20,7 +20,6 @@
20from django.db.models.query import QuerySet20from django.db.models.query import QuerySet
21from maasserver.models import Network21from maasserver.models import Network
22from maasserver.models.network import (22from maasserver.models.network import (
23 get_name_and_vlan_from_cluster_interface,
24 get_specifier_type,23 get_specifier_type,
25 IPSpecifier,24 IPSpecifier,
26 NameSpecifier,25 NameSpecifier,
@@ -30,7 +29,6 @@
30from maasserver.testing.factory import factory29from maasserver.testing.factory import factory
31from maasserver.testing.orm import reload_object30from maasserver.testing.orm import reload_object
32from maasserver.testing.testcase import MAASServerTestCase31from maasserver.testing.testcase import MAASServerTestCase
33from mock import sentinel
34from netaddr import (32from netaddr import (
35 IPAddress,33 IPAddress,
36 IPNetwork,34 IPNetwork,
@@ -132,43 +130,6 @@
132 self.assertRaises(ValidationError, parse_network_spec, '10.4.4.4')130 self.assertRaises(ValidationError, parse_network_spec, '10.4.4.4')
133131
134132
135class TestGetNameAndVlanFromClusterInterface(MAASServerTestCase):
136 """Tests for `get_name_and_vlan_from_cluster_interface`."""
137
138 def make_interface_name(self, basename):
139 interface = sentinel.interface
140 interface.nodegroup = sentinel.nodegroup
141 interface.nodegroup.name = factory.make_name('name')
142 interface.interface = basename
143 return interface
144
145 def test_returns_simple_name_unaltered(self):
146 interface = self.make_interface_name(factory.make_name('iface'))
147 name, vlan_tag = get_name_and_vlan_from_cluster_interface(interface)
148 expected_name = '%s-%s' % (
149 interface.nodegroup.name, interface.interface)
150 self.assertEqual((expected_name, None), (name, vlan_tag))
151
152 def test_substitutes_colon(self):
153 interface = self.make_interface_name('eth0:0')
154 name, vlan_tag = get_name_and_vlan_from_cluster_interface(interface)
155 expected_name = '%s-eth0-0' % interface.nodegroup.name
156 self.assertEqual((expected_name, None), (name, vlan_tag))
157
158 def test_returns_with_vlan_tag(self):
159 interface = self.make_interface_name('eth0.5')
160 name, vlan_tag = get_name_and_vlan_from_cluster_interface(interface)
161 expected_name = '%s-eth0-5' % interface.nodegroup.name
162 self.assertEqual((expected_name, '5'), (name, vlan_tag))
163
164 def test_returns_name_with_crazy_colon_and_vlan(self):
165 # For truly twisted network admins.
166 interface = self.make_interface_name('eth0:2.3')
167 name, vlan_tag = get_name_and_vlan_from_cluster_interface(interface)
168 expected_name = '%s-eth0-2-3' % interface.nodegroup.name
169 self.assertEqual((expected_name, '3'), (name, vlan_tag))
170
171
172class TestNetworkManager(MAASServerTestCase):133class TestNetworkManager(MAASServerTestCase):
173134
174 def test_get_from_spec_validates_first(self):135 def test_get_from_spec_validates_first(self):
175136
=== modified file 'src/maasserver/testing/factory.py'
--- src/maasserver/testing/factory.py 2014-11-13 02:17:06 +0000
+++ src/maasserver/testing/factory.py 2014-11-14 14:24:50 +0000
@@ -748,29 +748,6 @@
748748
749 make_zone = make_Zone749 make_zone = make_Zone
750750
751 def make_vlan_tag(self, allow_none=False, but_not=None):
752 """Create a random VLAN tag.
753
754 :param allow_none: Whether `None` ("no VLAN") can be allowed as an
755 outcome. If `True`, `None` will be included in the possible
756 results with a deliberately over-represented probability, in order
757 to help trip up bugs that might only show up once in about 4094
758 calls otherwise.
759 :param but_not: A list of tags that should not be returned. Any zero
760 or `None` entries will be ignored.
761 """
762 if but_not is None:
763 but_not = []
764 if allow_none and random.randint(0, 1) == 0:
765 return None
766 else:
767 for _ in range(100):
768 vlan_tag = random.randint(1, 0xffe)
769 if vlan_tag not in but_not:
770 return vlan_tag
771 raise maastesting.factory.TooManyRandomRetries(
772 "Could not find an available VLAN tag.")
773
774 def make_Network(self, name=None, network=None, vlan_tag=NO_VALUE,751 def make_Network(self, name=None, network=None, vlan_tag=NO_VALUE,
775 description=None, sortable_name=False,752 description=None, sortable_name=False,
776 disjoint_from=None, default_gateway=None,753 disjoint_from=None, default_gateway=None,
777754
=== modified file 'src/maasserver/testing/tests/test_factory.py'
--- src/maasserver/testing/tests/test_factory.py 2014-09-10 16:20:31 +0000
+++ src/maasserver/testing/tests/test_factory.py 2014-11-14 14:24:50 +0000
@@ -24,28 +24,7 @@
24from maasserver.testing.orm import reload_object24from maasserver.testing.orm import reload_object
25from maasserver.testing.testcase import MAASServerTestCase25from maasserver.testing.testcase import MAASServerTestCase
26from maastesting.factory import TooManyRandomRetries26from maastesting.factory import TooManyRandomRetries
2727from maastesting.utils import FakeRandInt
28
29class FakeRandInt:
30 """Fake `randint` which forced limitations on its range.
31
32 This lets you set a forced minimum, and/or a forced maximum, on the range
33 of any call. For example, if you pass `forced_maximum=3`, then a call
34 will never return more than 3. If you don't set a maximum, or if the
35 call's maximum argument is less than the forced maximum, then the call's
36 maximum will be respected.
37 """
38 def __init__(self, real_randint, forced_minimum=None, forced_maximum=None):
39 self.real_randint = real_randint
40 self.minimum = forced_minimum
41 self.maximum = forced_maximum
42
43 def __call__(self, minimum, maximum):
44 if self.minimum is not None:
45 minimum = max(minimum, self.minimum)
46 if self.maximum is not None:
47 maximum = min(maximum, self.maximum)
48 return self.real_randint(minimum, maximum)
4928
5029
51class TestFactory(MAASServerTestCase):30class TestFactory(MAASServerTestCase):
@@ -127,23 +106,6 @@
127 node = reload_object(node)106 node = reload_object(node)
128 self.assertEqual(previous_zone, node.zone)107 self.assertEqual(previous_zone, node.zone)
129108
130 def test_make_vlan_tag_excludes_None_by_default(self):
131 # Artificially limit randint to a very narrow range, to guarantee
132 # some repetition in its output, and virtually guarantee that we test
133 # both outcomes of the flip-a-coin call in make_vlan_tag.
134 self.patch(random, 'randint', FakeRandInt(random.randint, 0, 1))
135 outcomes = {factory.make_vlan_tag() for _ in range(1000)}
136 self.assertEqual({1}, outcomes)
137
138 def test_make_vlan_tags_includes_None_if_allow_none(self):
139 self.patch(random, 'randint', FakeRandInt(random.randint, 0, 1))
140 self.assertEqual(
141 {None, 1},
142 {
143 factory.make_vlan_tag(allow_none=True)
144 for _ in range(1000)
145 })
146
147 def test_make_Networks_lowers_names_if_sortable_name(self):109 def test_make_Networks_lowers_names_if_sortable_name(self):
148 networks = factory.make_Networks(10, sortable_name=True)110 networks = factory.make_Networks(10, sortable_name=True)
149 self.assertEqual(111 self.assertEqual(
@@ -183,4 +145,5 @@
183 def test_make_Networks_gives_up_if_random_tags_keep_clashing(self):145 def test_make_Networks_gives_up_if_random_tags_keep_clashing(self):
184 self.patch(factory, 'make_Network')146 self.patch(factory, 'make_Network')
185 self.patch(random, 'randint', lambda *args: 1)147 self.patch(random, 'randint', lambda *args: 1)
148 self.patch(factory, 'pick_bool', lambda *args: False)
186 self.assertRaises(TooManyRandomRetries, factory.make_Networks, 2)149 self.assertRaises(TooManyRandomRetries, factory.make_Networks, 2)
187150
=== modified file 'src/maasserver/tests/test_forms_nodegroupinterface.py'
--- src/maasserver/tests/test_forms_nodegroupinterface.py 2014-10-30 11:08:43 +0000
+++ src/maasserver/tests/test_forms_nodegroupinterface.py 2014-11-14 14:24:50 +0000
@@ -29,10 +29,12 @@
29 Network,29 Network,
30 NodeGroupInterface,30 NodeGroupInterface,
31 )31 )
32from maasserver.models.network import get_name_and_vlan_from_cluster_interface
33from maasserver.models.staticipaddress import StaticIPAddress32from maasserver.models.staticipaddress import StaticIPAddress
34from maasserver.testing.factory import factory33from maasserver.testing.factory import factory
35from maasserver.testing.testcase import MAASServerTestCase34from maasserver.testing.testcase import MAASServerTestCase
35from maasserver.utils.interfaces import (
36 get_name_and_vlan_from_cluster_interface,
37 )
36from maastesting.matchers import MockCalledOnceWith38from maastesting.matchers import MockCalledOnceWith
37from netaddr import (39from netaddr import (
38 IPAddress,40 IPAddress,
@@ -369,7 +371,8 @@
369 form = NodeGroupInterfaceForm(data=int_settings, instance=interface)371 form = NodeGroupInterfaceForm(data=int_settings, instance=interface)
370 form.save()372 form.save()
371 [network] = Network.objects.all()373 [network] = Network.objects.all()
372 expected, _ = get_name_and_vlan_from_cluster_interface(interface)374 expected, _ = get_name_and_vlan_from_cluster_interface(
375 interface.name, interface.interface)
373 self.assertEqual(expected, network.name)376 self.assertEqual(expected, network.name)
374377
375 def test_sets_vlan_tag(self):378 def test_sets_vlan_tag(self):
376379
=== modified file 'src/maasserver/utils/interfaces.py'
--- src/maasserver/utils/interfaces.py 2014-08-13 21:49:35 +0000
+++ src/maasserver/utils/interfaces.py 2014-11-14 14:24:50 +0000
@@ -13,6 +13,7 @@
1313
14__metaclass__ = type14__metaclass__ = type
15__all__ = [15__all__ = [
16 'get_name_and_vlan_from_cluster_interface',
16 'make_name_from_interface',17 'make_name_from_interface',
17 ]18 ]
1819
@@ -34,3 +35,21 @@
34 else:35 else:
35 base_name = interface36 base_name = interface
36 return re.sub(u'[^\w:.-]', '--', base_name)37 return re.sub(u'[^\w:.-]', '--', base_name)
38
39
40def get_name_and_vlan_from_cluster_interface(cluster_name, interface):
41 """Return a name suitable for a `Network` managed by a cluster interface.
42
43 :param interface: Network interface name, e.g. `eth0:1`.
44 :param cluster_name: Name of the cluster.
45 :return: a tuple of the new name and the interface's VLAN tag. The VLAN
46 tag may be None.
47 """
48 name = interface
49 vlan_tag = None
50 if '.' in name:
51 _, vlan_tag = name.split('.', 1)
52 name = name.replace('.', '-')
53 name = name.replace(':', '-')
54 network_name = "-".join((cluster_name, name))
55 return network_name, vlan_tag
3756
=== modified file 'src/maasserver/utils/tests/test_interfaces.py'
--- src/maasserver/utils/tests/test_interfaces.py 2014-07-04 12:10:01 +0000
+++ src/maasserver/utils/tests/test_interfaces.py 2014-11-14 14:24:50 +0000
@@ -14,7 +14,12 @@
14__metaclass__ = type14__metaclass__ = type
15__all__ = []15__all__ = []
1616
17from maasserver.utils.interfaces import make_name_from_interface17from random import randint
18
19from maasserver.utils.interfaces import (
20 get_name_and_vlan_from_cluster_interface,
21 make_name_from_interface,
22 )
18from maastesting.factory import factory23from maastesting.factory import factory
19from maastesting.testcase import MAASTestCase24from maastesting.testcase import MAASTestCase
2025
@@ -38,3 +43,56 @@
38 self.assertNotEqual(43 self.assertNotEqual(
39 make_name_from_interface(''),44 make_name_from_interface(''),
40 make_name_from_interface(''))45 make_name_from_interface(''))
46
47
48class TestGetNameAndVlanFromClusterInterface(MAASTestCase):
49 """Tests for `get_name_and_vlan_from_cluster_interface`."""
50
51 def make_interface(self):
52 """Return a simple network interface name."""
53 return 'eth%d' % randint(0, 99)
54
55 def test_returns_simple_name_unaltered(self):
56 cluster = factory.make_name('cluster')
57 interface = factory.make_name('iface')
58 expected_name = '%s-%s' % (cluster, interface)
59 self.assertEqual(
60 (expected_name, None),
61 get_name_and_vlan_from_cluster_interface(cluster, interface))
62
63 def test_substitutes_colon(self):
64 cluster = factory.make_name('cluster')
65 base_interface = self.make_interface()
66 alias = randint(0, 99)
67 interface = '%s:%d' % (base_interface, alias)
68 expected_name = '%s-%s-%d' % (cluster, base_interface, alias)
69 self.assertEqual(
70 (expected_name, None),
71 get_name_and_vlan_from_cluster_interface(cluster, interface))
72
73 def test_returns_with_vlan_tag(self):
74 cluster = factory.make_name('cluster')
75 base_interface = self.make_interface()
76 vlan_tag = factory.make_vlan_tag()
77 interface = '%s.%d' % (base_interface, vlan_tag)
78 expected_name = '%s-%s-%d' % (cluster, base_interface, vlan_tag)
79 self.assertEqual(
80 (expected_name, '%d' % vlan_tag),
81 get_name_and_vlan_from_cluster_interface(cluster, interface))
82
83 def test_returns_name_with_crazy_colon_and_vlan(self):
84 # For truly twisted network admins.
85 cluster = factory.make_name('cluster')
86 base_interface = self.make_interface()
87 vlan_tag = factory.make_vlan_tag()
88 alias = randint(0, 99)
89 interface = '%s:%d.%d' % (base_interface, alias, vlan_tag)
90 expected_name = '%s-%s-%d-%d' % (
91 cluster,
92 base_interface,
93 alias,
94 vlan_tag,
95 )
96 self.assertEqual(
97 (expected_name, '%d' % vlan_tag),
98 get_name_and_vlan_from_cluster_interface(cluster, interface))
4199
=== modified file 'src/maastesting/factory.py'
--- src/maastesting/factory.py 2014-11-07 16:11:53 +0000
+++ src/maastesting/factory.py 2014-11-14 14:24:50 +0000
@@ -133,6 +133,28 @@
133 assert port_min >= 0 and port_max <= 65535133 assert port_min >= 0 and port_max <= 65535
134 return random.randint(port_min, port_max)134 return random.randint(port_min, port_max)
135135
136 def make_vlan_tag(self, allow_none=False, but_not=None):
137 """Create a random VLAN tag.
138
139 :param allow_none: Whether `None` ("no VLAN") can be allowed as an
140 outcome. If `True`, `None` will be included in the possible
141 results with a deliberately over-represented probability, in order
142 to help trip up bugs that might only show up once in about 4094
143 calls otherwise.
144 :param but_not: A list of tags that should not be returned. Any zero
145 or `None` entries will be ignored.
146 """
147 if but_not is None:
148 but_not = []
149 if allow_none and self.pick_bool():
150 return None
151 else:
152 for _ in range(100):
153 vlan_tag = random.randint(1, 0xffe)
154 if vlan_tag not in but_not:
155 return vlan_tag
156 raise TooManyRandomRetries("Could not find an available VLAN tag.")
157
136 def make_ipv4_address(self):158 def make_ipv4_address(self):
137 octets = islice(self.random_octets, 4)159 octets = islice(self.random_octets, 4)
138 return '%d.%d.%d.%d' % tuple(octets)160 return '%d.%d.%d.%d' % tuple(octets)
139161
=== modified file 'src/maastesting/tests/test_factory.py'
--- src/maastesting/tests/test_factory.py 2014-09-17 08:31:08 +0000
+++ src/maastesting/tests/test_factory.py 2014-11-14 14:24:50 +0000
@@ -1,4 +1,5 @@
1# Copyright 2012-2014 Canonical Ltd. This software is licensed under the1# Copyright 2012-2014 Canonical Ltd. This software is licensed under the
2
2# GNU Affero General Public License version 3 (see the file LICENSE).3# GNU Affero General Public License version 3 (see the file LICENSE).
34
4"""Test the factory where appropriate. Don't overdo this."""5"""Test the factory where appropriate. Don't overdo this."""
@@ -17,6 +18,7 @@
17from datetime import datetime18from datetime import datetime
18from itertools import count19from itertools import count
19import os.path20import os.path
21import random
20from random import randint22from random import randint
21import subprocess23import subprocess
2224
@@ -25,6 +27,7 @@
25 TooManyRandomRetries,27 TooManyRandomRetries,
26 )28 )
27from maastesting.testcase import MAASTestCase29from maastesting.testcase import MAASTestCase
30from maastesting.utils import FakeRandInt
28from netaddr import (31from netaddr import (
29 IPAddress,32 IPAddress,
30 IPNetwork,33 IPNetwork,
@@ -52,6 +55,23 @@
52 def test_pick_port_returns_int(self):55 def test_pick_port_returns_int(self):
53 self.assertIsInstance(factory.pick_port(), int)56 self.assertIsInstance(factory.pick_port(), int)
5457
58 def test_make_vlan_tag_excludes_None_by_default(self):
59 # Artificially limit randint to a very narrow range, to guarantee
60 # some repetition in its output, and virtually guarantee that we test
61 # both outcomes of the flip-a-coin call in make_vlan_tag.
62 self.patch(random, 'randint', FakeRandInt(random.randint, 0, 1))
63 outcomes = {factory.make_vlan_tag() for _ in range(1000)}
64 self.assertEqual({1}, outcomes)
65
66 def test_make_vlan_tag_includes_None_if_allow_none(self):
67 self.patch(random, 'randint', FakeRandInt(random.randint, 0, 1))
68 self.assertEqual(
69 {None, 1},
70 {
71 factory.make_vlan_tag(allow_none=True)
72 for _ in range(1000)
73 })
74
55 def test_make_ipv4_address(self):75 def test_make_ipv4_address(self):
56 ip_address = factory.make_ipv4_address()76 ip_address = factory.make_ipv4_address()
57 self.assertIsInstance(ip_address, unicode)77 self.assertIsInstance(ip_address, unicode)
5878
=== modified file 'src/maastesting/utils.py'
--- src/maastesting/utils.py 2014-08-22 13:32:56 +0000
+++ src/maastesting/utils.py 2014-11-14 14:24:50 +0000
@@ -1,4 +1,4 @@
1# Copyright 2012 Canonical Ltd. This software is licensed under the1# Copyright 2012-2014 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""Testing utilities."""4"""Testing utilities."""
@@ -17,6 +17,7 @@
17 "content_from_file",17 "content_from_file",
18 "extract_word_list",18 "extract_word_list",
19 "get_write_time",19 "get_write_time",
20 "FakeRandInt",
20 "preexec_fn",21 "preexec_fn",
21 "run_isolated",22 "run_isolated",
22 "sample_binary_data",23 "sample_binary_data",
@@ -130,3 +131,25 @@
130# (1) Provided, of course, that man know only about ASCII and131# (1) Provided, of course, that man know only about ASCII and
131# UTF.132# UTF.
132sample_binary_data = codecs.BOM64_LE + codecs.BOM64_BE + b'\x00\xff\x00'133sample_binary_data = codecs.BOM64_LE + codecs.BOM64_BE + b'\x00\xff\x00'
134
135
136class FakeRandInt:
137 """Fake `randint` with forced limitations on its range.
138
139 This lets you set a forced minimum, and/or a forced maximum, on the range
140 of any call. For example, if you pass `forced_maximum=3`, then a call
141 will never return more than 3. If you don't set a maximum, or if the
142 call's maximum argument is less than the forced maximum, then the call's
143 maximum will be respected.
144 """
145 def __init__(self, real_randint, forced_minimum=None, forced_maximum=None):
146 self.real_randint = real_randint
147 self.minimum = forced_minimum
148 self.maximum = forced_maximum
149
150 def __call__(self, minimum, maximum):
151 if self.minimum is not None:
152 minimum = max(minimum, self.minimum)
153 if self.maximum is not None:
154 maximum = min(maximum, self.maximum)
155 return self.real_randint(minimum, maximum)