This is an automated email from the ASF dual-hosted git repository.

jimjag pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/openoffice-devtools.git


The following commit(s) were added to refs/heads/main by this push:
     new 6431a99  Provide func tests for our DMG signer
6431a99 is described below

commit 6431a9919782f9041c09b0fa89be0e03cdafdd1b
Author: Jim Jagielski <[email protected]>
AuthorDate: Sat Sep 12 15:21:55 2026 -0400

    Provide func tests for our DMG signer
---
 release-scripts/macosx-remote-sign.sh            |  48 ++++-
 release-scripts/pyproject.toml                   |  17 ++
 release-scripts/tests/conftest.py                | 169 +++++++++++++++
 release-scripts/tests/test_macosx_remote_sign.py | 257 +++++++++++++++++++++++
 4 files changed, 484 insertions(+), 7 deletions(-)

diff --git a/release-scripts/macosx-remote-sign.sh 
b/release-scripts/macosx-remote-sign.sh
index 93fe957..0af7dc4 100755
--- a/release-scripts/macosx-remote-sign.sh
+++ b/release-scripts/macosx-remote-sign.sh
@@ -37,7 +37,8 @@
 #                        extracted app and the rebuilt dmg (see its --help)
 #       --release        passed through: fail if spctl rejects the result
 #       --sha256 HASH    verify <unsigned.dmg> against this checksum before
-#                        doing anything else
+#                        doing anything else (a bare digest or a full
+#                        "shasum -a 256" checksum line both work)
 #   -h, --help
 #
 # Known limitation: the dmg is rebuilt with a plain "hdiutil create", without
@@ -80,7 +81,7 @@ while [ $# -gt 0 ]; do
                --sha256)
                        [ $# -ge 2 ] || { echo "$1 requires an argument" >&2; 
exit 2; }
                        EXPECT_SHA256="$2"; shift 2 ;;
-               -h|--help)         sed -n '2,49p' "$0"; exit 0 ;;
+               -h|--help)         sed -n '2,50p' "$0"; exit 0 ;;
                -*)                echo "unknown option: $1" >&2; exit 2 ;;
                *)
                        if [ -z "$SRC_DMG" ]; then SRC_DMG="$1"
@@ -104,17 +105,32 @@ done
        exit 2
 }
 [ -f "$SRC_DMG" ] || { echo "no such file: $SRC_DMG" >&2; exit 1; }
+[ -d "$OUT_DMG" ] && { echo "output path is a directory: $OUT_DMG" >&2; exit 
2; }
+case "$OUT_DMG" in
+       *.dmg) ;;
+       *) OUT_DMG="$OUT_DMG.dmg" ;;
+esac
+if [ "$SRC_DMG" -ef "$OUT_DMG" ]; then
+       echo "output path is the input dmg: $OUT_DMG (write to a different 
file)" >&2
+       exit 2
+fi
 
 MOUNT_POINT=""
 STAGING_DIR=""
+TEMP_DMG=""
 cleanup() {
        [ -z "$MOUNT_POINT" ] || hdiutil detach "$MOUNT_POINT" -quiet 
2>/dev/null || true
        [ -z "$STAGING_DIR" ] || rm -rf "$STAGING_DIR"
+       [ -z "$TEMP_DMG" ] || rm -f "$TEMP_DMG"
 }
 trap cleanup EXIT
 
 if [ -n "$EXPECT_SHA256" ]; then
        echo "==> verifying checksum of $SRC_DMG"
+       # Accept either a bare digest or a full checksum line as written by
+       # shasum/hash-sign.sh ("<hash>  <file>" / "<hash> *<file>"): compare
+       # against the first field.
+       EXPECT_SHA256="${EXPECT_SHA256%%[[:space:]]*}"
        actual=$(shasum -a 256 "$SRC_DMG" | awk '{print $1}')
        if [ "$actual" != "$EXPECT_SHA256" ]; then
                echo "checksum mismatch: expected $EXPECT_SHA256, got $actual" 
>&2
@@ -124,12 +140,23 @@ if [ -n "$EXPECT_SHA256" ]; then
 fi
 
 echo "==> mounting $SRC_DMG"
-MOUNT_POINT=$(hdiutil attach -readonly -nobrowse "$SRC_DMG" | tail -1 | awk 
-F'\t' '{print $NF}')
+# Parse the -plist form rather than the human-readable table: the mount point
+# is whichever system-entity has one (the first often does not), and its
+# tab-column position is not a documented guarantee.
+ATTACH_PLIST=$(hdiutil attach -readonly -nobrowse -plist "$SRC_DMG")
+MOUNT_POINT=""
+ATTACH_COUNT=$(printf '%s' "$ATTACH_PLIST" | plutil -extract system-entities 
raw -o - -)
+i=0
+while [ "$i" -lt "$ATTACH_COUNT" ]; do
+       mp=$(printf '%s' "$ATTACH_PLIST" | plutil -extract 
"system-entities.$i.mount-point" raw -o - - 2>/dev/null || true)
+       [ -z "$mp" ] || MOUNT_POINT="$mp"
+       i=$((i + 1))
+done
 [ -n "$MOUNT_POINT" ] && [ -d "$MOUNT_POINT" ] || {
        echo "could not mount $SRC_DMG" >&2
        exit 1
 }
-VOLUME_NAME=$(diskutil info "$MOUNT_POINT" | awk -F': +' '/Volume Name/{print 
$2; exit}')
+VOLUME_NAME=$(diskutil info "$MOUNT_POINT" | awk '/Volume Name/{sub(/^[^:]*: 
+/, ""); print; exit}')
 [ -n "$VOLUME_NAME" ] || VOLUME_NAME=$(basename "$MOUNT_POINT")
 
 STAGING_DIR=$(mktemp -d)
@@ -162,9 +189,16 @@ echo "==> signing $APP"
 
 echo "==> building $OUT_DMG  (volume: $VOLUME_NAME)"
 mkdir -p "$(dirname "$OUT_DMG")"
-hdiutil create -srcfolder "$STAGING_DIR" -volname "$VOLUME_NAME" -fs HFS+ 
-format UDZO -ov "$OUT_DMG"
+# Build and sign the image under a temp name in the output directory (same
+# filesystem, so the final mv is atomic) and only publish it once the signing
+# succeeds: a failure here must not leave an unsigned image at $OUT_DMG.
+TEMP_DMG="$OUT_DMG.tmp.$$.dmg"
+hdiutil create -srcfolder "$STAGING_DIR" -volname "$VOLUME_NAME" -fs HFS+ 
-format UDZO -ov "$TEMP_DMG"
+
+echo "==> signing $TEMP_DMG"
+"$CODESIGN" "${sign_args[@]}" "$TEMP_DMG"
 
-echo "==> signing $OUT_DMG"
-"$CODESIGN" "${sign_args[@]}" "$OUT_DMG"
+mv -f "$TEMP_DMG" "$OUT_DMG"
+TEMP_DMG=""
 
 echo "==> done: $OUT_DMG"
diff --git a/release-scripts/pyproject.toml b/release-scripts/pyproject.toml
new file mode 100644
index 0000000..a80130d
--- /dev/null
+++ b/release-scripts/pyproject.toml
@@ -0,0 +1,17 @@
+[project]
+name = "openoffice-devtools-release-scripts-tests"
+version = "0.1.0"
+description = "Tests for release-scripts/macosx-remote-sign.sh"
+requires-python = ">=3.12"
+
+[dependency-groups]
+test = ["pytest>=8.0"]
+
+[tool.uv]
+package = false
+default-groups = ["test"]
+
+[tool.pytest.ini_options]
+minversion = "8.0"
+testpaths = ["tests"]
+python_files = ["test_*.py"]
diff --git a/release-scripts/tests/conftest.py 
b/release-scripts/tests/conftest.py
new file mode 100644
index 0000000..3c0f37c
--- /dev/null
+++ b/release-scripts/tests/conftest.py
@@ -0,0 +1,169 @@
+################################################################
+#
+#  Licensed to the Apache Software Foundation (ASF) under one
+#  or more contributor license agreements.  See the NOTICE file
+#  distributed with this work for additional information
+#  regarding copyright ownership.  The ASF licenses this file
+#  to you under the Apache License, Version 2.0 (the
+#  "License"); you may not use this file except in compliance
+#  with the License.  You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing,
+#  software distributed under the License is distributed on an
+#  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+#  KIND, either express or implied.  See the License for the
+#  specific language governing permissions and limitations
+#  under the License.
+#
+################################################################
+
+"""Shared fixtures for the macosx-remote-sign.sh tests.
+
+No Developer ID key is available here, so the wrapper is exercised with a stub
+macosx-codesign.sh. That covers the wrapper's 
mount/copy/rebuild/publish/cleanup
+logic only; the signature itself is the delegate's responsibility.
+"""
+
+import os
+import shutil
+import stat
+import subprocess
+from pathlib import Path
+
+import pytest
+
+SCRIPT_DIR = Path(__file__).resolve().parent.parent
+WRAPPER = SCRIPT_DIR / "macosx-remote-sign.sh"
+
+IDENTITY = "Developer ID Application: Test"
+
+requires_macos = pytest.mark.skipif(
+    os.uname().sysname != "Darwin", reason="requires macOS (hdiutil, diskutil, 
plutil)"
+)
+
+
+def _run(args, **kwargs):
+    return subprocess.run(args, capture_output=True, text=True, **kwargs)
+
+
+def make_dmg(src: Path, volname: str, out: Path) -> None:
+    result = _run(
+        [
+            "hdiutil", "create",
+            "-srcfolder", str(src),
+            "-volname", volname,
+            "-fs", "HFS+",
+            "-format", "UDZO",
+            "-ov", str(out),
+        ]
+    )
+    assert result.returncode == 0, result.stderr
+
+
+def fixture_dmg(root: Path, name: str, app_count: int = 1, extras: bool = 
True) -> Path:
+    """Build an install-dmg-like fixture and return the path to its in.dmg."""
+    src = root / name / "src"
+    src.mkdir(parents=True)
+    for i in range(1, app_count + 1):
+        app = src / f"App{i}.app" / "Contents"
+        app.mkdir(parents=True)
+        (app / "Info.plist").write_text("fixture\n")
+    if extras:
+        (src / "READMEs").mkdir()
+        (src / "READMEs" / "readme.txt").write_text("readme\n")
+        (src / "Applications").symlink_to("/Applications")
+        # A nested .DS_Store survives dmg (re)creation; a root one written as a
+        # plain file does not (hdiutil create drops it). See REVIEW F7.
+        (src / "READMEs" / ".DS_Store").write_text("finder-layout\n")
+    out = root / name / "in.dmg"
+    make_dmg(src, f"remote-sign-{name}", out)
+    return out
+
+
+def _stub_log(root: Path) -> Path:
+    log = root / "stub.log"
+    log.write_text("")
+    return log
+
+
+class Signer:
+    """A scratch dir holding the wrapper plus a controllable stub delegate."""
+
+    def __init__(self, root: Path, name: str, fail: str = "none"):
+        self.root = root
+        self.path = root / name
+        self.path.mkdir(parents=True)
+        self.log = _stub_log(self.path)
+        self.fail = fail
+        shutil.copy(WRAPPER, self.path / "macosx-remote-sign.sh")
+        self._write_stub()
+
+    def _write_stub(self):
+        stub = self.path / "macosx-codesign.sh"
+        body = [
+            "#!/bin/bash",
+            'printf "STUB: %s\\n" "$*" >> "$STUB_LOG"',
+        ]
+        if self.fail == "dmg":
+            body += [
+                'case "$*" in',
+                '  *.dmg) echo "STUB refusing dmg sign" >&2; exit 1 ;;',
+                "esac",
+            ]
+        body.append("exit 0")
+        stub.write_text("\n".join(body) + "\n")
+        stub.chmod(stub.stat().st_mode | stat.S_IXUSR)
+
+    def run(self, *args, env=None):
+        run_env = os.environ.copy()
+        run_env["STUB_LOG"] = str(self.log)
+        if env:
+            run_env.update(env)
+        return subprocess.run(
+            [str(self.path / "macosx-remote-sign.sh"), *args],
+            capture_output=True,
+            text=True,
+            env=run_env,
+        )
+
+
[email protected]
+def workdir(tmp_path):
+    return tmp_path
+
+
[email protected]
+def signer(tmp_path):
+    return Signer(tmp_path, "w-main")
+
+
+def mount_point(dmg: Path):
+    """Context manager mounting dmg read-only, yielding the mount point."""
+    import contextlib
+
+    @contextlib.contextmanager
+    def _mount():
+        mp = Path(_run(["mktemp", "-d"]).stdout.strip())
+        attach = _run(
+            ["hdiutil", "attach", "-readonly", "-nobrowse", "-mountpoint", 
str(mp), str(dmg)]
+        )
+        assert attach.returncode == 0, attach.stderr
+        try:
+            yield mp
+        finally:
+            _run(["hdiutil", "detach", str(mp), "-quiet"])
+            shutil.rmtree(mp, ignore_errors=True)
+
+    return _mount()
+
+
+def volume_name(dmg: Path) -> str:
+    with mount_point(dmg) as mp:
+        info = _run(["diskutil", "info", str(mp)])
+        assert info.returncode == 0, info.stderr
+        for line in info.stdout.splitlines():
+            if line.strip().startswith("Volume Name:"):
+                return line.split(":", 1)[1].strip()
+    raise AssertionError(f"no volume name for {dmg}")
diff --git a/release-scripts/tests/test_macosx_remote_sign.py 
b/release-scripts/tests/test_macosx_remote_sign.py
new file mode 100644
index 0000000..6534765
--- /dev/null
+++ b/release-scripts/tests/test_macosx_remote_sign.py
@@ -0,0 +1,257 @@
+################################################################
+#
+#  Licensed to the Apache Software Foundation (ASF) under one
+#  or more contributor license agreements.  See the NOTICE file
+#  distributed with this work for additional information
+#  regarding copyright ownership.  The ASF licenses this file
+#  to you under the Apache License, Version 2.0 (the
+#  "License"); you may not use this file except in compliance
+#  with the License.  You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing,
+#  software distributed under the License is distributed on an
+#  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+#  KIND, either express or implied.  See the License for the
+#  specific language governing permissions and limitations
+#  under the License.
+#
+################################################################
+
+"""Tests for release-scripts/macosx-remote-sign.sh."""
+
+import hashlib
+import os
+import subprocess
+
+import pytest
+
+from conftest import IDENTITY, Signer, fixture_dmg, mount_point, 
requires_macos, volume_name
+
+pytestmark = requires_macos
+
+
+def test_help_exits_zero(signer):
+    r = signer.run("--help")
+    assert r.returncode == 0
+    assert "unsigned.dmg" in r.stdout
+
+
+def test_no_arguments_exits_two(signer):
+    assert signer.run().returncode == 2
+
+
+def test_unknown_option_exits_two(signer, workdir):
+    src = fixture_dmg(workdir, "args")
+    assert signer.run("--bogus", src, signer.path / "out.dmg").returncode == 2
+
+
+def test_missing_option_argument_exits_two(signer):
+    assert signer.run("-i").returncode == 2
+
+
+def test_third_positional_exits_two(signer, workdir):
+    src = fixture_dmg(workdir, "args")
+    r = signer.run("-i", IDENTITY, src, signer.path / "out.dmg", "extra")
+    assert r.returncode == 2
+
+
+def test_missing_identity_exits_two(signer, workdir):
+    src = fixture_dmg(workdir, "args")
+    assert signer.run(src, signer.path / "out.dmg").returncode == 2
+
+
+def test_adhoc_identity_exits_two(signer, workdir):
+    src = fixture_dmg(workdir, "args")
+    assert signer.run("-i", "-", src, signer.path / "out.dmg").returncode == 2
+
+
+def test_missing_source_exits_one(signer, workdir):
+    r = signer.run("-i", IDENTITY, workdir / "nope.dmg", signer.path / 
"out.dmg")
+    assert r.returncode == 1
+
+
+def test_missing_sibling_signer_exits_two(workdir, tmp_path):
+    import shutil
+
+    from conftest import WRAPPER
+
+    d = tmp_path / "no-signer"
+    d.mkdir()
+    shutil.copy(WRAPPER, d / "macosx-remote-sign.sh")
+    src = fixture_dmg(workdir, "args")
+    r = subprocess.run(
+        [str(d / "macosx-remote-sign.sh"), "-i", IDENTITY, str(src), str(d / 
"out.dmg")],
+        capture_output=True,
+        text=True,
+    )
+    assert r.returncode == 2
+
+
+# ------------------------------------------------------------ output guards
+
+
+def test_directory_output_exits_two(signer, workdir):
+    src = fixture_dmg(workdir, "args")
+    out = signer.path / "isadir"
+    out.mkdir()
+    r = signer.run("-i", IDENTITY, src, out)
+    assert r.returncode == 2
+    assert "directory" in r.stderr
+
+
+def test_missing_dmg_suffix_is_normalized(signer, workdir):
+    src = fixture_dmg(workdir, "args")
+    r = signer.run("-i", IDENTITY, src, signer.path / "suffixed")
+    assert r.returncode == 0
+    assert (signer.path / "suffixed.dmg").exists()
+    assert not (signer.path / "suffixed").exists()
+
+
+def test_output_equal_to_input_exits_two(signer, workdir):
+    src = fixture_dmg(workdir, "args")
+    r = signer.run("-i", IDENTITY, src, src)
+    assert r.returncode == 2
+    assert "input" in r.stderr
+
+
+# ------------------------------------------------------------- checksum (F2)
+
+
+def _digest(path):
+    return hashlib.sha256(path.read_bytes()).hexdigest()
+
+
[email protected](
+    "fmt",
+    [
+        pytest.param(lambda p, d: d, id="bare-digest"),
+        pytest.param(lambda p, d: f"{d}  {p.name}", id="shasum-line"),
+        pytest.param(lambda p, d: f"{d} *{p.name}", id="hash-sign-line"),
+    ],
+)
+def test_sha256_accepts_digest_and_lines(signer, workdir, fmt):
+    src = fixture_dmg(workdir, "args")
+    value = fmt(src, _digest(src))
+    out = signer.path / f"sum-{abs(hash(value))}.dmg"
+    r = signer.run("-i", IDENTITY, "--sha256", value, src, out)
+    assert r.returncode == 0, r.stderr
+
+
+def test_sha256_mismatch_exits_one_and_writes_nothing(signer, workdir):
+    src = fixture_dmg(workdir, "args")
+    out = signer.path / "sum-bad.dmg"
+    r = signer.run("-i", IDENTITY, "--sha256", "0" * 64, src, out)
+    assert r.returncode == 1
+    assert not out.exists()
+
+
+# ---------------------------------------------------------- .app discovery
+
+
+def test_no_app_exits_one(signer, workdir):
+    src = fixture_dmg(workdir, "zeroapp", app_count=0, extras=False)
+    r = signer.run("-i", IDENTITY, src, signer.path / "zero.dmg")
+    assert r.returncode == 1
+    assert "no .app" in r.stderr
+
+
+def test_multiple_app_exits_one(signer, workdir):
+    src = fixture_dmg(workdir, "twoapp", app_count=2, extras=False)
+    r = signer.run("-i", IDENTITY, src, signer.path / "two.dmg")
+    assert r.returncode == 1
+    assert "exactly one" in r.stderr
+
+
+# --------------------------------------------------------------- happy path
+
+
+def test_normal_run_publishes_output_and_no_temp(signer, workdir):
+    src = fixture_dmg(workdir, "args")
+    out = signer.path / "out.dmg"
+    r = signer.run("-i", IDENTITY, src, out)
+    assert r.returncode == 0, r.stderr
+    assert out.exists()
+    assert list(signer.path.glob("out.dmg.tmp.*")) == []
+
+
+def test_whole_volume_survives(signer, workdir):
+    src = fixture_dmg(workdir, "args")
+    out = signer.path / "out.dmg"
+    assert signer.run("-i", IDENTITY, src, out).returncode == 0
+    with mount_point(out) as mp:
+        assert (mp / "READMEs").is_dir()
+        assert (mp / "Applications").is_symlink()
+        assert (mp / "READMEs" / ".DS_Store").is_file()
+        assert (mp / "App1.app").is_dir()
+
+
+def test_original_volume_name_reused(signer, workdir):
+    src = fixture_dmg(workdir, "args")
+    out = signer.path / "out.dmg"
+    assert signer.run("-i", IDENTITY, src, out).returncode == 0
+    assert volume_name(out) == "remote-sign-args"
+
+
+def test_volume_name_parse_keeps_colons(signer, workdir):
+    """The wrapper reads the volume name from `diskutil info` output without
+    splitting on a colon that is part of the name. HFS+ cannot store ':' in a
+    volume name (hdiutil rewrites it to '/'), so the value cannot be produced
+    end-to-end; a stub `diskutil` on PATH drives the script's own parse."""
+    stub_bin = signer.path / "bin"
+    stub_bin.mkdir()
+    stub = stub_bin / "diskutil"
+    stub.write_text(
+        "#!/bin/bash\n"
+        "printf '   Volume Name:               Foo: Bar\\n'\n"
+    )
+    stub.chmod(0o755)
+    src = fixture_dmg(workdir, "colon", extras=False)
+    out = signer.path / "colon.dmg"
+    r = signer.run(
+        "-i", IDENTITY, src, out, env={"PATH": 
f"{stub_bin}:{os.environ['PATH']}"}
+    )
+    assert r.returncode == 0, r.stderr
+    assert "volume: Foo: Bar" in r.stdout
+
+
+# ----------------------------------------------- F1: no half-published output
+
+
+def test_dmg_sign_failure_writes_nothing(tmp_path, workdir):
+    signer = Signer(tmp_path, "w-dmgfail", fail="dmg")
+    src = fixture_dmg(workdir, "args")
+    out = signer.path / "final.dmg"
+    r = signer.run("-i", IDENTITY, src, out)
+    assert r.returncode != 0
+    assert not out.exists()
+    assert list(signer.path.glob("final.dmg.tmp.*")) == []
+
+
+def test_app_sign_failure_writes_nothing(tmp_path, workdir):
+    signer = Signer(tmp_path, "w-appfail")
+    stub = signer.path / "macosx-codesign.sh"
+    stub.write_text("#!/bin/bash\nexit 3\n")
+    stub.chmod(0o755)
+    src = fixture_dmg(workdir, "args")
+    out = signer.path / "final.dmg"
+    r = signer.run("-i", IDENTITY, src, out)
+    assert r.returncode != 0
+    assert not out.exists()
+
+
+# ------------------------------------------------------------ paths with 
spaces
+
+
+def test_spaces_in_paths(tmp_path, workdir):
+    signer = Signer(tmp_path, "sp ace")
+    src = fixture_dmg(workdir, "args")
+    spaced_in = signer.path / "in sp.dmg"
+    import shutil
+
+    shutil.copy(src, spaced_in)
+    out = signer.path / "out dir" / "signed out.dmg"
+    r = signer.run("-i", IDENTITY, spaced_in, out)
+    assert r.returncode == 0, r.stderr
+    assert out.exists()

Reply via email to