-
Notifications
You must be signed in to change notification settings - Fork 1
feat: Move test formatter from .github/workflows/format_diff.py to python-gardenlinux-lib #294
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 20 commits
Commits
Show all changes
25 commits
Select commit
Hold shift + click to select a range
96a78f7
feat: Move test formatter from .github/workflows/format_diff.py to py…
Leon-hk e1d34d8
fix: add ordering constraints for the markdown files
Leon-hk 7130e57
fix: sorting_function
Leon-hk 98c7e24
Fix Lint
Leon-hk 6cce0a9
Fix more linting
Leon-hk 91b4be0
Merge branch 'main' into feat/port-difference-formatter
Leon-hk 9abe888
fix ruff format
Leon-hk b5e0dc0
Implement feedback, implement difference generator, restructure code
Leon-hk dcda524
fix return value structure
Leon-hk 9ed5afb
fix test and linting
Leon-hk 43d488e
fix test
Leon-hk fc73c21
fix ruff lint
Leon-hk 47955e2
retry lint
Leon-hk 20f5470
fix security issue
Leon-hk 30623b9
Add tests for .oci files
Leon-hk 28c59b2
Merge branch 'main' into feat/port-difference-formatter
Leon-hk 752d9f8
fix test cases and security warning
Leon-hk 246320d
change exit code to 64
Leon-hk 1e952e3
fix nightly_stats extension
Leon-hk a15ba3e
add output option for generator to simplify usage in workflow
Leon-hk 6edf4be
implement feedback
Leon-hk 6dd7c4f
add missing whitelist
Leon-hk 868491c
fix linting
Leon-hk 8fd2af1
Rename gl-diff to gl-feature-fs-diff
Leon-hk 7b89b74
extend the template scope
Leon-hk File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,111 @@ | ||
| #!/usr/bin/env python3 | ||
| # -*- coding: utf-8 -*- | ||
|
|
||
| """ | ||
| gl-diff main entrypoint | ||
| """ | ||
|
|
||
| import argparse | ||
| import json | ||
| import pathlib | ||
| from os.path import basename, dirname | ||
|
|
||
| from .comparator import Comparator | ||
| from .markdown_formatter import MarkdownFormatter | ||
|
|
||
|
|
||
| def generate(args: argparse.Namespace) -> None: | ||
| """ | ||
| Call Comparator | ||
|
|
||
| :param args: Parsed args | ||
|
|
||
| :since: 1.0.0 | ||
| """ | ||
|
|
||
| comparator = Comparator(nightly=args.nightly) | ||
|
|
||
| files, whitelist = comparator.generate(args.a, args.b) | ||
|
|
||
| result = "\n".join(files) | ||
|
|
||
| if files == [] and whitelist: | ||
| result = "whitelist" | ||
|
|
||
| if result != "": | ||
| result += "\n" | ||
|
|
||
| if args.out: | ||
| with open(args.out, "w") as f: | ||
| f.write(result) | ||
| else: | ||
| print(result, end="") | ||
|
|
||
| if files != []: | ||
| exit(64) | ||
|
|
||
|
|
||
| def format(args: argparse.Namespace) -> None: | ||
| """ | ||
| Call MarkdownFormatter | ||
|
|
||
| :param args: Parsed args | ||
|
|
||
| :since: 1.0.0 | ||
| """ | ||
|
|
||
| gardenlinux_root = dirname(args.feature_dir) | ||
|
|
||
| if gardenlinux_root == "": | ||
| gardenlinux_root = "." | ||
|
|
||
| feature_dir_name = basename(args.feature_dir) | ||
|
|
||
| formatter = MarkdownFormatter( | ||
| json.loads(args.flavors_matrix), | ||
| json.loads(args.bare_flavors_matrix), | ||
| pathlib.Path(args.diff_dir), | ||
| pathlib.Path(args.nightly_stats), | ||
| gardenlinux_root, | ||
| feature_dir_name, | ||
| ) | ||
|
|
||
| print(str(formatter), end="") | ||
|
|
||
|
|
||
| def main() -> None: | ||
| """ | ||
| gl-diff main() | ||
|
|
||
| :since: 1.0.0 | ||
| """ | ||
|
|
||
| parser = argparse.ArgumentParser() | ||
|
|
||
| subparser = parser.add_subparsers( | ||
| title="Options", | ||
| description="You can eiter generate the comparison result or format the result to markdown.", | ||
| required=True, | ||
| ) | ||
|
|
||
| generate_parser = subparser.add_parser("generate") | ||
| generate_parser.add_argument("--nightly", action="store_true") | ||
| generate_parser.add_argument("--out") | ||
| generate_parser.add_argument("a") | ||
| generate_parser.add_argument("b") | ||
| generate_parser.set_defaults(func=generate) | ||
|
|
||
| format_parser = subparser.add_parser("format") | ||
| format_parser.add_argument("--feature-dir", default="features") | ||
| format_parser.add_argument("--diff-dir", default="diffs") | ||
| format_parser.add_argument("--nightly-stats", default="nightly_stats.csv") | ||
| format_parser.add_argument("flavors_matrix") | ||
| format_parser.add_argument("bare_flavors_matrix") | ||
| format_parser.set_defaults(func=format) | ||
|
|
||
| args = parser.parse_args() | ||
| args.func(args) | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| main() | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,177 @@ | ||
| # -*- coding: utf-8 -*- | ||
|
|
||
| """ | ||
| diff-files comparator generating the list of files for reproducibility test workflow | ||
| """ | ||
|
|
||
| import filecmp | ||
| import json | ||
| import re | ||
| import tarfile | ||
| import tempfile | ||
| from os import PathLike | ||
| from pathlib import Path | ||
| from typing import Optional | ||
|
|
||
|
|
||
| class Comparator(object): | ||
| """ | ||
| This class takes either two .tar or two .oci files and identifies differences in the filesystems | ||
|
|
||
| :author: Garden Linux Maintainers | ||
| :copyright: Copyright 2026 SAP SE | ||
| :package: gardenlinux | ||
| :subpackage: features | ||
| :since: 1.0.0 | ||
| :license: https://www.apache.org/licenses/LICENSE-2.0 | ||
| Apache License, Version 2.0 | ||
| """ | ||
|
|
||
| _default_whitelist: list[str] = [] | ||
|
|
||
| _nightly_whitelist = [ | ||
|
Leon-hk marked this conversation as resolved.
Outdated
|
||
| r"/etc/apt/sources\.list\.d/gardenlinux\.sources", | ||
| r"/etc/os-release", | ||
| r"/etc/shadow", | ||
| r"/etc/update-motd\.d/05-logo", | ||
| r"/var/lib/apt/lists/packages\.gardenlinux\.io_gardenlinux_dists_[0-9]*\.[0-9]*\.[0-9]*_.*", | ||
| r"/var/lib/apt/lists/packages\.gardenlinux\.io_gardenlinux_dists_[0-9]*\.[0-9]*\.[0-9]*_main_binary-(arm64|amd64)_Packages", | ||
| r"/efi/loader/entries/Default-[0-9]*\.[0-9]*\.[0-9]*-(cloud-)?(arm64|amd64)\.conf", | ||
| r"/efi/Default/[0-9]*\.[0-9]*\.[0-9]*-(cloud-)?(arm64|amd64)/initrd", | ||
| r"/boot/initrd\.img-[0-9]*\.[0-9]*\.[0-9]*-(cloud-)?(arm64|amd64)", | ||
| ] | ||
|
|
||
| def __init__( | ||
| self, nightly: bool = False, whitelist: list[str] = _default_whitelist | ||
| ): | ||
| """ | ||
| Constructor __init__(Comparator) | ||
|
|
||
| :param nightly: Flag indicating if the nightlywhitelist should be used | ||
| :param whitelst: Additional whitelist | ||
|
|
||
| :since: 1.0.0 | ||
| """ | ||
| self.whitelist = whitelist | ||
| if nightly: | ||
| self.whitelist += self._nightly_whitelist | ||
|
|
||
| @staticmethod | ||
| def _unpack(file: PathLike[str]) -> tempfile.TemporaryDirectory[str]: | ||
| """ | ||
| Unpack a .tar archive or .oci image into a temporary dictionary | ||
|
|
||
| :param file: .tar or .oci file | ||
|
|
||
| :return: TemporaryDirectory Temporary directory containing the unpacked file | ||
| :since: 1.0.0 | ||
| """ | ||
|
|
||
| output_dir = tempfile.TemporaryDirectory() | ||
| file = Path(file).resolve() | ||
| if file.name.endswith(".oci"): | ||
| with tempfile.TemporaryDirectory() as extracted: | ||
| # Extract .oci file | ||
| with tarfile.open(file, "r") as tar: | ||
| tar.extractall( | ||
| path=extracted, filter="fully_trusted", members=tar.getmembers() | ||
| ) | ||
|
|
||
| layers_dir = Path(extracted).joinpath("blobs/sha256") | ||
| assert layers_dir.is_dir() | ||
|
|
||
| with open(Path(extracted).joinpath("index.json"), "r") as f: | ||
| index = json.load(f) | ||
|
|
||
| # Only support first manifest | ||
| manifest = index["manifests"][0]["digest"].split(":")[1] | ||
|
|
||
| with open(layers_dir.joinpath(manifest), "r") as f: | ||
| manifest = json.load(f) | ||
|
|
||
| layers = [layer["digest"].split(":")[1] for layer in manifest["layers"]] | ||
|
|
||
| # Extract layers in order | ||
| for layer in layers: | ||
| layer_path = layers_dir.joinpath(layer) | ||
| if tarfile.is_tarfile(layer_path): | ||
| with tarfile.open(layer_path, "r") as tar: | ||
| for member in tar.getmembers(): | ||
| try: | ||
| tar.extract( | ||
| member, | ||
| path=output_dir.name, | ||
| filter="fully_trusted", | ||
| ) | ||
| except tarfile.AbsoluteLinkError: | ||
| # Convert absolute link to relative link | ||
| member.linkpath = ( | ||
| "../" * member.path.count("/") | ||
| + member.linkpath[1:] | ||
| ) | ||
| tar.extract( | ||
| member, | ||
| path=output_dir.name, | ||
| filter="fully_trusted", | ||
| ) | ||
| except tarfile.TarError as e: | ||
| print(f"Skipping {member.name} due to error: {e}") | ||
| else: | ||
| with tarfile.open(file, "r") as tar: | ||
| tar.extractall( | ||
| path=output_dir.name, | ||
| filter="fully_trusted", | ||
| members=tar.getmembers(), | ||
| ) | ||
|
|
||
| return output_dir | ||
|
|
||
| def _diff_files( | ||
| self, cmp: filecmp.dircmp[str], left_root: Optional[Path] = None | ||
| ) -> list[str]: | ||
| """ | ||
| Recursively compare files | ||
|
|
||
| :param cmp: Dircmp to recursively compare | ||
| :param left_root: Left root to obtain the archive relative path | ||
|
|
||
| :return: list[Path] List of paths with different content | ||
| :since: 1.0.0 | ||
| """ | ||
|
|
||
| result = [] | ||
| if not left_root: | ||
| left_root = Path(cmp.left) | ||
| for name in cmp.diff_files: | ||
| result.append(f"/{Path(cmp.left).relative_to(left_root).joinpath(name)}") | ||
| for sub_cmp in cmp.subdirs.values(): | ||
| result += self._diff_files(sub_cmp, left_root=left_root) | ||
| return result | ||
|
|
||
| def generate(self, a: PathLike[str], b: PathLike[str]) -> tuple[list[str], bool]: | ||
| """ | ||
| Compare two .tar/.oci images with each other | ||
|
|
||
| :param a: First .tar/.oci file | ||
| :param b: Second .tar/.oci file | ||
|
|
||
| :return: list[Path], bool Filtered list of paths with different content and flag indicating if whitelist was applied | ||
| :since: 1.0.0 | ||
| """ | ||
|
|
||
| if filecmp.cmp(a, b, shallow=False): | ||
| return [], False | ||
|
|
||
| with self._unpack(a) as unpacked_a, self._unpack(b) as unpacked_b: | ||
| cmp = filecmp.dircmp(unpacked_a, unpacked_b, shallow=False) | ||
|
|
||
| diff_files = self._diff_files(cmp) | ||
|
|
||
| filtered = [ | ||
| file | ||
| for file in diff_files | ||
| if not any(re.match(pattern, file) for pattern in self.whitelist) | ||
| ] | ||
| whitelist = len(diff_files) != len(filtered) | ||
|
|
||
| return filtered, whitelist | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.