Skip to content

Commit 28b470b

Browse files
committed
typing: Add hints to base functional test
We have no intention of typing the entire test suite, but this will catch some obvious boo-boos. Change-Id: I923b45c713862e1bd07d5cbd2543e6bcba65add8 Signed-off-by: Stephen Finucane <stephenfin@redhat.com>
1 parent cabbe59 commit 28b470b

2 files changed

Lines changed: 68 additions & 32 deletions

File tree

openstackclient/tests/functional/base.py

Lines changed: 54 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,13 @@
1010
# License for the specific language governing permissions and limitations
1111
# under the License.
1212

13+
from collections.abc import Iterable, Mapping, Sequence
1314
import json
1415
import logging
1516
import os
1617
import shlex
1718
import subprocess
19+
from typing import Any, Literal, cast, overload
1820

1921
from tempest.lib.cli import output_parser
2022
from tempest.lib import exceptions
@@ -24,7 +26,7 @@
2426
LOG = logging.getLogger(__name__)
2527

2628

27-
def execute(cmd, *, fail_ok=False):
29+
def execute(cmd: str, *, fail_ok: bool = False) -> str:
2830
"""Executes specified command for the given action."""
2931
LOG.debug('Executing: %s', cmd)
3032
cmdlist = shlex.split(cmd)
@@ -53,15 +55,37 @@ def execute(cmd, *, fail_ok=False):
5355

5456

5557
class TestCase(testtools.TestCase):
58+
@overload
5659
@classmethod
5760
def openstack(
5861
cls,
59-
cmd,
62+
cmd: str,
6063
*,
61-
cloud=ADMIN_CLOUD,
62-
fail_ok=False,
63-
parse_output=False,
64-
):
64+
cloud: str | None = ADMIN_CLOUD,
65+
fail_ok: bool = False,
66+
parse_output: Literal[False] = False,
67+
) -> str: ...
68+
69+
@overload
70+
@classmethod
71+
def openstack(
72+
cls,
73+
cmd: str,
74+
*,
75+
cloud: str | None = ADMIN_CLOUD,
76+
fail_ok: bool = False,
77+
parse_output: Literal[True] = ...,
78+
) -> Any: ...
79+
80+
@classmethod
81+
def openstack(
82+
cls,
83+
cmd: str,
84+
*,
85+
cloud: str | None = ADMIN_CLOUD,
86+
fail_ok: bool = False,
87+
parse_output: bool = False,
88+
) -> str | Any:
6589
"""Executes openstackclient command for the given action
6690
6791
:param cmd: A string representation of the command to execute.
@@ -106,7 +130,9 @@ def openstack(
106130
return output
107131

108132
@classmethod
109-
def is_service_enabled(cls, service, version=None):
133+
def is_service_enabled(
134+
cls, service: str, version: str | None = None
135+
) -> bool:
110136
"""Ask client cloud if service is available
111137
112138
:param service: The service name or type. This should be either an
@@ -126,7 +152,9 @@ def is_service_enabled(cls, service, version=None):
126152
return bool(ret)
127153

128154
@classmethod
129-
def is_extension_enabled(cls, alias, *, service='network'):
155+
def is_extension_enabled(
156+
cls, alias: str, *, service: str = 'network'
157+
) -> bool:
130158
"""Ask client cloud if extension is enabled"""
131159
extensions = cls.openstack(
132160
f'extension list --{service}',
@@ -135,38 +163,44 @@ def is_extension_enabled(cls, alias, *, service='network'):
135163
return alias in [x['Alias'] for x in extensions]
136164

137165
@classmethod
138-
def get_openstack_configuration_value(cls, configuration):
166+
def get_openstack_configuration_value(cls, configuration: str) -> str:
139167
opts = cls.get_opts([configuration])
140168
return cls.openstack('configuration show ' + opts)
141169

142170
@classmethod
143-
def get_opts(cls, fields, output_format='value'):
171+
def get_opts(cls, fields: list[str], output_format: str = 'value') -> str:
144172
return ' -f {} {}'.format(
145173
output_format, ' '.join(['-c ' + it for it in fields])
146174
)
147175

148176
@classmethod
149-
def assertOutput(cls, expected, actual):
177+
def assertOutput(cls, expected: str, actual: str) -> None:
150178
if expected != actual:
151179
raise Exception(expected + ' != ' + actual)
152180

153181
@classmethod
154-
def assertInOutput(cls, expected, actual):
182+
def assertInOutput(cls, expected: str, actual: str) -> None:
155183
if expected not in actual:
156184
raise Exception(expected + ' not in ' + actual)
157185

158186
@classmethod
159-
def assertsOutputNotNone(cls, observed):
187+
def assertsOutputNotNone(cls, observed: Any) -> None:
160188
if observed is None:
161189
raise Exception('No output observed')
162190

163-
def assert_table_structure(self, items, field_names):
191+
def assert_table_structure(
192+
self, items: Iterable[Mapping[str, Any]], field_names: Sequence[str]
193+
) -> None:
164194
"""Verify that all items have keys listed in field_names."""
165195
for item in items:
166196
for field in field_names:
167197
self.assertIn(field, item)
168198

169-
def assert_show_fields(self, show_output, field_names):
199+
def assert_show_fields(
200+
self,
201+
show_output: Iterable[Mapping[str, Any]],
202+
field_names: Sequence[str],
203+
) -> None:
170204
"""Verify that all items have keys listed in field_names."""
171205

172206
# field_names = ['name', 'description']
@@ -186,17 +220,18 @@ def parse_show_as_object(self, raw_output):
186220
o.update(item)
187221
return o
188222

189-
def parse_show(self, raw_output):
223+
def parse_show(self, raw_output: str) -> list[dict[str, Any]]:
190224
"""Return list of dicts with item values parsed from cli output."""
191225

192226
items = []
193227
table_ = output_parser.table(raw_output)
194228
for row in table_['values']:
195229
item = {}
196-
item[row[0]] = row[1]
230+
item[str(row[0])] = row[1]
197231
items.append(item)
198232
return items
199233

200-
def parse_listing(self, raw_output):
234+
def parse_listing(self, raw_output: str) -> list[dict[str, Any]]:
201235
"""Return list of dicts with basic item parsed from cli output."""
202-
return output_parser.listing(raw_output)
236+
# need to add hints to tempest
237+
return cast(list[dict[str, Any]], output_parser.listing(raw_output))

openstackclient/tests/functional/compute/v2/common.py

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
#
1313

1414
import time
15+
from typing import cast
1516
import uuid
1617

1718
from tempest.lib import exceptions
@@ -35,37 +36,36 @@ def setUp(self):
3536

3637
@classmethod
3738
def get_flavor(cls) -> str:
39+
valid_flavors = ['m1.tiny', 'cirros256']
3840
# NOTE(rtheis): Get cirros256 or m1.tiny flavors since functional
3941
# tests may create other flavors.
4042
flavors = cls.openstack("flavor list", parse_output=True)
41-
server_flavor = None
4243
for flavor in flavors:
4344
if flavor['Name'] in ['m1.tiny', 'cirros256']:
44-
server_flavor = flavor['Name']
45-
break
46-
47-
assert server_flavor is not None
45+
return cast(str, flavor['Name'])
4846

49-
return server_flavor
47+
raise Exception(
48+
f'Failed to find a suitable flavor. Required one of: '
49+
f'{", ".join(valid_flavors)}'
50+
)
5051

5152
@classmethod
5253
def get_image(cls) -> str:
5354
# NOTE(rtheis): Get first Cirros image since functional tests may
5455
# create other images. Image may be named '-uec' or
5556
# '-disk'.
5657
images = cls.openstack("image list", parse_output=True)
57-
server_image = None
5858
for image in images:
5959
if image['Name'].startswith('cirros-') and (
6060
image['Name'].endswith('-uec')
6161
or image['Name'].endswith('-disk')
6262
):
63-
server_image = image['Name']
64-
break
63+
return cast(str, image['Name'])
6564

66-
assert server_image is not None
67-
68-
return server_image
65+
raise Exception(
66+
'Failed to find a suitable image. Required one matching one of '
67+
'the following patterns: cirros-*-uec, cirros-*-disk'
68+
)
6969

7070
@classmethod
7171
def get_network(cls) -> str:
@@ -78,7 +78,8 @@ def get_network(cls) -> str:
7878
)
7979
except exceptions.CommandFailed:
8080
return ''
81-
return '--nic net-id=' + cmd_output['id']
81+
82+
return '='.join(('--nic net-id', cmd_output['id']))
8283

8384
def server_create(self, name=None, cleanup=True):
8485
"""Create server, with cleanup"""

0 commit comments

Comments
 (0)