Repository: osbuild/osbuild
Pull request #1915 (2024-10-24 18:05:38)

README: document test prerequisites
Is this a good place to document this or rather introduce an `extras_require` in `setup.py`?

Also there seem to be more missing and the tests requiring an nfs-server need some more prerequisites to be executed, I guess?
e.g. `sudo python -m pytest --rootdir=$(pwd) -v test/mod/test_util_fscache_coherency.py`  
fails even when starting an unconfigured nfs-server

schuellerf: I'll leave the `-rs` option for a followup PR to keep this one nice and tidy.
But I'm all in for adding _all_ needed packages (like podman) as `dnf` is fine when it's already installed and it's more a copy'n'paste command then 👌
schuellerf: btw. I was also tempted to replace 
```
	@[[ $${EUID} -eq 0 ]] || (echo "Error: Root privileges required!"; exit 1)
	@$(PYTHON3) -m pytest …
```
with
```
	@sudo $(PYTHON3) -m pytest …
```
It's _required_ anyway, any reason why to fail instead of just running as required?
mvo5: > I'll leave the `-rs` option for a followup PR to keep this one nice and tidy. But I'm all in for adding _all_ needed packages (like podman) as `dnf` is fine when it's already installed and it's more a copy'n'paste command then 👌

Yeah, totally, sorry, I didn't meant to imply it should be part of this PR :)

Patch:
diff --git a/README.md b/README.md
index 8c7f1307..156caccf 100644
--- a/README.md
+++ b/README.md
@@ -68,6 +68,13 @@ To verify changes made to the code use included makefile rules:
  * `make test-all` to run base set of tests
  * `sudo make test-run` to run extended set of tests (takes long time)
 
+Also keep in mind that some tests require those prerequisites,
+otherwise they are skipped
+
+```
+sudo dnf install -y systemd-boot-unsigned erofs-utils pykickstart podman xfsprogs
+```
+
 ## Installation
 
 Installing `osbuild` requires to not only install the `osbuild` module, but also
diff --git a/stages/org.osbuild.mkdir b/stages/org.osbuild.mkdir
index f04549f6..d2d11a7a 100755
--- a/stages/org.osbuild.mkdir
+++ b/stages/org.osbuild.mkdir
@@ -3,23 +3,26 @@ import os
 import sys
 
 import osbuild.api
-from osbuild.util.path import in_tree
+from osbuild.util import parsing
 
 
-def main(tree, options):
+def main(args):
+    options = args["options"]
+
     for item in options["paths"]:
         path = item["path"]
         mode = item.get("mode", 0o777)
         parents = item.get("parents", False)
         exist_ok = item.get("exist_ok", False)
 
-        if not path.startswith("/"):
-            print("WARNING: relative path used, this is discouraged!")
-
-        target = os.path.join(tree, path.lstrip("/"))
-        if not in_tree(target, tree):
-            raise ValueError(f"path {path} not in tree")
+        if "://" not in path:
+            if not path.startswith("/"):
+                print("WARNING: relative path used, this is discouraged!")
+                path = f"tree:///{path}"
+            else:
+                path = f"tree://{path}"
 
+        target = parsing.parse_location(path, args)
         if parents:
             os.makedirs(target, mode=mode, exist_ok=exist_ok)
         else:
@@ -33,5 +36,4 @@ def main(tree, options):
 
 
 if __name__ == "__main__":
-    args = osbuild.api.arguments()
-    sys.exit(main(args["tree"], args["options"]))
+    sys.exit(main(osbuild.api.arguments()))
diff --git a/stages/org.osbuild.mkdir.meta.json b/stages/org.osbuild.mkdir.meta.json
index 5534120a..6cebaaf5 100644
--- a/stages/org.osbuild.mkdir.meta.json
+++ b/stages/org.osbuild.mkdir.meta.json
@@ -1,5 +1,5 @@
 {
-  "summary": "Create directories within the tree.",
+  "summary": "Create directories within the tree or mount.",
   "description": [
     "Can create one or more directories, optionally also the",
     "intermediate directories. The stage can gracefully handle",
@@ -31,8 +31,23 @@
             ],
             "properties": {
               "path": {
-                "type": "string",
-                "pattern": "^\\/?(?!\\.\\.)((?!\\/\\.\\.\\/).)+$"
+                "anyOf": [
+                  {
+                    "type": "string",
+                    "description": "Target path, if a tree",
+                    "pattern": "^\\/?(?!\\.\\.)((?!\\/\\.\\.\\/).)+$"
+                  },
+                  {
+                    "type": "string",
+                    "description": "Target path, if a mount",
+                    "pattern": "^mount://.+"
+                  },
+                  {
+                    "type": "string",
+                    "description": "Target path, if a tree",
+                    "pattern": "^tree://.+"
+                  }
+                ]
               },
               "mode": {
                 "type": "number",


Test patch:
diff --git a/stages/test/test_mkdir.py b/stages/test/test_mkdir.py
new file mode 100644
index 00000000..6285a380
--- /dev/null
+++ b/stages/test/test_mkdir.py
@@ -0,0 +1,89 @@
+#!/usr/bin/python3
+
+import contextlib
+import os
+import subprocess
+
+import pytest  # type: ignore
+
+from osbuild.testutil import has_executable
+
+STAGE_NAME = "org.osbuild.mkdir"
+
+
+def test_mkdir(tmp_path, stage_module):
+    tree = tmp_path / "tree"
+    tree.mkdir()
+
+    options = {
+        "paths": [
+            {"path": "/fake_dir"},
+            {"path": "fake_relative_dir"}
+        ]
+    }
+    args = {
+        "tree": f"{tree}",
+        "options": options
+    }
+    stage_module.main(args)
+    assert (tree / "fake_dir").exists()
+    assert (tree / "fake_relative_dir").exists()
+
+
+def test_mkdir_on_a_tree(tmp_path, stage_module):
+    tree = tmp_path / "tree"
+    tree.mkdir()
+
+    options = {
+        "paths": [
+            {
+                "path": "tree:///fake_parent/fake_dir",
+                "parents": 1
+            }
+        ]
+    }
+    args = {
+        "tree": f"{tree}",
+        "options": options
+    }
+    stage_module.main(args)
+    assert (tree / "fake_parent/fake_dir").exists()
+
+
+@pytest.mark.skipif(os.getuid() != 0, reason="needs root")
+@pytest.mark.skipif(not has_executable("mkfs.ext4"), reason="need mkfs.ext4")
+def test_mkdir_on_a_mount(tmp_path, stage_module):
+    tree = tmp_path / "tree"
+    tree.mkdir()
+
+    # Create fake EXT4 disk image
+    fake_disk_path = tmp_path / "fake.img"
+    with fake_disk_path.open("w") as fp:
+        fp.truncate(10 * 1024 * 1024)
+    subprocess.run(
+        ["mkfs.ext4", os.fspath(fake_disk_path)], check=True)
+
+    fake_disk_mnt = tmp_path / "mounts"
+    fake_disk_mnt.mkdir()
+
+    with contextlib.ExitStack() as cm:
+        subprocess.run(["mount", fake_disk_path, fake_disk_mnt], check=True)
+        cm.callback(subprocess.run, ["umount", fake_disk_mnt], check=True)
+
+        options = {
+            "paths": [
+                {
+                    "path": "mount:///fake_parent/fake_dir",
+                    "parents": 1
+                }
+            ]
+        }
+        args = {
+            "tree": f"{tree}",
+            "options": options,
+            "paths": {
+                "mounts": fake_disk_mnt,
+            }
+        }
+        stage_module.main(args)
+        assert (fake_disk_mnt / "fake_parent/fake_dir").exists()