[PATCH 15/15] selftests: add Ceph BLOG smoke test and collection helpers

Alex Markuze <[email protected]>
Newsgroups org.kernel.vger.ceph-devel,org.kernel.vger.linux-kernel
Message-ID <[email protected]>
Add kselftests: blog_smoke_test.sh (dual-mount isolation, enable/
disable), blog_collect.sh, blog_analyze.sh, and kselftest wiring.

Add F: pattern for tools/testing/selftests/ceph_blog/ to the CephFS
MAINTAINERS entry.

Signed-off-by: Alex Markuze <[email protected]>
---
 MAINTAINERS                                   |   1 +
 tools/testing/selftests/Makefile              |   1 +
 tools/testing/selftests/ceph_blog/Makefile    |   7 +
 .../selftests/ceph_blog/blog_analyze.sh       | 124 ++++++
 .../selftests/ceph_blog/blog_collect.sh       | 116 ++++++
 .../selftests/ceph_blog/blog_smoke_test.sh    | 363 ++++++++++++++++++
 tools/testing/selftests/ceph_blog/config      |   2 +
 7 files changed, 614 insertions(+)
 create mode 100644 tools/testing/selftests/ceph_blog/Makefile
 create mode 100644 tools/testing/selftests/ceph_blog/blog_analyze.sh
 create mode 100644 tools/testing/selftests/ceph_blog/blog_collect.sh
 create mode 100644 tools/testing/selftests/ceph_blog/blog_smoke_test.sh
 create mode 100644 tools/testing/selftests/ceph_blog/config

diff --git a/MAINTAINERS b/MAINTAINERS
index 441b556b9671..9a7265ce7de5 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -5909,6 +5909,7 @@ B:	https://tracker.ceph.com/
 T:	git https://github.com/ceph/ceph-client.git
 F:	Documentation/filesystems/ceph.rst
 F:	fs/ceph/
+F:	tools/testing/selftests/ceph_blog/
 F:	tools/testing/selftests/filesystems/ceph/
 
 CERTIFICATE HANDLING
diff --git a/tools/testing/selftests/Makefile b/tools/testing/selftests/Makefile
index ab254ae793a9..d7e0138fcf3a 100644
--- a/tools/testing/selftests/Makefile
+++ b/tools/testing/selftests/Makefile
@@ -7,6 +7,7 @@ TARGETS += bpf
 TARGETS += breakpoints
 TARGETS += cachestat
 TARGETS += capabilities
+TARGETS += ceph_blog
 TARGETS += cgroup
 TARGETS += clone3
 TARGETS += connector
diff --git a/tools/testing/selftests/ceph_blog/Makefile b/tools/testing/selftests/ceph_blog/Makefile
new file mode 100644
index 000000000000..39f6d0c8990c
--- /dev/null
+++ b/tools/testing/selftests/ceph_blog/Makefile
@@ -0,0 +1,7 @@
+# SPDX-License-Identifier: GPL-2.0
+
+TEST_PROGS := blog_smoke_test.sh
+TEST_PROGS += blog_collect.sh
+TEST_PROGS += blog_analyze.sh
+
+include ../lib.mk
diff --git a/tools/testing/selftests/ceph_blog/blog_analyze.sh b/tools/testing/selftests/ceph_blog/blog_analyze.sh
new file mode 100644
index 000000000000..1f0bf049fbb9
--- /dev/null
+++ b/tools/testing/selftests/ceph_blog/blog_analyze.sh
@@ -0,0 +1,124 @@
+#!/bin/bash
+# SPDX-License-Identifier: GPL-2.0
+#
+# blog_analyze.sh - Analyze BLOG trace snapshots
+#
+# Parses the output of blog_collect.sh or blog_smoke_test.sh and
+# produces a summary: entry counts, source-file breakdown, timeline,
+# and per-client statistics.
+#
+# Usage:
+#   ./blog_analyze.sh <snapshot_dir>
+#   ./blog_analyze.sh /tmp/blog_snapshot_20260413_120000
+#
+set -euo pipefail
+
+DIR="${1:?Usage: $0 <snapshot_dir>}"
+[[ -d "$DIR" ]] || { echo "Not a directory: $DIR"; exit 1; }
+
+ENTRIES="$DIR/entries.txt"
+SOURCES="$DIR/sources.txt"
+CLIENTS="$DIR/clients.txt"
+STATS="$DIR/stats.txt"
+
+echo "=========================================="
+echo " BLOG Trace Analysis"
+echo " Source: $DIR"
+echo "=========================================="
+echo ""
+
+# ---------- entry summary ----------
+if [[ -f "$ENTRIES" ]]; then
+    TOTAL=$(wc -l < "$ENTRIES")
+    echo "--- Entries ---"
+    echo "  Total lines:  $TOTAL"
+
+    if (( TOTAL > 0 )); then
+        FIRST=$(head -1 "$ENTRIES" | awk '{print $1, $2}')
+        LAST=$(tail -1 "$ENTRIES" | awk '{print $1, $2}')
+        echo "  First entry:  $FIRST"
+        echo "  Last entry:   $LAST"
+
+        echo ""
+        echo "  Top 10 source locations:"
+        # entries format: "YYYY-MM-DD HH:MM:SS.mmm [file:func:line] message"
+        grep -oE '\[[^]]+\]' "$ENTRIES" 2>/dev/null | sed 's/^\[//;s/\]$//' | \
+            sort | uniq -c | sort -rn | head -10 | \
+            while read -r count loc; do
+                printf "    %6d  %s\n" "$count" "$loc"
+            done
+
+        echo ""
+        echo "  Top 10 most frequent messages (first 60 chars):"
+        # strip timestamp and source prefix, take first 60 chars
+        sed 's/^[0-9-]* [0-9:.]* \[[^]]*\] //' "$ENTRIES" | \
+            cut -c1-60 | sort | uniq -c | sort -rn | head -10 | \
+            while read -r count msg; do
+                printf "    %6d  %s\n" "$count" "$msg"
+            done
+
+        echo ""
+        echo "  Per-second entry rate:"
+        # extract HH:MM:SS, count per second
+        awk '{print $1" "$2}' "$ENTRIES" | \
+            sed 's/\.[0-9]*//' | sort | uniq -c | \
+            awk '{sum+=$1; count++} END {
+                if(count>0) printf "    avg %.1f entries/sec over %d seconds\n", sum/count, count
+            }'
+    fi
+else
+    echo "  (no entries.txt found)"
+fi
+
+echo ""
+
+# ---------- source summary ----------
+if [[ -f "$SOURCES" ]]; then
+    echo "--- Registered Sources ---"
+    SOURCE_COUNT=$(grep -c '^ID ' "$SOURCES" 2>/dev/null || echo 0)
+    echo "  Total registered: $SOURCE_COUNT"
+
+    if (( SOURCE_COUNT > 0 )); then
+        echo ""
+        echo "  By file:"
+        grep '^ID ' "$SOURCES" | \
+            sed 's/^ID [0-9]*: //' | cut -d: -f1 | \
+            sort | uniq -c | sort -rn | \
+            while read -r count file; do
+                printf "    %4d  %s\n" "$count" "$file"
+            done
+    fi
+else
+    echo "  (no sources.txt found)"
+fi
+
+echo ""
+
+# ---------- client summary ----------
+if [[ -f "$CLIENTS" ]]; then
+    echo "--- Registered Clients ---"
+    CLIENT_COUNT=$(grep -c '^Client ID' "$CLIENTS" 2>/dev/null || echo 0)
+    echo "  Total registered: $CLIENT_COUNT"
+
+    if (( CLIENT_COUNT > 0 )); then
+        grep -A2 '^Client ID' "$CLIENTS" | \
+            while IFS= read -r line; do
+                echo "    $line"
+            done
+    fi
+else
+    echo "  (no clients.txt found)"
+fi
+
+echo ""
+
+# ---------- stats summary ----------
+if [[ -f "$STATS" ]]; then
+    echo "--- Logger Statistics ---"
+    sed 's/^/    /' "$STATS"
+else
+    echo "  (no stats.txt found)"
+fi
+
+echo ""
+echo "=========================================="
diff --git a/tools/testing/selftests/ceph_blog/blog_collect.sh b/tools/testing/selftests/ceph_blog/blog_collect.sh
new file mode 100644
index 000000000000..9401724bf43d
--- /dev/null
+++ b/tools/testing/selftests/ceph_blog/blog_collect.sh
@@ -0,0 +1,116 @@
+#!/bin/bash
+# SPDX-License-Identifier: GPL-2.0
+#
+# blog_collect.sh - Collect BLOG debugfs snapshots from a mounted CephFS
+#
+# Finds the Ceph BLOG debugfs directory, snapshots all files, and
+# optionally tails the entries file for live monitoring.
+#
+# Usage:
+#   ./blog_collect.sh [snapshot|tail|clear|stats|sources|clients] [-d blog_dir] [-o dir]
+#
+set -euo pipefail
+
+ACTION="${1:-snapshot}"
+OUTDIR=""
+TARGET_BLOG_DIR="${BLOG_DIR:-}"
+
+shift || true
+while getopts "d:o:" opt; do
+    case $opt in
+        d) TARGET_BLOG_DIR="$OPTARG" ;;
+        o) OUTDIR="$OPTARG" ;;
+        *) ;;
+    esac
+done
+
+DEBUGFS_BASE="/sys/kernel/debug/ceph"
+shopt -s nullglob
+
+find_blog_dir() {
+    local dirs=("$DEBUGFS_BASE"/*/blog)
+
+    if [[ -n "$TARGET_BLOG_DIR" ]]; then
+        [[ -d "$TARGET_BLOG_DIR" ]] || {
+            echo "ERROR: BLOG debugfs directory does not exist: $TARGET_BLOG_DIR" >&2
+            return 1
+        }
+        echo "$TARGET_BLOG_DIR"
+        return 0
+    fi
+
+    if (( ${#dirs[@]} == 0 )); then
+        echo "ERROR: No BLOG debugfs directory found under $DEBUGFS_BASE" >&2
+        echo "Ensure CephFS is mounted and CONFIG_CEPH_FS=y|m" >&2
+        return 1
+    fi
+
+    if (( ${#dirs[@]} > 1 )); then
+        echo "ERROR: Multiple BLOG debugfs directories found:" >&2
+        printf '  %s\n' "${dirs[@]}" >&2
+        echo "Select one with -d <dir> or BLOG_DIR=<dir>." >&2
+        return 1
+    fi
+
+    echo "${dirs[0]}"
+}
+
+BLOG_DIR=$(find_blog_dir)
+echo "BLOG debugfs: $BLOG_DIR"
+
+case "$ACTION" in
+    snapshot)
+        OUTDIR="${OUTDIR:-/tmp/blog_snapshot_$(date +%Y%m%d_%H%M%S)}"
+        mkdir -p "$OUTDIR"
+        for f in enabled entries stats sources clients; do
+            [[ -r "$BLOG_DIR/$f" ]] && cat "$BLOG_DIR/$f" > "$OUTDIR/$f.txt"
+        done
+        echo "Snapshot saved to $OUTDIR/"
+        if [[ -f "$OUTDIR/enabled.txt" ]]; then
+            echo "  enabled:  $(tr -d '[:space:]' < "$OUTDIR/enabled.txt")"
+        fi
+        echo "  entries:  $(wc -l < "$OUTDIR/entries.txt") lines"
+        echo "  sources:  $(grep -c '^ID ' "$OUTDIR/sources.txt" 2>/dev/null || echo 0)"
+        echo "  clients:  $(grep -c '^FSID:' "$OUTDIR/clients.txt" 2>/dev/null || echo 0)"
+        ;;
+
+    tail)
+        echo "--- Live BLOG entries (Ctrl-C to stop) ---"
+        # Show new entries by polling; entries file is a seq_file so we
+        # re-read the whole thing each interval and diff.
+        PREV=$(mktemp)
+        CURR=""
+        trap 'rm -f "$PREV" "$CURR"' EXIT INT TERM
+        cat "$BLOG_DIR/entries" > "$PREV"
+        while true; do
+            sleep 1
+            CURR=$(mktemp)
+            cat "$BLOG_DIR/entries" > "$CURR"
+            diff "$PREV" "$CURR" | grep '^>' | sed 's/^> //' || true
+            mv "$CURR" "$PREV"
+            CURR=""
+        done
+        ;;
+
+    clear)
+        echo "clear" > "$BLOG_DIR/clear"
+        echo "BLOG entries cleared."
+        ;;
+
+    stats)
+        cat "$BLOG_DIR/stats"
+        ;;
+
+    sources)
+        cat "$BLOG_DIR/sources"
+        ;;
+
+    clients)
+        cat "$BLOG_DIR/clients"
+        ;;
+
+    *)
+        echo "Usage: $0 [snapshot|tail|clear|stats|sources|clients] [-d blog_dir] [-o dir]"
+        exit 1
+        ;;
+esac
diff --git a/tools/testing/selftests/ceph_blog/blog_smoke_test.sh b/tools/testing/selftests/ceph_blog/blog_smoke_test.sh
new file mode 100644
index 000000000000..1623630261c2
--- /dev/null
+++ b/tools/testing/selftests/ceph_blog/blog_smoke_test.sh
@@ -0,0 +1,363 @@
+#!/bin/bash
+# SPDX-License-Identifier: GPL-2.0
+#
+# blog_smoke_test.sh - Per-superblock smoke test for Ceph BLOG
+#
+# Verifies that BLOG stays disabled by default, that enabling BLOG on one
+# noshare-mounted CephFS instance only affects that instance, and that a second
+# noshare mount remains quiet until it is enabled separately.
+#
+# Usage:
+#   ./blog_smoke_test.sh -m <mon_addr> [-k <secret>] [-n <client_name>]
+#                        [-p <mount_a>] [-q <mount_b>] [-o <output_dir>]
+#
+# Environment variables:
+#   CEPH_MON      - monitor address(es)
+#   CEPH_SECRET   - CephX secret key
+#   CEPH_CLIENT   - client name, default "admin"
+#   CEPH_MOUNT    - mount A, default "/mnt/ceph_blog_test_a"
+#   CEPH_MOUNT_B  - mount B, default "${CEPH_MOUNT}_b"
+#   BLOG_OUTPUT   - output directory for collected traces
+#
+set -euo pipefail
+shopt -s nullglob
+
+MON="${CEPH_MON:-}"
+SECRET="${CEPH_SECRET:-}"
+CLIENT="${CEPH_CLIENT:-admin}"
+MNT_A="${CEPH_MOUNT:-/mnt/ceph_blog_test_a}"
+MNT_B="${CEPH_MOUNT_B:-}"
+OUTDIR="${BLOG_OUTPUT:-/tmp/blog_test_$(date +%Y%m%d_%H%M%S)}"
+
+TMP_STATE_DIR=""
+DEBUGFS_A=""
+DEBUGFS_B=""
+MOUNTED_A=0
+MOUNTED_B=0
+
+RED='\033[0;31m'
+GREEN='\033[0;32m'
+YELLOW='\033[0;33m'
+NC='\033[0m'
+
+pass()  { echo -e "  ${GREEN}PASS${NC}  $*"; }
+fail()  { echo -e "  ${RED}FAIL${NC}  $*"; }
+info()  { echo -e "  ${YELLOW}----${NC}  $*"; }
+die()   { echo -e "${RED}ERROR:${NC} $*" >&2; exit 1; }
+
+usage() {
+	sed -n '/^# Usage:/,/^$/p' "$0" | sed 's/^# //' >&2
+	exit 1
+}
+
+while getopts "m:k:n:p:q:o:h" opt; do
+	case $opt in
+		m) MON="$OPTARG" ;;
+		k) SECRET="$OPTARG" ;;
+		n) CLIENT="$OPTARG" ;;
+		p) MNT_A="$OPTARG" ;;
+		q) MNT_B="$OPTARG" ;;
+		o) OUTDIR="$OPTARG" ;;
+		h) usage ;;
+		*) usage ;;
+	esac
+done
+
+[[ -z "$MON" ]] && die "Monitor address required (-m or CEPH_MON)"
+[[ $(id -u) -ne 0 ]] && die "Must run as root"
+[[ -z "$MNT_B" ]] && MNT_B="${MNT_A}_b"
+
+cleanup() {
+	info "Cleaning up..."
+	if (( MOUNTED_B )); then
+		umount "$MNT_B" 2>/dev/null || true
+	fi
+	if (( MOUNTED_A )); then
+		umount "$MNT_A" 2>/dev/null || true
+	fi
+	[[ -n "$TMP_STATE_DIR" && -d "$TMP_STATE_DIR" ]] && rm -rf "$TMP_STATE_DIR"
+}
+trap cleanup EXIT
+
+assert_eq() {
+	local got="$1"
+	local want="$2"
+	local msg="$3"
+
+	if [[ "$got" != "$want" ]]; then
+		fail "$msg: expected '$want', got '$got'"
+		exit 1
+	fi
+	pass "$msg: $got"
+}
+
+assert_gt_zero() {
+	local value="$1"
+	local msg="$2"
+
+	if [[ "$value" -le 0 ]]; then
+		fail "$msg: expected > 0, got '$value'"
+		exit 1
+	fi
+	pass "$msg: $value"
+}
+
+have_kconfig() {
+	local pattern="$1"
+
+	grep -qE "$pattern" /boot/config-"$(uname -r)" 2>/dev/null ||
+		zgrep -qE "$pattern" /proc/config.gz 2>/dev/null
+}
+
+capture_blog_dirs() {
+	local dest="$1"
+	local dirs=(/sys/kernel/debug/ceph/*/blog)
+
+	: > "$dest"
+	if (( ${#dirs[@]} > 0 )); then
+		printf '%s\n' "${dirs[@]}" | sort -u > "$dest"
+	fi
+}
+
+find_new_blog_dir() {
+	local before="$1"
+	local after="$2"
+	local label="$3"
+	local new_dirs=()
+
+	mapfile -t new_dirs < <(comm -13 "$before" "$after" | sed '/^$/d')
+	if (( ${#new_dirs[@]} != 1 )); then
+		echo "Expected exactly one new BLOG debugfs directory for $label" >&2
+		echo "Before:" >&2
+		cat "$before" >&2
+		echo "After:" >&2
+		cat "$after" >&2
+		return 1
+	fi
+
+	echo "${new_dirs[0]}"
+}
+
+mount_ceph() {
+	local mnt="$1"
+	local opts="name=$CLIENT,noshare"
+
+	mkdir -p "$mnt"
+	[[ -n "$SECRET" ]] && opts="${opts},secret=$SECRET"
+	mount -t ceph "$MON":/ "$mnt" -o "$opts"
+}
+
+ensure_blog_dir() {
+	local dir="$1"
+
+	[[ -d "$dir" ]] || die "BLOG debugfs directory missing: $dir"
+	[[ -r "$dir/entries" ]] || die "BLOG entries file missing: $dir/entries"
+	[[ -r "$dir/stats" ]] || die "BLOG stats file missing: $dir/stats"
+	[[ -r "$dir/sources" ]] || die "BLOG sources file missing: $dir/sources"
+	[[ -r "$dir/clients" ]] || die "BLOG clients file missing: $dir/clients"
+	[[ -w "$dir/enabled" ]] || die "BLOG enabled file missing: $dir/enabled"
+}
+
+read_enabled() {
+	tr -d '[:space:]' < "$1/enabled"
+}
+
+set_blog_enabled() {
+	local dir="$1"
+	local value="$2"
+	local actual
+
+	echo "$value" > "$dir/enabled"
+	actual="$(read_enabled "$dir")"
+	case "$value" in
+		0)
+			[[ "$actual" == "N" || "$actual" == "0" ]] ||
+				die "Failed to disable BLOG in $dir (got '$actual')"
+			;;
+		1)
+			[[ "$actual" == "Y" || "$actual" == "1" ]] ||
+				die "Failed to enable BLOG in $dir (got '$actual')"
+			;;
+		*)
+			die "Unsupported BLOG enabled value: $value"
+			;;
+	esac
+}
+
+clear_blog() {
+	local dir="$1"
+
+	echo "clear" > "$dir/clear"
+}
+
+count_entries() {
+	local dir="$1"
+	local count
+
+	count="$(grep -Ec '^[0-9]{4}-[0-9]{2}-[0-9]{2} ' "$dir/entries" 2>/dev/null || true)"
+	echo "${count:-0}"
+}
+
+count_sources() {
+	local dir="$1"
+	local count
+
+	count="$(grep -c '^ID ' "$dir/sources" 2>/dev/null || true)"
+	echo "${count:-0}"
+}
+
+count_contexts() {
+	local dir="$1"
+	local count
+
+	count="$(awk -F': ' '/Total contexts allocated/{print $2; exit}' \
+		"$dir/stats" 2>/dev/null || true)"
+	echo "${count:-0}"
+}
+
+count_mount_clients() {
+	local dir="$1"
+	local count
+
+	count="$(grep -c '^FSID:' "$dir/clients" 2>/dev/null || true)"
+	echo "${count:-0}"
+}
+
+require_timestamped_entries() {
+	local dir="$1"
+	local msg="$2"
+
+	if ! grep -Eq '^[0-9]{4}-[0-9]{2}-[0-9]{2} ' "$dir/entries"; then
+		fail "$msg: no timestamped BLOG entries found"
+		exit 1
+	fi
+	pass "$msg"
+}
+
+snapshot_blog() {
+	local dir="$1"
+	local tag="$2"
+	local dest="$OUTDIR/$tag"
+
+	mkdir -p "$dest"
+	for f in enabled entries stats sources clients; do
+		cat "$dir/$f" > "$dest/$f.txt"
+	done
+}
+
+run_mount_io() {
+	local mnt="$1"
+	local tag="$2"
+	local root="$mnt/blog_smoke_$tag"
+
+	rm -rf "$root"
+	mkdir -p "$root/subdir"
+	printf 'ceph-blog-%s %s\n' "$tag" "$(date +%s%N)" > "$root/file.txt"
+	cat "$root/file.txt" > /dev/null
+	stat "$root/file.txt" > /dev/null
+	mv "$root/file.txt" "$root/subdir/file.txt"
+	ln -sf "$root/subdir/file.txt" "$root/link.txt"
+	readlink "$root/link.txt" > /dev/null
+	ls -l "$root" > /dev/null
+	rm -f "$root/link.txt" "$root/subdir/file.txt"
+	rmdir "$root/subdir"
+	rmdir "$root"
+}
+
+echo "=========================================="
+echo " Ceph BLOG Per-Superblock Smoke Test"
+echo "=========================================="
+echo ""
+
+KSFT_SKIP=4
+info "Checking kernel BLOG support..."
+if ! have_kconfig 'CONFIG_CEPH_FS=(y|m)'; then
+	info "SKIP: Cannot confirm CONFIG_CEPH_FS=y|m"
+	exit "$KSFT_SKIP"
+fi
+if ! have_kconfig 'CONFIG_DEBUG_FS=y'; then
+	info "SKIP: Cannot confirm CONFIG_DEBUG_FS=y"
+	exit "$KSFT_SKIP"
+fi
+if have_kconfig 'CONFIG_CEPH_FS=m'; then
+	modprobe ceph 2>/dev/null || die "Failed to load ceph module"
+fi
+
+mkdir -p "$OUTDIR" "$MNT_A" "$MNT_B"
+TMP_STATE_DIR="$(mktemp -d /tmp/ceph_blog_smoke.XXXXXX)"
+
+capture_blog_dirs "$TMP_STATE_DIR/before_a"
+info "Mounting CephFS A with noshare: $MON -> $MNT_A"
+mount_ceph "$MNT_A" || die "Mount A failed"
+MOUNTED_A=1
+capture_blog_dirs "$TMP_STATE_DIR/after_a"
+DEBUGFS_A="$(find_new_blog_dir "$TMP_STATE_DIR/before_a" \
+	"$TMP_STATE_DIR/after_a" "mount A")" || die "Failed to identify BLOG dir for mount A"
+ensure_blog_dir "$DEBUGFS_A"
+pass "Mount A BLOG debugfs: $DEBUGFS_A"
+
+capture_blog_dirs "$TMP_STATE_DIR/before_b"
+info "Mounting CephFS B with noshare: $MON -> $MNT_B"
+mount_ceph "$MNT_B" || die "Mount B failed"
+MOUNTED_B=1
+capture_blog_dirs "$TMP_STATE_DIR/after_b"
+DEBUGFS_B="$(find_new_blog_dir "$TMP_STATE_DIR/before_b" \
+	"$TMP_STATE_DIR/after_b" "mount B")" || die "Failed to identify BLOG dir for mount B"
+ensure_blog_dir "$DEBUGFS_B"
+pass "Mount B BLOG debugfs: $DEBUGFS_B"
+
+assert_eq "$(count_mount_clients "$DEBUGFS_A")" 1 "mount A clients view is local"
+assert_eq "$(count_mount_clients "$DEBUGFS_B")" 1 "mount B clients view is local"
+
+info "Phase 1: BLOG disabled by default on both mounts"
+set_blog_enabled "$DEBUGFS_A" 0
+set_blog_enabled "$DEBUGFS_B" 0
+run_mount_io "$MNT_A" "disabled_a"
+assert_eq "$(count_entries "$DEBUGFS_A")" 0 "mount A stays quiet when disabled"
+assert_eq "$(count_entries "$DEBUGFS_B")" 0 "mount B stays quiet while mount A runs"
+snapshot_blog "$DEBUGFS_A" "00_disabled_a"
+snapshot_blog "$DEBUGFS_B" "00_disabled_b"
+
+info "Phase 2: Enable BLOG only on mount A"
+set_blog_enabled "$DEBUGFS_A" 1
+set_blog_enabled "$DEBUGFS_B" 0
+clear_blog "$DEBUGFS_A"
+clear_blog "$DEBUGFS_B"
+run_mount_io "$MNT_A" "enabled_a"
+
+A_ENTRIES="$(count_entries "$DEBUGFS_A")"
+B_ENTRIES="$(count_entries "$DEBUGFS_B")"
+assert_gt_zero "$A_ENTRIES" "mount A produces BLOG entries when enabled"
+assert_eq "$B_ENTRIES" 0 "mount B remains quiet while only mount A is enabled"
+assert_gt_zero "$(count_sources "$DEBUGFS_A")" "mount A registers BLOG sources"
+assert_gt_zero "$(count_contexts "$DEBUGFS_A")" "mount A allocates BLOG contexts"
+require_timestamped_entries "$DEBUGFS_A" "mount A entries deserialize with timestamps"
+snapshot_blog "$DEBUGFS_A" "01_enabled_a"
+snapshot_blog "$DEBUGFS_B" "01_enabled_a_other_mount"
+
+info "Phase 3: Disable mount A, enable mount B"
+A_BEFORE_B="$A_ENTRIES"
+set_blog_enabled "$DEBUGFS_A" 0
+set_blog_enabled "$DEBUGFS_B" 1
+clear_blog "$DEBUGFS_B"
+run_mount_io "$MNT_B" "enabled_b"
+
+A_AFTER_B="$(count_entries "$DEBUGFS_A")"
+B_AFTER_B="$(count_entries "$DEBUGFS_B")"
+assert_eq "$A_AFTER_B" "$A_BEFORE_B" "mount A entries stay unchanged while mount B runs"
+assert_gt_zero "$B_AFTER_B" "mount B produces BLOG entries when enabled"
+assert_gt_zero "$(count_sources "$DEBUGFS_B")" "mount B registers BLOG sources"
+assert_gt_zero "$(count_contexts "$DEBUGFS_B")" "mount B allocates BLOG contexts"
+require_timestamped_entries "$DEBUGFS_B" "mount B entries deserialize with timestamps"
+snapshot_blog "$DEBUGFS_A" "02_enabled_b_other_mount"
+snapshot_blog "$DEBUGFS_B" "02_enabled_b"
+
+echo ""
+echo "=========================================="
+echo " Results Summary"
+echo "=========================================="
+pass "Default-off behavior verified on both mounts"
+pass "Enable-on-A only behavior verified"
+pass "Cross-mount non-interference verified with noshare"
+pass "Enable-on-B behavior verified"
+info "Traces saved to: $OUTDIR/"
diff --git a/tools/testing/selftests/ceph_blog/config b/tools/testing/selftests/ceph_blog/config
new file mode 100644
index 000000000000..7c5706ed8be8
--- /dev/null
+++ b/tools/testing/selftests/ceph_blog/config
@@ -0,0 +1,2 @@
+CONFIG_CEPH_FS=m
+CONFIG_DEBUG_FS=y
-- 
2.34.1
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.