#!/usr/bin/env bash
# SPDX-License-Identifier: LGPL-2.1-or-later
# vi: ts=4 sw=4 tw=0 et:

set -eux
set -o pipefail

# Check if all symlinks under /dev/disk/ are valid
# shellcheck disable=SC2120
helper_check_device_symlinks() {(
    set +x

    local dev link path paths target

    [[ $# -gt 0 ]] && paths=("$@") || paths=("/dev/disk" "/dev/mapper")

    # Check if all given paths are valid
    for path in "${paths[@]}"; do
        if ! test -e "$path"; then
            echo >&2 "Path '$path' doesn't exist"
            return 1
        fi
    done

    while read -r link; do
        target="$(readlink -f "$link")"
        # Both checks should do virtually the same thing, but check both to be
        # on the safe side
        if [[ ! -e "$link" || ! -e "$target" ]]; then
            echo >&2 "ERROR: symlink '$link' points to '$target' which doesn't exist"
            return 1
        fi

        # Check if the symlink points to the correct device in /dev
        dev="/dev/$(udevadm info -q name "$link")"
        if [[ "$target" != "$dev" ]]; then
            echo >&2 "ERROR: symlink '$link' points to '$target' but '$dev' was expected"
            return 1
        fi
    done < <(find "${paths[@]}" -type l)
)}

helper_check_udev_watch() {(
    set +x

    local link target id dev

    while read -r link; do
        target="$(readlink "$link")"
        if [[ ! -L "/run/udev/watch/$target" ]]; then
            echo >&2 "ERROR: symlink /run/udev/watch/$target does not exist"
            return 1
        fi
        if [[ "$(readlink "/run/udev/watch/$target")" != "$(basename "$link")" ]]; then
            echo >&2 "ERROR: symlink target of /run/udev/watch/$target is inconsistent with $link"
            return 1
        fi

        if [[ "$target" =~ ^[0-9]+$ ]]; then
            # $link is ID -> wd
            id="$(basename "$link")"
        else
            # $link is wd -> ID
            id="$target"
        fi

        if [[ "${id:0:1}" == "b" ]]; then
            dev="/dev/block/${id:1}"
        elif [[ "${id:0:1}" == "c" ]]; then
            dev="/dev/char/${id:1}"
        else
            echo >&2 "ERROR: unexpected device ID '$id'"
            return 1
        fi

        if [[ ! -e "$dev" ]]; then
            echo >&2 "ERROR: device '$dev' corresponding to symlink '$link' does not exist"
            return 1
        fi
    done < <(find /run/udev/watch -type l)
)}

check_device_unit() {(
    set +x

    local log_level link links path syspath unit

    log_level="${1?}"
    path="${2?}"
    unit=$(systemd-escape --path --suffix=device "$path")

    [[ "$log_level" == 1 ]] && echo "INFO: check_device_unit($unit)"

    syspath=$(systemctl show --value --property SysFSPath "$unit" 2>/dev/null)
    if [[ -z "$syspath" ]]; then
        [[ "$log_level" == 1 ]] && echo >&2 "ERROR: $unit not found."
        return 1
    fi

    if [[ ! -L "$path" ]]; then
        if [[ ! -d "$syspath" ]]; then
            [[ "$log_level" == 1 ]] && echo >&2 "ERROR: $unit exists for $syspath but it does not exist."
            return 1
        fi
        return 0
    fi

    if [[ ! -b "$path" && ! -c "$path" ]]; then
        [[ "$log_level" == 1 ]] && echo >&2 "ERROR: invalid file type $path"
        return 1
    fi

    read -r -a links < <(udevadm info -q symlink "$syspath" 2>/dev/null)
    for link in "${links[@]}"; do
        if [[ "/dev/$link" == "$path" ]]; then # DEVLINKS= given by -q symlink are relative to /dev
            return 0
        fi
    done

    read -r -a links < <(udevadm info "$syspath" | sed -ne '/SYSTEMD_ALIAS=/ { s/^E: SYSTEMD_ALIAS=//; p }' 2>/dev/null)
    for link in "${links[@]}"; do
        if [[ "$link" == "$path" ]]; then # SYSTEMD_ALIAS= are absolute
            return 0
        fi
    done

    [[ "$log_level" == 1 ]] && echo >&2 "ERROR: $unit exists for $syspath but it does not have the corresponding DEVLINKS or SYSTEMD_ALIAS."
    return 1
)}

check_device_units() {(
    set +x

    local log_level path paths

    log_level="${1?}"
    shift
    paths=("$@")

    for path in "${paths[@]}"; do
        if ! check_device_unit "$log_level" "$path"; then
           return 1
        fi
    done

    while read -r unit _; do
        path=$(systemd-escape --path --unescape "$unit")
        if ! check_device_unit "$log_level" "$path"; then
           return 1
        fi
    done < <(systemctl list-units --all --type=device --no-legend dev-* | awk '$1 !~ /dev-tty.+/ && $4 == "plugged" { print $1 }' | sed -e 's/\.device$//')

    return 0
)}

helper_check_device_units() {(
    set +x

    local i

    for i in {1..20}; do
        (( i > 1 )) && sleep 0.5
        if check_device_units 0 "$@"; then
            return 0
        fi
    done

    check_device_units 1 "$@"
)}

testcase_virtio_scsi_basic() {
    lsblk -S
    [[ "$(lsblk --scsi --noheadings | wc -l)" -ge 128 ]]
}

testcase_nvme_basic() {
    local expected_symlinks=()
    local i

    for i in {0..4}; do
        expected_symlinks+=(
            # both replace mode provides the same devlink
            /dev/disk/by-id/nvme-QEMU_NVMe_Ctrl_deadbeef"$i"
            # with nsid
            /dev/disk/by-id/nvme-QEMU_NVMe_Ctrl_deadbeef"$i"_1
        )
    done
    for i in {5..9}; do
        expected_symlinks+=(
            # old replace mode
            /dev/disk/by-id/nvme-QEMU_NVMe_Ctrl__deadbeef_"$i"
            # newer replace mode
            /dev/disk/by-id/nvme-QEMU_NVMe_Ctrl_____deadbeef__"$i"
            # with nsid
            /dev/disk/by-id/nvme-QEMU_NVMe_Ctrl_____deadbeef__"$i"_1
        )
    done
    for i in {10..14}; do
        expected_symlinks+=(
            # old replace mode does not provide devlink, as serial contains "/"
            # newer replace mode
            /dev/disk/by-id/nvme-QEMU_NVMe_Ctrl_____dead_beef_"$i"
            # with nsid
            /dev/disk/by-id/nvme-QEMU_NVMe_Ctrl_____dead_beef_"$i"_1
        )
    done
    for i in {15..19}; do
        expected_symlinks+=(
            # old replace mode does not provide devlink, as serial contains "/"
            # newer replace mode
            /dev/disk/by-id/nvme-QEMU_NVMe_Ctrl_dead_.._.._beef_"$i"
            # with nsid
            /dev/disk/by-id/nvme-QEMU_NVMe_Ctrl_dead_.._.._beef_"$i"_1
        )
    done

    udevadm settle
    ls /dev/disk/by-id
    for i in "${expected_symlinks[@]}"; do
        udevadm wait --settle --timeout=30 "$i"
    done
    test ! -e /dev/disk/by-id/nvme-QEMU_NVMe_Ctrl_deadbeef

    lsblk --noheadings | grep "^nvme"
    [[ "$(lsblk --noheadings | grep -c "^nvme")" -ge 20 ]]
}

testcase_nvme_subsystem() {
    local expected_symlinks=(
        # Controller(s)
        /dev/disk/by-id/nvme-QEMU_NVMe_Ctrl_deadbeef_16
        /dev/disk/by-id/nvme-QEMU_NVMe_Ctrl_deadbeef_17
        # Shared namespaces
        /dev/disk/by-path/*pci*-nvme-16
        /dev/disk/by-path/*pci*-nvme-17
    )

    udevadm wait --settle --timeout=30 "${expected_symlinks[@]}"
}

testcase_virtio_scsi_identically_named_partitions() {
    local num_part num_disk i j

    if [[ -v ASAN_OPTIONS || "$(systemd-detect-virt -v)" == "qemu" ]]; then
        num_part=4
        num_disk=4
    else
        num_part=8
        num_disk=16
    fi

    for ((i = 0; i < num_disk; i++)); do
        udevadm lock --device "/dev/disk/by-id/scsi-0QEMU_QEMU_HARDDISK_drive$i" \
                sfdisk "/dev/disk/by-id/scsi-0QEMU_QEMU_HARDDISK_drive$i" <<EOF
label: gpt

$(for ((j = 1; j <= num_part; j++)); do echo 'name="Hello world", size=2M'; done)
EOF
    done

    udevadm settle
    lsblk --noheadings -a -o NAME,PARTLABEL
    [[ "$(lsblk --noheadings -a -o NAME,PARTLABEL | grep -c "Hello world")" -eq "$((num_part * num_disk))" ]]
}

testcase_multipath_basic_failover() {
    local dmpath i path wwid

    # Configure multipath
    cat >/etc/multipath.conf <<\EOF
defaults {
    # Use /dev/mapper/$WWN paths instead of /dev/mapper/mpathX
    user_friendly_names no
    find_multipaths yes
    enable_foreign "^$"
}

blacklist_exceptions {
    property "(SCSI_IDENT_|ID_WWN)"
}

blacklist {
}
EOF

    udevadm lock --device /dev/disk/by-id/wwn-0xdeaddeadbeef0000 \
            sfdisk /dev/disk/by-id/wwn-0xdeaddeadbeef0000 <<EOF
label: gpt

name="first_partition", size=5M
uuid="deadbeef-dead-dead-beef-000000000000", name="failover_part", size=5M
EOF
    # Partitioning triggers a synthesized event. Wait for the event being finished.
    udevadm settle

    udevadm lock --device /dev/disk/by-id/wwn-0xdeaddeadbeef0000-part2 \
            mkfs.ext4 -U "deadbeef-dead-dead-beef-111111111111" -L "failover_vol" /dev/disk/by-id/wwn-0xdeaddeadbeef0000-part2
    # Making filesystem triggers a synthesized event. Wait for the event being finished.
    udevadm settle

    modprobe -v dm_multipath
    systemctl start multipathd.service
    systemctl status multipathd.service
    # multipathd touches many devices on start. multipath command may fail if it is invoked before the
    # initial setup finished. Let's wait for a while.
    udevadm settle
    multipath -ll
    ls -l /dev/disk/by-id/

    for i in {0..15}; do
        wwid="deaddeadbeef$(printf "%.4d" "$i")"
        path="/dev/disk/by-id/wwn-0x$wwid"
        dmpath="$(readlink -f "$path")"

        lsblk "$path"
        multipath -C "$dmpath"
        # We should have 4 active paths for each multipath device
        [[ "$(multipath -l "$path" | grep -c running)" -eq 4 ]]
    done

    # Test failover (with the first multipath device that has a partitioned disk)
    echo "${FUNCNAME[0]}: test failover"
    local device expected link mpoint part
    local -a devices
    mkdir -p /mnt
    mpoint="$(mktemp -d /mnt/mpathXXX)"
    wwid="deaddeadbeef0000"
    path="/dev/disk/by-id/wwn-0x$wwid"

    # All following symlinks should exists and should be valid
    local -a part_links=(
        "/dev/disk/by-id/wwn-0x$wwid-part2"
        "/dev/disk/by-partlabel/failover_part"
        "/dev/disk/by-partuuid/deadbeef-dead-dead-beef-000000000000"
        "/dev/disk/by-label/failover_vol"
        "/dev/disk/by-uuid/deadbeef-dead-dead-beef-111111111111"
    )
    udevadm wait --settle --timeout=30 "${part_links[@]}"
    helper_check_device_units "${part_links[@]}"

    # Choose a random symlink to the failover data partition each time, for
    # a better coverage
    part="${part_links[$RANDOM % ${#part_links[@]}]}"

    # Get all devices attached to a specific multipath device (in H:C:T:L format)
    # and sort them in a random order, so we cut off different paths each time
    mapfile -t devices < <(multipath -l "$path" | grep -Eo '[0-9]+:[0-9]+:[0-9]+:[0-9]+' | sort -R)
    if [[ "${#devices[@]}" -ne 4 ]]; then
        echo "Expected 4 devices attached to WWID=$wwid, got ${#devices[@]} instead"
        return 1
    fi
    # Drop the last path from the array, since we want to leave at least one path active
    unset "devices[3]"
    # Mount the first multipath partition, write some data we can check later,
    # and then disconnect the remaining paths one by one while checking if we
    # can still read/write from the mount
    mount -t ext4 "$part" "$mpoint"
    expected=0
    echo -n "$expected" >"$mpoint/test"
    # Sanity check we actually wrote what we wanted
    [[ "$(<"$mpoint/test")" == "$expected" ]]

    for device in "${devices[@]}"; do
        echo offline >"/sys/class/scsi_device/$device/device/state"
        [[ "$(<"$mpoint/test")" == "$expected" ]]
        expected="$((expected + 1))"
        echo -n "$expected" >"$mpoint/test"

        # Make sure all symlinks are still valid
        udevadm wait --settle --timeout=30 "${part_links[@]}"
        helper_check_device_units "${part_links[@]}"
    done

    multipath -l "$path"
    # Three paths should be now marked as 'offline' and one as 'running'
    [[ "$(multipath -l "$path" | grep -c offline)" -eq 3 ]]
    [[ "$(multipath -l "$path" | grep -c running)" -eq 1 ]]

    umount "$mpoint"
    rm -fr "$mpoint"
}

testcase_simultaneous_events_1() {
    local disk expected i iterations key link num_part part partscript rule target timeout
    local -a devices symlinks
    local -A running

    if [[ -v ASAN_OPTIONS || "$(systemd-detect-virt -v)" == "qemu" ]]; then
        num_part=2
        iterations=10
        timeout=240
    else
        num_part=10
        iterations=100
        timeout=60
    fi

    for disk in {0..9}; do
        link="/dev/disk/by-id/scsi-0QEMU_QEMU_HARDDISK_deadbeeftest${disk}"
        target="$(readlink -f "$link")"
        if [[ ! -b "$target" ]]; then
            echo "ERROR: failed to find the test SCSI block device $link"
            return 1
        fi

        devices+=("$target")
    done

    for ((part = 1; part <= num_part; part++)); do
        symlinks+=(
            "/dev/disk/by-partlabel/test${part}"
        )
    done

    partscript="$(mktemp)"

    cat >"$partscript" <<EOF
$(for ((part = 1; part <= num_part; part++)); do printf 'name="test%d", size=2M\n' "$part"; done)
EOF

    rule=/run/udev/rules.d/50-test.rules
    mkdir -p "${rule%/*}"
    cat >"$rule" <<EOF
SUBSYSTEM=="block", KERNEL=="${devices[4]##*/}*|${devices[5]##*/}*", OPTIONS="link_priority=10"
EOF

    udevadm control --reload

    # initialize partition table
    for disk in {0..9}; do
        echo 'label: gpt' | udevadm lock --device="${devices[$disk]}" sfdisk -q "${devices[$disk]}"
    done

    # Delete the partitions, immediately recreate them, wait for udev to settle
    # down, and then check if we have any dangling symlinks in /dev/disk/. Rinse
    # and repeat.
    #
    # On unpatched udev versions the delete-recreate cycle may trigger a race
    # leading to dead symlinks in /dev/disk/
    for ((i = 1; i <= iterations; i++)); do
        for disk in {0..9}; do
            if ((disk % 2 == i % 2)); then
                udevadm lock --device="${devices[$disk]}" sfdisk -q --delete "${devices[$disk]}" &
            else
                udevadm lock --device="${devices[$disk]}" sfdisk -q -X gpt "${devices[$disk]}" <"$partscript" &
            fi
            running[$disk]=$!
        done

        for key in "${!running[@]}"; do
            wait "${running[$key]}"
            unset "running[$key]"
        done

        if ((i % 10 <= 1)); then
            udevadm wait --settle --timeout="$timeout" "${devices[@]}" "${symlinks[@]}"
            helper_check_device_symlinks
            helper_check_udev_watch
            for ((part = 1; part <= num_part; part++)); do
                link="/dev/disk/by-partlabel/test${part}"
                target="$(readlink -f "$link")"
                if ((i % 2 == 0)); then
                    expected="${devices[5]}$part"
                else
                    expected="${devices[4]}$part"
                fi
                if [[ "$target" != "$expected" ]]; then
                    echo >&2 "ERROR: symlink '/dev/disk/by-partlabel/test${part}' points to '$target' but '$expected' was expected"
                    return 1
                fi
            done
        fi
    done

    helper_check_device_units
    rm -f "$rule" "$partscript"

    udevadm control --reload
}

testcase_simultaneous_events_2() {
    local disk expected i iterations key link num_part part script_dir target timeout
    local -a devices symlinks
    local -A running

    script_dir="$(mktemp --directory "/tmp/test-udev-storage.script.XXXXXXXXXX")"
    # shellcheck disable=SC2064
    trap "rm -rf '$script_dir'" RETURN

    if [[ -v ASAN_OPTIONS || "$(systemd-detect-virt -v)" == "qemu" ]]; then
        num_part=20
        iterations=1
        timeout=2400
    else
        num_part=100
        iterations=3
        timeout=300
    fi

    for disk in {0..9}; do
        link="/dev/disk/by-id/scsi-0QEMU_QEMU_HARDDISK_deadbeeftest${disk}"
        target="$(readlink -f "$link")"
        if [[ ! -b "$target" ]]; then
            echo "ERROR: failed to find the test SCSI block device $link"
            return 1
        fi

        devices+=("$target")
    done

    for ((i = 1; i <= iterations; i++)); do
        cat >"$script_dir/partscript-$i" <<EOF
$(for ((part = 1; part <= num_part; part++)); do printf 'name="testlabel-%d", size=1M\n' "$i"; done)
EOF
    done

    echo "## $iterations iterations start: $(date '+%H:%M:%S.%N')"
    for ((i = 1; i <= iterations; i++)); do

        for disk in {0..9}; do
            udevadm lock --device="${devices[$disk]}" sfdisk -q --delete "${devices[$disk]}" &
            running[$disk]=$!
        done

        for key in "${!running[@]}"; do
            wait "${running[$key]}"
            unset "running[$key]"
        done

        for disk in {0..9}; do
            udevadm lock --device="${devices[$disk]}" sfdisk -q -X gpt "${devices[$disk]}" <"$script_dir/partscript-$i" &
            running[$disk]=$!
        done

        for key in "${!running[@]}"; do
            wait "${running[$key]}"
            unset "running[$key]"
        done

        udevadm wait --settle --timeout="$timeout" "${devices[@]}" "/dev/disk/by-partlabel/testlabel-$i"
    done
    echo "## $iterations iterations end: $(date '+%H:%M:%S.%N')"
}

testcase_simultaneous_events() {
    testcase_simultaneous_events_1
    testcase_simultaneous_events_2
}

testcase_lvm_basic() {
    local i iterations partitions part timeout
    local vgroup="MyTestGroup$RANDOM"
    local devices=(
        /dev/disk/by-id/scsi-0systemd_foobar_deadbeeflvm{0..3}
    )

    . /etc/os-release
    if [[ "$ID" == "ubuntu" ]]; then
        echo "LVM on Ubuntu is broken, skipping the test" | tee --append /skipped
        exit 77
    fi

    if [[ -v ASAN_OPTIONS || "$(systemd-detect-virt -v)" == "qemu" ]]; then
        timeout=180
    else
        timeout=30
    fi
    # Make sure all the necessary soon-to-be-LVM devices exist
    ls -l "${devices[@]}"

    # Add all test devices into a volume group, create two logical volumes,
    # and check if necessary symlinks exist (and are valid)
    lvm pvcreate -y "${devices[@]}"
    lvm pvs
    lvm vgcreate "$vgroup" -y "${devices[@]}"
    lvm vgs
    lvm vgchange -ay "$vgroup"
    lvm lvcreate -y -L 4M "$vgroup" -n mypart1
    lvm lvcreate -y -L 32M "$vgroup" -n mypart2
    lvm lvs
    udevadm wait --settle --timeout="$timeout" "/dev/$vgroup/mypart1" "/dev/$vgroup/mypart2"
    mkfs.ext4 -L mylvpart1 "/dev/$vgroup/mypart1"
    udevadm trigger --settle "/dev/$vgroup/mypart1"
    udevadm wait --settle --timeout="$timeout" "/dev/disk/by-label/mylvpart1"
    helper_check_device_symlinks "/dev/disk" "/dev/$vgroup"
    helper_check_device_units

    # Mount mypart1 through by-label devlink
    mkdir -p /tmp/mypart1-mount-point
    mount /dev/disk/by-label/mylvpart1 /tmp/mypart1-mount-point
    timeout 30 bash -c "until systemctl -q is-active /tmp/mypart1-mount-point; do sleep .2; done"
    # Extend the partition and check if the device and mount units are still active.
    # See https://bugzilla.redhat.com/show_bug.cgi?id=2158628
    # Note, the test below may be unstable with LVM2 without the following patch:
    # https://github.com/lvmteam/lvm2/pull/105
    # But, to reproduce the issue, udevd must start to process the first 'change' uevent
    # earlier than extending the volume has been finished, and in most case, the extension
    # is hopefully fast.
    lvm lvextend -y --size 8M "/dev/$vgroup/mypart1"
    udevadm wait --settle --timeout="$timeout" "/dev/disk/by-label/mylvpart1"
    timeout 30 bash -c "until systemctl -q is-active '/dev/$vgroup/mypart1'; do sleep .2; done"
    timeout 30 bash -c "until systemctl -q is-active /tmp/mypart1-mount-point; do sleep .2; done"
    # Umount the partition, otherwise the underlying device unit will stay in
    # the inactive state and not be collected, and helper_check_device_units() will fail.
    systemctl show /tmp/mypart1-mount-point
    umount /tmp/mypart1-mount-point

    # Rename partitions (see issue #24518)
    lvm lvrename "/dev/$vgroup/mypart1" renamed1
    lvm lvrename "/dev/$vgroup/mypart2" renamed2
    udevadm wait --settle --timeout="$timeout" --removed "/dev/$vgroup/mypart1" "/dev/$vgroup/mypart2"
    udevadm wait --settle --timeout="$timeout" "/dev/$vgroup/renamed1" "/dev/$vgroup/renamed2"
    helper_check_device_symlinks "/dev/disk" "/dev/$vgroup"
    helper_check_device_units

    # Rename them back
    lvm lvrename "/dev/$vgroup/renamed1" mypart1
    lvm lvrename "/dev/$vgroup/renamed2" mypart2
    udevadm wait --settle --timeout="$timeout" --removed "/dev/$vgroup/renamed1" "/dev/$vgroup/renamed2"
    udevadm wait --settle --timeout="$timeout" "/dev/$vgroup/mypart1" "/dev/$vgroup/mypart2"
    helper_check_device_symlinks "/dev/disk" "/dev/$vgroup"
    helper_check_device_units

    # Do not "unready" suspended encrypted devices w/o superblock info
    # See:
    #   - https://github.com/systemd/systemd/pull/24177
    #   - https://bugzilla.redhat.com/show_bug.cgi?id=1985288
    dd if=/dev/urandom of=/etc/lvm_keyfile bs=64 count=1 iflag=fullblock
    chmod 0600 /etc/lvm_keyfile
    # Intentionally use weaker cipher-related settings, since we don't care
    # about security here as it's a throwaway LUKS partition
    cryptsetup luksFormat -q --use-urandom --pbkdf pbkdf2 --pbkdf-force-iterations 1000 \
                          "/dev/$vgroup/mypart2" /etc/lvm_keyfile
    # Mount the LUKS partition & create a filesystem on it
    mkdir -p /tmp/lvmluksmnt
    cryptsetup open --key-file=/etc/lvm_keyfile "/dev/$vgroup/mypart2" "lvmluksmap"
    udevadm wait --settle --timeout="$timeout" "/dev/mapper/lvmluksmap"
    mkfs.ext4 -L lvmluksfs "/dev/mapper/lvmluksmap"
    udevadm trigger --settle "/dev/mapper/lvmluksmap"
    udevadm wait --settle --timeout="$timeout" "/dev/disk/by-label/lvmluksfs"
    # Make systemd "interested" in the mount by adding it to /etc/fstab
    echo "/dev/disk/by-label/lvmluksfs /tmp/lvmluksmnt ext4 defaults 0 2" >>/etc/fstab
    systemctl daemon-reload
    mount "/tmp/lvmluksmnt"
    mountpoint "/tmp/lvmluksmnt"
    # Temporarily suspend the LUKS device and trigger udev - basically what `cryptsetup resize`
    # does but in a more deterministic way suitable for a test/reproducer
    for _ in {0..5}; do
        dmsetup suspend "/dev/mapper/lvmluksmap"
        udevadm trigger -v --settle "/dev/mapper/lvmluksmap"
        dmsetup resume "/dev/mapper/lvmluksmap"
        # The mount should survive this sequence of events
        mountpoint "/tmp/lvmluksmnt"
    done
    # Cleanup
    umount "/tmp/lvmluksmnt"
    cryptsetup close "/dev/mapper/lvmluksmap"
    sed -i "/lvmluksfs/d" "/etc/fstab"
    systemctl daemon-reload

    # Disable the VG and check symlinks...
    lvm vgchange -an "$vgroup"
    udevadm wait --settle --timeout="$timeout" --removed "/dev/$vgroup" "/dev/disk/by-label/mylvpart1"
    helper_check_device_symlinks "/dev/disk"
    helper_check_device_units

    # reenable the VG and check the symlinks again if all LVs are properly activated
    lvm vgchange -ay "$vgroup"
    udevadm wait --settle --timeout="$timeout" "/dev/$vgroup/mypart1" "/dev/$vgroup/mypart2" "/dev/disk/by-label/mylvpart1"
    helper_check_device_symlinks "/dev/disk" "/dev/$vgroup"
    helper_check_device_units

    # Same as above, but now with more "stress"
    if [[ -v ASAN_OPTIONS || "$(systemd-detect-virt -v)" == "qemu" ]]; then
        iterations=10
    else
        iterations=50
    fi

    for ((i = 1; i <= iterations; i++)); do
        lvm vgchange -an "$vgroup"
        lvm vgchange -ay "$vgroup"

        if ((i % 5 == 0)); then
            udevadm wait --settle --timeout="$timeout" "/dev/$vgroup/mypart1" "/dev/$vgroup/mypart2" "/dev/disk/by-label/mylvpart1"
            helper_check_device_symlinks "/dev/disk" "/dev/$vgroup"
            helper_check_device_units
        fi
    done

    # Remove the first LV
    lvm lvremove -y "$vgroup/mypart1"
    udevadm wait --settle --timeout="$timeout" --removed "/dev/$vgroup/mypart1"
    udevadm wait --timeout=0 "/dev/$vgroup/mypart2"
    helper_check_device_symlinks "/dev/disk" "/dev/$vgroup"
    helper_check_device_units

    # Create & remove LVs in a loop, i.e. with more "stress"
    if [[ -v ASAN_OPTIONS ]]; then
        iterations=8
        partitions=16
    elif [[ "$(systemd-detect-virt -v)" == "qemu" ]]; then
        iterations=8
        partitions=8
    else
        iterations=16
        partitions=16
    fi

    for ((i = 1; i <= iterations; i++)); do
        # 1) Create some logical volumes
        for ((part = 0; part < partitions; part++)); do
            lvm lvcreate -y -L 4M "$vgroup" -n "looppart$part"
        done

        # 2) Immediately remove them
        lvm lvremove -y $(seq -f "$vgroup/looppart%g" 0 "$((partitions - 1))")

        # 3) On every 4th iteration settle udev and check if all partitions are
        #    indeed gone, and if all symlinks are still valid
        if ((i % 4 == 0)); then
            for ((part = 0; part < partitions; part++)); do
                udevadm wait --settle --timeout="$timeout" --removed "/dev/$vgroup/looppart$part"
            done
            helper_check_device_symlinks "/dev/disk" "/dev/$vgroup"
            helper_check_device_units
        fi
    done
}

testcase_btrfs_basic() {
    local dev_stub i label mpoint uuid
    local devices=(
        /dev/disk/by-id/scsi-0systemd_foobar_deadbeefbtrfs{0..3}
    )

    if ! modinfo btrfs; then
        echo "This test requires the btrfs kernel module but it is not installed, skipping the test" | tee --append /skipped
        exit 77
    fi

    ls -l "${devices[@]}"

    echo "Single device: default settings"
    uuid="deadbeef-dead-dead-beef-000000000000"
    label="btrfs_root"
    udevadm lock --device="${devices[0]}" mkfs.btrfs -f -L "$label" -U "$uuid" "${devices[0]}"
    udevadm wait --settle --timeout=30 "${devices[0]}" "/dev/disk/by-uuid/$uuid" "/dev/disk/by-label/$label"
    btrfs filesystem show
    helper_check_device_symlinks
    helper_check_device_units

    echo "Multiple devices: using partitions, data: single, metadata: raid1"
    uuid="deadbeef-dead-dead-beef-000000000001"
    label="btrfs_mpart"
    udevadm lock --device="${devices[0]}" sfdisk --wipe=always "${devices[0]}" <<EOF
label: gpt

name="diskpart1", size=85M
name="diskpart2", size=85M
name="diskpart3", size=85M
name="diskpart4", size=85M
EOF
    udevadm wait --settle --timeout=30 /dev/disk/by-partlabel/diskpart{1..4}
    udevadm lock --device="${devices[0]}" mkfs.btrfs -f -d single -m raid1 -L "$label" -U "$uuid" /dev/disk/by-partlabel/diskpart{1..4}
    udevadm wait --settle --timeout=30 "/dev/disk/by-uuid/$uuid" "/dev/disk/by-label/$label"
    btrfs filesystem show
    helper_check_device_symlinks
    helper_check_device_units
    wipefs -a -f "${devices[0]}"
    udevadm wait --settle --timeout=30 --removed /dev/disk/by-partlabel/diskpart{1..4}

    echo "Multiple devices: using disks, data: raid10, metadata: raid10, mixed mode"
    uuid="deadbeef-dead-dead-beef-000000000002"
    label="btrfs_mdisk"
    udevadm lock \
            --device=/dev/disk/by-id/scsi-0systemd_foobar_deadbeefbtrfs0 \
            --device=/dev/disk/by-id/scsi-0systemd_foobar_deadbeefbtrfs1 \
            --device=/dev/disk/by-id/scsi-0systemd_foobar_deadbeefbtrfs2 \
            --device=/dev/disk/by-id/scsi-0systemd_foobar_deadbeefbtrfs3 \
            mkfs.btrfs -f -M -d raid10 -m raid10 -L "$label" -U "$uuid" "${devices[@]}"
    udevadm wait --settle --timeout=30 "/dev/disk/by-uuid/$uuid" "/dev/disk/by-label/$label"
    btrfs filesystem show
    helper_check_device_symlinks
    helper_check_device_units

    echo "Multiple devices: using LUKS encrypted disks, data: raid1, metadata: raid1, mixed mode"
    uuid="deadbeef-dead-dead-beef-000000000003"
    label="btrfs_mencdisk"
    mpoint="/btrfs_enc$RANDOM"
    mkdir "$mpoint"
    # Create a key-file
    dd if=/dev/urandom of=/etc/btrfs_keyfile bs=64 count=1 iflag=fullblock
    chmod 0600 /etc/btrfs_keyfile
    # Encrypt each device and add it to /etc/crypttab, so it can be mounted
    # automagically later
    : >/etc/crypttab
    for ((i = 0; i < ${#devices[@]}; i++)); do
        # Intentionally use weaker cipher-related settings, since we don't care
        # about security here as it's a throwaway LUKS partition
        udevadm lock --device="${devices[$i]}" \
                cryptsetup luksFormat -q \
                --use-urandom --pbkdf pbkdf2 --pbkdf-force-iterations 1000 \
                --uuid "deadbeef-dead-dead-beef-11111111111$i" --label "encdisk$i" "${devices[$i]}" /etc/btrfs_keyfile
        udevadm wait --settle --timeout=30 "/dev/disk/by-uuid/deadbeef-dead-dead-beef-11111111111$i" "/dev/disk/by-label/encdisk$i"
        # Add the device into /etc/crypttab, reload systemd, and then activate
        # the device so we can create a filesystem on it later
        echo "encbtrfs$i UUID=deadbeef-dead-dead-beef-11111111111$i /etc/btrfs_keyfile luks" >>/etc/crypttab
        systemctl daemon-reload
        systemctl start "systemd-cryptsetup@encbtrfs$i"
    done
    helper_check_device_symlinks
    helper_check_device_units
    # Check if we have all necessary DM devices
    ls -l /dev/mapper/encbtrfs{0..3}
    # Create a multi-device btrfs filesystem on the LUKS devices
    udevadm lock \
            --device=/dev/mapper/encbtrfs0 \
            --device=/dev/mapper/encbtrfs1 \
            --device=/dev/mapper/encbtrfs2 \
            --device=/dev/mapper/encbtrfs3 \
            mkfs.btrfs -f -M -d raid1 -m raid1 -L "$label" -U "$uuid" /dev/mapper/encbtrfs{0..3}
    udevadm wait --settle --timeout=30 "/dev/disk/by-uuid/$uuid" "/dev/disk/by-label/$label"
    btrfs filesystem show
    helper_check_device_symlinks
    helper_check_device_units
    # Mount it and write some data to it we can compare later
    mount -t btrfs /dev/mapper/encbtrfs0 "$mpoint"
    echo "hello there" >"$mpoint/test"
    # "Deconstruct" the btrfs device and check if we're in a sane state (symlink-wise)
    umount "$mpoint"
    systemctl stop systemd-cryptsetup@encbtrfs{0..3}
    udevadm wait --settle --timeout=30 --removed "/dev/disk/by-uuid/$uuid"
    helper_check_device_symlinks
    helper_check_device_units
    # Add the mount point to /etc/fstab and check if the device can be put together
    # automagically. The source device is the DM name of the first LUKS device
    # (from /etc/crypttab). We have to specify all LUKS devices manually, as
    # registering the necessary devices is usually initrd's job (via btrfs device scan)
    dev_stub="/dev/mapper/encbtrfs"
    echo "/dev/mapper/encbtrfs0 $mpoint btrfs device=${dev_stub}0,device=${dev_stub}1,device=${dev_stub}2,device=${dev_stub}3 0 2" >>/etc/fstab
    # Tell systemd about the new mount
    systemctl daemon-reload
    # Restart cryptsetup.target to trigger autounlock of partitions in /etc/crypttab
    systemctl restart cryptsetup.target
    # Start the corresponding mount unit and check if the btrfs device was reconstructed
    # correctly
    systemctl start "${mpoint##*/}.mount"
    udevadm wait --settle --timeout=30 "/dev/disk/by-uuid/$uuid" "/dev/disk/by-label/$label"
    btrfs filesystem show
    helper_check_device_symlinks
    helper_check_device_units
    grep "hello there" "$mpoint/test"
    # Cleanup
    systemctl stop "${mpoint##*/}.mount"
    systemctl stop systemd-cryptsetup@encbtrfs{0..3}
    sed -i "/${mpoint##*/}/d" /etc/fstab
    : >/etc/crypttab
    rm -fr "$mpoint"
    systemctl daemon-reload
    udevadm settle
}

testcase_iscsi_lvm() {
    local dev i label link lun_id mpoint target_name uuid
    local target_ip="127.0.0.1"
    local target_port="3260"
    local vgroup="iscsi_lvm$RANDOM"
    local expected_symlinks=()
    local devices=(
        /dev/disk/by-id/scsi-0systemd_foobar_deadbeefiscsi{0..3}
    )

    . /etc/os-release
    if [[ "$ID" == "ubuntu" ]]; then
        echo "LVM on Ubuntu is broken, skipping the test" | tee --append /skipped
        exit 77
    fi

    ls -l "${devices[@]}"

    # Start the target daemon (debian names it tgt.service so make sure we handle that)
    if systemctl list-unit-files tgt.service; then
        systemctl start tgt
        systemctl status tgt
    elif systemctl list-unit-files tgtd.service; then
        systemctl start tgtd
        systemctl status tgtd
    else
        echo "This test requires tgtd but it is not installed, skipping ..." | tee --append /skipped
        exit 77
    fi

    echo "iSCSI LUNs backed by devices"
    # See RFC3721 and RFC7143
    target_name="iqn.2021-09.com.example:iscsi.test"
    # Initialize a new iSCSI target <$target_name> consisting of 4 LUNs, each
    # backed by a device
    tgtadm --lld iscsi --op new --mode target --tid=1 --targetname "$target_name"
    for ((i = 0; i < ${#devices[@]}; i++)); do
        # lun-0 is reserved by iSCSI
        lun_id="$((i + 1))"
        tgtadm --lld iscsi --op new --mode logicalunit --tid 1 --lun "$lun_id" -b "${devices[$i]}"
        tgtadm --lld iscsi --op update --mode logicalunit --tid 1 --lun "$lun_id"
        expected_symlinks+=(
            "/dev/disk/by-path/ip-$target_ip:$target_port-iscsi-$target_name-lun-$lun_id"
        )
    done
    tgtadm --lld iscsi --op bind --mode target --tid 1 -I ALL
    # Configure the iSCSI initiator
    iscsiadm --mode discoverydb --type sendtargets --portal "$target_ip" --discover
    iscsiadm --mode node --targetname "$target_name" --portal "$target_ip:$target_port" --login
    udevadm wait --settle --timeout=30 "${expected_symlinks[@]}"
    helper_check_device_symlinks
    helper_check_device_units
    # Cleanup
    iscsiadm --mode node --targetname "$target_name" --portal "$target_ip:$target_port" --logout
    tgtadm --lld iscsi --op delete --mode target --tid=1

    echo "iSCSI LUNs backed by files + LVM"
    # Note: we use files here to "trick" LVM the disks are indeed on a different
    #       host, so it doesn't automagically detect another path to the backing
    #       device once we disconnect the iSCSI devices
    target_name="iqn.2021-09.com.example:iscsi.lvm.test"
    mpoint="$(mktemp -d /iscsi_storeXXX)"
    expected_symlinks=()
    # Use the first device as it's configured with larger capacity
    udevadm lock --device "${devices[0]}" mkfs.ext4 -L iscsi_store "${devices[0]}"
    udevadm wait --settle --timeout=30 "${devices[0]}"
    mount "${devices[0]}" "$mpoint"
    for i in {1..4}; do
        dd if=/dev/zero of="$mpoint/lun$i.img" bs=1M count=32
    done
    # Initialize a new iSCSI target <$target_name> consisting of 4 LUNs, each
    # backed by a file
    tgtadm --lld iscsi --op new --mode target --tid=2 --targetname "$target_name"
    # lun-0 is reserved by iSCSI
    for i in {1..4}; do
        tgtadm --lld iscsi --op new --mode logicalunit --tid 2 --lun "$i" -b "$mpoint/lun$i.img"
        tgtadm --lld iscsi --op update --mode logicalunit --tid 2 --lun "$i"
        expected_symlinks+=(
            "/dev/disk/by-path/ip-$target_ip:$target_port-iscsi-$target_name-lun-$i"
        )
    done
    tgtadm --lld iscsi --op bind --mode target --tid 2 -I ALL
    # Configure the iSCSI initiator
    iscsiadm --mode discoverydb --type sendtargets --portal "$target_ip" --discover
    iscsiadm --mode node --targetname "$target_name" --portal "$target_ip:$target_port" --login
    udevadm wait --settle --timeout=30 "${expected_symlinks[@]}"
    helper_check_device_symlinks
    helper_check_device_units
    # Add all iSCSI devices into a LVM volume group, create two logical volumes,
    # and check if necessary symlinks exist (and are valid)
    lvm pvcreate -y "${expected_symlinks[@]}"
    lvm pvs
    lvm vgcreate "$vgroup" -y "${expected_symlinks[@]}"
    lvm vgs
    lvm vgchange -ay "$vgroup"
    lvm lvcreate -y -L 4M "$vgroup" -n mypart1
    lvm lvcreate -y -L 8M "$vgroup" -n mypart2
    lvm lvs
    udevadm wait --settle --timeout=30 "/dev/$vgroup/mypart1" "/dev/$vgroup/mypart2"
    mkfs.ext4 -L mylvpart1 "/dev/$vgroup/mypart1"
    udevadm trigger --settle "/dev/$vgroup/mypart1"
    udevadm wait --settle --timeout=30 "/dev/disk/by-label/mylvpart1"
    helper_check_device_symlinks "/dev/disk" "/dev/$vgroup"
    helper_check_device_units
    # Disconnect the iSCSI devices and check all the symlinks
    iscsiadm --mode node --targetname "$target_name" --portal "$target_ip:$target_port" --logout
    # "Reset" the DM state, since we yanked the backing storage from under the LVM,
    # so the currently active VGs/LVs are invalid
    dmsetup remove_all --deferred
    # The LVM and iSCSI related symlinks should be gone
    udevadm wait --settle --timeout=30 --removed "/dev/$vgroup" "/dev/disk/by-label/mylvpart1" "${expected_symlinks[@]}"
    helper_check_device_symlinks "/dev/disk"
    helper_check_device_units
    # Reconnect the iSCSI devices and check if everything get detected correctly
    iscsiadm --mode discoverydb --type sendtargets --portal "$target_ip" --discover
    iscsiadm --mode node --targetname "$target_name" --portal "$target_ip:$target_port" --login
    udevadm wait --settle --timeout=30 "${expected_symlinks[@]}" "/dev/$vgroup/mypart1" "/dev/$vgroup/mypart2" "/dev/disk/by-label/mylvpart1"
    helper_check_device_symlinks "/dev/disk" "/dev/$vgroup"
    helper_check_device_units
    # Cleanup
    iscsiadm --mode node --targetname "$target_name" --portal "$target_ip:$target_port" --logout
    tgtadm --lld iscsi --op delete --mode target --tid=2
    umount "$mpoint"
    rm -rf "$mpoint"
}

testcase_long_sysfs_path() {
    local cursor link logfile mpoint
    local expected_symlinks=(
        "/dev/disk/by-label/data_vol"
        "/dev/disk/by-label/swap_vol"
        "/dev/disk/by-partlabel/test_swap"
        "/dev/disk/by-partlabel/test_part"
        "/dev/disk/by-partuuid/deadbeef-dead-dead-beef-000000000000"
        "/dev/disk/by-uuid/deadbeef-dead-dead-beef-111111111111"
        "/dev/disk/by-uuid/deadbeef-dead-dead-beef-222222222222"
    )

    # Create a cursor file to skip messages generated by udevd in initrd, as it
    # might not be the same up-to-date version as we currently run (hence generating
    # messages we check for later and making the test fail)
    cursor="$(mktemp)"
    journalctl --cursor-file="${cursor:?}" -n0 -q

    # Make sure the test device is connected and show its "wonderful" path
    dev="$(udevadm info -e --property-match=ID_SERIAL=long-sysfs-path --property-match DEVTYPE=disk --json=short | jq -r .DEVNAME)"
    dev="${dev#/dev/}"

    stat "/sys/block/${dev}"
    readlink -f "/sys/block/${dev}/dev"

    udevadm lock --device "/dev/${dev}" sfdisk "/dev/${dev}" <<EOF
label: gpt

name="test_swap", size=32M
uuid="deadbeef-dead-dead-beef-000000000000", name="test_part", size=5M
EOF
    udevadm settle
    udevadm lock --device "/dev/${dev}1" mkswap -U "deadbeef-dead-dead-beef-111111111111" -L "swap_vol" "/dev/${dev}1"
    udevadm lock --device "/dev/${dev}2" mkfs.ext4 -U "deadbeef-dead-dead-beef-222222222222" -L "data_vol" "/dev/${dev}2"
    udevadm wait --settle --timeout=30 "${expected_symlinks[@]}"

    # Try to mount the data partition manually (using its label)
    mpoint="$(mktemp -d /logsysfsXXX)"
    mount LABEL=data_vol "$mpoint"
    touch "$mpoint/test"
    umount "$mpoint"
    # Do the same, but with UUID and using fstab
    echo "UUID=deadbeef-dead-dead-beef-222222222222 $mpoint ext4 defaults 0 0" >>/etc/fstab
    systemctl daemon-reload
    mount "$mpoint"
    timeout 30 bash -c "until systemctl -q is-active '$mpoint'; do sleep .2; done"
    test -e "$mpoint/test"
    umount "$mpoint"

    # Test out the swap partition
    swapon -v -L swap_vol
    swapoff -v -L swap_vol

    udevadm settle

    logfile="$(mktemp)"
    # Check state of affairs after https://github.com/systemd/systemd/pull/22759
    # Note: can't use `--cursor-file` here, since we don't want to update the cursor
    #       after using it
    [[ "$(journalctl --after-cursor="$(<"$cursor")" -q --no-pager -o short-monotonic -p info --grep "Device path.*${dev}.?' too long to fit into unit name" | wc -l)" -eq 0 ]]
    [[ "$(journalctl --after-cursor="$(<"$cursor")" -q --no-pager -o short-monotonic --grep "Unit name .*${dev}.?\.device\" too long, falling back to hashed unit name" | wc -l)" -gt 0 ]]
    # Check if the respective "hashed" units exist and are active (plugged)
    systemctl status --no-pager "$(readlink -f "/sys/block/${dev}/${dev}1")"
    systemctl status --no-pager "$(readlink -f "/sys/block/${dev}/${dev}2")"
    # Make sure we don't unnecessarily spam the log
    { journalctl -b -q --no-pager -o short-monotonic -p info --grep "/sys/devices/.+/${dev}[0-9]?" _PID=1 + UNIT=systemd-udevd.service || :;} | tee "$logfile"
    [[ "$(wc -l <"$logfile")" -lt 10 ]]

    : >/etc/fstab
    rm -fr "${cursor:?}" "${logfile:?}" "${mpoint:?}"
}

testcase_mdadm_basic() {
    local i part_name raid_name raid_dev uuid
    local expected_symlinks=()
    local devices=(
        /dev/disk/by-id/scsi-0systemd_foobar_deadbeefmdadm{0..4}
    )

    ls -l "${devices[@]}"

    echo "Mirror raid (RAID 1)"
    raid_name="mdmirror"
    raid_dev="/dev/md/$raid_name"
    part_name="${raid_name}_part"
    uuid="aaaaaaaa:bbbbbbbb:cccccccc:00000001"
    expected_symlinks=(
        "$raid_dev"
        "/dev/disk/by-id/md-name-H:$raid_name"
        "/dev/disk/by-id/md-uuid-$uuid"
        "/dev/disk/by-label/$part_name" # ext4 partition
    )
    # Create a simple RAID 1 with an ext4 filesystem
    echo y | mdadm --create "$raid_dev" --name "$raid_name" --uuid "$uuid" /dev/disk/by-id/scsi-0systemd_foobar_deadbeefmdadm{0..1} -v -f --level=1 --raid-devices=2
    udevadm wait --settle --timeout=30 "$raid_dev"
    # udevd does not lock md devices, hence we need to trigger uevent after creating filesystem.
    mkfs.ext4 -L "$part_name" "$raid_dev"
    udevadm trigger --settle "$raid_dev"
    udevadm wait --settle --timeout=30 "${expected_symlinks[@]}"
    for i in {0..9}; do
        echo "Disassemble - reassemble loop, iteration #$i"
        mdadm -v --stop "$raid_dev"
        udevadm wait --settle --timeout=30 --removed "${expected_symlinks[@]}"
        mdadm --assemble "$raid_dev" --name "$raid_name" -v
        udevadm wait --settle --timeout=30 "${expected_symlinks[@]}"
    done
    helper_check_device_symlinks
    helper_check_device_units
    # Cleanup
    mdadm -v --stop "$raid_dev"
    udevadm wait --settle --timeout=30 --removed "${expected_symlinks[@]}"

    echo "Parity raid (RAID 5)"
    raid_name="mdparity"
    raid_dev="/dev/md/$raid_name"
    part_name="${raid_name}_part"
    uuid="aaaaaaaa:bbbbbbbb:cccccccc:00000101"
    expected_symlinks=(
        "$raid_dev"
        "/dev/disk/by-id/md-name-H:$raid_name"
        "/dev/disk/by-id/md-uuid-$uuid"
        "/dev/disk/by-label/$part_name" # ext4 partition
    )
    # Create a simple RAID 5 with an ext4 filesystem
    echo y | mdadm --create "$raid_dev" --name "$raid_name" --uuid "$uuid" /dev/disk/by-id/scsi-0systemd_foobar_deadbeefmdadm{0..2} -v -f --level=5 --raid-devices=3
    udevadm wait --settle --timeout=30 "$raid_dev"
    mkfs.ext4 -L "$part_name" "$raid_dev"
    udevadm trigger --settle "$raid_dev"
    udevadm wait --settle --timeout=30 "${expected_symlinks[@]}"
    for i in {0..9}; do
        echo "Disassemble - reassemble loop, iteration #$i"
        mdadm -v --stop "$raid_dev"
        udevadm wait --settle --timeout=30 --removed "${expected_symlinks[@]}"
        mdadm --assemble "$raid_dev" --name "$raid_name" -v
        udevadm wait --settle --timeout=30 "${expected_symlinks[@]}"
    done
    helper_check_device_symlinks
    helper_check_device_units
    # Cleanup
    mdadm -v --stop "$raid_dev"
    udevadm wait --settle --timeout=30 --removed "${expected_symlinks[@]}"
    helper_check_device_units

    echo "Mirror + parity raid (RAID 10) + multiple partitions"
    raid_name="mdmirpar"
    raid_dev="/dev/md/$raid_name"
    part_name="${raid_name}_part"
    uuid="aaaaaaaa:bbbbbbbb:cccccccc:00001010"
    expected_symlinks=(
        "$raid_dev"
        "/dev/disk/by-id/md-name-H:$raid_name"
        "/dev/disk/by-id/md-uuid-$uuid"
        "/dev/disk/by-label/$part_name" # ext4 partition
        # Partitions
        "${raid_dev}1"
        "${raid_dev}2"
        "${raid_dev}3"
        "/dev/disk/by-id/md-name-H:$raid_name-part1"
        "/dev/disk/by-id/md-name-H:$raid_name-part2"
        "/dev/disk/by-id/md-name-H:$raid_name-part3"
        "/dev/disk/by-id/md-uuid-$uuid-part1"
        "/dev/disk/by-id/md-uuid-$uuid-part2"
        "/dev/disk/by-id/md-uuid-$uuid-part3"
    )
    # Create a simple RAID 10 with an ext4 filesystem
    echo y | mdadm --create "$raid_dev" --name "$raid_name" --uuid "$uuid" /dev/disk/by-id/scsi-0systemd_foobar_deadbeefmdadm{0..3} -v -f --level=10 --raid-devices=4
    udevadm wait --settle --timeout=30 "$raid_dev"
    # Partition the raid device
    # Here, 'udevadm lock' is meaningless, as udevd does not lock MD devices.
    # We need to trigger uevents after sfdisk and mkfs.
    sfdisk --wipe=always "$raid_dev" <<EOF
label: gpt

uuid="deadbeef-dead-dead-beef-111111111111", name="mdpart1", size=8M
uuid="deadbeef-dead-dead-beef-222222222222", name="mdpart2", size=32M
uuid="deadbeef-dead-dead-beef-333333333333", name="mdpart3", size=16M
EOF
    udevadm trigger --settle --parent-match "$raid_dev"
    udevadm wait --settle --timeout=30 "/dev/disk/by-id/md-uuid-$uuid-part2"
    mkfs.ext4 -L "$part_name" "/dev/disk/by-id/md-uuid-$uuid-part2"
    udevadm trigger --settle "/dev/disk/by-id/md-uuid-$uuid-part2"
    udevadm wait --settle --timeout=30 "${expected_symlinks[@]}"
    for i in {0..9}; do
        echo "Disassemble - reassemble loop, iteration #$i"
        mdadm -v --stop "$raid_dev"
        udevadm wait --settle --timeout=30 --removed "${expected_symlinks[@]}"
        mdadm --assemble "$raid_dev" --name "$raid_name" -v
        udevadm wait --settle --timeout=30 "${expected_symlinks[@]}"
    done
    helper_check_device_symlinks
    helper_check_device_units
    # Cleanup
    mdadm -v --stop "$raid_dev"
    # Check if all expected symlinks were removed after the cleanup
    udevadm wait --settle --timeout=30 --removed "${expected_symlinks[@]}"
    helper_check_device_units
}

testcase_mdadm_lvm() {
    local part_name raid_name raid_dev uuid vgroup
    local expected_symlinks=()
    local devices=(
        /dev/disk/by-id/scsi-0systemd_foobar_deadbeefmdadmlvm{0..4}
    )

    ls -l "${devices[@]}"

    raid_name="mdlvm"
    raid_dev="/dev/md/$raid_name"
    part_name="${raid_name}_part"
    vgroup="${raid_name}_vg"
    uuid="aaaaaaaa:bbbbbbbb:ffffffff:00001010"
    expected_symlinks=(
        "$raid_dev"
        "/dev/$vgroup/mypart1"          # LVM partition
        "/dev/$vgroup/mypart2"          # LVM partition
        "/dev/disk/by-id/md-name-H:$raid_name"
        "/dev/disk/by-id/md-uuid-$uuid"
        "/dev/disk/by-label/$part_name" # ext4 partition
    )
    # Create a RAID 10 with LVM + ext4
    echo y | mdadm --create "$raid_dev" --name "$raid_name" --uuid "$uuid" /dev/disk/by-id/scsi-0systemd_foobar_deadbeefmdadmlvm{0..3} -v -f --level=10 --raid-devices=4
    udevadm wait --settle --timeout=30 "$raid_dev"
    # Create an LVM on the MD
    lvm pvcreate -y "$raid_dev"
    lvm pvs
    lvm vgcreate "$vgroup" -y "$raid_dev"
    lvm vgs
    lvm vgchange -ay "$vgroup"
    lvm lvcreate -y -L 4M "$vgroup" -n mypart1
    lvm lvcreate -y -L 8M "$vgroup" -n mypart2
    lvm lvs
    udevadm wait --settle --timeout=30 "/dev/$vgroup/mypart1" "/dev/$vgroup/mypart2"
    mkfs.ext4 -L "$part_name" "/dev/$vgroup/mypart2"
    udevadm trigger --settle "/dev/$vgroup/mypart2"
    udevadm wait --settle --timeout=30 "${expected_symlinks[@]}"
    # Disassemble the array
    lvm vgchange -an "$vgroup"
    mdadm -v --stop "$raid_dev"
    udevadm wait --settle --timeout=30 --removed "${expected_symlinks[@]}"
    helper_check_device_symlinks
    helper_check_device_units
    # Reassemble it and check if all required symlinks exist
    mdadm --assemble "$raid_dev" --name "$raid_name" -v
    udevadm wait --settle --timeout=30 "${expected_symlinks[@]}"
    helper_check_device_symlinks
    helper_check_device_units
    # Cleanup
    lvm vgchange -an "$vgroup"
    mdadm -v --stop "$raid_dev"
    # Check if all expected symlinks were removed after the cleanup
    udevadm wait --settle --timeout=30 --removed "${expected_symlinks[@]}"
    helper_check_device_units
}

udevadm settle
udevadm control --log-level debug
lsblk -a

echo "Check if all symlinks under /dev/disk/ are valid (pre-test)"
helper_check_device_symlinks

# TEST_FUNCTION_NAME is passed on the kernel command line via systemd.setenv=
# in the respective test.sh file
if ! command -v "${TEST_FUNCTION_NAME:?}"; then
    echo >&2 "Missing verification handler for test case '$TEST_FUNCTION_NAME'"
    exit 1
fi

echo "TEST_FUNCTION_NAME=$TEST_FUNCTION_NAME"
"$TEST_FUNCTION_NAME"
udevadm settle

echo "Check if all symlinks under /dev/disk/ are valid (post-test)"
helper_check_device_symlinks

udevadm control --log-level info

systemctl status systemd-udevd

touch /testok
