Merge ppa-dev-tools:test-create-config into ppa-dev-tools:main

Proposed by Bryce Harrington
Status: Merged
Merge reported by: Bryce Harrington
Merged at revision: 6b06eaa91dc8931aea7767a9474a680412a0eff9
Proposed branch: ppa-dev-tools:test-create-config
Merge into: ppa-dev-tools:main
Diff against target: 275 lines (+97/-27)
4 files modified
ppa/ppa.py (+2/-0)
scripts/ppa (+21/-14)
tests/helpers.py (+16/-3)
tests/test_scripts_ppa.py (+58/-10)
Reviewer Review Type Date Requested Status
Athos Ribeiro (community) Approve
Alberto Contreras (community) Needs Information
Canonical Server Reporter Pending
Review via email: mp+438274@code.launchpad.net

Description of the change

This adds unit tests for the create_config() command, which I thought might be a bit superfluous but it did uncover some bad handling of invalid inputs. There's a few other testsuite related fixes and cleanups included.

Best way to test this branch is to run the full testsuite:

    $ pytest-3

The current codebase had a test failure relating to processors; this fixes that, and all subsequent commits should likewise pass the full testsuite.

To post a comment you must log in.
Revision history for this message
Alberto Contreras (aciba) wrote :

Thanks, Bryce, for adding unit tests. The changes look good.

I have left some nits and two questions / concerns.

1) The main one is the inclusion of the mocking machinery in `Ppa.archive`.

This helps us to unit test the code and it is okay as is.

But, adds new code that it is not needed at runtime and could cause problems in the future. In this case, this is probably an exaggeration, as the modifications are simple and easy to track. But if we establish this pattern of allowing testing code within production code, things could get complicated and bite us at some point.

Have we thought about using other alternatives, as using `pytest.fixtures` and/or `unittests.mock` in the long term? Or are there restrictions to use those that I do not see?

2) The xfail comment.

review: Needs Information
Revision history for this message
Athos Ribeiro (athos-ribeiro) wrote :

Thanks Bryce!

Other than Alberto's questions/comments (in special, the XFAIL one), this LGTM :)

ppa-dev-tools:test-create-config updated
93aba18... by Bryce Harrington

tests: Create mock PPAs with the default processors enabled

Revision history for this message
Bryce Harrington (bryce) wrote :

Hi Alberto and Athos, thank you for reviewing! You're right the xfail can be dropped; I was using it to isolate some debugging work and appear to have forgotten to re-enable it.

Alberto, you're right that the _archive addition is a bit awkward. I took a fresh look this morning, and redid the code to avoid needing to add that, by creating the ArchiveMock just at the point of need, and avoid having to store it. For test code that should be fine, and it avoids impacting the run-time code, which could have side effects as you point out.

I also did the cleanups you suggested, squashed in the code review changes, and force pushed the branch. I think it looks a more solid, thank you both again for taking time to look at this.

Revision history for this message
Athos Ribeiro (athos-ribeiro) wrote :

LGTM.

I just did not get why we needed the dict typecast there. Is it to satisfy type checking?

review: Approve
Revision history for this message
Bryce Harrington (bryce) wrote :

Thanks! Answer to the question below.

Revision history for this message
Bryce Harrington (bryce) wrote :

Landed:

To git+ssh://git.launchpad.net/ppa-dev-tools
   555b089..7e59879 main -> main

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/ppa/ppa.py b/ppa/ppa.py
2index dfbe5df..45e9886 100755
3--- a/ppa/ppa.py
4+++ b/ppa/ppa.py
5@@ -97,6 +97,8 @@ class Ppa:
6 :returns: The Launchpad archive object.
7 :raises PpaDoesNotExist: Raised if a PPA does not exist in Launchpad.
8 """
9+ if not self._service:
10+ raise AttributeError("Ppa object not connected to the Launchpad service")
11 try:
12 owner = self._service.people[self.team_name]
13 return owner.getPPAByName(name=self.ppa_name)
14diff --git a/scripts/ppa b/scripts/ppa
15index 6be138e..ad6a09a 100755
16--- a/scripts/ppa
17+++ b/scripts/ppa
18@@ -52,6 +52,7 @@ import time
19 import argparse
20 from inspect import currentframe
21 from textwrap import indent
22+from typing import Any
23 from distro_info import UbuntuDistroInfo
24
25 try:
26@@ -111,7 +112,7 @@ def load_yaml_as_dict(filename):
27 return d
28
29
30-def add_global_options(parser) -> None:
31+def add_global_options(parser: argparse.ArgumentParser) -> None:
32 """Adds arguments to the given parser for generic options.
33
34 :param argparse.ArgumentParser parser: A parser or subparser.
35@@ -138,7 +139,7 @@ def add_global_options(parser) -> None:
36 help="Minimize output during processing")
37
38
39-def add_basic_config_options(parser) -> None:
40+def add_basic_config_options(parser: argparse.ArgumentParser) -> None:
41 """Adds to a parser the command line options to configure the PPA.
42
43 The config options are supported by the 'create' and 'set' command,
44@@ -146,6 +147,8 @@ def add_basic_config_options(parser) -> None:
45
46 These options represent what can be set as a user from the Launchpad
47 web interface.
48+
49+ :param argparse.ArgumentParser parser: A parser or subparser.
50 """
51 # Architectures
52 parser.add_argument(
53@@ -203,7 +206,7 @@ def add_basic_config_options(parser) -> None:
54 )
55
56
57-def create_arg_parser():
58+def create_arg_parser() -> argparse.ArgumentParser:
59 """Sets up the command line parser object.
60
61 :rtype: argparse.ArgumentParser
62@@ -364,7 +367,14 @@ def create_arg_parser():
63 return parser
64
65
66-def create_config(lp, args):
67+DEFAULT_CONFIG = {
68+ 'debug': False,
69+ 'ppa_name': None,
70+ 'team_name': None,
71+ 'wait_seconds': 10.0
72+ }
73+
74+def create_config(lp: Lp, args: argparse.Namespace) -> dict[str, Any]:
75 """Creates config object by loading from file and adding args.
76
77 This routine merges the command line parameter values with data
78@@ -380,19 +390,13 @@ def create_config(lp, args):
79 :rtype: dict
80 :returns: dict of configuration parameters and values, or None on error
81 """
82- DEFAULT_CONFIG = {
83- 'debug': False,
84- 'ppa_name': None,
85- 'team_name': None,
86- 'wait_seconds': 10.0
87- }
88 config_path = os.path.expanduser(args.config_filename)
89 try:
90 config = load_yaml_as_dict(config_path)
91 except FileNotFoundError:
92 # Assume defaults
93 dbg("Using default config since no config file found at {}".format(config_path))
94- config = DEFAULT_CONFIG
95+ config = dict(DEFAULT_CONFIG)
96
97 # Map all non-empty elements from argparse Namespace into config dict
98 config.update({k: v for k, v in vars(args).items() if v})
99@@ -404,10 +408,13 @@ def create_config(lp, args):
100 lp_username = None
101 if lp.me:
102 lp_username = lp.me.name
103+ if not hasattr(args, 'ppa_name'):
104+ warn("No ppa name given")
105+ return None
106+
107 config['team_name'], config['ppa_name'] = ppa_address_split(args.ppa_name, lp_username)
108 if not config['team_name'] or not config['ppa_name']:
109- warn("Invalid ppa name '{}'".format(args.ppa_name))
110- return None
111+ raise ValueError("Invalid ppa name '{}'".format(args.ppa_name))
112
113 if args.dry_run:
114 config['dry_run'] = True
115@@ -877,7 +884,7 @@ COMMANDS = {
116 }
117
118
119-def main(args):
120+def main(args: argparse.Namespace) -> int:
121 """Main entrypoint for the command.
122
123 :param argparse.Namespace args: Command line arguments.
124diff --git a/tests/helpers.py b/tests/helpers.py
125index c6a55eb..07ad462 100644
126--- a/tests/helpers.py
127+++ b/tests/helpers.py
128@@ -15,18 +15,32 @@ sys.path.insert(0, os.path.realpath(
129 os.path.join(os.path.dirname(os.path.realpath(__file__)), "..")))
130
131 from ppa.ppa import Ppa
132+from ppa.constants import ARCHES_PPA_DEFAULT
133 from ppa.ppa_group import PpaAlreadyExists
134
135
136+class ProcessorMock:
137+ """A stand-in for a Launchpad Processor object."""
138+ def __init__(self, name):
139+ self.name = name
140+
141+
142 class ArchiveMock:
143 """A stand-in for a Launchpad Archive object."""
144 def __init__(self, name, description):
145 self.displayname = name
146 self.description = description
147+ self.processors = [ProcessorMock(proc_name) for proc_name in ARCHES_PPA_DEFAULT]
148
149 def lp_save(self):
150 return True
151
152+ def setProcessors(self, processors):
153+ self.processors = []
154+ for proc in processors:
155+ proc_name = proc.split('/')[-1]
156+ self.processors.append(ProcessorMock(proc_name))
157+
158
159 class PersonMock:
160 """A stand-in for a Launchpad Person object."""
161@@ -38,14 +52,13 @@ class PersonMock:
162 for ppa in self._ppas:
163 if ppa.name == name:
164 raise PpaAlreadyExists(name)
165- new_ppa = Ppa(name, self.name, description)
166- self._ppas.append(new_ppa)
167+ self._ppas.append(Ppa(name, self.name, description))
168 return True
169
170 def getPPAByName(self, name):
171 for ppa in self._ppas:
172 if ppa.name == name:
173- return ArchiveMock(name, ppa.description)
174+ return ArchiveMock(ppa.name, ppa.description)
175 return None
176
177 def lp_save(self):
178diff --git a/tests/test_scripts_ppa.py b/tests/test_scripts_ppa.py
179index 10acc30..40c7714 100644
180--- a/tests/test_scripts_ppa.py
181+++ b/tests/test_scripts_ppa.py
182@@ -226,7 +226,7 @@ def test_create_arg_parser_basic_config(command):
183 assert args.description == 'x'
184 args.description = None
185
186- # Check --enable / -D|--disable
187+ # Check --enable / --disable
188 args = parser.parse_args([command, 'test-ppa', '--enable'])
189 assert args.set_enabled is True
190 args.set_enabled = False
191@@ -355,27 +355,75 @@ def test_create_arg_parser_tests():
192 args.show_urls = None
193
194
195-@pytest.mark.xfail(reason="Unimplemented")
196-def test_create_config():
197- # args = []
198- # TODO: Set config_filename, ppa_name, team_name in args
199- # config = script.create_config(args)
200+@pytest.mark.parametrize('command_line_options, expected_config', [
201+ pytest.param([], {}),
202+ (['status', 'ppa:aa/bb'], {'command': 'status', 'team_name': 'aa', 'ppa_name': 'bb'}),
203+ (['status', 'aa/bb'], {'team_name': 'aa', 'ppa_name': 'bb'}),
204+ (['status', 'bb'], {'team_name': 'me', 'ppa_name': 'bb'}),
205+ (['--debug', 'status', 'ppa:aa/bb'], {'debug': True}),
206+ (['--dry-run', 'status', 'ppa:aa/bb'], {'dry_run': True}),
207+ (['--verbose', 'status', 'ppa:aa/bb'], {'verbose': True}),
208+ (['--quiet', 'status', 'ppa:aa/bb'], {'quiet': True}),
209+ ])
210+def test_create_config_from_args(command_line_options, expected_config):
211+ '''Checks creation of a config object from an argparser object.
212+
213+ Prior tests cover the creation of proper args from the command line;
214+ this test relies on the already-tested argparse machinery to create
215+ various args to pass to create_config() in order to assure that the
216+ right config dict is generated in response.
217+ '''
218+ lp = LpServiceMock()
219+ parser = script.create_arg_parser()
220+ args = parser.parse_args(command_line_options)
221+ config = script.create_config(lp, args)
222+
223+ expected_result = script.DEFAULT_CONFIG
224+ for key, value in expected_config.items():
225+ assert key in config.keys()
226+ assert config[key] == value
227+
228+
229+@pytest.mark.parametrize('args, expected_exception', [
230+ # Bad command
231+ ([None], SystemExit),
232+ ([''], SystemExit),
233+ (['INVALID'], SystemExit),
234+ ([1], TypeError),
235+
236+ # Bad ppa name
237+ (['status'], SystemExit),
238+ (['status', None], ValueError),
239+ (['status', ''], ValueError),
240+ (['status', 'INVALID'], ValueError),
241+ (['status', 1], TypeError),
242+
243+ # Bad argument name
244+ (['--invalid', 'status', 'ppa:aa/bb'], SystemExit),
245+ ])
246+def test_create_config_from_args_error(args, expected_exception):
247+ '''Checks creation of a config object from an argparser object.'''
248+ lp = LpServiceMock()
249+ parser = script.create_arg_parser()
250
251- pass
252+ with pytest.raises(expected_exception):
253+ args = parser.parse_args(args)
254+ script.create_config(lp, args)
255
256
257 def test_command_create(fake_config, monkeypatch):
258 lp = LpServiceMock()
259 monkeypatch.setattr("sys.stdin", io.StringIO('test description'))
260- assert script.command_create(lp, fake_config) == 0
261
262+ assert script.command_create(lp, fake_config) == 0
263 the_ppa = lp.me.getPPAByName(fake_config['ppa_name'])
264+ assert the_ppa
265
266 # Check basic attributes (further coverage is in test_ppa_group.py)
267 assert the_ppa.displayname == fake_config['ppa_name']
268 assert the_ppa.description == 'test description'
269-
270- # TODO: Check processors
271+ assert the_ppa.processors
272+ assert type(the_ppa.processors) is list
273
274
275 @pytest.mark.xfail(reason="Unimplemented")

Subscribers

People subscribed via source and target branches

to all changes: