Skip to content

Commit 6edf4be

Browse files
committed
implement feedback
1 parent a15ba3e commit 6edf4be

File tree

5 files changed

+92
-94
lines changed

5 files changed

+92
-94
lines changed

src/gardenlinux/features/parser.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ def graph(self) -> networkx.Graph:
8484

8585
if self._graph is None:
8686
feature_yaml_files = glob("{0}/*/info.yaml".format(self._feature_base_dir))
87-
features = [self.read_feature_yaml(i) for i in feature_yaml_files]
87+
features = [self._read_feature_yaml(i) for i in feature_yaml_files]
8888

8989
feature_graph = networkx.DiGraph()
9090

@@ -345,7 +345,7 @@ def _get_node_features(self, node: Dict[str, Any]) -> Dict[str, Any]:
345345

346346
return node.get("content", {}).get("features", {}) # type: ignore[no-any-return]
347347

348-
def read_feature_yaml(self, feature_yaml_file: str) -> Dict[str, Any]:
348+
def _read_feature_yaml(self, feature_yaml_file: str) -> Dict[str, Any]:
349349
"""
350350
Reads and returns the content of the given features file.
351351

src/gardenlinux/features/reproducibility/__main__.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@
1313
from .comparator import Comparator
1414
from .markdown_formatter import MarkdownFormatter
1515

16+
# Use custom exit code to make a controlled failure visible
17+
DIFFERENCE_DETECTED_EXIT_CODE = 64
18+
1619

1720
def generate(args: argparse.Namespace) -> None:
1821
"""
@@ -42,7 +45,7 @@ def generate(args: argparse.Namespace) -> None:
4245
print(result, end="")
4346

4447
if files != []:
45-
exit(64)
48+
exit(DIFFERENCE_DETECTED_EXIT_CODE)
4649

4750

4851
def format(args: argparse.Namespace) -> None:

src/gardenlinux/features/reproducibility/comparator.py

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
"""
66

77
import filecmp
8+
import importlib
9+
import importlib.resources
810
import json
911
import re
1012
import tarfile
@@ -29,17 +31,9 @@ class Comparator(object):
2931

3032
_default_whitelist: list[str] = []
3133

32-
_nightly_whitelist = [
33-
r"/etc/apt/sources\.list\.d/gardenlinux\.sources",
34-
r"/etc/os-release",
35-
r"/etc/shadow",
36-
r"/etc/update-motd\.d/05-logo",
37-
r"/var/lib/apt/lists/packages\.gardenlinux\.io_gardenlinux_dists_[0-9]*\.[0-9]*\.[0-9]*_.*",
38-
r"/var/lib/apt/lists/packages\.gardenlinux\.io_gardenlinux_dists_[0-9]*\.[0-9]*\.[0-9]*_main_binary-(arm64|amd64)_Packages",
39-
r"/efi/loader/entries/Default-[0-9]*\.[0-9]*\.[0-9]*-(cloud-)?(arm64|amd64)\.conf",
40-
r"/efi/Default/[0-9]*\.[0-9]*\.[0-9]*-(cloud-)?(arm64|amd64)/initrd",
41-
r"/boot/initrd\.img-[0-9]*\.[0-9]*\.[0-9]*-(cloud-)?(arm64|amd64)",
42-
]
34+
_nightly_whitelist = json.loads(
35+
importlib.resources.read_text(__name__, "nightly_whitelist.json")
36+
)
4337

4438
def __init__(
4539
self, nightly: bool = False, whitelist: list[str] = _default_whitelist

src/gardenlinux/features/reproducibility/diff_parser.py

Lines changed: 46 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -56,25 +56,23 @@ def __init__(
5656
self._parser = Parser(gardenlinux_root, feature_dir_name, logger)
5757
self._feature_dir_name = Path(self._gardenlinux_root).joinpath(feature_dir_name)
5858

59-
self.all: set[str] = set()
60-
self.successful: set[str] = set()
61-
self.whitelist: set[str] = set()
59+
self.all_flavors: set[str] = set()
60+
self.reproducible_flavors: set[str] = set()
61+
self.passed_by_whitelist: set[str] = set()
6262
self.expected_falvors: set[str] = set()
6363
self.missing_flavors: set[str] = set()
6464
self.unexpected_falvors: set[str] = set()
6565

66-
def read_feature_info(self, feature: str) -> Any:
66+
def sort_features(self, graph: nx.DiGraph) -> list[str]:
6767
"""
68-
Read the content of the feature info.yaml
68+
Forward sorting implemented in the parser
6969
70-
:param feature: The queried feature
70+
:param graph: The feature graph
7171
72-
:return: Dict[str, Any] Parsed content of the features' info.yaml file
72+
:return: list[str] Sorted features considering the type
7373
:since: 1.0.0
7474
"""
75-
return self._parser.read_feature_yaml(
76-
str(self._feature_dir_name.joinpath(f"{feature}/info.yaml"))
77-
)["content"]
75+
return self._parser.sort_graph_nodes(graph)
7876

7977
def parse(
8078
self,
@@ -92,10 +90,10 @@ def parse(
9290
:since: 1.0.0
9391
"""
9492

95-
self.all = set()
96-
self.successful = set()
97-
self.whitelist = set()
98-
failed = {} # {flavor: [files...]}
93+
self.all_flavors = set()
94+
self.reproducible_flavors = set()
95+
self.passed_by_whitelist = set()
96+
non_reproducible_flavors = {} # {flavor: [files...]}
9997

10098
diff_dir = Path(self._gardenlinux_root).joinpath(diff_dir)
10199

@@ -110,32 +108,32 @@ def parse(
110108
content = f.read()
111109

112110
flavor = flavor.rstrip(self._SUFFIX)
113-
self.all.add(flavor)
111+
self.all_flavors.add(flavor)
114112
if content == "":
115-
self.successful.add(flavor)
113+
self.reproducible_flavors.add(flavor)
116114
elif content == "whitelist\n":
117-
self.successful.add(flavor)
118-
self.whitelist.add(flavor)
115+
self.reproducible_flavors.add(flavor)
116+
self.passed_by_whitelist.add(flavor)
119117
else:
120-
failed[flavor] = content.split("\n")[:-1]
118+
non_reproducible_flavors[flavor] = content.split("\n")[:-1]
121119

122-
self.missing_flavors = self.expected_falvors - self.all
123-
self.unexpected_falvors = self.all - self.expected_falvors
120+
self.missing_flavors = self.expected_falvors - self.all_flavors
121+
self.unexpected_falvors = self.all_flavors - self.expected_falvors
124122

125123
# Map files to flavors
126-
affected: Dict[str, set[str]] = {} # {file: {flavors...}}
127-
for flavor in failed:
128-
for file in failed[flavor]:
129-
if file not in affected:
130-
affected[file] = set()
131-
affected[file].add(flavor)
132-
133-
# Merge files affected by the same flavors by mapping flavor sets to files
124+
affected_flavors: Dict[str, set[str]] = {} # {file: {flavors...}}
125+
for flavor in non_reproducible_flavors:
126+
for file in non_reproducible_flavors[flavor]:
127+
if file not in affected_flavors:
128+
affected_flavors[file] = set()
129+
affected_flavors[file].add(flavor)
130+
131+
# Merge files affected_flavors by the same flavors by mapping flavor sets to files
134132
self._bundled: Dict[frozenset[str], set[str]] = {} # {{flavors...}: {files...}}
135-
for file in affected:
136-
if frozenset(affected[file]) not in self._bundled:
137-
self._bundled[frozenset(affected[file])] = set()
138-
self._bundled[frozenset(affected[file])].add(file)
133+
for file in affected_flavors:
134+
if frozenset(affected_flavors[file]) not in self._bundled:
135+
self._bundled[frozenset(affected_flavors[file])] = set()
136+
self._bundled[frozenset(affected_flavors[file])].add(file)
139137

140138
def intersectionTrees(
141139
self,
@@ -150,27 +148,32 @@ def intersectionTrees(
150148
# Compute the intersecting features of the affected flavors and store them in a graph to allow hierarchical formatting
151149
trees = {}
152150
for flavors in self._bundled:
153-
first = True
154151
tree = None
155152
# Compute the intersecting features of all affected flavors
156153
for flavor in flavors:
157154
# Ignore bare flavors, as they may not be affected due to removing the file and could therefore disrupt the analysis
158155
if not flavor.startswith("bare-"):
159-
t = self._parser.filter(self._remove_arch.sub("", flavor))
160-
if first:
161-
first = False
162-
tree = t
163-
else:
164-
tree = nx.intersection(tree, t)
156+
# Compute intersecting features
157+
tree = self._parser.filter(
158+
self._remove_arch.sub("", flavor),
159+
additional_filter_func=tree.__contains__
160+
if tree is not None
161+
else lambda _: True,
162+
)
165163

166164
# Remove any features which are contained in unaffected flavors, as they cannot cause the problem
167165
if tree is not None:
168-
unaffected = self.all - flavors
166+
# Unfreeze tree
167+
tree = nx.DiGraph(tree)
168+
unaffected = self.all_flavors - flavors
169+
merged_features: set[str] = set()
169170
for flavor in unaffected:
170171
# Again, ignore bare flavors
171172
if not flavor.startswith("bare-"):
172-
t = self._parser.filter(self._remove_arch.sub("", flavor))
173-
tree.remove_nodes_from(n for n in t)
173+
merged_features.update(
174+
self._parser.filter(self._remove_arch.sub("", flavor))
175+
)
176+
tree.remove_nodes_from(n for n in merged_features)
174177
else:
175178
tree = nx.DiGraph()
176179

src/gardenlinux/features/reproducibility/markdown_formatter.py

Lines changed: 35 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,16 @@
77
import logging
88
from os import PathLike
99
from pathlib import Path
10+
from string import Template
1011
from typing import Collection, Optional
1112

1213
import networkx as nx
1314
from attr import dataclass
14-
from networkx.algorithms.traversal.depth_first_search import dfs_tree
1515

1616
from gardenlinux.features.reproducibility.diff_parser import DiffParser
1717

18+
SUCCESS_TRESHOLD = 50.0
19+
1820

1921
@dataclass
2022
class Nightly:
@@ -40,6 +42,10 @@ class MarkdownFormatter(object):
4042

4143
_DROPDOWN_THRESHOLD = 10
4244

45+
_nighly_url_template = Template(
46+
"https://github.com/gardenlinux/gardenlinux/actions/runs/$id"
47+
)
48+
4349
def __init__(
4450
self,
4551
flavors_matrix: dict[str, list[dict[str, str]]],
@@ -69,24 +75,6 @@ def __init__(
6975

7076
self._nightly_stats = Path(nightly_stats)
7177

72-
def _node_key(self, node: str) -> str:
73-
"""
74-
Key order function to sort platforms before elements, platforms before flags and elements before flags
75-
76-
:param node: The node name (can be any of platform, element or flag)
77-
78-
:return: ("1-" || "2-" || "2-") + node
79-
:since: 1.0.0
80-
"""
81-
82-
info = self._diff_parser.read_feature_info(node)
83-
if info["type"] == "platform":
84-
return "1-" + node
85-
elif info["type"] == "element":
86-
return "2-" + node
87-
else:
88-
return "3-" + node
89-
9078
def _treeStr(
9179
self, graph: nx.DiGraph, found: Optional[set[str]] = None
9280
) -> tuple[str, set[str]]:
@@ -104,17 +92,25 @@ def _treeStr(
10492
found = set()
10593

10694
s = ""
107-
for node in sorted(graph, key=self._node_key):
95+
for node in self._diff_parser.sort_features(graph):
10896
if node not in found and len(list(graph.predecessors(node))) == 0:
10997
found.add(node)
11098
if len(set(graph.successors(node)) - found) == 0:
11199
s += str(node) + "\n"
112100
else:
113101
s += str(node) + ":\n"
114-
for successor in sorted(
115-
set(graph.successors(node)) - found, key=self._node_key
102+
# This filters all direct successors of node in a sorted order
103+
for successor in self._diff_parser.sort_features(
104+
nx.subgraph_view(
105+
graph, filter_node=list(graph.successors(node)).__contains__
106+
)
116107
):
117-
st, fnd = self._treeStr(dfs_tree(graph, successor), found)
108+
# Manually create dfs tree to keep attributes
109+
subtree = nx.DiGraph(graph)
110+
subtree.remove_edges_from(
111+
subtree.edges - nx.dfs_edges(graph, successor)
112+
)
113+
st, fnd = self._treeStr(subtree, found)
118114
found.update(fnd)
119115
s += " " + st.replace("\n", "\n ") + "\n"
120116
# Remove last linebreak as the last line can contain spaces
@@ -156,14 +152,14 @@ def _format_nighlty_stats(self) -> str:
156152
Nightly(*n.split(",")) for n in f.read().rstrip().split("\n")
157153
)
158154
if nightly_a.run_number != "":
159-
result += f"\n\nComparison of nightly **[#{nightly_a.run_number}](https://github.com/gardenlinux/gardenlinux/actions/runs/{nightly_a.id})** \
160-
and **[#{nightly_b.run_number}](https://github.com/gardenlinux/gardenlinux/actions/runs/{nightly_b.id})**"
155+
result += f"\n\nComparison of nightly **[#{nightly_a.run_number}]({self._nighly_url_template.substitute(id=nightly_a.id)})** \
156+
and **[#{nightly_b.run_number}]({self._nighly_url_template.substitute(id=nightly_b.id)})**"
161157
if nightly_a.commit != nightly_b.commit:
162158
result += f"\n\n⚠️ The nightlies used different commits: `{nightly_a.commit[:7]}` (#{nightly_a.run_number}) != `{nightly_b.commit[:7]}` (#{nightly_b.run_number})"
163159
if nightly_a.run_number == nightly_b.run_number:
164-
result += f"\n\n⚠️ Comparing the nightly **[#{nightly_a.run_number}](https://github.com/gardenlinux/gardenlinux/actions/runs/{nightly_a.id})** to itself can not reveal any issues"
160+
result += f"\n\n⚠️ Comparing the nightly **[#{nightly_a.run_number}]({self._nighly_url_template.substitute(id=nightly_a.id)})** to itself can not reveal any issues"
165161
else:
166-
result += f"\n\nComparison of the latest nightly **[#{nightly_b.run_number}](https://github.com/gardenlinux/gardenlinux/actions/runs/{nightly_b.id})** \
162+
result += f"\n\nComparison of the latest nightly **[#{nightly_b.run_number}]({self._nighly_url_template.substitute(id=nightly_b.id)})** \
167163
with a new build"
168164
if nightly_a.commit != nightly_b.commit:
169165
result += f"\n\n⚠️ The build used different commits: `{nightly_b.commit[:7]}` (#{nightly_b.run_number}) != `{nightly_a.commit[:7]}` (new build)"
@@ -193,7 +189,7 @@ def _header(
193189
successrate = round(
194190
100
195191
* (
196-
len(self._diff_parser.successful)
192+
len(self._diff_parser.reproducible_flavors)
197193
/ len(self._diff_parser.expected_falvors)
198194
),
199195
1,
@@ -202,8 +198,8 @@ def _header(
202198
emoji = (
203199
"✅"
204200
if len(self._diff_parser.expected_falvors)
205-
== len(self._diff_parser.successful)
206-
else ("⚠️" if successrate >= 50.0 else "❌")
201+
== len(self._diff_parser.reproducible_flavors)
202+
else ("⚠️" if successrate >= SUCCESS_TRESHOLD else "❌")
207203
)
208204

209205
total_count = len(self._diff_parser.expected_falvors)
@@ -223,10 +219,10 @@ def _header(
223219
if self._nightly_stats.is_file():
224220
explanation += self._format_nighlty_stats()
225221

226-
if len(self._diff_parser.whitelist) > 0:
222+
if len(self._diff_parser.passed_by_whitelist) > 0:
227223
explanation += (
228224
"\n\n<details><summary>📃 These flavors only passed due to the nightly whitelist</summary><pre>"
229-
+ "<br>".join(sorted(self._diff_parser.whitelist))
225+
+ "<br>".join(sorted(self._diff_parser.passed_by_whitelist))
230226
+ "</pre></details>"
231227
)
232228

@@ -242,7 +238,7 @@ def _header(
242238
explanation += (
243239
""
244240
if len(self._diff_parser.expected_falvors)
245-
<= len(self._diff_parser.successful)
241+
<= len(self._diff_parser.reproducible_flavors)
246242
else "\n\n*The mentioned features are included in every affected flavor and not included in every unaffected flavor.*"
247243
)
248244

@@ -299,19 +295,21 @@ def sorting_function(files: frozenset[str]) -> tuple[int, str]:
299295
row += "|\n"
300296
rows += row
301297

302-
if len(self._diff_parser.successful) > 0:
298+
if len(self._diff_parser.reproducible_flavors) > 0:
303299
# Success row
304300
row = "|"
305301
row += "✅ No problems found"
306302
row += "|"
307-
row += f"**{round(100 * (len(self._diff_parser.successful) / len(self._diff_parser.expected_falvors)), 1)}%**<br>"
308-
row += self._dropdown(self._diff_parser.successful)
303+
row += f"**{round(100 * (len(self._diff_parser.reproducible_flavors) / len(self._diff_parser.expected_falvors)), 1)}%**<br>"
304+
row += self._dropdown(self._diff_parser.reproducible_flavors)
309305
row += "|"
310306
row += "-"
311307
row += "|\n"
312308
rows += row
313309

314-
if len(self._diff_parser.successful) < len(self._diff_parser.expected_falvors):
310+
if len(self._diff_parser.reproducible_flavors) < len(
311+
self._diff_parser.expected_falvors
312+
):
315313
rows += "\n*To add affected files to the whitelist, edit the `whitelist` variable in python-gardenlinux-lib `src/gardenlinux/features/reproducibility/comparator.py`*\n"
316314

317315
return table.format(rows=rows)

0 commit comments

Comments
 (0)