Skip to content

Commit b9d7ca4

Browse files
richtjaclebergnu
authored andcommitted
Remove duplicities in dependencies
This creates a Dependency class to make dependencies hashable. Thanks to this change, we can easily find duplicates and remove them. This change is really necessary for Job dependencies, where the duplicates can be easily created by adding the same dependency to a test and job. Signed-off-by: Jan Richter <[email protected]>
1 parent 09a6847 commit b9d7ca4

File tree

7 files changed

+120
-19
lines changed

7 files changed

+120
-19
lines changed
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
# This program is free software; you can redistribute it and/or modify
2+
# it under the terms of the GNU General Public License as published by
3+
# the Free Software Foundation; either version 2 of the License, or
4+
# (at your option) any later version.
5+
#
6+
# This program is distributed in the hope that it will be useful,
7+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
8+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
9+
#
10+
# See LICENSE for more details.
11+
#
12+
# Copyright: Red Hat Inc. 2024
13+
# Authors: Jan Richter <[email protected]>
14+
15+
from avocado.core.nrunner.runnable import Runnable
16+
17+
18+
class Dependency:
19+
"""
20+
Data holder for dependency.
21+
"""
22+
23+
def __init__(self, kind=None, uri=None, args=(), kwargs=None):
24+
self._kind = kind
25+
self._uri = uri
26+
self._args = args
27+
self._kwargs = kwargs or {}
28+
29+
@property
30+
def kind(self):
31+
return self._kind
32+
33+
@property
34+
def uri(self):
35+
return self._uri
36+
37+
@property
38+
def args(self):
39+
return self._args
40+
41+
@property
42+
def kwargs(self):
43+
return self._kwargs
44+
45+
def __hash__(self):
46+
return hash(
47+
(
48+
self.kind,
49+
self.uri,
50+
tuple(sorted(self.args)),
51+
tuple(sorted(self.kwargs.items())),
52+
)
53+
)
54+
55+
def __eq__(self, other):
56+
if isinstance(other, Dependency):
57+
return hash(self) == hash(other)
58+
return False
59+
60+
def to_runnable(self, config):
61+
return Runnable(self.kind, self.uri, *self.args, config=config, **self.kwargs)
62+
63+
@classmethod
64+
def from_dictionary(cls, dictionary):
65+
return cls(
66+
dictionary.pop("type", None),
67+
dictionary.pop("uri", None),
68+
dictionary.pop("args", ()),
69+
dictionary,
70+
)

avocado/core/safeloader/docstring.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import json
22
import re
33

4+
from avocado.core.dependencies.dependency import Dependency
5+
46
#: Gets the docstring directive value from a string. Used to tweak
57
#: test behavior in various ways
68
DOCSTRING_DIRECTIVE_RE_RAW = (
@@ -78,7 +80,9 @@ def get_docstring_directives_dependencies(docstring):
7880
if item.startswith("dependency="):
7981
_, dependency_str = item.split("dependency=", 1)
8082
try:
81-
dependencies.append(json.loads(dependency_str))
83+
dependencies.append(
84+
Dependency.from_dictionary(json.loads(dependency_str))
85+
)
8286
except json.decoder.JSONDecodeError:
8387
# ignore dependencies in case of malformed dictionary
8488
continue

avocado/plugins/dependency.py

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
# Copyright: Red Hat Inc. 2021
1313
# Authors: Willian Rampazzo <[email protected]>
1414

15-
from avocado.core.nrunner.runnable import Runnable
1615
from avocado.core.plugin_interfaces import PreTest
1716

1817

@@ -33,15 +32,7 @@ def pre_test_runnables(test_runnable, suite_config=None): # pylint: disable=W02
3332
if not test_runnable.dependencies:
3433
return []
3534
dependency_runnables = []
36-
for dependency in test_runnable.dependencies:
37-
# make a copy to change the dictionary and do not affect the
38-
# original `dependencies` dictionary from the test
39-
dependency_copy = dependency.copy()
40-
kind = dependency_copy.pop("type")
41-
uri = dependency_copy.pop("uri", None)
42-
args = dependency_copy.pop("args", ())
43-
dependency_runnable = Runnable(
44-
kind, uri, *args, config=test_runnable.config, **dependency_copy
45-
)
46-
dependency_runnables.append(dependency_runnable)
35+
unique_dependencies = list(dict.fromkeys(test_runnable.dependencies))
36+
for dependency in unique_dependencies:
37+
dependency_runnables.append(dependency.to_runnable(test_runnable.config))
4738
return dependency_runnables

selftests/check.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,11 @@
2626
"job-api-6": 4,
2727
"job-api-7": 1,
2828
"nrunner-interface": 70,
29-
"nrunner-requirement": 16,
29+
"nrunner-requirement": 20,
3030
"unit": 668,
3131
"jobs": 11,
3232
"functional-parallel": 299,
33-
"functional-serial": 4,
33+
"functional-serial": 5,
3434
"optional-plugins": 0,
3535
"optional-plugins-golang": 2,
3636
"optional-plugins-html": 3,

selftests/functional/serial/requirements.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,26 @@ def test_c(self):
9393
"""
9494
'''
9595

96+
SINGLE_SUCCESS_DUPLCITIES = '''from avocado import Test
97+
from avocado.utils import process
98+
99+
100+
class SuccessTest(Test):
101+
102+
def check_hello(self):
103+
result = process.run("hello", ignore_status=True)
104+
self.assertEqual(result.exit_status, 0)
105+
self.assertIn('Hello, world!', result.stdout_text,)
106+
107+
def test_a(self):
108+
"""
109+
:avocado: dependency={"type": "package", "name": "hello"}
110+
:avocado: dependency={"type": "package", "name": "hello"}
111+
:avocado: dependency={"type": "package", "name": "hello"}
112+
"""
113+
self.check_hello()
114+
'''
115+
96116

97117
class BasicTest(TestCaseTmpDir, Test):
98118

@@ -223,6 +243,20 @@ def test_multiple_fails(self):
223243
result.stdout_text,
224244
)
225245

246+
@skipUnless(os.getenv("CI"), skip_install_message)
247+
def test_dependency_duplicates(self):
248+
with script.Script(
249+
os.path.join(self.tmpdir.name, "test_single_success.py"),
250+
SINGLE_SUCCESS_DUPLCITIES,
251+
) as test:
252+
command = self.get_command(test.path)
253+
result = process.run(command, ignore_status=True)
254+
self.assertEqual(result.exit_status, exit_codes.AVOCADO_ALL_OK)
255+
self.assertIn(
256+
"PASS 1",
257+
result.stdout_text,
258+
)
259+
226260

227261
if __name__ == "__main__":
228262
unittest.main()

selftests/unit/dependencies_resolver.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import unittest
22

3+
from avocado.core.dependencies.dependency import Dependency
34
from avocado.core.nrunner.runnable import Runnable
45
from avocado.plugins.dependency import DependencyResolver
56

@@ -12,8 +13,8 @@ def test_dependencies_runnables(self):
1213
kind="package",
1314
uri=None,
1415
dependencies=[
15-
{"type": "package", "name": "foo"},
16-
{"type": "package", "name": "bar"},
16+
Dependency("package", kwargs={"name": "foo"}),
17+
Dependency("package", kwargs={"name": "bar"}),
1718
],
1819
)
1920
dependency_runnables = DependencyResolver.pre_test_runnables(runnable)

selftests/unit/safeloader_docstring.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import unittest
22

3+
from avocado.core.dependencies.dependency import Dependency
34
from avocado.core.safeloader.docstring import (
45
DOCSTRING_DIRECTIVE_RE,
56
check_docstring_directive,
@@ -165,12 +166,12 @@ def test_get_dependency_empty(self):
165166

166167
def test_dependency_single(self):
167168
raw = ':avocado: dependency={"foo":"bar"}'
168-
exp = [{"foo": "bar"}]
169+
exp = [Dependency(kwargs={"foo": "bar"})]
169170
self.assertEqual(get_docstring_directives_dependencies(raw), exp)
170171

171172
def test_dependency_double(self):
172173
raw = ':avocado: dependency={"foo":"bar"}\n:avocado: dependency={"uri":"http://foo.bar"}'
173-
exp = [{"foo": "bar"}, {"uri": "http://foo.bar"}]
174+
exp = [Dependency(kwargs={"foo": "bar"}), Dependency(uri="http://foo.bar")]
174175
self.assertEqual(get_docstring_directives_dependencies(raw), exp)
175176

176177
def test_directives_regex(self):

0 commit comments

Comments
 (0)