Viewing: sanity-ec.sh

#!/usr/bin/bash
# SPDX-License-Identifier: GPL-2.0-only
#
# Run select tests by setting ONLY, or as arguments to the script.
# Skip specific tests by setting EXCEPT.
set -e
set +o posix


ONLY=${ONLY:-"$*"}

LUSTRE=${LUSTRE:-$(dirname $0)/..}
. $LUSTRE/tests/test-framework.sh
init_test_env "$@"
init_logging

ALWAYS_EXCEPT="$SANITY_EC_EXCEPT "

# tests 12a: EC parity calculation produces incorrect content (LU-19631)
# tests 41d: a degraded mmap read never completes.  The OSC alternates
# between "too many resent retries" and osc_brw_redo_request() forever,
# so -EIO never reaches the LOV layer and CIT_FAULT never switches to
# CIT_EC_RD.  The read hangs until the harness kills the node rather
# than failing, so it stays off until the retry loop is fixed.
# tests 53a: a recovery read at a non-zero offset intermittently
# reconstructs wrong data.  Measured at 2 failures in 10 runs, and it
# still fails 1 in 10 with the proactive dead-OST detection applied, so
# it is not the reconstruction bug the other offset tests hit (LU-12668)
always_except LU-19631 12a
always_except LU-20708 41d
always_except LU-20709 53a

# aio EC recovery is not working yet
always_except LU-20566 41j 41k 41l 41m 41n

if [[ "$ost1_FSTYPE" == "zfs" ]]; then
	# test 5b: EC resync hole-punches stale parity, but osd-zfs has no
	# fallocate hole-punch, so resync fails with EOPNOTSUPP (LU-20435)
	always_except LU-20435 5b 12b 12e
fi

build_test_filter

check_and_setup_lustre

(( MDS1_VERSION >= $(version_code 2.17.52) )) ||
	skip "Need MDS version at least 2.17.52 for EC support"
DIR=${DIR:-$MOUNT}
assert_DIR
rm -rf $DIR/[Rdfs][0-9]*

(( UID != 0 || RUNAS_ID != 0 )) ||
	error "\$RUNAS_ID set to 0, but \$UID is also 0!"

check_runas_id $RUNAS_ID $RUNAS_GID $RUNAS

# the EC read-fault helpers select OSTs through a 16-bit fail_val mask
(( OSTCOUNT <= 16 )) ||
	error "limit used OST indexes to <= 15 for EC_FAULT_MASK"

# Enable EC support for testing
export LFS_EC_OK=yes
stack_trap "unset LFS_EC_OK" EXIT

#
# Verify mirror count with an expected value for a given file.
#
verify_mirror_count() {
	local tf=$1
	local expected=$2
	local mirror_count=$($LFS getstripe -N $tf)

	(( mirror_count == expected )) || {
		$LFS getstripe -v $tf
		error "verify mirror count failed on $tf:" \
		      "$mirror_count != $expected"
	}
}

#
# Verify component count with an expected value for a given file.
#	$1 composite layout file
#	$2 expected component number
#
verify_comp_count() {
	local tf=$1
	local expected=$2
	local comp_count=$($LFS getstripe --component-count $tf)

	[[ $comp_count = $expected ]] || {
		$LFS getstripe -v $tf
		error "verify component count failed on $tf:" \
		      "$comp_count != $expected"
	}
}

#
# Verify component has the parity flag set
#
verify_comp_parity() {
	local tf=$1
	local comp_id=$2
	local flags=$($LFS getstripe -I$comp_id $tf |
		      awk '/lcme_flags:/ { print $2 }')

	[[ $flags =~ "parity" ]] || {
		$LFS getstripe -I$comp_id -v $tf
		error "verify parity flag failed on $tf component $comp_id:" \
		      "flags=$flags"
	}
}

#
# Enable erasure coding and restore on exit
#
enable_ec() {
	(( OSTCOUNT >= 3 )) || skip "needs >= 3 OSTs for EC"

	local ec_enable=($($LCTL get_param -n llite.*.enable_erasure_coding))

	$LCTL set_param llite.*.enable_erasure_coding=1
	stack_trap "$LCTL set_param -n llite.*.enable_erasure_coding=$ec_enable"

	# Scale a generic EC layout to the number of available OSTs so the
	# tests run on any configuration with a small number of OSTs instead
	# of hard-coding a fixed geometry that needs a large OST count.
	# The raidset allocator requires a unique OST for every data stripe
	# plus one per parity in a single raidset, so keep data + parity <=
	# OSTCOUNT. Tests that need a specific geometry set their own values.
	EC_CSTRIPE=$((OSTCOUNT > 4 ? 2 : 1))
	EC_DSTRIPE=$((OSTCOUNT > 7 ? 6 : OSTCOUNT - EC_CSTRIPE))
	EC_MINSTRIPE=$((EC_DSTRIPE + EC_CSTRIPE))
	EC_ARG="--ec $EC_DSTRIPE+$EC_CSTRIPE"
}

#
# Verify EC stripe counts (data and coding stripes)
#
verify_ec_stripe_count() {
	local tf=$1
	local comp_id=$2
	local expected_dstripe=$3
	local expected_cstripe=$4
	local dstripe=$($LFS getstripe -I$comp_id $tf | \
			awk '/lcme_dstripe_count:/ { print $2 }')
	local cstripe=$($LFS getstripe -I$comp_id $tf | \
			awk '/lcme_cstripe_count:/ { print $2 }')

	[[ $dstripe = $expected_dstripe ]] || {
		$LFS getstripe -I$comp_id -v $tf
		error "verify dstripe count failed on $tf component $comp_id:" \
		      "$dstripe != $expected_dstripe"
	}

	[[ $cstripe = $expected_cstripe ]] || {
		$LFS getstripe -I$comp_id -v $tf
		error "verify cstripe count failed on $tf component $comp_id:" \
		      "$cstripe != $expected_cstripe"
	}
}

#
# Verify EC raidset map on a parity component.
#	$1 file
#	$2 parity component ID (empty to dump whole file)
#	$3 expected raidset count
#	$4+ substrings expected in raidset output lines
#
verify_ec_raidsets() {
	local tf=$1
	local comp_id=$2
	local expected_count=$3
	local output
	local -a cmd

	cmd=($LFS getstripe --ec-map)
	[[ -n "$comp_id" ]] && cmd+=(-I$comp_id)
	cmd+=($tf)

	output=$("${cmd[@]}")
	echo "$output"

	echo "$output" | verify_yaml ||
		error "verify raidset map failed on $tf: invalid YAML"

	echo "$output" | grep -q "lcme_id:" ||
		error "verify raidset map failed on $tf: missing lcme_id"

	local count=$(echo "$output" | awk '/lcme_ec_raidset_count:/ { print $2 }')
	(( count == expected_count )) ||
		error "$tf raidset count on comp $comp_id: $count != $expected_count"

	shift 3
	while (( $# > 0 )); do
		echo "$output" | grep -q "$1" ||
			error "verify raidset map failed on $tf:missing '$1'"
		shift
	done
}

#
# Verify component extent with expected start and end extent values
# for a given file and component ID.
#
verify_comp_extent() {
	local tf=$1
	local comp_id=$2
	local expected_start=$3
	local expected_end=$4
	local start=$($LFS getstripe -I$comp_id --component-start $tf)
	local end=$($LFS getstripe -I$comp_id --component-end $tf)

	[[ $start = $expected_start ]] || {
		$LFS getstripe -I$comp_id -v $tf
		error "verify component start failed on $tf comp $comp_id:" \
		      "$start != $expected_start"
	}

	[[ $end = $expected_end ]] || {
		$LFS getstripe -I$comp_id -v $tf
		error "verify component end failed on $tf comp $comp_id:" \
		      "$end != $expected_end"
	}
}

#
# Verify FLR state (ro, wp, sp) for a given file
#
verify_flr_state() {
	local tf=$1
	local expected_state=$2

	local state=$($LFS getstripe -v $tf | awk '/lcm_flags/{ print $2 }')
	[[ $expected_state = $state ]] ||
		error "expected: $expected_state, actual $state"
}

#
# Verify component has stale flag set
#
verify_comp_stale() {
	local tf=$1
	local comp_id=$2
	local flags=$($LFS getstripe -I$comp_id $tf |
			awk '/lcme_flags:/ { print $2 }')

	[[ $flags =~ "stale" ]] || {
		$LFS getstripe -I$comp_id -v $tf
		error "verify stale flag failed on $tf component $comp_id:" \
		      "flags=$flags"
	}
}

#
# Verify stripe size matches between data and EC components
#
verify_ec_stripe_size() {
	local tf=$1
	local data_comp_id=$2
	local ec_comp_id=$3
	local data_stripe_size=$($LFS getstripe -I$data_comp_id $tf | \
				awk '/lmm_stripe_size:/ { print $2 }')
	local ec_stripe_size=$($LFS getstripe -I$ec_comp_id $tf | \
				awk '/lmm_stripe_size:/ { print $2 }')

	[[ $data_stripe_size = $ec_stripe_size ]] || {
		$LFS getstripe -v $tf
		error "stripe size mismatch on $tf:" \
		      "data component $data_comp_id has $data_stripe_size," \
		      "EC component $ec_comp_id has $ec_stripe_size"
	}
}

#
# Read from parity mirror and check for zeros.
# Usage: check_parity_read <file> <mirror_id> <offset> <expect_zeros>
#
check_parity_read() {
	local file=$1
	local mirror_id=$2
	local offset=$3
	local expect_zeros=$4
	local rc

	# test_parity_read returns 0 if all zeros, 1 if non-zero bytes found
	$LUSTRE/tests/test_parity_read $file $mirror_id $offset > /dev/null 2>&1
	rc=$?

	if [[ $expect_zeros == "yes" ]]; then
		(( rc == 0 )) || error "offset $offset: expected zeros"
	else
		(( rc == 1 )) || error "offset $offset: expected parity data"
	fi
}

#
# Classify the mirrors of an EC file into the globals ec_parity_id (carries the
# parity flag), ec_data_id (the data mirror linked to it via
# lcme_mirror_link_id) and ec_plain_id (a mirror with no link). Any of them is
# empty if the file has no such mirror.
#
identify_ec_mirrors() {
	local tf=$1
	local id flags link

	ec_data_id=""
	ec_parity_id=""
	ec_plain_id=""

	for id in $($LFS getstripe $tf | awk '/lcme_mirror_id:/ {print $2}' |
		    sort -u); do
		flags=$($LFS getstripe --mirror-id=$id $tf |
			awk '/lcme_flags:/ {print $2; exit}')
		if [[ $flags =~ "parity" ]]; then
			ec_parity_id=$id
			continue
		fi
		link=$($LFS getstripe -v $tf | awk -v m=$id '
			/lcme_mirror_id:/ { cur = $2 }
			/lcme_mirror_link_id:/ {
				if (cur == m) { print $2; exit }
			}')
		if [[ -n $link && $link != 0x0 ]]; then
			ec_data_id=$id
		else
			ec_plain_id=$id
		fi
	done
}

test_1a() {
	enable_ec
	local tf=$DIR/$tfile
	local ids

	# Single component with EC
	$LFS setstripe -E -1 -c $EC_DSTRIPE $EC_ARG $tf ||
		error "setstripe failed"

	verify_mirror_count $tf 2
	verify_comp_count $tf 2

	# Get component IDs
	ids=($($LFS getstripe $tf | awk '/lcme_id/{print $2}' | tr '\n' ' '))

	# Verify data mirror (mirror 1, component 0)
	verify_comp_extent $tf ${ids[0]} 0 EOF

	# Verify parity mirror (mirror 2, component 1)
	verify_comp_parity $tf ${ids[1]}
	verify_ec_stripe_count $tf ${ids[1]} $EC_DSTRIPE $EC_CSTRIPE
	verify_comp_extent $tf ${ids[1]} 0 EOF

	# Verify stripe size matches between data and EC components
	verify_ec_stripe_size $tf ${ids[0]} ${ids[1]}
}
run_test 1a "basic setstripe with single component and EC"

test_1b() {
	enable_ec
	local tf=$DIR/$tfile
	local ids

	# Order independence: --ec before -E
	$LFS setstripe $EC_ARG -E -1 -c $EC_DSTRIPE $tf ||
		error "setstripe failed"

	verify_mirror_count $tf 2
	verify_comp_count $tf 2

	# Get component IDs
	ids=($($LFS getstripe $tf | awk '/lcme_id/{print $2}' | tr '\n' ' '))

	# Verify data mirror (mirror 1, component 0)
	verify_comp_extent $tf ${ids[0]} 0 EOF

	# Verify parity mirror (mirror 2, component 1)
	verify_comp_parity $tf ${ids[1]}
	verify_ec_stripe_count $tf ${ids[1]} $EC_DSTRIPE $EC_CSTRIPE
	verify_comp_extent $tf ${ids[1]} 0 EOF
}
run_test 1b "setstripe with --ec before -E (order independence)"

test_1c() {
	enable_ec
	local tf=$DIR/$tfile
	local ids

	# Test colon separator: --ec 4:2 instead of 4+2
	$LFS setstripe -E -1 -c $EC_DSTRIPE --ec $EC_DSTRIPE:$EC_CSTRIPE $tf ||
		error "setstripe with colon separator failed"

	verify_mirror_count $tf 2
	verify_comp_count $tf 2

	# Get component IDs
	ids=($($LFS getstripe $tf | awk '/lcme_id/{print $2}' | tr '\n' ' '))

	# Verify data mirror (mirror 1, component 0)
	verify_comp_extent $tf ${ids[0]} 0 EOF

	# Verify parity mirror (mirror 2, component 1)
	verify_comp_parity $tf ${ids[1]}
	verify_ec_stripe_count $tf ${ids[1]} $EC_DSTRIPE $EC_CSTRIPE
	verify_comp_extent $tf ${ids[1]} 0 EOF
}
run_test 1c "setstripe with colon separator (--ec dstripe:cstripe)"

test_1d() {
	enable_ec
	local tf=$DIR/$tfile
	local ids

	# Multiple components, single EC spec
	$LFS setstripe -E 128M -E -1 $EC_ARG $tf ||
		error "setstripe failed"

	verify_mirror_count $tf 2
	verify_comp_count $tf 4

	# Get component IDs
	ids=($($LFS getstripe $tf | awk '/lcme_id/{print $2}' | tr '\n' ' '))

	# Verify data mirror components (mirror 1)
	verify_comp_extent $tf ${ids[0]} 0 134217728
	verify_comp_extent $tf ${ids[1]} 134217728 EOF

	# Verify parity mirror components (mirror 2)
	# First parity component
	verify_comp_parity $tf ${ids[2]}
	verify_ec_stripe_count $tf ${ids[2]} $EC_DSTRIPE $EC_CSTRIPE
	verify_comp_extent $tf ${ids[2]} 0 134217728

	# Second parity component
	verify_comp_parity $tf ${ids[3]}
	verify_ec_stripe_count $tf ${ids[3]} $EC_DSTRIPE $EC_CSTRIPE
	verify_comp_extent $tf ${ids[3]} 134217728 EOF
}
run_test 1d "setstripe with multiple components and single EC spec"

test_1e() {
	enable_ec
	local tf=$DIR/$tfile
	local ids

	# EC specified on first component, should inherit to second
	$LFS setstripe -E 128M $EC_ARG -E -1 -c 2 $tf ||
		error "setstripe failed"

	verify_mirror_count $tf 2
	verify_comp_count $tf 4

	# Get component IDs
	ids=($($LFS getstripe $tf | awk '/lcme_id/{print $2}' | tr '\n' ' '))

	# Verify data mirror components (mirror 1)
	verify_comp_extent $tf ${ids[0]} 0 134217728
	verify_comp_extent $tf ${ids[1]} 134217728 EOF

	# Verify parity mirror components (mirror 2)
	# First component keeps the spec EC($EC_DSTRIPE+$EC_CSTRIPE)
	verify_comp_parity $tf ${ids[2]}
	verify_ec_stripe_count $tf ${ids[2]} $EC_DSTRIPE $EC_CSTRIPE
	verify_comp_extent $tf ${ids[2]} 0 134217728

	# Second component reduced to EC(2+$EC_CSTRIPE) because the data
	# component has -c 2 (fewer than $EC_DSTRIPE data stripes)
	verify_comp_parity $tf ${ids[3]}
	verify_ec_stripe_count $tf ${ids[3]} 2 $EC_CSTRIPE
	verify_comp_extent $tf ${ids[3]} 134217728 EOF
}
run_test 1e "setstripe with EC inheriting to second component"

test_1f() {
	(( $OSTCOUNT >= 6 )) || skip "needs >= 6 OSTs"

	enable_ec
	local tf=$DIR/$tfile
	local ids

	# Different EC for different components: first 2+1, second 4+2 with -c 4
	$LFS setstripe -E 128M --ec 2+1 -E -1 -c 4 --ec 4+2 $tf ||
		error "setstripe failed"

	verify_mirror_count $tf 2
	verify_comp_count $tf 4

	# Get component IDs
	ids=($($LFS getstripe $tf | awk '/lcme_id/{print $2}' | tr '\n' ' '))

	# Verify data mirror components (mirror 1)
	verify_comp_extent $tf ${ids[0]} 0 134217728
	verify_comp_extent $tf ${ids[1]} 134217728 EOF

	# Verify parity mirror components (mirror 2)
	# First component should have EC(2+1)
	verify_comp_parity $tf ${ids[2]}
	verify_ec_stripe_count $tf ${ids[2]} 2 1
	verify_comp_extent $tf ${ids[2]} 0 134217728

	# Second component has EC adjusted to 4+2 (data component has -c 4)
	verify_comp_parity $tf ${ids[3]}
	verify_ec_stripe_count $tf ${ids[3]} 4 2
	verify_comp_extent $tf ${ids[3]} 134217728 EOF
}
run_test 1f "setstripe with different EC for different components"

test_1g() {
	(( OSTCOUNT >= 5 )) || skip "needs >= 5 OSTs"

	enable_ec
	local tf=$DIR/$tfile
	local ids

	# Different parity counts
	# Note: second component has -c 4, so EC will be adjusted to 4+1
	# (can't have 8 data stripes with only 4 total stripes)
	$LFS setstripe -E 128M --ec 4+1 -E -1 -c 4 --ec 8+1 $tf ||
		error "setstripe failed"

	verify_mirror_count $tf 2
	verify_comp_count $tf 4

	# Get component IDs
	ids=($($LFS getstripe $tf | awk '/lcme_id/{print $2}' | tr '\n' ' '))

	# Verify data mirror components (mirror 1)
	verify_comp_extent $tf ${ids[0]} 0 134217728
	verify_comp_extent $tf ${ids[1]} 134217728 EOF

	# Verify parity mirror components (mirror 2)
	# First component should have EC(4+1)
	verify_comp_parity $tf ${ids[2]}
	verify_ec_stripe_count $tf ${ids[2]} 4 1
	verify_comp_extent $tf ${ids[2]} 0 134217728

	# Second component has EC adjusted to 4+1 (data component has -c 4)
	verify_comp_parity $tf ${ids[3]}
	verify_ec_stripe_count $tf ${ids[3]} 4 1
	verify_comp_extent $tf ${ids[3]} 134217728 EOF
}
run_test 1g "setstripe with different parity counts"

test_1h() {
	(( OSTCOUNT >= 7 )) || skip "needs >= 7 OSTs"

	enable_ec
	local tf=$DIR/$tfile
	local ids

	# Test -N flag with explicit stripe counts and --ec
	# This should create:
	# Mirror 1: data mirror 2 comps ([0, 1M] with -c 1, [1M, EOF] with -c 1)
	# Mirror 2: data mirror 2 comps ([0, 1M] with -c 4, [1M, EOF] with -c 2)
	# Mirror 3: parity 2 EC comps ([0, 1M] EC(4+2), [1M, EOF] EC(2+1))
	#
	# This test verifies that when using -N with multiple mirrors and --ec,
	# the EC parity components correctly bind to the data components in the
	# mirror they were added with (Mirror 2), not to components from other
	# mirrors (Mirror 1).

	$LFS setstripe -N -E 1M -c 1 -E -1 -c 1 \
		-N -E 1M -c 4 --ec 4+2 -E -1 -c 2 --ec 2+1 $tf ||
		error "setstripe failed"

	verify_mirror_count $tf 3
	verify_comp_count $tf 6

	# Get component IDs
	ids=($($LFS getstripe $tf | awk '/lcme_id/{print $2}' | tr '\n' ' '))

	# Verify Mirror 1: data mirror without EC
	verify_comp_extent $tf ${ids[0]} 0 1048576
	verify_comp_extent $tf ${ids[1]} 1048576 EOF

	# Verify Mirror 2: data mirror with explicit stripe counts
	verify_comp_extent $tf ${ids[2]} 0 1048576
	verify_comp_extent $tf ${ids[3]} 1048576 EOF

	# Verify Mirror 3: parity mirror with EC
	# First component should have EC(4+2) with dstripe=4
	verify_comp_parity $tf ${ids[4]}
	verify_ec_stripe_count $tf ${ids[4]} 4 2
	verify_comp_extent $tf ${ids[4]} 0 1048576

	# Second component should have EC(2+1) with dstripe=2
	verify_comp_parity $tf ${ids[5]}
	verify_ec_stripe_count $tf ${ids[5]} 2 1
	verify_comp_extent $tf ${ids[5]} 1048576 EOF
}
run_test 1h "setstripe with -N flag and explicit stripe counts with --ec"

test_1i() {
	enable_ec
	local td=$DIR/$tdir
	local ids

	test_mkdir $td

	# Set default EC layout on directory
	$LFS setstripe -E 128M -E -1 $EC_ARG $td ||
		error "setstripe on directory failed"

	# Create file in directory - should inherit EC layout
	touch $td/$tfile || error "touch failed"

	verify_mirror_count $td/$tfile 2
	verify_comp_count $td/$tfile 4

	# Get component IDs
	ids=($($LFS getstripe $td/$tfile | awk '/lcme_id/{print $2}' | tr '\n' ' '))

	# Verify data mirror components (mirror 1)
	verify_comp_extent $td/$tfile ${ids[0]} 0 134217728
	verify_comp_extent $td/$tfile ${ids[1]} 134217728 EOF

	# Verify parity mirror components (mirror 2)
	verify_comp_parity $td/$tfile ${ids[2]}
	verify_ec_stripe_count $td/$tfile ${ids[2]} $EC_DSTRIPE $EC_CSTRIPE
	verify_comp_extent $td/$tfile ${ids[2]} 0 134217728

	verify_comp_parity $td/$tfile ${ids[3]}
	verify_ec_stripe_count $td/$tfile ${ids[3]} $EC_DSTRIPE $EC_CSTRIPE
	verify_comp_extent $td/$tfile ${ids[3]} 134217728 EOF

	# Create another file to verify inheritance works consistently
	touch $td/${tfile}.2 || error "touch second file failed"
	verify_mirror_count $td/${tfile}.2 2
	verify_comp_count $td/${tfile}.2 4
}
run_test 1i "default EC layout on directory"

test_1j() {
	(( $OSTCOUNT >= 6 )) || skip "needs >= 6 OSTs"

	enable_ec
	local tf=$DIR/$tfile
	local ids
	local data_stripe_size
	local ec_stripe_size

	stack_trap "rm -f $tf"

	# Test with explicit stripe size
	$LFS setstripe -E -1 -S 4M -c 4 --ec 4+2 $tf ||
		error "setstripe with explicit stripe size failed"

	verify_mirror_count $tf 2
	verify_comp_count $tf 2

	# Get component IDs
	ids=($($LFS getstripe $tf | awk '/lcme_id/{print $2}' | tr '\n' ' '))

	# Verify data mirror (mirror 1, component 0)
	verify_comp_extent $tf ${ids[0]} 0 EOF

	# Verify parity mirror (mirror 2, component 1)
	verify_comp_parity $tf ${ids[1]}
	verify_ec_stripe_count $tf ${ids[1]} 4 2
	verify_comp_extent $tf ${ids[1]} 0 EOF

	# Verify stripe size matches between data and EC components
	verify_ec_stripe_size $tf ${ids[0]} ${ids[1]}

	# Verify the stripe size is what we set (4M)
	data_stripe_size=$($LFS getstripe -I${ids[0]} $tf | \
			awk '/lmm_stripe_size:/ { print $2 }')
	ec_stripe_size=$($LFS getstripe -I${ids[1]} $tf | \
			awk '/lmm_stripe_size:/ { print $2 }')

	[[ $data_stripe_size = 4194304 ]] || {
		$LFS getstripe -v $tf
		error "data component stripe size should be 4M (4194304)," \
		      "got $data_stripe_size"
	}

	[[ $ec_stripe_size = 4194304 ]] || {
		$LFS getstripe -v $tf
		error "EC component stripe size should be 4M (4194304)," \
		      "got $ec_stripe_size"
	}
}
run_test 1j "verify EC component inherits stripe size from data component"

test_1k() {
	local td=$DIR/$tdir
	local subdir=$td/subdir
	local tf=$subdir/$tfile
	local ef=$subdir/explicit
	local ids

	(( OSTCOUNT >= 6 )) || skip_env "needs >= 6 OSTs"

	enable_ec

	test_mkdir $td

	# Set default EC layout on the parent directory
	$LFS setstripe -E 128M -E -1 --ec 4+2 $td ||
		error "setstripe for $td failed"

	# create another dir layer to verify the link id is inherited correctly
	mkdir $subdir || error "mkdir for $subdir failed"

	# ... and create a file in that subdir
	touch $tf || error "touch for $tf failed"

	verify_mirror_count $tf 2
	verify_comp_count $tf 4

	# Get component IDs
	ids=($($LFS getstripe $tf | awk '/lcme_id/{print $2}' | tr '\n' ' '))

	# Verify data mirror components (mirror 1)
	verify_comp_extent $tf ${ids[0]} 0 134217728
	verify_comp_extent $tf ${ids[1]} 134217728 EOF

	# Verify parity mirror components (mirror 2)
	verify_comp_parity $tf ${ids[2]}
	verify_ec_stripe_count $tf ${ids[2]} 4 2
	verify_comp_extent $tf ${ids[2]} 0 134217728

	verify_comp_parity $tf ${ids[3]}
	verify_ec_stripe_count $tf ${ids[3]} 4 2
	verify_comp_extent $tf ${ids[3]} 134217728 EOF

	# verify that explicit EC layouts on new files overwrite dir defaults
	$LFS setstripe -E -1 --ec 2+1 $ef ||
		error "explicit setstripe fir $ef failed"
	verify_mirror_count $ef 2
	verify_comp_count $ef 2
	ids=($($LFS getstripe $ef | awk '/lcme_id/{print $2}' | tr '\n' ' '))
	verify_comp_parity $ef ${ids[1]}
	verify_ec_stripe_count $ef ${ids[1]} 2 1
}
run_test 1k "default EC layout inherited through nested directories"

test_1l() {
	local td=$DIR/$tdir
	local subdir=$td/subdir
	local tf=$subdir/$tfile
	local ids

	(( OSTCOUNT >= 8 )) || skip_env "needs >= 8 OSTs"

	enable_ec

	test_mkdir $td

	# Two EC mirrors, each a PFL with mixed EC params
	#   mirror 1: [0,128M) 2+1, [128M,EOF) 4+2
	#   mirror 2: [0,64M)  4+1, [64M,EOF)  3+2
	# yields 4 mirrors (2 data + 2 parity) and 8 components.
	$LFS setstripe -N -E 128M --ec 2+1 -E -1 --ec 4+2 \
		       -N -E 64M --ec 4+1 -E -1 --ec 3+2 $td ||
		error "setstripe complex EC layout for $td failed"

	# A subdir inherits the complex default, and a file created in it must
	# inherit the full layout (every component/mirror and its EC params).
	mkdir $subdir || error "mkdir for $subdir failed"
	touch $tf || error "touch for $tf failed"

	verify_mirror_count $tf 4
	verify_comp_count $tf 8

	# Get component IDs (4 mirrors x 2 PFL comps, in mirror order)
	ids=($($LFS getstripe $tf | awk '/lcme_id/{print $2}' | tr '\n' ' '))

	# Mirror 1 data components
	verify_comp_extent $tf ${ids[0]} 0 134217728
	verify_comp_extent $tf ${ids[1]} 134217728 EOF

	# Mirror 1 parity components (2+1, then 4+2)
	verify_comp_parity $tf ${ids[2]}
	verify_ec_stripe_count $tf ${ids[2]} 2 1
	verify_comp_extent $tf ${ids[2]} 0 134217728

	verify_comp_parity $tf ${ids[3]}
	verify_ec_stripe_count $tf ${ids[3]} 4 2
	verify_comp_extent $tf ${ids[3]} 134217728 EOF

	# Mirror 2 data components
	verify_comp_extent $tf ${ids[4]} 0 67108864
	verify_comp_extent $tf ${ids[5]} 67108864 EOF

	# Mirror 2 parity components (4+1, then 3+2)
	verify_comp_parity $tf ${ids[6]}
	verify_ec_stripe_count $tf ${ids[6]} 4 1
	verify_comp_extent $tf ${ids[6]} 0 67108864

	verify_comp_parity $tf ${ids[7]}
	verify_ec_stripe_count $tf ${ids[7]} 3 2
	verify_comp_extent $tf ${ids[7]} 67108864 EOF
}
run_test 1l "default EC layout inherited through nested dirs (complex)"

test_2a() {
	enable_ec
	local tf=$DIR/$tfile
	local ids

	# Single data mirror with EC using -N
	$LFS setstripe -N -E 128M -E -1 $EC_ARG $tf ||
		error "setstripe failed"

	verify_mirror_count $tf 2
	verify_comp_count $tf 4

	# Get component IDs
	ids=($($LFS getstripe $tf | awk '/lcme_id/{print $2}' | tr '\n' ' '))

	# Verify data mirror (mirror 1)
	verify_comp_extent $tf ${ids[0]} 0 134217728
	verify_comp_extent $tf ${ids[1]} 134217728 EOF

	# Verify parity mirror (mirror 2)
	verify_comp_parity $tf ${ids[2]}
	verify_ec_stripe_count $tf ${ids[2]} $EC_DSTRIPE $EC_CSTRIPE
	verify_comp_extent $tf ${ids[2]} 0 134217728

	verify_comp_parity $tf ${ids[3]}
	verify_ec_stripe_count $tf ${ids[3]} $EC_DSTRIPE $EC_CSTRIPE
	verify_comp_extent $tf ${ids[3]} 134217728 EOF
}
run_test 2a "setstripe with -N and EC"

test_2b() {
	(( OSTCOUNT >= 7 )) || skip "needs >= 7 OSTs"

	enable_ec
	local tf=$DIR/$tfile
	local ids

	# Two data mirrors, one with EC
	$LFS setstripe -N -E 128M -E -1 --ec 4+2 \
			-N -E 256M -E -1 $tf || error "setstripe failed"

	verify_mirror_count $tf 3
	verify_comp_count $tf 6

	# Get component IDs
	ids=($($LFS getstripe $tf | awk '/lcme_id/{print $2}' | tr '\n' ' '))

	# Verify mirror 1 (data with EC)
	verify_comp_extent $tf ${ids[0]} 0 134217728
	verify_comp_extent $tf ${ids[1]} 134217728 EOF

	# Verify mirror 2 (parity for mirror 1)
	verify_comp_parity $tf ${ids[2]}
	verify_ec_stripe_count $tf ${ids[2]} 4 2
	verify_comp_extent $tf ${ids[2]} 0 134217728

	verify_comp_parity $tf ${ids[3]}
	verify_ec_stripe_count $tf ${ids[3]} 4 2
	verify_comp_extent $tf ${ids[3]} 134217728 EOF

	# Verify mirror 3 (data without EC)
	verify_comp_extent $tf ${ids[4]} 0 268435456
	verify_comp_extent $tf ${ids[5]} 268435456 EOF
}
run_test 2b "setstripe with -N: two data mirrors, one with EC"

test_2c() {
	(( OSTCOUNT >= 8 )) || skip "needs >= 8 OSTs"

	enable_ec
	local tf=$DIR/$tfile
	local ids

	# Two data mirrors, both with EC
	$LFS setstripe -N -E 128M --ec 2+2 -E -1 --ec 4+2 \
			-N -E 256M --ec 3+1 -E -1 \
			$tf || error "setstripe failed"

	verify_mirror_count $tf 4
	verify_comp_count $tf 8

	# Get component IDs
	ids=($($LFS getstripe $tf | awk '/lcme_id/{print $2}' | tr '\n' ' '))

	# Verify mirror 1 (data)
	verify_comp_extent $tf ${ids[0]} 0 134217728
	verify_comp_extent $tf ${ids[1]} 134217728 EOF

	# Verify mirror 2 (parity for mirror 1)
	verify_comp_parity $tf ${ids[2]}
	verify_ec_stripe_count $tf ${ids[2]} 2 2
	verify_comp_extent $tf ${ids[2]} 0 134217728

	verify_comp_parity $tf ${ids[3]}
	verify_ec_stripe_count $tf ${ids[3]} 4 2
	verify_comp_extent $tf ${ids[3]} 134217728 EOF

	# Verify mirror 3 (data)
	verify_comp_extent $tf ${ids[4]} 0 268435456
	verify_comp_extent $tf ${ids[5]} 268435456 EOF

	# Verify mirror 4 (parity for mirror 3)
	verify_comp_parity $tf ${ids[6]}
	verify_ec_stripe_count $tf ${ids[6]} 3 1
	verify_comp_extent $tf ${ids[6]} 0 268435456

	verify_comp_parity $tf ${ids[7]}
	verify_ec_stripe_count $tf ${ids[7]} 3 1
	verify_comp_extent $tf ${ids[7]} 268435456 EOF
}
run_test 2c "setstripe with -N: two data mirrors, both with EC"

test_2d() {
	(( OSTCOUNT >= 6 )) || skip "needs >= 6 OSTs"

	enable_ec
	local tf=$DIR/$tfile
	local ids

	# Multiple identical EC mirrors using -N count
	$LFS setstripe -N2 -E 128M -E -1 --ec 2+1 $tf ||
		error "setstripe failed"

	verify_mirror_count $tf 4
	verify_comp_count $tf 8

	# Get component IDs
	ids=($($LFS getstripe $tf | awk '/lcme_id/{print $2}' | tr '\n' ' '))

	# Verify mirror 1 (data)
	verify_comp_extent $tf ${ids[0]} 0 134217728
	verify_comp_extent $tf ${ids[1]} 134217728 EOF

	# Verify mirror 2 (parity for mirror 1)
	verify_comp_parity $tf ${ids[2]}
	verify_ec_stripe_count $tf ${ids[2]} 2 1
	verify_comp_extent $tf ${ids[2]} 0 134217728

	verify_comp_parity $tf ${ids[3]}
	verify_ec_stripe_count $tf ${ids[3]} 2 1
	verify_comp_extent $tf ${ids[3]} 134217728 EOF

	# Verify mirror 3 (data, identical to mirror 1)
	verify_comp_extent $tf ${ids[4]} 0 134217728
	verify_comp_extent $tf ${ids[5]} 134217728 EOF

	# Verify mirror 4 (parity for mirror 3, identical to mirror 2)
	verify_comp_parity $tf ${ids[6]}
	verify_ec_stripe_count $tf ${ids[6]} 2 1
	verify_comp_extent $tf ${ids[6]} 0 134217728

	verify_comp_parity $tf ${ids[7]}
	verify_ec_stripe_count $tf ${ids[7]} 2 1
	verify_comp_extent $tf ${ids[7]} 134217728 EOF
}
run_test 2d "setstripe with -N2: multiple identical EC mirrors"

test_2e() {
	(( OSTCOUNT >= 8 )) || skip "needs >= 8 OSTs"

	enable_ec
	local tf=$DIR/$tfile
	local ids

	# Mixed: regular mirror + EC mirror
	$LFS setstripe -N -E -1 -c 2 \
			-N -E -1 -c 4 --ec 4+2 \
			$tf || error "setstripe failed"

	verify_mirror_count $tf 3
	verify_comp_count $tf 3

	# Get component IDs
	ids=($($LFS getstripe $tf | awk '/lcme_id/{print $2}' | tr '\n' ' '))

	# Verify mirror 1 (data, no EC)
	verify_comp_extent $tf ${ids[0]} 0 EOF

	# Verify mirror 2 (data with EC)
	verify_comp_extent $tf ${ids[1]} 0 EOF

	# Verify mirror 3 (parity for mirror 2)
	verify_comp_parity $tf ${ids[2]}
	verify_ec_stripe_count $tf ${ids[2]} 4 2
	verify_comp_extent $tf ${ids[2]} 0 EOF
}
run_test 2e "setstripe with -N: mixed regular and EC mirrors"

test_2f() {
	(( OSTCOUNT >= 8 )) || skip "needs >= 8 OSTs"

	enable_ec
	local td=$DIR/$tdir
	local ids

	test_mkdir $td

	# Set default EC layout on directory using mirror mode (-N)
	$LFS setstripe -N -E 128M -E -1 --ec 4+2 $td ||
		error "setstripe with -N on directory failed"

	# Create file in directory - should inherit EC layout
	touch $td/$tfile || error "touch failed"

	verify_mirror_count $td/$tfile 2
	verify_comp_count $td/$tfile 4

	# Get component IDs
	ids=($($LFS getstripe $td/$tfile | awk '/lcme_id/{print $2}' | tr '\n' ' '))

	# Verify data mirror (mirror 1)
	verify_comp_extent $td/$tfile ${ids[0]} 0 134217728
	verify_comp_extent $td/$tfile ${ids[1]} 134217728 EOF

	# Verify parity mirror (mirror 2)
	verify_comp_parity $td/$tfile ${ids[2]}
	verify_ec_stripe_count $td/$tfile ${ids[2]} 4 2
	verify_comp_extent $td/$tfile ${ids[2]} 0 134217728

	verify_comp_parity $td/$tfile ${ids[3]}
	verify_ec_stripe_count $td/$tfile ${ids[3]} 4 2
	verify_comp_extent $td/$tfile ${ids[3]} 134217728 EOF

	# Test with multiple data mirrors + EC
	$LFS setstripe -d $td || error "delete default layout failed"
	$LFS setstripe -N -E -1 -c 2 -N -E -1 --ec 4+2 $td ||
		error "setstripe with mixed mirrors on directory failed"

	touch $td/${tfile}.mixed || error "touch mixed file failed"

	# Should have 3 mirrors: 1 regular data + 1 EC data + 1 EC parity
	verify_mirror_count $td/${tfile}.mixed 3
	verify_comp_count $td/${tfile}.mixed 3
}
run_test 2f "default EC layout on directory using mirror mode"

# Test 3: lfs mirror create with EC
test_3a() {
	enable_ec
	local tf=$DIR/$tfile
	local ids

	$LFS mirror create -N -E 128M -E -1 $EC_ARG $tf ||
		error "failed to create mirrored file with EC"

	verify_mirror_count $tf 2
	verify_comp_count $tf 4

	# Get component IDs
	ids=($($LFS getstripe $tf | awk '/lcme_id/{print $2}' | tr '\n' ' '))

	# Verify parity components (components 2 and 3 in parity mirror)
	verify_comp_parity $tf ${ids[2]}
	verify_comp_parity $tf ${ids[3]}

	# Verify EC stripe counts
	verify_ec_stripe_count $tf ${ids[2]} $EC_DSTRIPE $EC_CSTRIPE
	verify_ec_stripe_count $tf ${ids[3]} $EC_DSTRIPE $EC_CSTRIPE
}
run_test 3a "lfs mirror create with single EC mirror"

test_3b() {
	(( OSTCOUNT >= 6 )) || skip "needs >= 6 OSTs"

	enable_ec
	local tf=$DIR/$tfile
	local ids

	$LFS mirror create -N2 -E 128M -E -1 --ec 2+1 $tf ||
		error "failed to create mirrored file with -N2"

	verify_mirror_count $tf 4
	verify_comp_count $tf 8

	# Get component IDs
	ids=($($LFS getstripe $tf | awk '/lcme_id/{print $2}' | tr '\n' ' '))

	# Verify parity components (components 2,3 and 6,7)
	verify_comp_parity $tf ${ids[2]}
	verify_comp_parity $tf ${ids[3]}
	verify_comp_parity $tf ${ids[6]}
	verify_comp_parity $tf ${ids[7]}

	# Verify EC stripe counts
	verify_ec_stripe_count $tf ${ids[2]} 2 1
	verify_ec_stripe_count $tf ${ids[3]} 2 1
	verify_ec_stripe_count $tf ${ids[6]} 2 1
	verify_ec_stripe_count $tf ${ids[7]} 2 1
}
run_test 3b "lfs mirror create with -N2 and EC"

test_3c() {
	enable_ec
	local tf=$DIR/$tfile
	local ids

	$LFS mirror create -N -E 128M -E -1 $EC_ARG \
			   -N -E 256M -E -1 $tf ||
		error "failed to create mixed mirror file"

	verify_mirror_count $tf 3
	verify_comp_count $tf 6

	# Get component IDs
	ids=($($LFS getstripe $tf | awk '/lcme_id/{print $2}' | tr '\n' ' '))

	# Verify parity components (only first mirror has EC, components 2,3)
	verify_comp_parity $tf ${ids[2]}
	verify_comp_parity $tf ${ids[3]}

	# Verify EC stripe counts
	verify_ec_stripe_count $tf ${ids[2]} $EC_DSTRIPE $EC_CSTRIPE
	verify_ec_stripe_count $tf ${ids[3]} $EC_DSTRIPE $EC_CSTRIPE
}
run_test 3c "lfs mirror create with mixed EC and regular mirrors"

test_3d() {
	(( OSTCOUNT >= 6 )) || skip "needs >= 6 OSTs"

	enable_ec
	local tf=$DIR/$tfile
	local ids

	$LFS mirror create -N -E 128M -E -1 --ec 2+1 \
			   -N -E 256M -E -1 --ec 2+1 $tf ||
		error "failed to create file with different EC configs"

	verify_mirror_count $tf 4
	verify_comp_count $tf 8

	# Get component IDs
	ids=($($LFS getstripe $tf | awk '/lcme_id/{print $2}' | tr '\n' ' '))

	# Verify parity components (components 2,3 and 6,7)
	verify_comp_parity $tf ${ids[2]}
	verify_comp_parity $tf ${ids[3]}
	verify_comp_parity $tf ${ids[6]}
	verify_comp_parity $tf ${ids[7]}

	# Verify EC stripe counts
	verify_ec_stripe_count $tf ${ids[2]} 2 1
	verify_ec_stripe_count $tf ${ids[3]} 2 1
	verify_ec_stripe_count $tf ${ids[6]} 2 1
	verify_ec_stripe_count $tf ${ids[7]} 2 1
}
run_test 3d "lfs mirror create with different EC configs per mirror"

test_3e() {
	(( OSTCOUNT >= 6 )) || skip "needs >= 6 OSTs"

	enable_ec
	local tf=$DIR/$tfile
	local ids

	$LFS mirror create -N -E 64M --ec 4+1 -E 128M --ec 4+2 -E -1 $tf ||
		error "failed to create file with multiple EC specs"

	verify_mirror_count $tf 2
	verify_comp_count $tf 6

	# Get component IDs
	ids=($($LFS getstripe $tf | awk '/lcme_id/{print $2}' | tr '\n' ' '))

	# Verify parity components (components 3,4,5)
	verify_comp_parity $tf ${ids[3]}
	verify_comp_parity $tf ${ids[4]}
	verify_comp_parity $tf ${ids[5]}

	# Verify EC stripe counts
	verify_ec_stripe_count $tf ${ids[3]} 4 1
	verify_ec_stripe_count $tf ${ids[4]} 4 2
	verify_ec_stripe_count $tf ${ids[5]} 4 2
}
run_test 3e "lfs mirror create with multiple EC specs in one mirror"

test_3f() {
	enable_ec
	local tf=$DIR/$tfile
	local ids

	# Test EC inheritance with "holes" - the middle component has no --ec
	# and should inherit the EC layout from the first component
	$LFS mirror create -N -E 128M $EC_ARG -E 512M -E -1 $EC_ARG $tf ||
		error "failed to create file with EC holes"

	verify_mirror_count $tf 2
	verify_comp_count $tf 6

	# Get component IDs
	ids=($($LFS getstripe $tf | awk '/lcme_id/{print $2}' | tr '\n' ' '))

	# Verify parity components (components 3,4,5)
	verify_comp_parity $tf ${ids[3]}
	verify_comp_parity $tf ${ids[4]}
	verify_comp_parity $tf ${ids[5]}

	# Verify EC stripe counts
	# First component: 4+2 (explicitly set)
	verify_ec_stripe_count $tf ${ids[3]} $EC_DSTRIPE $EC_CSTRIPE
	# Second component inherits EC from the first
	verify_ec_stripe_count $tf ${ids[4]} $EC_DSTRIPE $EC_CSTRIPE
	# Third component: explicitly set
	verify_ec_stripe_count $tf ${ids[5]} $EC_DSTRIPE $EC_CSTRIPE
}
run_test 3f "EC inheritance with holes"

test_3g() {
	(( OSTCOUNT >= 6 )) || skip_env "needs >= 6 OSTs"
	enable_ec

	local tf=$DIR/$tfile
	local pool_name=$TESTNAME
	local ids
	local data_pool
	local ec_pool

	stack_trap "rm -f $tf"

	# create a new OST pool and add all OSTs to it
	create_pool $FSNAME.$pool_name ||
		error "create OST pool $pool_name failed"

	pool_add_targets $pool_name 0 $((OSTCOUNT - 1)) ||
		error "add OSTs into pool $pool_name failed"

	# Create EC file with data + parity mirror. The data mirror is
	# explicitly on pool; parity mirror should inherit it.
	$LFS setstripe -N -E -1 -c 4 -p $pool_name --ec 4+2 $tf ||
		error "create EC file with pool failed"

	verify_mirror_count $tf 2
	verify_comp_count $tf 2

	ids=($($LFS getstripe $tf | awk '/lcme_id/{print $2}' |
	     tr '\n' ' '))

	# Component 0: data mirror; Component 1: parity mirror
	data_pool=$($LFS getstripe -I${ids[0]} -p $tf)
	ec_pool=$($LFS getstripe -I${ids[1]} -p $tf)

	[[ $data_pool = $pool_name ]] ||
		error "data mirror pool $data_pool != $pool_name"

	[[ $ec_pool = $pool_name ]] ||
		error "EC mirror pool $ec_pool != $pool_name"

	destroy_test_pools
	}
run_test 3g "EC parity mirror inherits pool from data mirror"

# Test 4: Invalid EC parameters
test_4a() {
	enable_ec
	local tf=$DIR/$tfile

	# Invalid: parity > data
	$LFS setstripe -E -1 --ec 4+5 $tf 2>&1 |
		grep -qi "parity.*must be less than or equal" ||
		error "should reject EC with parity > data"

	! [[ -f $tf ]] || error "file should not have been created"
}
run_test 4a "reject invalid EC parameters: parity > data"

test_4b() {
	enable_ec
	local tf=$DIR/$tfile

	# Invalid: data stripe count = 1 (must be at least 2)
	$LFS setstripe -E -1 --ec 1+0 $tf 2>&1 |
		grep -qi "invalid data stripe count" ||
		error "should reject EC with data count < 2"

	# Invalid: data stripe count = 0
	$LFS setstripe -E -1 --ec 0+1 $tf 2>&1 |
		grep -qi "invalid data stripe count" ||
		error "should reject EC with data count = 0"

	! [[ -f $tf ]] || error "file should not have been created"
}
run_test 4b "reject invalid EC parameters: data count < 2"

test_4c() {
	enable_ec
	local tf=$DIR/$tfile

	# Invalid: parity = 0
	$LFS setstripe -E -1 --ec 4+0 $tf 2>&1 |
		grep -qi "invalid parity stripe count" ||
		error "should reject EC with parity = 0"

	! [[ -f $tf ]] || error "file should not have been created"
}
run_test 4c "reject invalid EC parameters: parity = 0"

test_4d() {
	enable_ec
	local tf=$DIR/$tfile

	# Invalid: malformed EC specification (missing +)
	$LFS setstripe -E -1 --ec 42 $tf 2>&1 | grep -qi "invalid.*format" ||
		error "should reject malformed EC spec (missing +)"

	# Invalid: malformed EC specification (non-numeric)
	$LFS setstripe -E -1 --ec abc+def $tf 2>&1 | grep -qi "invalid" ||
		error "should reject malformed EC spec (non-numeric)"

	! [[ -f $tf ]] || error "file should not have been created"
}
run_test 4d "reject malformed EC specifications"

test_4e() {
	enable_ec
	local tf=$DIR/$tfile
	local ids

	stack_trap "rm -f $tf"

	# --ec without -E should automatically create [0,EOF] component
	$LFS setstripe --ec 4+2 $tf || error "setstripe failed"

	verify_mirror_count $tf 2
	verify_comp_count $tf 2

	# Get component IDs
	ids=($($LFS getstripe $tf | awk '/lcme_id/{print $2}' | tr '\n' ' '))

	# Verify data component has [0,EOF] extent
	verify_comp_extent $tf ${ids[0]} 0 EOF

	# Verify parity component
	verify_comp_parity $tf ${ids[1]}
	verify_ec_stripe_count $tf ${ids[1]} 4 2
	verify_comp_extent $tf ${ids[1]} 0 EOF
}
run_test 4e "auto-create [0,EOF] component when --ec without -E"

test_4f() {
	enable_ec
	local tf=$DIR/$tfile

	# Invalid: --ec with --foreign
	$LFS setstripe -E -1 --ec 4+2 --foreign=none --xattr=test $tf 2>&1 |
		grep -qi "only.*options are valid with --foreign" ||
		error "should reject --ec with --foreign"

	! [[ -f $tf ]] || error "file should not have been created"
}
run_test 4f "reject --ec with incompatible options (--foreign)"

test_4g() {
	enable_ec
	local tf=$DIR/$tfile
	local output

	stack_trap "rm -f $tf"

	# Test 1: Reject data stripe count > 32 without --ec-expert
	output=$($LFS setstripe -E -1 -c 64 --ec 64+2 $tf 2>&1) ||
		true
	echo "$output" | grep -qi "exceeds supported limit.*--ec-expert" ||
		error "should reject data count > 32 without --ec-expert"
	[[ -f $tf ]] && error "file should not have been created"

	# Test 2: Reject parity stripe count > 4 without --ec-expert
	output=$($LFS setstripe -E -1 -c 16 --ec 16+5 $tf 2>&1) ||
		true
	echo "$output" | grep -qi "exceeds supported limit.*--ec-expert" ||
		error "should reject parity count > 4 without --ec-expert"
	[[ -f $tf ]] && error "file should not have been created"

	# Test 3: Allow data stripe count > 32 with --ec-expert
	# (may fail for other reasons, but not due to limit check)
	output=$($LFS setstripe -E -1 -c 64 --ec-expert 64+2 $tf 2>&1) ||
		true
	echo "$output" | grep -qi "exceeds supported limit" &&
		error "should not reject data count > 32 with --ec-expert"

	rm -f $tf

	# Test 4: Allow parity stripe count > 4 with --ec-expert
	# (may fail for other reasons, but not due to limit check)
	output=$($LFS setstripe -E -1 -c 16 --ec-expert 16+5 $tf 2>&1) ||
		true
	echo "$output" | grep -qi "exceeds supported limit" &&
		error "should not reject parity count > 4 with --ec-expert"
	return 0
}
run_test 4g "reject EC stripe counts exceeding limits without --ec-expert"

test_5a() {
	enable_ec

	local ids
	local tf=$DIR/$tfile

	stack_trap "rm -f $tf $TMP/$tfile.mirror $TMP/$tfile.data"

	# Create EC file with data mirror + parity mirror
	# Layout: N1 = data, N2 = data, N3 = parity (4+1)
	$LFS setstripe -N -E 1M -c 1 -E -1 -c 1 \
		-N -E 1M -c 4 --ec 4+1 -E -1 -c 4 --ec 4+1 $tf ||
		error "create EC file failed"

	# Get component IDs
	ids=($($LFS getstripe $tf | awk '/lcme_id/{print $2}' | tr '\n' ' '))

	# Verify EC parameters on parity components (mirror 3, N3)
	# Components: 0,1=N1(data), 2,3=N2(data), 4,5=N3(parity)
	verify_comp_parity $tf ${ids[4]}
	verify_ec_stripe_count $tf ${ids[4]} 4 1
	verify_comp_parity $tf ${ids[5]}
	verify_ec_stripe_count $tf ${ids[5]} 4 1

	# Test 1: Read from empty parity mirror - should return immediately
	# with 0 bytes (not hang)
	$LFS mirror read -N3 -o $TMP/$tfile.mirror $tf ||
		error "mirror read from empty parity failed"
	local empty_size=$(stat -c %s $TMP/$tfile.mirror)
	(( empty_size == 0 )) ||
		error "empty parity read should return 0 bytes, got $empty_size"
	rm -f $TMP/$tfile.mirror

	# Test 2: Write data, resync, then read from parity
	cp /etc/passwd $tf || error "failed to write data"

	# Resync to compute parity
	$LFS mirror resync $tf || error "mirror resync failed"

	# Read from parity mirror (N3) - should work after resync
	$LFS mirror read -N3 -o $TMP/$tfile.mirror $tf ||
		error "mirror read from parity failed"

	# Parity data won't match /etc/passwd directly (it's computed parity),
	# but verify we got non-empty data of reasonable size
	local parity_size=$(stat -c %s $TMP/$tfile.mirror)
	(( parity_size > 0 )) || error "parity mirror read returned empty"

	# Also verify data mirror can be read correctly
	$LFS mirror read -N1 -o $TMP/$tfile.data $tf ||
		error "mirror read from data failed"
	cmp $TMP/$tfile.data /etc/passwd ||
		error "data mirror read mismatch"
}
run_test 5a "EC mirror read/write commands"

test_5b() {
	enable_ec

	local tf=$DIR/$tfile
	local flags
	local ids

	stack_trap "rm -f $tf"

	# Create EC file with lfs setstripe
	# Layout: Mirror 1 has 3 data components [0,128M], [128M,1G], [1G,EOF]
	#         Mirror 2 has 3 EC components at same extents
	$LFS setstripe -E 128M -E 1G -E -1 --ec 4+2 $tf ||
		error "setstripe failed"

	# Verify file starts in RDONLY state
	verify_flr_state $tf "ro"

	# Get component IDs for both mirrors
	ids=($($LFS getstripe $tf | awk '/lcme_id/{print $2}' | tr '\n' ' '))
	echo "Component IDs: ${ids[@]}"

	# Verify EC parameters on parity components (mirror 2)
	verify_comp_parity $tf ${ids[3]}
	verify_ec_stripe_count $tf ${ids[3]} 4 2
	verify_comp_parity $tf ${ids[4]}
	verify_ec_stripe_count $tf ${ids[4]} 4 2
	verify_comp_parity $tf ${ids[5]}
	verify_ec_stripe_count $tf ${ids[5]} 4 2

	# Write to first component (0-1M, within [0,128M] extent)
	dd if=/dev/zero of=$tf conv=notrunc bs=1M count=1 ||
		error "write to first component failed"

	# Verify file is now in WRITE_PENDING state
	verify_flr_state $tf "wp"

	# Verify the corresponding EC component in mirror 2 is stale
	# Mirror 1 components are ids[0], ids[1], ids[2]
	# Mirror 2 components are ids[3], ids[4], ids[5]
	# After writing to first component, ids[3] should be stale
	verify_comp_stale $tf ${ids[3]}

	# Verify other EC components are NOT stale (component-level granularity)
	flags=$($LFS getstripe -I${ids[4]} $tf |
			awk '/lcme_flags:/ { print $2 }')
	[[ ! $flags =~ "stale" ]] ||
		error "component ${ids[4]} should not be stale after write to first component"

	flags=$($LFS getstripe -I${ids[5]} $tf |
			awk '/lcme_flags:/ { print $2 }')
	[[ ! $flags =~ "stale" ]] ||
		error "component ${ids[5]} should not be stale after write to first component"

	# Resync the file
	$LFS mirror resync $tf || error "mirror resync failed"

	# Verify file is back to RDONLY state
	verify_flr_state $tf "ro"

	# Verify component is no longer stale
	local flags=$($LFS getstripe -I${ids[3]} $tf |
			awk '/lcme_flags:/ { print $2 }')
	[[ ! $flags =~ "stale" ]] ||
		error "component ${ids[3]} still stale after resync: $flags"

	echo "** Write to second component **"

	# Write to second component (at offset 256M, within [128M,1G] extent)
	dd if=/dev/zero of=$tf conv=notrunc bs=1M count=1 seek=256 ||
		error "write to second component failed"

	# Verify file is in WRITE_PENDING state
	verify_flr_state $tf "wp"

	# Verify the corresponding EC component in mirror 2 is stale
	# ids[4] is the second EC component
	verify_comp_stale $tf ${ids[4]}

	# Verify other EC components are NOT stale (component-level granularity)
	flags=$($LFS getstripe -I${ids[3]} $tf |
			awk '/lcme_flags:/ { print $2 }')
	[[ ! $flags =~ "stale" ]] ||
		error "component ${ids[3]} should not be stale after write to second component"

	flags=$($LFS getstripe -I${ids[5]} $tf |
			awk '/lcme_flags:/ { print $2 }')
	[[ ! $flags =~ "stale" ]] ||
		error "component ${ids[5]} should not be stale after write to second component"

	# Resync the file
	$LFS mirror resync $tf || error "mirror resync failed"

	# Verify file is back to RDONLY state
	verify_flr_state $tf "ro"

	# Verify component is no longer stale
	flags=$($LFS getstripe -I${ids[4]} $tf |
			awk '/lcme_flags:/ { print $2 }')
	[[ ! $flags =~ "stale" ]] ||
		error "component ${ids[4]} still stale after resync: $flags"
}
run_test 5b "EC FLR state transitions with writes to different components"

test_6a() {
	enable_ec

	local tf=$DIR/$tfile

	stack_trap "rm -f $tf"

	# Create EC file with two components in each mirror
	# Mirror 1 (data): [0, 1M], [1M, EOF]
	# Mirror 2 (parity): [0, 1M], [1M, EOF] with EC 4+2
	$LFS setstripe -E 1M -c 2 -E -1 -c 4 --ec 4+2 $tf ||
		error "create EC file failed"

	verify_mirror_count $tf 2
	verify_comp_count $tf 4

	# Get component IDs
	local ids=($($LFS getstripe $tf | awk '/lcme_id/{print $2}' |
			tr '\n' ' '))
	echo "Component IDs: ${ids[@]}"

	local data_comp1=${ids[0]}
	local data_comp2=${ids[1]}
	local parity_comp1=${ids[2]}
	local parity_comp2=${ids[3]}

	# Verify EC parameters on all parity components
	verify_comp_parity $tf $parity_comp1
	verify_ec_stripe_count $tf $parity_comp1 2 2
	verify_comp_parity $tf $parity_comp2
	verify_ec_stripe_count $tf $parity_comp2 4 2

	# Test 1: Cannot set prefer on parity component
	$LFS setstripe --comp-set -I $parity_comp1 --comp-flags=prefer \
		$tf 2>&1 |
		grep -q "cannot set prefer flags on parity component" ||
		error "should not allow prefer on parity component"

	# Test 2: Cannot set prefrd on parity component
	$LFS setstripe --comp-set -I $parity_comp2 --comp-flags=prefrd \
		$tf 2>&1 |
		grep -q "cannot set prefer flags on parity component" ||
		error "should not allow prefrd on parity component"

	# Test 3: Cannot set prefwr on parity component
	$LFS setstripe --comp-set -I $parity_comp1 --comp-flags=prefwr \
		$tf 2>&1 |
		grep -q "cannot set prefer flags on parity component" ||
		error "should not allow prefwr on parity component"

	# Test 4: Can set prefwr on data component in data mirror
	$LFS setstripe --comp-set -I $data_comp1 --comp-flags=prefwr \
		$tf || error "should allow prefwr on data component"
	$LFS getstripe -I$data_comp1 $tf | grep -q "prefwr" ||
		error "prefwr flag not set on data component"

	# Test 5: Can set prefer on data component in data mirror
	$LFS setstripe --comp-set -I $data_comp2 --comp-flags=prefer \
		$tf || error "should allow prefer on data component"
	$LFS getstripe -I$data_comp2 $tf | grep -q "prefer" ||
		error "prefer flag not set on data component"

	# Test 6: Can set prefrd on data component in data mirror
	$LFS setstripe --comp-set -I $data_comp1 --comp-flags=^prefwr \
		$tf || error "failed to clear prefwr"
	$LFS setstripe --comp-set -I $data_comp1 --comp-flags=prefrd \
		$tf || error "should allow prefrd on data component"
	$LFS getstripe -I$data_comp1 $tf | grep -q "prefrd" ||
		error "prefrd flag not set on data component"
}
run_test 6a "Block setting prefer flags on parity components"

test_6b() {
	enable_ec

	local tf=$DIR/$tfile

	# Create EC file: data mirror + parity mirror
	$LFS setstripe -E -1 -c $EC_DSTRIPE $EC_ARG $tf ||
		error "create EC file failed"

	verify_mirror_count $tf 2
	verify_comp_count $tf 2

	# Find data and parity component IDs via the parity flag rather than
	# relying on mirror id ordering.
	local data_comp_id=$($LFS getstripe $tf |
		awk '/lcme_id:/ {id=$2}
		     /lcme_flags:/ && !/parity/ {print id; exit}')
	local parity_comp_id=$($LFS getstripe $tf |
		awk '/lcme_id:/ {id=$2}
		     /lcme_flags:.*parity/ {print id; exit}')

	echo "Data component: $data_comp_id, Parity component: $parity_comp_id"

	[[ -n "$data_comp_id" ]] || error "could not find data component ID"
	[[ -n "$parity_comp_id" ]] ||
		error "could not find parity component ID"

	# Verify EC parameters on parity component
	verify_comp_parity $tf $parity_comp_id
	verify_ec_stripe_count $tf $parity_comp_id $EC_DSTRIPE $EC_CSTRIPE

	# Write to file - should select data mirror as primary
	dd if=/dev/urandom of=$tf bs=1M count=2 ||
		error "write to EC file failed"

	# After write, data mirror should be non-stale (init),
	# parity mirror should be stale
	$LFS getstripe -I$data_comp_id $tf | grep -q "init" ||
		error "data mirror should be init after write"

	# Verify parity mirror is stale
	$LFS getstripe -I$parity_comp_id $tf | grep -q "stale" ||
		error "parity mirror should be stale after write"

	# Snapshot the data before resync so we can verify integrity across it
	local sum1=$(md5sum $tf | awk '{print $1}')

	# Resync to update parity mirror
	$LFS mirror resync $tf || error "mirror resync failed"

	# After resync, both mirrors should be init (non-stale)
	$LFS getstripe -I$data_comp_id $tf | grep -q "init" ||
		error "data mirror should be init after resync"
	$LFS getstripe -I$parity_comp_id $tf | grep -q "init" ||
		error "parity mirror should be init after resync"

	local sum2=$(md5sum $tf | awk '{print $1}')
	[[ "$sum1" == "$sum2" ]] ||
		error "data changed after resync: $sum1 vs $sum2"
}
run_test 6b "EC write selects data mirror, not parity mirror"

test_6c() {
	(( OSTCOUNT >= 8 )) || skip "needs >= 8 OSTs"

	enable_ec

	local tf=$DIR/$tfile

	# Create file with 2 data mirrors + 1 EC mirror (data + parity)
	# Mirror 1 (data): stripe on OST0,1
	# Mirror 2 (data): stripe on OST2,3
	# Mirror 3 (data): EC data component with 2 stripes
	# Mirror 4 (parity): EC parity component with 2+2
	$LFS setstripe -N -E -1 -c 2 -o 0,1 \
		-N -E -1 -c 2 -o 2,3 \
		-N -E -1 -c 2 --ec 2+2 $tf ||
		error "create multi-mirror EC file failed"

	verify_mirror_count $tf 4
	verify_comp_count $tf 4

	# Get component IDs for EC verification
	local ids=($($LFS getstripe $tf | awk '/lcme_id/{print $2}' |
			tr '\n' ' '))
	echo "Component IDs: ${ids[@]}"

	# Verify EC parameters on EC parity component (mirror 4)
	verify_comp_parity $tf ${ids[3]}
	verify_ec_stripe_count $tf ${ids[3]} 2 2

	# Write to file - should select one of the data mirrors
	dd if=/dev/urandom of=$tf bs=1M count=2 ||
		error "write to EC file failed"
	$LFS getstripe $tf

	# Get mirror IDs
	local mirror_ids=($($LFS getstripe $tf |
		awk '/lcme_mirror_id:/ {print $2}' | sort -u))
	echo "Mirror IDs: ${mirror_ids[@]}"

	# Count non-stale mirrors (should be 1 - the primary data mirror)
	local non_stale_count=0
	local stale_count=0
	local parity_mirror_id=""

	for mirror_id in "${mirror_ids[@]}"; do
		# Get first component of this mirror
		# lcme_id comes before lcme_mirror_id, so we need to save it
		local comp_id=$($LFS getstripe $tf |
			awk -v mid="$mirror_id" \
			'/lcme_id:/ {id=$2} \
			/lcme_mirror_id:/ {if ($2 == mid) {print id; exit}}')

		if $LFS getstripe -I$comp_id $tf | grep -q "stale"; then
			((stale_count++))
		else
			((non_stale_count++))
		fi

		# Check if this is the parity mirror
		if $LFS getstripe -I$comp_id $tf | grep -q "parity"; then
			parity_mirror_id=$mirror_id
		fi
	done

	echo "Non-stale mirrors: $non_stale_count, Stale mirrors: $stale_count"
	echo "Parity mirror ID: $parity_mirror_id"

	# Should have exactly 1 non-stale mirror (the selected data mirror)
	(( non_stale_count == 1 )) ||
		error "expected 1 non-stale mirror, got $non_stale_count"

	# Should have 3 stale mirrors (2 other data mirrors + parity mirror)
	(( stale_count == 3 )) ||
		error "expected 3 stale mirrors, got $stale_count"

	# Parity mirror should be stale
	[[ -n "$parity_mirror_id" ]] ||
		error "could not find parity mirror"
	local parity_comp_id=$($LFS getstripe $tf |
		awk -v mid="$parity_mirror_id" \
		'/lcme_id:/ {id=$2} \
		/lcme_mirror_id:/ {if ($2 == mid) {print id; exit}}')
	$LFS getstripe -I$parity_comp_id $tf | grep -q "stale" ||
		error "parity mirror should be stale after write"
}
run_test 6c "EC with multiple data mirrors - parity never selected"

test_6d() {
	enable_ec

	local tf=$DIR/$tfile

	stack_trap "rm -f $tf"

	# Create EC file: data mirror + parity mirror
	$LFS setstripe -E -1 -c 4 --ec 4+2 $tf ||
		error "create EC file failed"

	verify_mirror_count $tf 2
	verify_comp_count $tf 2

	# Find data and parity component IDs via the parity flag rather than
	# relying on mirror id ordering.
	local data_comp_id=$($LFS getstripe $tf |
		awk '/lcme_id:/ {id=$2}
		     /lcme_flags:/ && !/parity/ {print id; exit}')
	local parity_comp_id=$($LFS getstripe $tf |
		awk '/lcme_id:/ {id=$2}
		     /lcme_flags:.*parity/ {print id; exit}')

	echo "Data component: $data_comp_id, Parity component: $parity_comp_id"

	[[ -n "$data_comp_id" ]] || error "could not find data component ID"
	[[ -n "$parity_comp_id" ]] ||
		error "could not find parity component ID"

	# Verify EC parameters on parity component
	verify_comp_parity $tf $parity_comp_id
	verify_ec_stripe_count $tf $parity_comp_id 4 2

	# Write and resync to get both mirrors in sync
	dd if=/dev/urandom of=$tf bs=1M count=2 ||
		error "write to EC file failed"
	$LFS mirror resync $tf || error "mirror resync failed"

	# Verify both mirrors are in sync (init, not stale)
	$LFS getstripe -I$data_comp_id $tf | grep -q "init" ||
		error "data mirror should be init after resync"
	$LFS getstripe -I$parity_comp_id $tf | grep -q "init" ||
		error "parity mirror should be init after resync"

	# Manually mark parity mirror stale
	$LFS setstripe --comp-set -I $parity_comp_id --comp-flags=stale \
		$tf || error "failed to mark parity mirror stale"

	# Verify parity mirror is now stale
	$LFS getstripe -I$parity_comp_id $tf | grep -q "stale" ||
		error "parity mirror should be stale after manual marking"

	# Verify data mirror is still init
	$LFS getstripe -I$data_comp_id $tf | grep -q "init" ||
		error "data mirror should still be init"

	# Resync should restore parity mirror from data mirror
	$LFS mirror resync $tf || error "mirror resync failed"

	# After resync, both should be init again
	$LFS getstripe -I$data_comp_id $tf | grep -q "init" ||
		error "data mirror should be init after resync"
	$LFS getstripe -I$parity_comp_id $tf | grep -q "init" ||
		error "parity mirror should be init after resync"

	# Verify data integrity
	local sum1=$(md5sum $tf | awk '{print $1}')
	$LFS mirror resync $tf || error "final resync failed"
	local sum2=$(md5sum $tf | awk '{print $1}')
	[[ "$sum1" == "$sum2" ]] ||
		error "data changed after resync: $sum1 vs $sum2"
}
run_test 6d "Manually mark parity mirror stale and resync"

test_6e() {
	enable_ec

	local tf=$DIR/$tfile

	stack_trap "rm -f $tf"

	# Create EC file: data mirror + parity mirror
	$LFS setstripe -E -1 -c 4 --ec 4+2 $tf ||
		error "create EC file failed"

	verify_mirror_count $tf 2
	verify_comp_count $tf 2

	# Find data and parity component IDs via the parity flag rather than
	# relying on mirror id ordering.
	local data_comp_id=$($LFS getstripe $tf |
		awk '/lcme_id:/ {id=$2}
		     /lcme_flags:/ && !/parity/ {print id; exit}')
	local parity_comp_id=$($LFS getstripe $tf |
		awk '/lcme_id:/ {id=$2}
		     /lcme_flags:.*parity/ {print id; exit}')

	echo "Data component: $data_comp_id, Parity component: $parity_comp_id"

	[[ -n "$data_comp_id" ]] || error "could not find data component ID"
	[[ -n "$parity_comp_id" ]] ||
		error "could not find parity component ID"

	# Verify EC parameters on parity component
	verify_comp_parity $tf $parity_comp_id
	verify_ec_stripe_count $tf $parity_comp_id 4 2

	# Write and resync to get both mirrors in sync
	dd if=/dev/urandom of=$tf bs=1M count=2 ||
		error "write to EC file failed"
	$LFS mirror resync $tf || error "mirror resync failed"

	# Verify both mirrors are in sync (init, not stale)
	$LFS getstripe -I$data_comp_id $tf | grep -q "init" ||
		error "data mirror should be init after resync"
	$LFS getstripe -I$parity_comp_id $tf | grep -q "init" ||
		error "parity mirror should be init after resync"

	# Manually mark data mirror stale, leaving only parity mirror valid
	$LFS setstripe --comp-set -I $data_comp_id --comp-flags=stale $tf ||
		error "failed to mark data mirror stale"

	# Verify data mirror is now stale
	$LFS getstripe -I$data_comp_id $tf | grep -q "stale" ||
		error "data mirror should be stale after manual marking"

	# Verify parity mirror is still init (non-stale)
	$LFS getstripe -I$parity_comp_id $tf | grep -q "init" ||
		error "parity mirror should still be init"

	# Attempt to write with only parity mirror non-stale should fail
	# with ENODATA (61 - No data available) because parity mirrors
	# cannot be selected as write targets
	dd if=/dev/urandom of=$tf bs=1M count=1 conv=notrunc 2>&1 |
		grep -q "No data available" ||
		error "write should fail with ENODATA when only parity" \
			"mirror is valid"

	# Verify file state unchanged - data mirror still stale
	$LFS getstripe -I$data_comp_id $tf | grep -q "stale" ||
		error "data mirror should still be stale after failed write"
}
run_test 6e "Write fails when only parity mirror is non-stale"

test_7a() {
	enable_ec

	local tf=$DIR/$tfile

	stack_trap "rm -f $tf"

	# Create EC file: data mirror + parity mirror
	$LFS setstripe -E -1 -c 4 --ec 4+2 $tf ||
		error "create EC file failed"

	verify_mirror_count $tf 2
	verify_comp_count $tf 2

	# Get component IDs - data mirror is mirror_id 1, parity is mirror_id 2
	local data_comp_id=$($LFS getstripe $tf |
		awk '/lcme_id:/ {id=$2} /lcme_mirror_id:.*1$/ {print id; exit}')
	local parity_comp_id=$($LFS getstripe $tf |
		awk '/lcme_id:/ {id=$2} /lcme_mirror_id:.*2$/ {print id; exit}')

	echo "Data component: $data_comp_id, Parity component: $parity_comp_id"

	[[ -n "$data_comp_id" ]] || error "could not find data component ID"
	[[ -n "$parity_comp_id" ]] ||
		error "could not find parity component ID"

	# Verify EC parameters on parity component
	verify_comp_parity $tf $parity_comp_id
	verify_ec_stripe_count $tf $parity_comp_id 4 2

	# Write initial data and resync
	dd if=/dev/urandom of=$tf bs=1M count=2 ||
		error "write to EC file failed"
	$LFS mirror resync $tf || error "mirror resync failed"

	# Verify both mirrors are in sync (init, not stale)
	$LFS getstripe -I$data_comp_id $tf | grep -q "init" ||
		error "data mirror should be init after resync"
	$LFS getstripe -I$parity_comp_id $tf | grep -q "init" ||
		error "parity mirror should be init after resync"

	# Save checksum of parity mirror
	local sum0=$($LFS mirror read -N2 $tf | dd bs=1M count=2 | md5sum)
	echo "Initial parity mirror checksum: $sum0"

	# Set nosync flag on parity mirror to snapshot it
	$LFS setstripe --comp-set -I $parity_comp_id --comp-flags=nosync \
		$tf || error "failed to set nosync on parity mirror"

	# Verify nosync flag is set
	$LFS getstripe -I$parity_comp_id $tf | grep -q "nosync" ||
		error "nosync flag not set on parity mirror"

	# Write new data - this should update data mirror but not parity
	dd if=/dev/urandom of=$tf bs=1M count=2 ||
		error "second write to EC file failed"

	# Resync - parity mirror should remain stale due to nosync flag
	$LFS mirror resync $tf || error "mirror resync failed"

	# Verify parity mirror is still stale and has nosync flag
	$LFS getstripe -I$parity_comp_id $tf | grep -q "stale" ||
		error "parity mirror should be stale after resync with nosync"
	$LFS getstripe -I$parity_comp_id $tf | grep -q "nosync" ||
		error "nosync flag should still be set on parity mirror"

	# Verify parity mirror content hasn't changed
	local sum1=$($LFS mirror read -N1 $tf | md5sum)
	local sum2=$($LFS mirror read -N2 $tf | dd bs=1M count=2 | md5sum)

	echo "Data mirror checksum: $sum1"
	echo "Parity mirror checksum: $sum2"
	[[ $sum0 = $sum2 ]] ||
		error "parity mirror changed: $sum0 vs $sum2"

	# Clear nosync flag and resync to update parity mirror
	$LFS setstripe --comp-set -I $parity_comp_id \
		--comp-flags=^nosync $tf ||
		error "failed to clear nosync on parity mirror"

	$LFS mirror resync $tf || error "final resync failed"

	# After clearing nosync and resyncing, both mirrors should be init
	$LFS getstripe -I$data_comp_id $tf | grep -q "init" ||
		error "data mirror should be init after final resync"
	$LFS getstripe -I$parity_comp_id $tf | grep -q "init" ||
		error "parity mirror should be init after final resync"

	# Verify parity mirror is now updated
	sum1=$($LFS mirror read -N1 $tf | md5sum)
	sum2=$($LFS mirror read -N2 $tf | md5sum)
	echo "Final data mirror checksum: $sum1"
	echo "Final parity mirror checksum: $sum2"
}
run_test 7a "nosync flag on parity mirror prevents resync updates"

test_7b() {
	(( OSTCOUNT >= 6 )) || skip_env "needs >= 6 OSTs"

	enable_ec

	local tf=$DIR/$tfile
	local victim=$DIR/$tfile.victim

	stack_trap "rm -f $tf $victim"

	$LFS setstripe -E -1 -c 4 --ec 4+2 $tf ||
		error "setstripe failed"
	verify_mirror_count $tf 2

	identify_ec_mirrors $tf
	[[ -n $ec_data_id && -n $ec_parity_id ]] ||
		error "failed to identify data/parity mirrors"

	# A parity mirror holds no file data, so a file left holding only one
	# cannot be read.  Splitting a parity mirror into a file of its own is
	# refused, both with an explicit victim and with the default
	# '<file>.mirror~ID' name.  Its contents are still reachable with
	# 'lfs mirror read -N ID', and 'lfs mirror delete' removes it.
	$LFS mirror split --mirror-id $ec_parity_id -f $victim $tf &&
		error "split of a parity mirror to a file should be refused"
	[[ ! -e $victim ]] ||
		error "refused split left victim file '$victim' behind"

	$LFS mirror split --mirror-id $ec_parity_id $tf &&
		error "split of a parity mirror should be refused"
	[[ ! -e $tf.mirror~$ec_parity_id ]] ||
		error "refused split left a victim file behind"

	verify_mirror_count $tf 2

	return 0
}
run_test 7b "mirror split of a parity mirror to a file is refused"

test_7c() {
	(( OSTCOUNT >= 6 )) || skip_env "needs >= 6 OSTs"

	enable_ec

	local tf=$DIR/$tfile
	local ids

	# Create file with multiple components and EC
	$LFS setstripe -E 128M --ec 4+2 -E -1 --ec 4+2 $tf ||
		error "setstripe failed"

	verify_mirror_count $tf 2
	verify_comp_count $tf 4

	# Get mirror IDs
	ids=($($LFS getstripe $tf | awk '/lcme_mirror_id:/ {print $2}' |
		sort -u | tr '\n' ' '))

	(( ${#ids[@]} == 2 )) ||
		error "expected 2 mirrors, got ${#ids[@]}"

	# Find parity mirror
	local parity_mirror_id
	local flags=$($LFS getstripe --mirror-id=${ids[1]} $tf |
		      awk '/lcme_flags:/ {print $2; exit}')

	if [[ $flags =~ "parity" ]]; then
		parity_mirror_id=${ids[1]}
	else
		parity_mirror_id=${ids[0]}
	fi

	# Split parity mirror with -d
	$LFS mirror split --mirror-id $parity_mirror_id -d $tf ||
		error "split -d failed for parity mirror"

	# Verify only data mirror remains with 2 components
	verify_mirror_count $tf 1
	verify_comp_count $tf 2
}
run_test 7c "mirror split -d works for multi-component parity mirrors"

test_7d() {
	(( OSTCOUNT >= 6 )) || skip_env "needs >= 6 OSTs"

	enable_ec

	local tf=$DIR/$tfile
	local ids

	# Create file with regular mirror + EC mirror
	$LFS mirror create -N -E -1 -c 2 \
			   -N -E -1 -c 2 --ec 2+2 $tf ||
		error "mirror create failed"

	verify_mirror_count $tf 3

	# Get all mirror IDs
	ids=($($LFS getstripe $tf | awk '/lcme_mirror_id:/ {print $2}' |
		sort -u | tr '\n' ' '))

	(( ${#ids[@]} == 3 )) ||
		error "expected 3 mirrors, got ${#ids[@]}"

	# Find parity mirror (should be the last one)
	local parity_mirror_id
	for id in "${ids[@]}"; do
		local flags=$($LFS getstripe --mirror-id=$id $tf |
			      awk '/lcme_flags:/ {print $2; exit}')
		if [[ $flags =~ "parity" ]]; then
			parity_mirror_id=$id
			break
		fi
	done

	[[ -n $parity_mirror_id ]] ||
		error "could not find parity mirror"

	# Split parity mirror with -d
	$LFS mirror split --mirror-id $parity_mirror_id -d $tf ||
		error "split -d failed for parity mirror"

	# Verify 2 mirrors remain (2 data mirrors)
	verify_mirror_count $tf 2
}
run_test 7d "mirror split -d works with mixed regular and EC mirrors"

test_7e() {
	(( OSTCOUNT >= 6 )) || skip_env "needs >= 6 OSTs"

	enable_ec

	local tf=$DIR/$tfile

	# Build a 3-mirror file: a plain mirror plus an EC data+parity pair, so
	# co-splitting the data mirror (which drags its parity along) still
	# leaves a mirror behind and does not trip the empty-file guard.
	$LFS mirror create -N -E -1 -c 2 \
			   -N -E -1 -c 2 --ec 2+2 $tf ||
		error "mirror create failed"
	verify_mirror_count $tf 3

	identify_ec_mirrors $tf
	[[ -n $ec_data_id && -n $ec_parity_id && -n $ec_plain_id ]] ||
		error "failed to identify data/parity/plain mirrors"

	local plain_id=$ec_plain_id

	# Splitting the EC data mirror must co-split its parity mirror
	# automatically, leaving only the plain mirror behind.
	$LFS mirror split --mirror-id $ec_data_id -d $tf ||
		error "split -d of EC data mirror failed"

	verify_mirror_count $tf 1
	local remaining_id=$($LFS getstripe $tf |
			     awk '/lcme_mirror_id:/ {print $2; exit}')
	(( remaining_id == plain_id )) ||
		error "expected plain mirror $plain_id, got $remaining_id"

	# A lone data+parity pair cannot be split apart with -d: removing the
	# data mirror would co-split the parity and empty the file, so the MDS
	# must refuse it and leave the file untouched.
	local tf2=$DIR/$tfile.pair
	$LFS setstripe -E -1 -c 2 --ec 2+2 $tf2 ||
		error "setstripe failed"
	verify_mirror_count $tf2 2

	identify_ec_mirrors $tf2
	[[ -n $ec_data_id && -n $ec_parity_id ]] ||
		error "failed to identify data/parity mirrors of $tf2"

	$LFS mirror split --mirror-id $ec_data_id -d $tf2 &&
		error "splitting a lone data+parity pair should be refused"
	verify_mirror_count $tf2 2
}
run_test 7e "split data mirror -d co-splits its EC parity mirror"

test_7f() {
	(( OSTCOUNT >= 6 )) || skip_env "needs >= 6 OSTs"

	enable_ec

	local tf=$DIR/$tfile

	stack_trap "rm -f $tf"

	$LFS setstripe -E -1 -c 4 --ec 4+2 $tf ||
		error "setstripe failed"
	verify_mirror_count $tf 2

	identify_ec_mirrors $tf
	[[ -n $ec_data_id && -n $ec_parity_id ]] ||
		error "failed to identify data/parity mirrors"

	# Removing the parity mirror is allowed, and warns while naming the
	# data mirror that is losing EC protection.
	local expect="parity mirror $ec_parity_id .* data mirror $ec_data_id"
	local out

	out=$($LFS mirror delete --mirror-id $ec_parity_id $tf 2>&1) ||
		error "mirror delete of parity mirror $ec_parity_id failed"
	echo "$out" | grep -q "$expect" ||
		error "expected warning naming data mirror $ec_data_id: $out"

	verify_mirror_count $tf 1

	local remaining_id=$($LFS getstripe $tf |
			     awk '/lcme_mirror_id:/ {print $2; exit}')
	(( remaining_id == ec_data_id )) ||
		error "expected data mirror $ec_data_id, got $remaining_id"

	# The data mirror left behind is no longer protected, so its link to
	# the removed parity mirror must be gone.
	local link=$($LFS getstripe -v --mirror-id=$ec_data_id $tf |
		     awk '/lcme_mirror_link_id:/ {print $2; exit}')
	(( ${link:-0} == 0 )) ||
		error "data mirror $ec_data_id still links to '$link'"

	# Removing the parity of a file that keeps another data mirror takes
	# away no redundancy, so it must not warn.
	local tf2=$DIR/$tfile.redundant

	stack_trap "rm -f $tf2"

	$LFS mirror create -N -E -1 -c 2 \
			   -N -E -1 -c 2 --ec 2+2 $tf2 ||
		error "mirror create failed"
	verify_mirror_count $tf2 3

	identify_ec_mirrors $tf2
	[[ -n $ec_parity_id ]] || error "failed to identify parity mirror"

	out=$($LFS mirror delete --mirror-id $ec_parity_id $tf2 2>&1) ||
		error "mirror delete of parity mirror failed"
	echo "$out" | grep -qi "warning" &&
		error "warned though a data mirror remains: $out"
	verify_mirror_count $tf2 2
}
run_test 7f "mirror delete of a parity mirror warns only if redundancy lost"

test_7g() {
	(( OSTCOUNT >= 6 )) || skip_env "needs >= 6 OSTs"

	enable_ec

	local tf=$DIR/$tfile
	local victim=$DIR/$tfile.victim

	stack_trap "rm -f $tf $victim"

	# Build a 3-mirror file: a plain mirror plus an EC data+parity pair,
	# so co-splitting the data mirror to the victim (which drags its
	# parity along) still leaves a mirror behind on the source.
	$LFS mirror create -N -E -1 -c 2 \
			   -N -E -1 -c 2 --ec 2+2 $tf ||
		error "mirror create failed"
	verify_mirror_count $tf 3

	dd if=/dev/urandom of=$tf bs=1M count=2 || error "write failed"
	$LFS mirror resync $tf || error "mirror resync failed"
	local sum=$(md5sum $tf | awk '{print $1}')

	identify_ec_mirrors $tf
	[[ -n $ec_data_id && -n $ec_parity_id && -n $ec_plain_id ]] ||
		error "failed to identify data/parity/plain mirrors"

	local data_id=$ec_data_id
	local parity_id=$ec_parity_id
	local plain_id=$ec_plain_id

	# Splitting the EC data mirror to a victim must move both the data
	# mirror and its parity mirror to the victim, making the victim a
	# standalone read-only 2-mirror EC file.
	$LFS mirror split --mirror-id $data_id -f $victim $tf ||
		error "split -f of EC data mirror failed"

	# Source keeps only the plain mirror.
	verify_mirror_count $tf 1
	local remaining_id=$($LFS getstripe $tf |
			     awk '/lcme_mirror_id:/ {print $2; exit}')
	(( remaining_id == plain_id )) ||
		error "expected plain mirror $plain_id, got $remaining_id"

	# Victim holds exactly the data+parity pair.
	verify_mirror_count $victim 2
	local victim_parity_flags=$($LFS getstripe --mirror-id=$parity_id \
				    $victim |
				    awk '/lcme_flags:/ {print $2; exit}')
	[[ $victim_parity_flags =~ "parity" ]] ||
		error "mirror $parity_id in victim should keep the parity flag"

	local victim_flags=$($LFS getstripe -v $victim |
			     awk '/lcm_flags:/ {print $2; exit}')
	[[ $victim_flags =~ "ro" ]] ||
		error "victim should be read-only, got flags '$victim_flags'"

	# The data<->parity link must survive the split.
	local vlink=$($LFS getstripe -v $victim | awk -v m=$data_id '
		/lcme_mirror_id:/ { cur = $2 }
		/lcme_mirror_link_id:/ { if (cur == m) { print $2; exit } }')
	(( vlink == parity_id )) ||
		error "victim data mirror links to '$vlink', not $parity_id"

	local vsum=$(md5sum $victim | awk '{print $1}')
	[[ $vsum == $sum ]] ||
		error "victim content differs from source ($vsum != $sum)"

	# A lone data+parity pair cannot be co-split to a victim: moving both
	# mirrors would empty the source, so the MDS must refuse it and leave
	# the file untouched.
	local tf2=$DIR/$tfile.pair
	local victim2=$DIR/$tfile.victim2

	stack_trap "rm -f $tf2 $victim2"
	$LFS setstripe -E -1 -c 2 --ec 2+2 $tf2 ||
		error "setstripe failed"
	verify_mirror_count $tf2 2

	identify_ec_mirrors $tf2
	[[ -n $ec_data_id && -n $ec_parity_id ]] ||
		error "failed to identify data/parity mirrors of $tf2"

	local out
	out=$($LFS mirror split --mirror-id $ec_data_id -f $victim2 $tf2 \
	      2>&1) &&
		error "co-splitting a lone data+parity pair should be refused"
	verify_mirror_count $tf2 2
	echo "$out" | grep -q "only data mirror" ||
		error "refusal should name the only data mirror, got: $out"
	[[ ! -e $victim2 ]] ||
		error "refused split left victim file '$victim2' behind"
}
run_test 7g "split data mirror -f co-splits its EC parity mirror to victim"

test_7h() {
	(( OSTCOUNT >= 6 )) || skip_env "needs >= 6 OSTs"

	enable_ec

	local tf=$DIR/$tfile
	local victim=$DIR/$tfile.victim

	stack_trap "rm -f $tf $victim"

	# Build a plain mirror plus an EC data+parity pair, so the plain
	# mirror stays in-sync and the split is not resynced away first.
	$LFS mirror create -N -E -1 -c 2 \
			   -N -E -1 -c 2 --ec 2+2 $tf ||
		error "mirror create failed"
	verify_mirror_count $tf 3

	dd if=/dev/urandom of=$tf bs=1M count=2 || error "write failed"
	$LFS mirror resync $tf || error "mirror resync failed"

	identify_ec_mirrors $tf
	[[ -n $ec_data_id && -n $ec_parity_id && -n $ec_plain_id ]] ||
		error "failed to identify data/parity/plain mirrors"

	local data_id=$ec_data_id
	local parity_id=$ec_parity_id

	# Mark only the EC data mirror stale, leaving its parity in-sync.
	$LFS setstripe --comp-set --mirror-id $data_id --comp-flags=stale \
		$tf || error "failed to mark data mirror $data_id stale"
	$LFS getstripe --mirror-id=$data_id $tf | grep -q "stale" ||
		error "data mirror $data_id should be stale"

	# The victim's data mirror is its only copy of the file, so the split
	# must clear its stale flag.  Its parity was computed from a different
	# version of that data, so the parity is the one that must be marked
	# stale for `lfs mirror resync` to rebuild it.
	$LFS mirror split --mirror-id $data_id -f $victim $tf ||
		error "split of stale EC data mirror failed"

	verify_mirror_count $victim 2

	local dflags=$($LFS getstripe --mirror-id=$data_id $victim |
		       awk '/lcme_flags:/ {print $2; exit}')
	[[ ! $dflags =~ "stale" ]] ||
		error "victim data mirror $data_id is stale: '$dflags'"

	local pflags=$($LFS getstripe --mirror-id=$parity_id $victim |
		       awk '/lcme_flags:/ {print $2; exit}')
	[[ $pflags =~ "stale" ]] ||
		error "victim parity mirror $parity_id not stale: '$pflags'"

	# A victim left with a stale data mirror is unusable: it reads back as
	# a 0-length file, and `lfs mirror resync` cannot repair it because
	# the only non-stale mirror is the parity one.
	$LFS mirror resync $victim || error "resync of victim failed"

	$LFS getstripe $victim | grep -q "stale" &&
		error "victim still has a stale mirror after resync"

	# Both mirrors in-sync on the source must leave both in-sync on the
	# victim: there is nothing for the split to invalidate.
	local tf2=$DIR/$tfile.sync
	local victim2=$DIR/$tfile.victim2

	stack_trap "rm -f $tf2 $victim2"
	$LFS mirror create -N -E -1 -c 2 \
			   -N -E -1 -c 2 --ec 2+2 $tf2 ||
		error "mirror create failed"
	dd if=/dev/urandom of=$tf2 bs=1M count=2 || error "write failed"
	$LFS mirror resync $tf2 || error "mirror resync failed"

	identify_ec_mirrors $tf2
	[[ -n $ec_data_id && -n $ec_parity_id ]] ||
		error "failed to identify data/parity mirrors of $tf2"

	$LFS mirror split --mirror-id $ec_data_id -f $victim2 $tf2 ||
		error "split of in-sync EC data mirror failed"

	$LFS getstripe $victim2 | grep -q "stale" &&
		error "in-sync split should not mark any victim mirror stale"

	return 0
}
run_test 7h "split of a stale EC data mirror leaves a usable victim"

test_7i() {
	(( OSTCOUNT >= 6 )) || skip_env "needs >= 6 OSTs"

	enable_ec

	local tf=$DIR/$tfile
	local tf2=$DIR/$tfile.pair
	local sum new

	stack_trap "rm -f $tf $tf2"

	# A parity mirror holds no copy of the file data, so it must not count
	# as the good copy that lets a split skip the last-good-copy resync.

	# Splitting the only in-sync data mirror while the other data mirror
	# is stale: the in-sync parity keeps the layout from looking all-stale,
	# so nothing downstream refuses the split and the file is destroyed
	# unless the resync runs first.
	$LFS mirror create -N -E -1 -c 2 \
			   -N -E -1 -c 2 --ec 2+2 $tf ||
		error "mirror create failed"
	dd if=/dev/urandom of=$tf bs=1M count=2 || error "write failed"
	$LFS mirror resync $tf || error "mirror resync failed"
	sum=$(md5sum $tf | awk '{print $1}')

	identify_ec_mirrors $tf
	[[ -n $ec_data_id && -n $ec_parity_id && -n $ec_plain_id ]] ||
		error "failed to identify data/parity/plain mirrors"

	$LFS setstripe --comp-set --mirror-id $ec_data_id \
		--comp-flags=stale $tf ||
		error "failed to mark EC data mirror $ec_data_id stale"

	$LFS mirror split --mirror-id $ec_plain_id -d $tf ||
		error "split of the last good copy failed"

	new=$(md5sum $tf 2>/dev/null | awk '{print $1}')
	[[ $new == $sum ]] ||
		error "source content lost after split ('$new' != '$sum')"

	$LFS getstripe $tf | grep -q "stale" &&
		error "source left with a stale mirror after split"

	# Co-splitting the only in-sync data mirror drags its parity along, so
	# the parity left behind cannot stand in for the resync either.
	$LFS mirror create -N -E -1 -c 2 --ec 2+2 \
			   -N -E -1 -c 2 $tf2 ||
		error "mirror create failed"
	dd if=/dev/urandom of=$tf2 bs=1M count=2 || error "write failed"
	$LFS mirror resync $tf2 || error "mirror resync failed"
	sum=$(md5sum $tf2 | awk '{print $1}')

	identify_ec_mirrors $tf2
	[[ -n $ec_data_id && -n $ec_parity_id && -n $ec_plain_id ]] ||
		error "failed to identify mirrors of $tf2"

	$LFS setstripe --comp-set --mirror-id $ec_plain_id \
		--comp-flags=stale $tf2 ||
		error "failed to mark plain mirror $ec_plain_id stale"

	$LFS mirror split --mirror-id $ec_data_id -d $tf2 ||
		error "co-split of the last good copy failed"

	verify_mirror_count $tf2 1

	new=$(md5sum $tf2 2>/dev/null | awk '{print $1}')
	[[ $new == $sum ]] ||
		error "source content lost after co-split ('$new' != '$sum')"

	$LFS getstripe $tf2 | grep -q "stale" &&
		error "source left with a stale mirror after co-split"

	return 0
}
run_test 7i "split resyncs when only parity mirrors are non-stale"

test_10() {
	local tf=${DIR}/${tdir}/$tfile

	# The number of parity stripes in the EC mirror must be equal to or
	# less than the number of data stripes in the same EC mirror.
	(( OSTCOUNT >= 5 )) || skip_env "needs >= 5 OSTs"
	enable_ec

	test_mkdir $DIR/$tdir

	# Test that creating an ec 2+3 mirror fails (parity > data)
	$LFS setstripe -E -1 -S 4M -c 4 --ec 2+3 $tf >/dev/null &&
		error "setstripe --ec 2+3 succeeded when it shouldn't"

	return 0
}
run_test 10 "cannot create overly large ec mirrors"

test_11() {
	# The number of parity stripes in the EC mirror can be equal to
	# the number of data stripes in the same EC mirror.
	(( OSTCOUNT >= 4 )) || skip_env "needs >= 4 OSTs"
	enable_ec

	test_mkdir $DIR/$tdir
	# Test that creating ec 2+2 mirror works
	$LFS setstripe -E -1 -S 4M -c 4 --ec 2+2 $DIR/$tdir/$tfile ||
	    error "setstripe --ec 2+2 failed"
}
run_test 11 "can create --ec 2+2"

test_12a() {
	local tf=${DIR}/${tdir}/$tfile
	local tf_data=${DIR}/${tdir}/${tfile}.data
	local tf_ec=${DIR}/${tdir}/${tfile}.ec

	# test resyncing a stale ec mirror
	(( OSTCOUNT >= 4 )) || skip_env "needs >= 4 OSTs"
	enable_ec


	test_mkdir $DIR/$tdir

	$LFS setstripe -E -1 -S 4M -c 4 --ec 2+2 $tf ||
	    error "setstripe --ec 2+2 failed"

	# Write the first 3 stripes with \001, \002 and \003
	tr "\000" "\001" < /dev/zero | dd bs=64k count=64          \
		iflag=fullblock of=$tf 2>/dev/null
	tr "\000" "\002" < /dev/zero | dd bs=64k count=64 seek=64  \
		iflag=fullblock of=$tf 2>/dev/null
	tr "\000" "\003" < /dev/zero | dd bs=64k count=64 seek=128 \
		iflag=fullblock of=$tf 2>/dev/null

	# Expected file content:
	#  od -t x1 -A x $tf
	#  000000 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01
	#  *
	#  400000 02 02 02 02 02 02 02 02 02 02 02 02 02 02 02 02
	#  *
	#  800000 03 03 03 03 03 03 03 03 03 03 03 03 03 03 03 03
	#  *
	#  c00000

	echo "fa9fe1782aee74e978e806fb6a0e7a4a1c83610f $tf" \
	    | sha1sum -c - || error "wrong content in $tf after write"

	# resync the ec mirror:
	$LFS mirror resync $tf || error "failed to resync ec mirror"

	# Verify the mirror is no longer stale
	$LFS getstripe $tf | grep lcme_flags | grep stale &&
	    error "after resyncing $tf, it still contains stale component"

	# verify the file content did not change after updating ec mirror
	echo "fa9fe1782aee74e978e806fb6a0e7a4a1c83610f $tf" |
		sha1sum -c - || error "wrong content in $tf after resync"

	# Verify the data mirror is still correct
	rm -f $tf_data
	$LFS mirror read --mirror-id 1 -o $tf_data $tf
	echo "fa9fe1782aee74e978e806fb6a0e7a4a1c83610f $tf_data" |
		sha1sum -c - || error "wrong content in data mirror"

	# Expected content of the ec mirror.
	# With --ec 2+2 on a 4-stripe file, ec_split_stripes creates
	# 2 raid sets of 2 data stripes each, with 2 parity stripes per set
	# (4 parity stripes total, 16 MiB parity mirror).
	#
	# Raid set 0: data stripes 0 (0x01) and 1 (0x02) -> parity stripes 0, 1
	# Raid set 1: data stripes 2 (0x03) and 3 (0x00) -> parity stripes 2, 3
	#
	#  000000 7b 7b 7b ...  (parity stripe 0)
	#  400000 f5 f5 f5 ...  (parity stripe 1)
	#  800000 8f 8f 8f ...  (parity stripe 2)
	#  c00000 01 01 01 ...  (parity stripe 3)

	# Verify we have expected content in the ec mirror
	rm -f $tf_ec
	$LFS mirror read --mirror-id 2 -o $tf_ec $tf
	echo "f1a95d68d4c98de8833bb4ce63756a338df99795 $tf_ec" |
	    sha1sum -c - || error "wrong content in ec mirror"
}
run_test 12a "resync stale parities"

test_12b() {
	# ZFS lseek does not reliably report holes for dirty data
	# (dmu_offset_next() returns EBUSY and the txg_wait_synced() retry in
	# osd_lseek() is not enough), so the resync computes and writes ec
	# parities across the whole sparse region instead of skipping the holes,
	# eventually exhausting OST space and failing with ENOSPC.
	#
	# This seems to be related to LU-14217 where the fix was not enough. It
	# needs to be investigated separately.
	[[ "$ost1_FSTYPE" != "zfs" ]] ||
		skip "LU-14217: lseek holes unreliable on ZFS"

	# test resyncing a stale ec mirror with huge sparse file
	(( OSTCOUNT >= 3 )) || skip_env "needs >= 3 OSTs"
	enable_ec

	test_mkdir $DIR/$tdir

	$LFS setstripe -E -1 -S 4M -c 3 --ec 2+1 $DIR/$tdir/$tfile ||
		error "setstripe --ec 2+1 failed"

	# gauge I/O performance so resync can be bounded relative to it later
	local start=$SECONDS
	# Sparse file: one stripe at the head and one near 4 TiB, gap in between.
	tr "\000" "\001" < /dev/zero | dd bs=64k count=64          \
		iflag=fullblock of=$DIR/$tdir/$tfile 2>/dev/null
	tr "\000" "\002" < /dev/zero | dd bs=64k count=64          \
		seek=64000000  \
		iflag=fullblock of=$DIR/$tdir/$tfile 2>/dev/null
	local elapsed=$((SECONDS - start))
	# SECONDS has 1s resolution; treat sub-second writes as 1s.
	(( elapsed >= 1 )) || elapsed=1

	# Truncate to 10 TiB to have a hole at the end
	$TRUNCATE $DIR/$tdir/$tfile 10995116277760 ||
		error "failed to truncate file to 10 TiB"

	# Resync the EC mirror. Holes in the sparse file cause stale parity
	# regions to be punched on resync. When the OST backend lacks fallocate
	# hole-punch (e.g. osd-zfs), resync falls back to writing zeros; with
	# holes this large that fallback is rejected as unsupported and resync
	# fails with EOPNOTSUPP. Keep this test disabled on ZFS until osd-zfs
	# hole-punch is implemented (LU-20435).
	#
	# When holes are skipped correctly, resync should only touch the two
	# data regions above and finish within 10x of the initial write time.
	# A regression would hit the timeout instead of running for hours.
	local timeout=$((elapsed * 10))
	echo "mirror resync timeout set to $timeout"
	timeout -k 10 $timeout $LFS mirror resync $DIR/$tdir/$tfile ||
		error "failed to resync ec mirror"

	# Verify the mirror is no longer stale
	$LFS getstripe $DIR/$tdir/$tfile | grep lcme_flags | grep stale &&
		error "$DIR/$tdir/$tfile still stale after resync" || true
}
run_test 12b "resync huge sparse file (10 TiB)"

test_12c() {
	# Sparse file like 12b: stripe sets 0 and 3 written, 1 and 2 are holes.
	# Resync must skip holed stripe sets and punch the matching parity
	# regions instead of rewriting them.
	(( OSTCOUNT >= 6 )) || skip_env "needs >= 6 OSTs"
	enable_ec

	local tf=${DIR}/${tdir}/$tfile
	local stripe_size=$((4 * 1024 * 1024))
	local stripe_set_size=$((4 * stripe_size))
	local ec_set_size=$((2 * stripe_size))

	test_mkdir $DIR/$tdir
	stack_trap "rm -f $tf"

	$LFS setstripe -E -1 -S 4M -c 4 --ec 4+2 $tf ||
		error "setstripe --ec 4+2 failed"

	tr "\000" "\001" < /dev/zero | dd bs=$stripe_set_size count=1 \
		iflag=fullblock of=$tf 2>/dev/null ||
		error "failed to write stripe set 0"
	tr "\000" "\001" < /dev/zero | dd bs=$stripe_set_size count=1 seek=3 \
		iflag=fullblock of=$tf 2>/dev/null ||
		error "failed to write stripe set 3"

	(( $($LUSTRE/tests/lseek_test -l 0 $tf) == $stripe_set_size )) ||
		error "expected hole at $stripe_set_size"
	(( $($LUSTRE/tests/lseek_test -d $stripe_set_size $tf) ==
		$((3 * stripe_set_size)) )) ||
		error "expected next data at $((3 * stripe_set_size))"

	$LFS mirror resync $tf || error "failed to resync ec mirror"
	$LFS getstripe $tf | grep lcme_flags | grep stale &&
		error "stale component after resync"

	#  lcme_id:             131074
	#  lcme_mirror_id:      2
	#  lcme_flags:          init,parity
	local parity_mirror_id=$($LFS getstripe $tf |
		grep -B1 "lcme_flags.*parity" |
		grep "lcme_mirror_id" | awk '{print $2}')
	# expect_zeros: no = parity data present, yes = punched (all zeros)
	check_parity_read $tf $parity_mirror_id 0 no
	check_parity_read $tf $parity_mirror_id $ec_set_size yes
	check_parity_read $tf $parity_mirror_id $((2 * ec_set_size)) yes
	check_parity_read $tf $parity_mirror_id $((3 * ec_set_size)) no

	$LFS mirror verify $tf || error "mirror verify failed after resync"
}
run_test 12c "resync skips holed stripe sets"

test_12d() {
	# Sparse file at raidset granularity: with 3+2 and -c 6 each stripe
	# set holds two raidsets (k=3). Raidsets 0 and 2 written; raidset
	# 1 is a hole. Resync must skip the holed raidset and punch parity.
	(( OSTCOUNT >= 6 )) || skip_env "needs >= 6 OSTs"
	enable_ec

	local tf=${DIR}/${tdir}/$tfile
	local stripe_size=$((1024 * 1024))
	local raidset_size=$((3 * stripe_size))
	local ec_raidset_size=$((2 * stripe_size))

	test_mkdir $DIR/$tdir
	stack_trap "rm -f $tf"

	$LFS setstripe -E -1 -S 1M -c 6 --ec 3+2 $tf ||
		error "setstripe --ec 3+2 failed"

	tr "\000" "\001" < /dev/zero | dd bs=$raidset_size count=1 \
		iflag=fullblock of=$tf 2>/dev/null ||
		error "failed to write raidset 0"
	tr "\000" "\001" < /dev/zero | dd bs=$raidset_size count=1 seek=2 \
		iflag=fullblock of=$tf 2>/dev/null ||
		error "failed to write raidset 2"

	(( $($LUSTRE/tests/lseek_test -l 0 $tf) == $raidset_size )) ||
		error "expected hole at $raidset_size"
	(( $($LUSTRE/tests/lseek_test -d $raidset_size $tf) ==
		$((2 * raidset_size)) )) ||
		error "expected next data at $((2 * raidset_size))"

	$LFS mirror resync $tf || error "failed to resync ec mirror"
	$LFS getstripe $tf | grep lcme_flags | grep stale &&
		error "stale component after resync"

	#  lcme_id:             131074
	#  lcme_mirror_id:      2
	#  lcme_flags:          init,parity
	local parity_mirror_id=$($LFS getstripe $tf |
		grep -B1 "lcme_flags.*parity" |
		grep "lcme_mirror_id" | awk '{print $2}')
	# expect_zeros: no = parity data present, yes = punched (all zeros)
	check_parity_read $tf $parity_mirror_id 0 no
	check_parity_read $tf $parity_mirror_id $ec_raidset_size yes
	check_parity_read $tf $parity_mirror_id $((2 * ec_raidset_size)) no

	$LFS mirror verify $tf || error "mirror verify failed after resync"
}
run_test 12d "resync skips holed raidsets"

test_12e() {
	# Punch overlapping holes on both data stripes; the overlap [8k, 16k)
	# leaves a matching hole in the parity stripe after resync.
	(( OSTCOUNT >= 3 )) || skip_env "needs >= 3 OSTs"
	enable_ec

	local tf=${DIR}/${tdir}/$tfile
	local stripe_size=$((1024 * 1024))
	local stripe_set_size=$((2 * stripe_size))
	local punch_len=$((12 * 1024))

	test_mkdir $DIR/$tdir
	stack_trap "rm -f $tf"

	$LFS setstripe -E -1 -S 1M -c 2 --ec 2+1 $tf ||
		error "setstripe --ec 2+1 failed"

	tr "\000" "\001" < /dev/zero | dd bs=$stripe_set_size count=1 \
		iflag=fullblock of=$tf 2>/dev/null ||
		error "failed to write one raidset"

	$LFS mirror resync $tf || error "failed to resync ec mirror"
	$LFS getstripe $tf | grep lcme_flags | grep stale &&
		error "stale component after initial resync"

	local parity_mirror_id=$($LFS getstripe $tf |
		grep -B1 "lcme_flags.*parity" |
		grep "lcme_mirror_id" | awk '{print $2}')

	check_parity_read $tf $parity_mirror_id 0 no
	check_parity_read $tf $parity_mirror_id $stripe_size yes

	# Punch data0 [4k, 16k) and data1 [8k, 20k); overlap is [8k, 16k).
	local fallocate_out
	fallocate_out=$(fallocate -p --offset 4096 -l $punch_len $tf 2>&1) ||
		skip_eopnotsupp "$fallocate_out|fallocate punch data0 failed"
	fallocate_out=$(fallocate -p --offset $((stripe_size + 8192)) \
		-l $punch_len $tf 2>&1) ||
		skip_eopnotsupp "$fallocate_out|fallocate punch data1 failed"

	$LFS getstripe $tf | grep lcme_flags | grep stale ||
		error "parity should be stale after punching data stripes"

	$LFS mirror resync $tf || error "failed to resync after punch"
	$LFS getstripe $tf | grep lcme_flags | grep stale &&
		error "stale component after resync following punch"

	# Parity: [0, 8k) and [16k, ...) have data; [8k, 16k) is a hole.
	check_parity_read $tf $parity_mirror_id 0 no
	check_parity_read $tf $parity_mirror_id $((8 * 1024)) yes
	check_parity_read $tf $parity_mirror_id $((16 * 1024)) no

	$LFS mirror verify $tf || error "mirror verify failed after punch resync"
}
run_test 12e "overlapping data holes punch parity gap verify"

test_12f() {
	# Write 2k (EOF not 4k-aligned), resync, and verify parity is written
	# through the enclosing 4k block: data at [0, 4k), hole from 4k on.
	(( OSTCOUNT >= 3 )) || skip_env "needs >= 3 OSTs"
	enable_ec

	local tf=${DIR}/${tdir}/$tfile
	local write_len=$((2 * 1024))
	local parity_len=$((4 * 1024))

	test_mkdir $DIR/$tdir
	stack_trap "rm -f $tf"

	$LFS setstripe -E -1 -S 1M -c 2 --ec 2+1 $tf || error "setstripe failed"

	tr "\000" "\001" < /dev/zero | dd bs=$write_len count=1 \
		iflag=fullblock of=$tf 2>/dev/null ||
		error "failed to write $write_len bytes"

	$LFS mirror resync $tf || error "failed to resync ec mirror"
	$LFS getstripe $tf | grep lcme_flags | grep stale &&
		error "stale component after resync"

	local parity_mirror_id=$($LFS getstripe $tf |
		grep -B1 "lcme_flags.*parity" |
		grep "lcme_mirror_id" | awk '{print $2}')

	check_parity_read $tf $parity_mirror_id 0 no
	check_parity_read $tf $parity_mirror_id $parity_len yes

	$LFS mirror verify $tf || error "mirror verify failed after resync"
}
run_test 12f "short write resync parity spans [0, 4k) verify"

test_12g() {
	# EC-only stale resync must emit --stats YAML (lamigo liveness).
	(( OSTCOUNT >= 3 )) || skip_env "needs >= 3 OSTs"
	enable_ec

	local tf=${DIR}/${tdir}/$tfile
	local output_file=${DIR}/${tdir}/${tfile}.stats

	test_mkdir $DIR/$tdir
	stack_trap "rm -f $tf $output_file"

	$LFS setstripe -E -1 -S 4M -c 2 --ec 2+1 $tf ||
		error "setstripe --ec 2+1 failed"

	tr "\000" "\001" < /dev/zero | dd bs=1M count=8 \
		iflag=fullblock of=$tf 2>/dev/null ||
		error "failed to write $tf"
	[[ "$($LFS getstripe $tf)" =~ lcme_flags:.*stale ]] ||
		error "expected stale parity after write"

	$LFS mirror resync --stats --stats-interval=1 $tf | tee $output_file
	rc=${PIPESTATUS[0]}
	(( rc == 0 )) || error "resync failed rc = $rc"
	[[ ! "$($LFS getstripe $tf)" =~ lcme_flags:.*stale ]] ||
		error "stale component after resync"

	grep -q "copied:" $output_file ||
		error "no stats output from EC resync"
	verify_yaml_available || skip_env "YAML verification not installed"
	verify_yaml < $output_file || error "stats is not valid YAML"
}
run_test 12g "lfs mirror resync --stats for EC parity"

test_12h() {
	# -W must throttle EC parity writes, not only FLR data copies.
	(( OSTCOUNT >= 3 )) || skip_env "needs >= 3 OSTs"
	enable_ec

	local tf=${DIR}/${tdir}/$tfile
	local output_file=${DIR}/${tdir}/${tfile}.stats
	# 16 MiB data, 2+1: two raidsets write ~8 MiB of parity.
	local data_mb=16
	local parity_mb=8

	test_mkdir $DIR/$tdir
	stack_trap "rm -f $tf $output_file"

	$LFS setstripe -E -1 -S 4M -c 2 --ec 2+1 $tf ||
		error "setstripe --ec 2+1 failed"

	tr "\000" "\001" < /dev/zero | dd bs=1M count=$data_mb \
		iflag=fullblock of=$tf 2>/dev/null ||
		error "failed to write $tf"
	[[ "$($LFS getstripe $tf)" =~ lcme_flags:.*stale ]] ||
		error "expected stale parity after write"

	local start=$SECONDS
	$LFS mirror resync --stats --stats-interval=1 -W 1M $tf |
		tee $output_file
	rc=${PIPESTATUS[0]}
	(( rc == 0 )) || error "resync failed rc = $rc"
	local elapsed=$((SECONDS - start))
	[[ ! "$($LFS getstripe $tf)" =~ lcme_flags:.*stale ]] ||
		error "stale component after resync"

	(( elapsed >= parity_mb * 95 / 100 )) ||
		error "'lfs mirror resync -W' too fast ($elapsed < 0.95 * $parity_mb)?"

	(( elapsed <= parity_mb * 120 / 100 )) ||
		error_not_in_vm "'lfs mirror resync -W' slow ($elapsed > 1.2 * $parity_mb)"

	(( elapsed <= parity_mb * 350 / 100 )) ||
		error "'lfs mirror resync -W' too slow in VM ($elapsed > 3.5 * $parity_mb)"
}
run_test 12h "lfs mirror resync -W for EC parity"

test_13() {
	local tf=${DIR}/${tdir}/$tfile
	local tf_data=${DIR}/${tdir}/${tfile}.data
	local tf_ec=${DIR}/${tdir}/${tfile}.ec

	(( OSTCOUNT >= 4 )) || skip_env "needs >= 4 OSTs"
	enable_ec

	test_mkdir $DIR/$tdir

	$LFS setstripe -E -1 -S 4M -c 1 --ec 3+1 $tf ||
	    error "setstripe -c 1 --ec 3+1 failed"

	# Write the first 3 stripes with \001, \002 and \003
	tr "\000" "\001" < /dev/zero | dd bs=64k iflag=fullblock count=64          \
		of=$tf 2>/dev/null
	tr "\000" "\002" < /dev/zero | dd bs=64k iflag=fullblock count=64 seek=64  \
		of=$tf 2>/dev/null
	tr "\000" "\003" < /dev/zero | dd bs=64k iflag=fullblock count=64 seek=128 \
		of=$tf 2>/dev/null

	# resync the ec mirror:
	$LFS mirror resync $tf || error "failed to resync ec mirror"

	# Expected content of both data and "ec" mirror
	#  od -t x1 -A x $tf
	#  000000 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01
	#  *
	#  400000 02 02 02 02 02 02 02 02 02 02 02 02 02 02 02 02
	#  *
	#  800000 03 03 03 03 03 03 03 03 03 03 03 03 03 03 03 03
	#  *
	#  c00000
	rm -f $tf_data
	stack_trap "rm -f $tf_data"
	$LFS mirror read --mirror-id 1 -o $tf_data $tf
	echo "fa9fe1782aee74e978e806fb6a0e7a4a1c83610f $tf_data" |
	    sha1sum -c - || error "wrong content in data mirror"

	rm -f $tf_ec
	stack_trap "rm -f $tf_ec"
	$LFS mirror read --mirror-id 2 -o $tf_ec $tf
	echo "fa9fe1782aee74e978e806fb6a0e7a4a1c83610f $tf_ec" |
	    sha1sum -c - || error "wrong content in ec mirror"
}
run_test 13 "parity of single stripe data is just a copy"

test_20() {
	local tf=${DIR}/${tdir}/$tfile

	(( OSTCOUNT >= 4 )) || skip_env "needs >= 4 OSTs"
	enable_ec

	test_mkdir $DIR/$tdir

	# 4M stripe size
	$LFS setstripe -E -1 -S 4M -c 1 --ec 3+1 $tf ||
	    error "setstripe -c 1 -S 4M --ec 3+1 failed"

	$LFS getstripe $tf | grep lmm_stripe_size | grep -v 4194304 2>/dev/null &&
		error "Stripe size mismatch for -S 4M"
	rm -f $tf

	# 1M stripe size
	$LFS setstripe -E -1 -S 1M -c 1 --ec 3+1 $tf ||
	    error "setstripe -c 1 -S 1M --ec 3+1 failed"

	$LFS getstripe $tf |
	    grep lmm_stripe_size | grep -v 1048576 2>/dev/null &&
	    error "Stripe size mismatch for -S 1M"
	rm -f $tf

	# 64k stripe size
	$LFS setstripe -E -1 -S 64k -c 1 --ec 3+1 $tf ||
	    error "setstripe -c 1 -S 64k --ec 3+1 failed"

	$LFS getstripe $tf | grep lmm_stripe_size | grep -v 65536 2>/dev/null &&
		error "Stripe size mismatch for -S 64k"

	return 0
}
run_test 20 "test that stripe size of parity mirror is set correctly"

# Test 21: lfs migrate with EC layouts
test_21a() {
	(( OSTCOUNT >= 3 )) || skip_env "needs >= 3 OSTs"
	enable_ec

	local tf=$DIR/$tdir/$tfile

	test_mkdir $DIR/$tdir
	stack_trap "rm -rf $DIR/$tdir"

	# Create a plain (non-EC) file with data
	$LFS setstripe -c 1 $tf || error "setstripe failed"
	dd if=/dev/urandom of=$tf bs=1M count=5 ||
		error "write to plain file failed"
	local old_chksum=$(md5sum $tf | awk '{print $1}')

	# Migrate plain file to EC layout
	$LFS migrate -E -1 -c 2 --ec 2+1 $tf ||
		error "migrate (plain -> EC) failed"

	# Verify EC layout after migration
	verify_mirror_count $tf 2
	verify_comp_count $tf 2

	# Get component IDs
	local ids=($($LFS getstripe $tf |
		awk '/lcme_id/{print $2}' | tr '\n' ' '))

	# Verify parity component layout
	verify_comp_parity $tf ${ids[1]}
	verify_ec_stripe_count $tf ${ids[1]} 2 1

	# Verify data integrity
	local new_chksum=$(md5sum $tf | awk '{print $1}')
	[[ "$old_chksum" == "$new_chksum" ]] ||
		error "data changed after migrate: $old_chksum != $new_chksum"

	# For multi-stripe EC (2+1), parity is XOR across stripes, so
	# parity content must differ from the data mirror content.
	local data_sum=$($LFS mirror read -N1 $tf | md5sum)
	local parity_sum=$($LFS mirror read -N2 $tf | md5sum)
	[[ "$data_sum" != "$parity_sum" ]] ||
		error "parity identical to data - migrate didn't compute parity"
}
run_test 21a "migrate plain file to EC layout"

test_21b() {
	(( OSTCOUNT >= 3 )) || skip_env "needs >= 3 OSTs"
	enable_ec

	local tf=$DIR/$tdir/$tfile

	test_mkdir $DIR/$tdir
	stack_trap "rm -rf $DIR/$tdir"

	# Create EC file with data
	$LFS setstripe -E -1 -c 2 --ec 2+1 $tf ||
		error "setstripe EC failed"
	dd if=/dev/urandom of=$tf bs=1M count=5 ||
		error "write to EC file failed"
	$LFS mirror resync $tf || error "resync failed"

	local old_chksum=$(md5sum $tf | awk '{print $1}')

	# Verify EC layout before migration
	verify_mirror_count $tf 2
	verify_comp_count $tf 2

	# Migrate EC file to plain layout
	$LFS migrate -c 2 $tf || error "migrate (EC -> plain) failed"

	# Verify no parity components remain
	$LFS getstripe $tf | grep -q "parity" &&
		error "parity components should not exist after migrate to plain"

	# Verify no composite/FLR layout remains
	$LFS getstripe $tf | grep -q "lcm_mirror_count" &&
		error "should not have mirror layout after migrate to plain"

	# Verify data integrity
	local new_chksum=$(md5sum $tf | awk '{print $1}')
	[[ "$old_chksum" == "$new_chksum" ]] ||
		error "data changed after migrate: $old_chksum != $new_chksum"
}
run_test 21b "migrate EC file to plain layout"

test_21c() {
	(( OSTCOUNT >= 4 )) || skip_env "needs >= 4 OSTs"
	enable_ec

	local tf=$DIR/$tdir/$tfile

	test_mkdir $DIR/$tdir
	stack_trap "rm -rf $DIR/$tdir"

	# Create EC file with 2+1 and write data
	$LFS setstripe -E -1 -c 2 --ec 2+1 $tf ||
		error "setstripe EC 2+1 failed"
	dd if=/dev/urandom of=$tf bs=1M count=5 ||
		error "write to EC file failed"
	$LFS mirror resync $tf || error "initial resync failed"

	local old_chksum=$(md5sum $tf | awk '{print $1}')

	# Migrate EC 2+1 to EC 3+1
	$LFS migrate -E -1 -c 3 --ec 3+1 $tf ||
		error "migrate (EC 2+1 -> EC 3+1) failed"

	# Verify new EC layout
	verify_mirror_count $tf 2
	verify_comp_count $tf 2

	local ids=($($LFS getstripe $tf |
		awk '/lcme_id/{print $2}' | tr '\n' ' '))

	verify_comp_parity $tf ${ids[1]}
	verify_ec_stripe_count $tf ${ids[1]} 3 1

	# Verify data integrity
	local new_chksum=$(md5sum $tf | awk '{print $1}')
	[[ "$old_chksum" == "$new_chksum" ]] ||
		error "data changed after migrate: $old_chksum != $new_chksum"

	# For multi-stripe EC, parity is XOR across stripes and must
	# differ from data content.
	local data_sum=$($LFS mirror read -N1 $tf | md5sum)
	local parity_sum=$($LFS mirror read -N2 $tf | md5sum)
	[[ "$data_sum" != "$parity_sum" ]] ||
		error "parity identical to data after EC config change"
}
run_test 21c "migrate between different EC configurations"

test_21d() {
	(( OSTCOUNT >= 3 )) || skip_env "needs >= 3 OSTs"
	enable_ec

	local tf=$DIR/$tdir/$tfile

	test_mkdir $DIR/$tdir
	stack_trap "rm -rf $DIR/$tdir"

	# Create multi-component PFL file with data
	$LFS setstripe -E 1M -c 1 -E -1 -c 2 $tf ||
		error "setstripe PFL failed"
	dd if=/dev/urandom of=$tf bs=1M count=5 ||
		error "write to PFL file failed"

	local old_chksum=$(md5sum $tf | awk '{print $1}')

	# Migrate PFL to EC with multiple components
	$LFS migrate -E 1M -c 2 -E -1 -c 2 --ec 2+1 $tf ||
		error "migrate (PFL -> EC) failed"

	# Verify EC layout
	verify_mirror_count $tf 2
	verify_comp_count $tf 4

	local ids=($($LFS getstripe $tf |
		awk '/lcme_id/{print $2}' | tr '\n' ' '))

	# Verify parity components
	verify_comp_parity $tf ${ids[2]}
	verify_comp_parity $tf ${ids[3]}
	verify_ec_stripe_count $tf ${ids[2]} 2 1
	verify_ec_stripe_count $tf ${ids[3]} 2 1

	# Verify data integrity
	local new_chksum=$(md5sum $tf | awk '{print $1}')
	[[ "$old_chksum" == "$new_chksum" ]] ||
		error "data changed after migrate: $old_chksum != $new_chksum"
}
run_test 21d "migrate PFL file to EC layout"

# Test 22: mirror split with EC
test_22a() {
	(( OSTCOUNT >= 6 )) || skip_env "needs >= 6 OSTs"
	enable_ec

	local tf=$DIR/$tdir/$tfile

	test_mkdir $DIR/$tdir
	stack_trap "rm -rf $DIR/$tdir"

	# Create file with plain data mirror + EC data mirror + parity mirror
	$LFS setstripe -N -E -1 -c 2 \
		-N -E -1 -c 2 --ec 2+2 $tf ||
		error "setstripe failed"
	dd if=/dev/urandom of=$tf bs=1M count=3 ||
		error "write failed"
	$LFS mirror resync $tf || error "resync failed"

	local old_chksum=$(md5sum $tf | awk '{print $1}')

	# Should have 3 mirrors: data, data+EC, parity
	verify_mirror_count $tf 3

	# Split off mirror 1 (plain data mirror) - should work fine
	$LFS mirror split --mirror-id 1 -d $tf ||
		error "split plain data mirror failed"

	# Should now have 2 mirrors: data+EC, parity
	verify_mirror_count $tf 2

	# Parity component should still exist
	$LFS getstripe $tf | grep -q "parity" ||
		error "parity component lost after split"

	# Verify data integrity
	local new_chksum=$(md5sum $tf | awk '{print $1}')
	[[ "$old_chksum" == "$new_chksum" ]] ||
		error "data changed after split: $old_chksum != $new_chksum"

	# Verify remaining EC layout is functional: write new data,
	# resync, and confirm parity is recomputed
	dd if=/dev/urandom of=$tf bs=1M count=1 conv=notrunc ||
		error "write after split failed"
	verify_flr_state $tf "wp"
	$LFS mirror resync $tf || error "resync after split+write failed"
	verify_flr_state $tf "ro"

	# For multi-stripe EC (2+2), parity is computed across stripes and
	# must differ from data content, confirming EC is functional.
	local data_sum=$($LFS mirror read -N1 $tf | md5sum)
	local parity_sum=$($LFS mirror read -N2 $tf | md5sum)
	[[ "$data_sum" != "$parity_sum" ]] ||
		error "parity identical to data after split - EC not functional"
}
run_test 22a "mirror split removes plain mirror, keeps EC"

# Test 23: truncate and fallocate with EC
test_23a() {
	enable_ec

	local tf=$DIR/$tdir/$tfile
	local ids

	test_mkdir $DIR/$tdir
	stack_trap "rm -rf $DIR/$tdir"

	# Create EC file and write data
	$LFS setstripe -E -1 -c 4 --ec 4+2 $tf ||
		error "setstripe failed"
	dd if=/dev/urandom of=$tf bs=1M count=5 ||
		error "write failed"
	$LFS mirror resync $tf || error "resync failed"
	verify_flr_state $tf "ro"

	# Save parity mirror checksum before truncate
	local parity_sum_before=$($LFS mirror read -N2 $tf | md5sum)

	# Truncate to smaller size
	$TRUNCATE $tf 2097152 || error "truncate to 2M failed"

	# After truncate, file should be in write-pending state
	verify_flr_state $tf "wp"

	# Get component IDs
	ids=($($LFS getstripe $tf |
		awk '/lcme_id/{print $2}' | tr '\n' ' '))

	# Parity component should be stale after truncate
	verify_comp_stale $tf ${ids[1]}

	# Resync - this should recompute parity for truncated data
	$LFS mirror resync $tf || error "resync after truncate failed"
	verify_flr_state $tf "ro"

	local size=$(stat -c%s $tf)
	(( size == 2097152 )) ||
		error "file size wrong after truncate: $size != 2097152"

	# Verify parity was actually recomputed (checksum should change
	# because parity of 2M of data differs from parity of 5M of data)
	local parity_sum_after=$($LFS mirror read -N2 $tf | md5sum)
	[[ "$parity_sum_before" != "$parity_sum_after" ]] ||
		error "parity mirror unchanged after truncate+resync"

	# Verify data mirror still has correct content
	local data_sum=$($LFS mirror read -N1 $tf | md5sum)
	local file_sum=$(md5sum < $tf)
	[[ "$data_sum" == "$file_sum" ]] ||
		error "data mirror content mismatch after truncate"
}
run_test 23a "truncate EC file to smaller size"

test_23b() {
	enable_ec

	local tf=$DIR/$tdir/$tfile
	local ids

	test_mkdir $DIR/$tdir
	stack_trap "rm -rf $DIR/$tdir"

	# Create EC file and write data
	$LFS setstripe -E -1 -c 4 --ec 4+2 $tf ||
		error "setstripe failed"
	dd if=/dev/urandom of=$tf bs=1M count=2 ||
		error "write failed"
	$LFS mirror resync $tf || error "resync failed"
	verify_flr_state $tf "ro"

	# Truncate to larger size (extends with zeros)
	$TRUNCATE $tf 10485760 || error "truncate to 10M failed"

	# After truncate, file should be in write-pending state
	verify_flr_state $tf "wp"

	# Get component IDs
	ids=($($LFS getstripe $tf |
		awk '/lcme_id/{print $2}' | tr '\n' ' '))

	# Parity component should be stale after truncate
	verify_comp_stale $tf ${ids[1]}

	# Resync - parity must be recomputed for the extended data
	$LFS mirror resync $tf || error "resync after truncate failed"
	verify_flr_state $tf "ro"

	local size=$(stat -c%s $tf)
	(( size == 10485760 )) ||
		error "file size wrong after truncate: $size != 10485760"

	# Verify data mirror content matches file read
	local data_sum=$($LFS mirror read -N1 $tf | md5sum)
	local file_sum=$(md5sum < $tf)
	[[ "$data_sum" == "$file_sum" ]] ||
		error "data mirror content mismatch after truncate"

	# Verify parity covers the extended region: write non-zero data
	# into the previously zero-extended area and confirm parity is
	# recomputed (a zero-only extension produces zero parity, which
	# is indistinguishable from unwritten parity storage, so we need
	# real data to meaningfully exercise parity over the new range).
	local parity_sum_before=$($LFS mirror read -N2 $tf | md5sum)
	dd if=/dev/urandom of=$tf bs=1M count=4 seek=4 conv=notrunc ||
		error "write into extended region failed"
	$LFS mirror resync $tf || error "resync after extended write failed"
	verify_flr_state $tf "ro"
	local parity_sum_after=$($LFS mirror read -N2 $tf | md5sum)
	[[ "$parity_sum_before" != "$parity_sum_after" ]] ||
		error "parity mirror unchanged after write into extended region"
}
run_test 23b "truncate EC file to larger size"

test_23c() {
	enable_ec
	check_fallocate_or_skip ost1 alloc # Probe for feature support
	check_set_fallocate_or_skip

	local tf=$DIR/$tdir/$tfile
	local ids

	test_mkdir $DIR/$tdir
	stack_trap "rm -rf $DIR/$tdir"

	# Create EC file
	$LFS setstripe -E -1 -c 4 --ec 4+2 $tf ||
		error "setstripe failed"

	# fallocate space - preallocate without writing data
	fallocate -l 5M $tf || error "fallocate failed"

	# After fallocate, parity should be stale
	verify_flr_state $tf "wp"
	ids=($($LFS getstripe $tf |
		awk '/lcme_id/{print $2}' | tr '\n' ' '))
	verify_comp_stale $tf ${ids[1]}

	# Resync parity for the preallocated region
	$LFS mirror resync $tf || error "resync after fallocate failed"
	verify_flr_state $tf "ro"

	# Now write actual data into the preallocated region
	dd if=/dev/urandom of=$tf bs=1M count=5 conv=notrunc ||
		error "write to preallocated file failed"

	# Parity must go stale again from the actual write
	verify_flr_state $tf "wp"
	verify_comp_stale $tf ${ids[1]}

	# Resync parity with real data
	$LFS mirror resync $tf || error "resync after write failed"
	verify_flr_state $tf "ro"

	# Verify data mirror matches file content (proving EC resync
	# correctly processed the preallocated+written region)
	local file_sum=$(md5sum < $tf)
	local data_sum=$($LFS mirror read -N1 $tf | md5sum)
	[[ "$file_sum" == "$data_sum" ]] ||
		error "data mirror content mismatch after fallocate+write"

	# For multi-stripe EC (4+2), parity is XOR across stripes and
	# must differ from data content.
	local parity_sum=$($LFS mirror read -N2 $tf | md5sum)
	[[ "$data_sum" != "$parity_sum" ]] ||
		error "parity mirror identical to data - not real parity"
}
run_test 23c "fallocate on EC file"

# Test 24: comp-add/del with EC
test_24a() {
	enable_ec

	local tf=$DIR/$tdir/$tfile

	test_mkdir $DIR/$tdir
	stack_trap "rm -rf $DIR/$tdir"

	# Create EC file
	$LFS setstripe -E -1 -c 4 --ec 4+2 $tf ||
		error "setstripe failed"

	# Attempting --comp-add with --ec should be rejected
	local output
	output=$($LFS setstripe --comp-add -E -1 --ec 4+2 $tf 2>&1) &&
		error "should reject --ec with --comp-add"
	echo "$output" | grep -q "cannot be used with" ||
		error "unexpected error message: $output"

	return 0
}
run_test 24a "reject --ec with --comp-add"

test_24b() {
	enable_ec

	local tf=$DIR/$tdir/$tfile

	test_mkdir $DIR/$tdir
	stack_trap "rm -rf $DIR/$tdir"

	# Create EC file with two components per mirror
	# Mirror 1 (data): [0, 1M], [1M, EOF]
	# Mirror 2 (parity): [0, 1M], [1M, EOF]
	$LFS setstripe -E 1M -c 2 -E -1 -c 4 --ec 4+2 $tf ||
		error "setstripe failed"

	verify_mirror_count $tf 2
	verify_comp_count $tf 4

	local ids=($($LFS getstripe $tf |
		awk '/lcme_id/{print $2}' | tr '\n' ' '))

	# ids[0], ids[1] = data mirror components
	# ids[2], ids[3] = parity mirror components

	# Attempting to delete data component that is protected by
	# parity should fail
	$LFS setstripe --comp-del -I ${ids[1]} $tf 2>&1 &&
		error "should not allow deleting data comp protected by parity"

	# Verify layout unchanged
	verify_comp_count $tf 4

	# Attempting to delete parity component directly should also fail
	$LFS setstripe --comp-del -I ${ids[2]} $tf 2>&1 &&
		error "should not allow deleting parity component directly"

	verify_comp_count $tf 4

	return 0
}
run_test 24b "reject deleting data component protected by parity"

# Test 25: lfs find with EC attributes
test_25a() {
	enable_ec

	local td=$DIR/$tdir

	test_mkdir $td
	stack_trap "rm -rf $td"

	# Create EC file
	$LFS setstripe -E -1 -c 4 --ec 4+2 $td/ec_file ||
		error "setstripe EC failed"

	# Create plain file
	$LFS setstripe -c 1 $td/plain_file ||
		error "setstripe plain failed"

	# Create mirrored file without EC
	$LFS mirror create -N2 -E -1 -c 2 $td/mirror_file ||
		error "mirror create failed"

	# Find files with parity components
	local found=$($LFS find $td --component-flags parity | wc -l)
	(( found == 1 )) ||
		error "expected 1 file with parity, found $found"

	# The found file should be the EC file
	$LFS find $td --component-flags parity | grep -q "ec_file" ||
		error "find did not return the EC file"

	# Verify that plain and mirror files are not returned
	local parity_files=$($LFS find $td --component-flags parity)
	echo "$parity_files" | grep -q "plain_file" &&
		error "find returned plain_file as having parity"
	echo "$parity_files" | grep -q "mirror_file" &&
		error "find returned mirror_file as having parity"

	return 0
}
run_test 25a "lfs find with --component-flags parity"

# Test 26: larger I/O and multi-stripe EC data integrity
test_26a() {
	enable_ec

	local tf=$DIR/$tdir/$tfile
	local tf_data=$DIR/$tdir/${tfile}.data
	local tf_ec=$DIR/$tdir/${tfile}.ec

	test_mkdir $DIR/$tdir
	stack_trap "rm -rf $DIR/$tdir"

	# Create EC file with 4 data stripes and 2 parity stripes
	# Use 1M stripe size so 20M of data spans 5 full stripe rows
	$LFS setstripe -E -1 -S 1M -c 4 --ec 4+2 $tf ||
		error "setstripe failed"

	# Write 20M of random data (5 full rows of 4x1M stripes)
	dd if=/dev/urandom of=$tf bs=1M count=20 ||
		error "write failed"

	local old_chksum=$(md5sum $tf | awk '{print $1}')

	# Resync EC parity
	$LFS mirror resync $tf || error "resync failed"

	# Verify data unchanged after resync
	local new_chksum=$(md5sum $tf | awk '{print $1}')
	[[ "$old_chksum" == "$new_chksum" ]] ||
		error "data changed after resync: $old_chksum != $new_chksum"

	# Read data mirror and verify
	$LFS mirror read --mirror-id 1 -o $tf_data $tf ||
		error "mirror read data failed"
	echo "$old_chksum  $tf_data" | md5sum -c - ||
		error "data mirror content mismatch"

	# Save parity checksum before overwrite
	local parity_sum_before=$($LFS mirror read -N2 $tf | md5sum)

	# Overwrite part of the file (middle 4M of 20M)
	dd if=/dev/urandom of=$tf bs=1M count=4 seek=8 conv=notrunc ||
		error "overwrite failed"

	# After overwrite, parity should be stale
	verify_flr_state $tf "wp"

	# Resync after overwrite - parity must be recomputed
	$LFS mirror resync $tf || error "resync after overwrite failed"
	verify_flr_state $tf "ro"

	# Verify parity was recomputed (data changed so parity must differ)
	local parity_sum_after=$($LFS mirror read -N2 $tf | md5sum)
	[[ "$parity_sum_before" != "$parity_sum_after" ]] ||
		error "parity unchanged after data overwrite+resync"

	# Verify data mirror has correct content after overwrite+resync
	local final_chksum=$(md5sum $tf | awk '{print $1}')
	rm -f $tf_data
	$LFS mirror read --mirror-id 1 -o $tf_data $tf ||
		error "mirror read data after overwrite failed"
	echo "$final_chksum  $tf_data" | md5sum -c - ||
		error "data mirror content mismatch after overwrite"
}
run_test 26a "multi-stripe EC data integrity with 20M file"

test_26b() {
	enable_ec

	local tf=$DIR/$tdir/$tfile
	local tf_data=$DIR/$tdir/${tfile}.data
	local tf_ec=$DIR/$tdir/${tfile}.ec

	test_mkdir $DIR/$tdir
	stack_trap "rm -rf $DIR/$tdir"

	# Create EC file with 2 data stripes and 2 parity stripes
	# Use 4M stripe size for larger stripe coverage
	$LFS setstripe -E -1 -S 4M -c 2 --ec 2+2 $tf ||
		error "setstripe failed"

	# Write a pattern: alternating blocks of \xAA and \x55
	tr "\000" "\252" < /dev/zero | dd of=$tf bs=1M count=8 \
		iflag=fullblock 2>/dev/null
	tr "\000" "\125" < /dev/zero | dd of=$tf bs=1M count=8 seek=8 \
		iflag=fullblock 2>/dev/null

	local old_chksum=$(md5sum $tf | awk '{print $1}')

	# Resync
	$LFS mirror resync $tf || error "resync failed"

	# Verify data
	local new_chksum=$(md5sum $tf | awk '{print $1}')
	[[ "$old_chksum" == "$new_chksum" ]] ||
		error "data changed after resync: $old_chksum != $new_chksum"

	# Read back and verify both mirrors
	$LFS mirror read --mirror-id 1 -o $tf_data $tf ||
		error "mirror read data failed"
	echo "$old_chksum  $tf_data" | md5sum -c - ||
		error "data mirror content mismatch"

	# Read EC parity mirror
	$LFS mirror read --mirror-id 2 -o $tf_ec $tf ||
		error "mirror read ec failed"
	[[ -s $tf_ec ]] || error "EC mirror is empty"

	# For multi-stripe EC (2+2), parity is XOR across data stripes,
	# so parity content must differ from data content.
	local data_sum=$(md5sum < $tf_data)
	local ec_sum=$(md5sum < $tf_ec)
	[[ "$data_sum" != "$ec_sum" ]] ||
		error "parity mirror identical to data mirror - not real parity"

	# Overwrite first half with different pattern, resync,
	# and verify parity changes
	local ec_sum_before=$ec_sum
	tr "\000" "\377" < /dev/zero | dd of=$tf bs=1M count=8 \
		iflag=fullblock conv=notrunc 2>/dev/null
	$LFS mirror resync $tf || error "resync after overwrite failed"
	rm -f $tf_ec
	$LFS mirror read --mirror-id 2 -o $tf_ec $tf ||
		error "mirror read ec after overwrite failed"
	local ec_sum_after=$(md5sum < $tf_ec)
	[[ "$ec_sum_before" != "$ec_sum_after" ]] ||
		error "parity unchanged after data modification"
}
run_test 26b "EC data integrity with patterned data and 4M stripes"

# Test 27: PFL lazy instantiation with EC
test_27a() {
	(( OSTCOUNT >= 5 )) || skip "needs >= 5 OSTs"

	enable_ec

	local tf=$DIR/$tdir/$tfile
	local ids
	local flags

	test_mkdir $DIR/$tdir

	# Create EC file with PFL: [0, 1M], [1M, 8M], [8M, EOF]
	# Use 4+1 so parity count stays valid for all component stripe counts
	$LFS setstripe -E 1M -c 4 -E 8M -c 4 -E -1 -c 4 --ec 4+1 $tf ||
		error "setstripe failed"

	verify_mirror_count $tf 2
	verify_comp_count $tf 6

	# Component layout:
	# ids[0]: data [0, 1M]    ids[3]: parity [0, 1M]
	# ids[1]: data [1M, 8M]   ids[4]: parity [1M, 8M]
	# ids[2]: data [8M, EOF]  ids[5]: parity [8M, EOF]

	# Write only to the first component (< 1M)
	dd if=/dev/urandom of=$tf bs=512K count=1 ||
		error "write to first component failed"

	# Get component IDs
	ids=($($LFS getstripe $tf |
		awk '/lcme_id/{print $2}' | tr '\n' ' '))

	# First data component should be init (instantiated by write)
	$LFS getstripe -I${ids[0]} $tf | grep -q "init" ||
		error "first data component should be init"

	# Second and third parity components should NOT be init yet
	# (no data written to those extents, so parity not needed)
	flags=$($LFS getstripe -I${ids[4]} $tf |
		awk '/lcme_flags:/ { print $2 }')
	[[ ! "$flags" =~ "init" ]] ||
		error "second parity component should not be init yet"
	flags=$($LFS getstripe -I${ids[5]} $tf |
		awk '/lcme_flags:/ { print $2 }')
	[[ ! "$flags" =~ "init" ]] ||
		error "third parity component should not be init yet"

	# Resync first component's parity
	$LFS mirror resync $tf || error "resync failed"
	verify_flr_state $tf "ro"

	# First parity component should now be init after resync
	flags=$($LFS getstripe -I${ids[3]} $tf |
		awk '/lcme_flags:/ { print $2 }')
	[[ "$flags" =~ "init" ]] ||
		error "first parity component should be init after resync"

	# Save parity checksum after first resync
	local parity_sum1=$($LFS mirror read -N2 $tf | md5sum)

	# Now write to the second component extent (at 2M)
	dd if=/dev/urandom of=$tf bs=1M count=1 seek=2 conv=notrunc ||
		error "write to second component failed"
	verify_flr_state $tf "wp"

	# Resync
	$LFS mirror resync $tf || error "resync after second write failed"
	verify_flr_state $tf "ro"

	# Parity should have changed (new data in second component)
	local parity_sum2=$($LFS mirror read -N2 $tf | md5sum)
	[[ "$parity_sum1" != "$parity_sum2" ]] ||
		error "parity unchanged after writing to second component"

	# Write to the third component extent (at 10M)
	dd if=/dev/urandom of=$tf bs=1M count=1 seek=10 conv=notrunc ||
		error "write to third component failed"
	verify_flr_state $tf "wp"

	# Resync everything
	$LFS mirror resync $tf || error "final resync failed"
	verify_flr_state $tf "ro"

	# Parity should have changed again
	local parity_sum3=$($LFS mirror read -N2 $tf | md5sum)
	[[ "$parity_sum2" != "$parity_sum3" ]] ||
		error "parity unchanged after writing to third component"

	# Verify data can be read back correctly
	local file_sum=$(md5sum < $tf)
	local data_sum=$($LFS mirror read -N1 $tf | md5sum)
	[[ "$file_sum" == "$data_sum" ]] ||
		error "data mirror content mismatch"
}
run_test 27a "PFL lazy instantiation with EC across multiple components"

test_27b() {
	(( OSTCOUNT >= 6 )) || skip "needs >= 6 OSTs"

	enable_ec

	local tf=$DIR/$tfile

	# Create a a stripeset using all available OSTs and
	# multiple raidsets.
	$LFS setstripe -E -1 -S 1M -c ${OSTCOUNT} --ec 4+2 $tf ||
		error "setstripe failed when using ALL OSTs"
}
run_test 27b "test we can create -c n --ec 4+2 with n OSTs"

test_27c() {
	(( OSTCOUNT >= 6 )) || skip "needs >= 6 OSTs"

	enable_ec

	local tf=$DIR/$tfile
	local aoet=$((OSTCOUNT + 2))
	local sc

	# Without overstriping, -c n+2 is reduced to the available OSTs
	# (75% EC floor / geometry shrink) rather than failing create.
	$LFS setstripe -E -1 -S 1M -c ${aoet} --ec 4+2 $tf ||
		error "setstripe -c ${aoet} --ec 4+2 failed"
	sc=$($LFS getstripe $tf | awk '/lmm_stripe_count:/{print $2; exit}')
	(( sc > 0 && sc <= OSTCOUNT )) ||
		error "expected 0 < stripe count <= $OSTCOUNT, got '$sc'"
	rm -f $tf

	# With overstriping, n+2 stripes are allocated as requested
	$LFS setstripe -E -1 -S 1M -C ${aoet} --ec 4+2 $tf ||
		error "setstripe -C ${aoet} --ec 4+2 failed"
	sc=$($LFS getstripe $tf | awk '/lmm_stripe_count:/{print $2; exit}')
	(( sc == aoet )) ||
		error "expected stripe count $aoet with -C, got '$sc'"
}
run_test 27c "test creating with n+2 OSTs"

#
# Get a range of OSTs from a component.
# NOTE: Tail numbering starts at 1, not 0
#
get_osts_by_comp() {
	local filename="$1"
	local component="$2"
	local stripe_idx="$3"
	local stripe_cnt="$4"

	$LFS getstripe --component-id=$component "$filename" |
	    grep "l_ost_idx" |
	    tail -n +$((stripe_idx + 1)) | head -n +$stripe_cnt |
	    sed -e "s/^.*l_ost_idx: *//" -e "s/\,.*$//"
}

#
# Verifies the allocation of a raidset that OSTS are not reused
#
verify_raidset_allocation() {
	local filename="$1"
	local raidset="$2"
	local dstripe_idx="$3"
	local dstripe_cnt="$4"
	local cstripe_idx="$5"
	local cstripe_cnt="$6"

	local dosts=$(get_osts_by_comp "$filename" 65537 \
		$dstripe_idx $dstripe_cnt)
	local costs=$(get_osts_by_comp "$filename" 131074 \
		$cstripe_idx $cstripe_cnt)
	declare -A all_osts
	for ost in $dosts $costs; do
		all_osts[$ost]=$ost
	done
	local nosts=${#all_osts[*]}

	if (( nosts != cstripe_cnt + dstripe_cnt )); then
		echo "Raidset # $raidset"
		echo "Current allocation:"
		$LFS getstripe "$filename"
		echo "Data stripes:" $dosts
		echo "Parity stripes:" $costs
		return 1
	fi

	return 0
}

test_27d() {
	(( OSTCOUNT >= 6 )) || skip "needs >= 6 OSTs"
	enable_ec

	local tf=$DIR/$tfile

	# Test -C 8 --ec 5+2
	# Will result in 2x4+2 raidsets
	$LFS setstripe -E -1 -S 1M -C 8 --ec 4+2 $tf ||
		error "setstripe failed to create test file"

	# First raidset are data stripes 0-3 and parity stripes 0-1
	verify_raidset_allocation $tf 0 0 4 0 2 ||
	    error "Failed. OST reused within a single raidset"

	# Second raidset are data stripes 4-7 and parity stripes 2-3
	verify_raidset_allocation $tf 1 4 4 2 2 ||
	    error "Failed. OST reused within a single raidset"
}
run_test 27d "test raidset OSTs allocations are valid for -C 8 --ec 4+2"

test_27e() {
	(( OSTCOUNT >= 7 )) || skip "needs >= 7 OSTs"
	enable_ec

	local tf=$DIR/$tfile

	# Test -C 13 --ec 5+2
	# Will result in 1x5+2 2x4+2 raidsets
	$LFS setstripe -E -1 -S 1M -C 13 --ec 5+2 $tf ||
		error "setstripe failed to create test file"

	# First raidset are data stripes 0-4 and parity stripes 0-1
	verify_raidset_allocation $tf 0 0 5 0 2 ||
	    error "Failed. OST reused within a single raidset"

	# Second raidset are data stripes 5-8 and parity stripes 2-3
	verify_raidset_allocation $tf 1 5 4 2 2 ||
	    error "Failed. OST reused within a single raidset"

	# Third raidset are data stripes 5-8 and parity stripes 2-3
	verify_raidset_allocation $tf 2 9 4 4 2 ||
	    error "Failed. OST reused within a single raidset"
}
run_test 27e "test raidset OSTs allocations are valid for -C 13 --ec 5+2"

test_27f() {
	(( OSTCOUNT >= 7 )) || skip "needs >= 7 OSTs"
	enable_ec

	local tf=$DIR/$tfile

	# Test -C 20 --ec 5+2
	# Will result in 4x5+2 raidsets
	$LFS setstripe -E -1 -S 1M -C 20 --ec 5+2 $tf ||
		error "setstripe failed to create test file"

	# First raidset are data stripes 0-4 and parity stripes 0-1
	verify_raidset_allocation $tf 0 0 5 0 2 ||
	    error "Failed. OST reused within a single raidset"

	# Second raidset are data stripes 5-9 and parity stripes 2-3
	verify_raidset_allocation $tf 1 5 5 2 2 ||
	    error "Failed. OST reused within a single raidset"

	# Third raidset are data stripes 10-14 and parity stripes 4-6
	verify_raidset_allocation $tf 2 10 5 4 2 ||
	    error "Failed. OST reused within a single raidset"

	# Fourth raidset are data stripes 15-19 and parity stripes 7-8
	verify_raidset_allocation $tf 3 15 5 6 2 ||
	    error "Failed. OST reused within a single raidset"
}
run_test 27f "test raidset OSTs allocations are valid for -C 20 --ec 5+2"

test_27g() {
	(( OSTCOUNT >= 4 )) || skip "needs >= 4 OSTs"
	enable_ec

	local tf=$DIR/$tfile

	# Create a single raidset EC file with data mirror + parity mirror
	local aoet=$(($OSTCOUNT - 2))
	$LFS setstripe -E -1 -S 1M -c ${aoet} --ec ${aoet}+2 $tf ||
		error "setstripe --ec ${aoet}+2 failed"
}
run_test 27g "test we can create -c (n-2) --ec (n-2)+2 with n OSTs"

test_27h() {
	(( OSTCOUNT >= 5 )) || skip "needs >= 5 OSTs"
	enable_ec

	local tf=$DIR/$tfile

	# Create a single raidset EC file with data mirror + parity mirror
	local aoet=$(($OSTCOUNT - 2))
	$LFS setstripe -E -1 -S 1M -c ${aoet} --ec ${aoet}+3 $tf &&
		error "setstripe --ec ${aoet}+3 failed" || true
}
run_test 27h "test stripe creation fails when parity needs one stripe too many"

# Test 28: file operations (hardlink, rename, symlink) on EC files
test_28a() {
	enable_ec

	local tf=$DIR/$tdir/$tfile
	local tf_link=$DIR/$tdir/${tfile}.link

	test_mkdir $DIR/$tdir
	stack_trap "rm -rf $DIR/$tdir"

	# Create EC file with data and sync parity
	$LFS setstripe -E -1 -c 4 --ec 4+2 $tf ||
		error "setstripe failed"
	dd if=/dev/urandom of=$tf bs=1M count=2 ||
		error "write failed"
	$LFS mirror resync $tf || error "resync failed"
	verify_flr_state $tf "ro"

	local old_chksum=$(md5sum $tf | awk '{print $1}')
	local parity_sum_before=$($LFS mirror read -N2 $tf | md5sum)

	# Create hardlink
	ln $tf $tf_link || error "hardlink failed"

	# Verify EC layout accessible through hardlink
	verify_mirror_count $tf_link 2
	$LFS getstripe $tf_link | grep -q "parity" ||
		error "parity not visible through hardlink"

	# Write through the hardlink - should make parity stale
	dd if=/dev/urandom of=$tf_link bs=1M count=1 conv=notrunc ||
		error "write through hardlink failed"
	verify_flr_state $tf "wp"

	# Verify parity is stale when checked through original name
	local ids=($($LFS getstripe $tf |
		awk '/lcme_id/{print $2}' | tr '\n' ' '))
	verify_comp_stale $tf ${ids[1]}

	# Resync through original name
	$LFS mirror resync $tf || error "resync through original name failed"
	verify_flr_state $tf "ro"

	# Verify parity was recomputed (data changed via hardlink write)
	local parity_sum_after=$($LFS mirror read -N2 $tf | md5sum)
	[[ "$parity_sum_before" != "$parity_sum_after" ]] ||
		error "parity unchanged after write-through-hardlink+resync"

	# Verify data readable through both names matches
	local sum_orig=$(md5sum < $tf)
	local sum_link=$(md5sum < $tf_link)
	[[ "$sum_orig" == "$sum_link" ]] ||
		error "data differs between original and hardlink after resync"
}
run_test 28a "EC parity correctly tracks writes through hardlinks"

#
# Failure injection / degraded mode tests (test_29x - test_33x)
#
# These tests exercise EC behavior under OST failures, degraded OSTs,
# and injected I/O errors.
#

drop_client_cache() {
	echo 3 > /proc/sys/vm/drop_caches
}

test_29a() {
	# With 2+1 EC, losing 2 OSTs exceeds parity tolerance.
	# Read must fail (not return corrupt data). This test should
	# pass today -- we expect an error, and we get one.
	(( OSTCOUNT >= 4 )) || skip "need >= 4 OSTs"
	enable_ec

	local tf=$DIR/$tfile

	stack_trap "rm -f $tf" EXIT

	# 2+1 EC: can only tolerate 1 failure
	$LFS setstripe -E -1 -S 1M -c 2 --ec 2+1 $tf ||
		error "setstripe failed"

	dd if=/dev/urandom of=$tf bs=1M count=4 || error "write failed"
	$LFS mirror resync $tf || error "resync failed"
	verify_flr_state $tf "ro"

	# Identify two distinct OSTs used by the data stripe
	local ost_indices=($($LFS getstripe $tf |
		awk '/l_ost_idx:/ {print $5}' | head -2 | tr -d ','))

	local facet1=ost$((ost_indices[0] + 1))
	local facet2=ost$((ost_indices[1] + 1))

	echo "Stopping $facet1 and $facet2 (exceeds 2+1 parity tolerance)"
	stop $facet1
	stop $facet2
	wait_osc_import_state client $facet1 "\(DISCONN\|IDLE\)"
	wait_osc_import_state client $facet2 "\(DISCONN\|IDLE\)"
	drop_client_cache

	# Read must fail -- returning success with wrong data would be
	# catastrophic. Either an I/O error or timeout is acceptable.
	if md5sum $tf 2>/dev/null; then
		error "read should have failed with 2 OSTs down on 2+1 EC"
	else
		echo "Read correctly failed when losses exceed parity tolerance"
	fi

	start $facet1 $(ostdevname $((ost_indices[0] + 1))) $OST_MOUNT_OPTS
	start $facet2 $(ostdevname $((ost_indices[1] + 1))) $OST_MOUNT_OPTS
	wait_recovery_complete $facet1
	wait_recovery_complete $facet2
}
run_test 29a "EC read fails when OST losses exceed parity tolerance"

test_30a() {
	# Test that writes still succeed when an OST used by the parity
	# mirror is down. The data mirror write should succeed, and the
	# parity mirror should be marked stale.
	(( OSTCOUNT >= 4 )) || skip "need >= 4 OSTs"
	enable_ec

	local tf=$DIR/$tfile

	stack_trap "rm -f $tf" EXIT

	$LFS setstripe -E -1 -S 1M -c 2 --ec 2+1 $tf ||
		error "setstripe failed"

	dd if=/dev/urandom of=$tf bs=1M count=2 || error "initial write failed"
	$LFS mirror resync $tf || error "resync failed"
	verify_flr_state $tf "ro"

	# Write new data - parity becomes stale, no OST failure needed
	dd if=/dev/urandom of=$tf bs=1M count=2 conv=notrunc ||
		error "overwrite failed"
	verify_flr_state $tf "wp"

	# Verify parity mirror is stale
	local ids=($($LFS getstripe $tf |
		awk '/lcme_id/{print $2}' | tr '\n' ' '))
	verify_comp_stale $tf ${ids[1]}

	# Resync should recover parity
	$LFS mirror resync $tf || error "resync after write failed"
	verify_flr_state $tf "ro"

	# Verify data integrity through the whole cycle
	local cksum=$(md5sum $tf | awk '{print $1}')
	[[ -n "$cksum" ]] || error "cannot read file after resync"
}
run_test 30a "write makes parity stale, resync recovers"

test_30b() {
	# Test resync after parity OST recovery. Write data (parity
	# becomes stale), cycle the parity OST down and back up, then
	# verify resync succeeds and data is intact.
	# Note: we do NOT attempt resync while the OST is down because
	# designated writes to a downed OST enter uninterruptible kernel
	# sleep and cannot be timed out.
	(( OSTCOUNT >= 4 )) || skip "need >= 4 OSTs"
	enable_ec

	local tf=$DIR/$tfile

	stack_trap "rm -f $tf" EXIT

	$LFS setstripe -E -1 -S 1M -c 2 --ec 2+1 $tf ||
		error "setstripe failed"

	dd if=/dev/urandom of=$tf bs=1M count=4 || error "write failed"
	local cksum_before=$(md5sum $tf | awk '{print $1}')

	# Parity is stale after the write. Cycle the parity OST.
	local parity_ost_idx=$($LFS getstripe --mirror-id=2 $tf |
		awk '/l_ost_idx:/ {print $5; exit}' | tr -d ',')
	[[ -n "$parity_ost_idx" ]] || error "could not find parity OST"
	local parity_facet=ost$((parity_ost_idx + 1))

	echo "Cycling $parity_facet (parity OST index $parity_ost_idx)"
	stop $parity_facet
	wait_osc_import_state client $parity_facet "\(DISCONN\|IDLE\)"

	# Restart the parity OST
	start $parity_facet $(ostdevname $((parity_ost_idx + 1))) \
		$OST_MOUNT_OPTS
	wait_recovery_complete $parity_facet

	# Resync should succeed now that the OST is back
	$LFS mirror resync $tf || error "resync failed after OST recovery"
	verify_flr_state $tf "ro"

	# Verify data integrity is maintained through the whole sequence
	local cksum_after=$(md5sum $tf | awk '{print $1}')
	[[ "$cksum_before" == "$cksum_after" ]] ||
		error "data corrupted: $cksum_before != $cksum_after"
}
run_test 30b "resync succeeds after parity OST recovery"

test_30c() {
	# Test that resync correctly recomputes parity after an OST
	# that was down comes back. The sequence is:
	# 1. Create EC file, write data, resync parity (all good)
	# 2. Stop a data-mirror OST
	# 3. Start it back
	# 4. Verify data is intact and resync still works
	(( OSTCOUNT >= 4 )) || skip "need >= 4 OSTs"
	enable_ec

	local tf=$DIR/$tfile

	stack_trap "rm -f $tf" EXIT

	$LFS setstripe -E -1 -S 1M -c 2 --ec 2+1 $tf ||
		error "setstripe failed"

	dd if=/dev/urandom of=$tf bs=1M count=4 || error "write failed"
	$LFS mirror resync $tf || error "initial resync failed"
	verify_flr_state $tf "ro"

	local cksum_before=$(md5sum $tf | awk '{print $1}')

	# Stop a data OST and bring it back
	local data_ost_idx=$($LFS getstripe --mirror-id=1 $tf |
		awk '/l_ost_idx:/ {print $5; exit}' | tr -d ',')
	local data_facet=ost$((data_ost_idx + 1))

	echo "Cycling $data_facet (data OST index $data_ost_idx)"
	stop $data_facet
	wait_osc_import_state client $data_facet "\(DISCONN\|IDLE\)"

	start $data_facet $(ostdevname $((data_ost_idx + 1))) $OST_MOUNT_OPTS
	wait_recovery_complete $data_facet

	# Data should still be readable and correct
	drop_client_cache
	local cksum_after=$(md5sum $tf | awk '{print $1}')
	[[ "$cksum_before" == "$cksum_after" ]] ||
		error "data corrupted after OST cycle: $cksum_before != $cksum_after"

	# Write new data and resync - everything should still work
	dd if=/dev/urandom of=$tf bs=1M count=2 conv=notrunc ||
		error "write after OST recovery failed"
	$LFS mirror resync $tf || error "resync after OST recovery failed"
	verify_flr_state $tf "ro"
}
run_test 30c "data survives OST restart cycle, resync works after recovery"

test_30d() {
	# test that reading from ec mirror reads the full set of parities
	(( OSTCOUNT >= 4 )) || skip "needs >= 4 OSTs"

	enable_ec

	local tf=$DIR/$tfile

	# Create a simple EC file with data mirror + parity mirror
	$LFS setstripe -E -1 -S 64k -c 4 --ec 2+2 $tf ||
		error "setstripe --ec 2+2 failed"

	# Write some data to the file at block 40
	tr "\000" "\002" < /dev/zero | dd bs=64k count=1 seek=40  \
		iflag=fullblock of=$tf 2>/dev/null

	rm -f $TMP/$tfile.parity
	stack_trap "rm -f $TMP/$tfile.parity"

	# stripe size 64k, 4 data stripes, ec 2+2
	# With EC_MIN_SPLIT_SIZE=4, 4 data stripes split into 2 raid
	# sets of 2, giving 4 parity stripes per row.
	# seek=40: data reaches row 10, so 11 rows of parity.
	# parity mirror size = 11 * 4 * 64k = 2883584
	$LFS mirror read --mirror-id 2 -o $TMP/$tfile.parity $tf
	stat $TMP/$tfile.parity | grep "Size: 2883584" ||
	    error "Wrong size of parity mirror"
}
run_test 30d "test that size of parity mirror is 11*4*64k"

test_31a() {
	# Inject OBD_FAIL_OST_BRW_WRITE_BULK on a parity OST during
	# resync. The data mirror must remain intact regardless of
	# whether the resync succeeds or fails.
	(( OSTCOUNT >= 4 )) || skip "need >= 4 OSTs"
	enable_ec

	local tf=$DIR/$tfile

	stack_trap "rm -f $tf" EXIT

	$LFS setstripe -E -1 -S 1M -c 2 --ec 2+1 $tf ||
		error "setstripe failed"

	dd if=/dev/urandom of=$tf bs=1M count=4 || error "write failed"
	local cksum_before=$(md5sum $tf | awk '{print $1}')

	# Parity is stale. Now inject write failure on a parity OST.
	local parity_ost_idx=$($LFS getstripe --mirror-id=2 $tf |
		awk '/l_ost_idx:/ {print $5; exit}' | tr -d ',')
	[[ -n "$parity_ost_idx" ]] || error "could not find parity OST"
	local parity_facet=ost$((parity_ost_idx + 1))

	#define OBD_FAIL_OST_BRW_WRITE_BULK  0x20e
	echo "Injecting OBD_FAIL_OST_BRW_WRITE_BULK on $parity_facet"
	do_facet $parity_facet $LCTL set_param fail_loc=0x8000020e

	echo "Attempting resync with write failure injected..."
	$LFS mirror resync $tf 2>/dev/null
	local rc=$?

	# Clear the fail_loc
	do_facet $parity_facet $LCTL set_param fail_loc=0

	# Data mirror must be untouched regardless of resync outcome
	local cksum_after=$(md5sum $tf | awk '{print $1}')
	[[ "$cksum_before" == "$cksum_after" ]] ||
		error "data corrupted by failed resync: $cksum_before != $cksum_after"

	if (( rc != 0 )); then
		echo "Resync correctly failed with write error on parity OST"
	else
		echo "Resync appeared to succeed despite injected write failure"
	fi

	# The parity may now be corrupt (partial write). Write new data
	# to make parity stale again, then resync cleanly.
	dd if=/dev/urandom of=$tf bs=1M count=1 conv=notrunc ||
		error "write to re-stale parity failed"
	$LFS mirror resync $tf ||
		error "resync failed after re-staling parity"
	verify_flr_state $tf "ro"
}
run_test 31a "data mirror intact after write failure on parity OST during resync"

test_31b() {
	# test that reading from ec mirror reads the full set of parities
	(( OSTCOUNT >= 6 )) || skip "needs >= 6 OSTs"

	enable_ec

	local tf=$DIR/$tfile

	# Create a simple EC file with data mirror + parity mirror
	$LFS setstripe -E -1 -S 1M -c 4 --ec 4+2 $tf ||
		error "setstripe --ec 4+2 failed"

	# Write some data to the file
	echo "Hello" > $tf || error "error writing to file"

	rm -f $TMP/$tfile.parity
	stack_trap "rm -f $TMP/$tfile.parity"

	# stripe size 1M
	# 4 data stripes, ec 4+2
	# 1 raid set with 2 parities
	# parity mirror size should be 1 * 2 * 1M
	$LFS mirror read --mirror-id 2 -o $TMP/$tfile.parity $tf
	stat $TMP/$tfile.parity | grep "Size: 2097152" ||
	    error "Wrong size of parity mirror"
}
run_test 31b "test that size of parity mirror is 1*2*1M"

test_31c() {
	# test that reading from ec mirror reads the full set of parities
	(( OSTCOUNT >= 6 )) || skip "needs >= 6 OSTs"

	enable_ec

	local tf=$DIR/$tfile

	# Create a simple EC file with data mirror + parity mirror
	$LFS setstripe -E -1 -S 64k -c 4 --ec 4+2 $tf ||
		error "setstripe --ec 4+2 failed"

	# Write some data to the file
	echo "Hello" > $tf || error "error writing to file"

	rm -f $TMP/$tfile.parity
	stack_trap "rm -f $TMP/$tfile.parity"

	# stripe size 64k
	# 4 data stripes, ec 4+2
	# 1 raid set with 2 parities
	# parity mirror size should be 1 * 2 * 64k
	$LFS mirror read --mirror-id 2 -o $TMP/$tfile.parity $tf
	stat $TMP/$tfile.parity | grep "Size: 131072" ||
	    error "Wrong size of parity mirror"
}
run_test 31c "test that size of parity mirror is 1*2*64k"

test_31d() {
	# test that reading from ec mirror reads the full set of parities
	(( OSTCOUNT >= 4 )) || skip "needs >= 4 OSTs"

	enable_ec

	local tf=$DIR/$tfile

	# Create a simple EC file with data mirror + parity mirror
	$LFS setstripe -E -1 -S 64k -c 4 --ec 2+2 $tf ||
		error "setstripe --ec 2+2 failed"

	# Write some data to the file
	echo "Hello" > $tf || error "error writing to file"

	rm -f $TMP/$tfile.parity
	stack_trap "rm -f $TMP/$tfile.parity"

	# stripe size 64k
	# 4 data stripes, ec 2+2
	# With EC_MIN_SPLIT_SIZE=4, split into 2 raid sets of 2,
	# each with 2 parities = 4 parity stripes
	# parity mirror size should be 2 * 2 * 64k
	$LFS mirror read --mirror-id 2 -o $TMP/$tfile.parity $tf
	stat $TMP/$tfile.parity | grep "Size: 262144" ||
	    error "Wrong size of parity mirror"
}
run_test 31d "test that size of parity mirror is 2*2*64k"

test_31e() {
	(( OSTCOUNT >= 6 )) || skip_env "needs >= 6 OSTs"

	enable_ec

	local tf=$DIR/$tfile
	local stripe_size=$((128 * 1024))

	rm -f $TMP/$tfile.parity
	stack_trap "rm -f $TMP/$tfile.parity"

	# Data and parity mirrors each have two components:
	# data [0, 1M) and [1M, EOF), both EC(4+2), 4 data stripes, 128k stripes
	$LFS setstripe -E 1M -S $stripe_size -c 4 -E -1 -S $stripe_size \
		-c 4 --ec 4+2 $tf ||
		error "setstripe --ec 4+2 with PFL failed"

	verify_mirror_count $tf 2
	verify_comp_count $tf 4

	# Write 1.5M so data spans both components (1M + 512k).
	dd if=/dev/urandom of=$tf bs=512K count=3 2>/dev/null ||
		error "failed to write 1.5M"
	(( $(stat -c %s $tf) == $((3 * 512 * 1024)) )) ||
		error "unexpected file size"

	$LFS mirror resync $tf || error "mirror resync failed"

	# Parity lsme_extent matches data lsme_extent (same file offsets, not
	# packed parity bytes).
	# ec_raidset_size = 2 parities * stripe_size = 262144.
	# Parity comp 1 (ext [1M, EOF), data 512k):
	# comp_eof = 1M + 262144 = 1310720
	local ec_raidset_size=$((2 * stripe_size))
	local expected=$((1024 * 1024 + ec_raidset_size))
	$LFS mirror read --mirror-id 2 -o $TMP/$tfile.parity $tf ||
		error "mirror read from parity failed"
	stat $TMP/$tfile.parity | grep "Size: $expected" ||
		error "Wrong size of parity mirror (expected $expected)"
}
run_test 31e "test parity mirror size with multiple PFL components"

test_31f() {
	(( OSTCOUNT >= 8 )) || skip_env "needs >= 8 OSTs"
	verify_yaml_available || skip_env "YAML verification not installed"
	enable_ec

	local tf=$DIR/$tfile

	# 7 data stripes, EC 3+2 -> uneven raidsets (k=3,2,2)
	$LFS setstripe -E -1 -c 7 --ec 3+2 $tf || error "setstripe failed"

	local parity_mirror_id=$($LFS getstripe $tf |
		awk '/lcme_mirror_id:/ { id=$2 }
		     /lcme_flags.*parity/ { print id; exit }')
	[[ -n "$parity_mirror_id" ]] || error "could not find parity mirror"

	local parity_comp_id=$($LFS getstripe $tf |
		awk -v mid="$parity_mirror_id" '
		/lcme_id:/ { id=$2 }
		/lcme_mirror_id:/ { if ($2 == mid) { print id; exit } }')

	verify_comp_parity $tf $parity_comp_id

	local data_comp_id=$($LFS getstripe $tf |
		awk '/lcme_id:/ {id=$2}
		     /lcme_flags:/ {
			if ($0 !~ /parity/) { print id; exit }
		     }')
	[[ -n "$data_comp_id" ]] || error "could not find data component"

	# Whole-file --ec-map (no -I): every parity component, with lcme_id
	verify_ec_raidsets $tf "" 3 \
		"lcme_id:.*$parity_comp_id" \
		'lcme_ec:[[:space:]]*3+2' \
		'0: { ec_data_count: 3, data_stripes: "0-2", parity_stripes: "0-1" }' \
		'1: { ec_data_count: 2, data_stripes: "3-4", parity_stripes: "2-3" }' \
		'2: { ec_data_count: 2, data_stripes: "5-6", parity_stripes: "4-5" }'

	# Filtered --ec-map with -I on parity
	verify_ec_raidsets $tf $parity_comp_id 3 \
		"lcme_id:.*$parity_comp_id" \
		'lcme_ec:[[:space:]]*3+2' \
		'0: { ec_data_count: 3, data_stripes: "0-2", parity_stripes: "0-1" }' \
		'1: { ec_data_count: 2, data_stripes: "3-4", parity_stripes: "2-3" }' \
		'2: { ec_data_count: 2, data_stripes: "5-6", parity_stripes: "4-5" }'

	# -I on data: reverse-lookup parity, same raidset map
	verify_ec_raidsets $tf $data_comp_id 3 \
		"lcme_id:.*$data_comp_id" \
		'lcme_ec:[[:space:]]*3+2' \
		'0: { ec_data_count: 3, data_stripes: "0-2", parity_stripes: "0-1" }' \
		'1: { ec_data_count: 2, data_stripes: "3-4", parity_stripes: "2-3" }' \
		'2: { ec_data_count: 2, data_stripes: "5-6", parity_stripes: "4-5" }'
}
run_test 31f "test getstripe EC raidset map"

test_31g() {
	(( OSTCOUNT >= 6 )) || skip_env "needs >= 6 OSTs"
	verify_yaml_available || skip_env "YAML verification not installed"
	enable_ec

	local tf=$DIR/$tfile
	local id out

	# Later PFL component is a template: k+p known, raidsets empty
	$LFS setstripe -E 1G -c 4 --ec 2+1 -E -1 -c 4 --ec 4+2 $tf ||
		error "setstripe failed"
	id=$($LFS getstripe $tf |
		awk '/lcme_id:/ { id=$2 }
		     /lcme_flags:/ && /parity/ && !/init/ { print id; exit }')
	[[ -n "$id" ]] || error "no uninstantiated parity component"
	verify_ec_raidsets $tf $id 0 \
		'lcme_ec:[[:space:]]*4+2' \
		'lcme_ec_raidsets: \[\]'

	# Non-EC: no stdout, warning on stderr
	$LFS setstripe -c 1 $tf.plain || error "setstripe plain failed"
	out=$($LFS getstripe --ec-map $tf.plain 2>/dev/null)
	[[ -z "$out" ]] || error "--ec-map on non-EC file: $out"
	$LFS getstripe --ec-map $tf.plain 2>&1 >/dev/null |
		grep -q "no parity component found" ||
		error "--ec-map on non-EC file missing warning"
}
run_test 31g "test getstripe --ec-map empty and uninstantiated"

test_32a() {
	# Verify that after an OST goes down and comes back, a full
	# write+resync cycle produces correct parity. This validates
	# the end-to-end recovery path.
	(( OSTCOUNT >= 4 )) || skip "need >= 4 OSTs"
	enable_ec

	local tf=$DIR/$tdir/$tfile

	test_mkdir $DIR/$tdir
	stack_trap "rm -rf $DIR/$tdir" EXIT

	$LFS setstripe -E -1 -S 1M -c 2 --ec 2+1 $tf ||
		error "setstripe failed"

	# Write known pattern and resync to establish good parity
	dd if=/dev/urandom of=$tf bs=1M count=4 || error "write 1 failed"
	$LFS mirror resync $tf || error "resync 1 failed"
	verify_flr_state $tf "ro"

	# Save parity checksum
	local parity_sum_1=$($LFS mirror read -N2 $tf | md5sum)

	# Stop a data OST, restart it
	local data_ost_idx=$($LFS getstripe --mirror-id=1 $tf |
		awk '/l_ost_idx:/ {print $5; exit}' | tr -d ',')
	local data_facet=ost$((data_ost_idx + 1))

	stop $data_facet
	wait_osc_import_state client $data_facet "\(DISCONN\|IDLE\)"
	start $data_facet $(ostdevname $((data_ost_idx + 1))) $OST_MOUNT_OPTS
	wait_recovery_complete $data_facet

	# Write new data and resync
	dd if=/dev/urandom of=$tf bs=1M count=4 conv=notrunc ||
		error "write 2 after recovery failed"
	$LFS mirror resync $tf || error "resync 2 after recovery failed"
	verify_flr_state $tf "ro"

	# Parity must have changed (new data -> new parity)
	local parity_sum_2=$($LFS mirror read -N2 $tf | md5sum)
	[[ "$parity_sum_1" != "$parity_sum_2" ]] ||
		error "parity unchanged after writing new data post-recovery"

	# Verify data is readable and consistent
	local cksum=$(md5sum $tf | awk '{print $1}')
	local mirror_cksum
	mirror_cksum=$($LFS mirror read --mirror-id 1 $tf | md5sum |
		awk '{print $1}')
	[[ "$cksum" == "$mirror_cksum" ]] ||
		error "data mirror inconsistent after recovery cycle"
}
run_test 32a "full write+resync cycle correct after OST recovery"

test_33a() {
	# Multiple writes and resyncs with an intermittent OST failure
	# in between. This exercises the stale tracking and resync logic
	# across multiple failure/recovery cycles.
	(( OSTCOUNT >= 4 )) || skip "need >= 4 OSTs"
	enable_ec

	local tf=$DIR/$tfile
	local i

	stack_trap "rm -f $tf" EXIT

	$LFS setstripe -E -1 -S 1M -c 2 --ec 2+1 $tf ||
		error "setstripe failed"

	for i in 1 2 3; do
		echo "=== Cycle $i ==="

		dd if=/dev/urandom of=$tf bs=1M count=4 conv=notrunc ||
			error "write $i failed"
		verify_flr_state $tf "wp"

		# Resync parity
		$LFS mirror resync $tf || error "resync $i failed"
		verify_flr_state $tf "ro"

		# Cycle a random data OST
		local ost_idx=$($LFS getstripe --mirror-id=1 $tf |
			awk '/l_ost_idx:/ {print $5; exit}' | tr -d ',')
		local facet=ost$((ost_idx + 1))

		stop $facet
		wait_osc_import_state client $facet "\(DISCONN\|IDLE\)"
		start $facet $(ostdevname $((ost_idx + 1))) $OST_MOUNT_OPTS
		wait_recovery_complete $facet
	done

	# Final data integrity check
	drop_client_cache
	md5sum $tf > /dev/null || error "final read failed"
	echo "Survived $i write+resync+OST-cycle iterations"
}
run_test 33a "EC survives repeated write/resync/OST-failure cycles"

test_34a() {
	# test resyncing a stale ec mirror
	(( OSTCOUNT >= 5 )) || skip_env "needs >= 5 OSTs"

	enable_ec

	test_mkdir $DIR/$tdir

	$LFS setstripe -E -1 -S 4M -c 4 --ec 3+2 $DIR/$tdir/$tfile ||
	    error "setstripe --ec 3+2 failed"

	# create a small file
	echo "hello" > $DIR/$tdir/$tfile
	SIZE1=`stat -c "%s" $DIR/$tdir/$tfile`

	# resync the ec mirror:
	$LFS mirror resync $DIR/$tdir/$tfile ||
	    error "failed to resync ec mirror"
	SIZE2=`stat -c "%s" $DIR/$tdir/$tfile`
	(( SIZE1 == SIZE2 )) ||
		error "mirror resync changed eof: ${SIZE1} vs ${SIZE2}"

	# read from the ec mirror:
	$LFS mirror read -N 2 $DIR/$tdir/$tfile >/dev/null ||
	    error "failed to read ec mirror"
	SIZE3=`stat -c "%s" $DIR/$tdir/$tfile`

	(( SIZE1 == SIZE3 )) ||
		error "mirror read changed eof: ${SIZE1} vs ${SIZE3}"
}
run_test 34a "test that lfs mirror read from ec does not change eof"

test_34b() {
	# test resyncing a stale ec mirror
	(( OSTCOUNT >= 6 )) || skip_env "needs >= 6 OSTs"

	enable_ec
	test_mkdir $DIR/$tdir

	$LFS setstripe -E -1 -S 4M -c 4 --ec 4+2 $DIR/$tdir/$tfile ||
	    error "setstripe --ec 4+2 failed"

	# create a small file
	echo "hello" > $DIR/$tdir/$tfile
	SIZE1=$(stat -c "%s" $DIR/$tdir/$tfile)
	BLOCKS1=$(stat -c "%b" $DIR/$tdir/$tfile)
	APPARENT1=$(stat -c "%B" $DIR/$tdir/$tfile)
	echo "After write: size=$SIZE1 blocks=$BLOCKS1 apparent_blksize=$APPARENT1"

	# resync the ec mirror:
	$LFS mirror resync $DIR/$tdir/$tfile ||
	    error "failed to resync ec mirror"
	SIZE2=$(stat -c "%s" $DIR/$tdir/$tfile)
	BLOCKS2=$(stat -c "%b" $DIR/$tdir/$tfile)
	APPARENT2=$(stat -c "%B" $DIR/$tdir/$tfile)
	echo "After resync: size=$SIZE2 blocks=$BLOCKS2 apparent_blksize=$APPARENT2"
	(( SIZE1 == SIZE2 )) ||
		error "mirror resync changed eof: ${SIZE1} vs ${SIZE2}"

	# verify the ec mirror:
	$LFS mirror verify $DIR/$tdir/$tfile ||
	    error "failed to verify ec mirror"
	SIZE3=$(stat -c "%s" $DIR/$tdir/$tfile)
	BLOCKS3=$(stat -c "%b" $DIR/$tdir/$tfile)
	APPARENT3=$(stat -c "%B" $DIR/$tdir/$tfile)
	echo "After verify: size=$SIZE3 blocks=$BLOCKS3 apparent_blksize=$APPARENT3"

	(( SIZE1 == SIZE3 )) ||
		error "mirror verify changed eof: ${SIZE1} vs ${SIZE3}"
}
run_test 34b "test that lfs mirror verify for ec does not change eof"

test_34c() {
	# test that reading past parity data returns zeros, not garbage
	(( OSTCOUNT >= 8 )) || skip_env "needs >= 8 OSTs"

	enable_ec

	test_mkdir $DIR/$tdir

	# Test with data sizes that span at least one full stripe.
	# Reed-Solomon parity is a linear code over GF(2^8), so parity
	# bytes at byte offset i are 0 whenever every data stripe is 0
	# at offset i.  A sub-stripe file has zero input at every offset
	# past the data tail across all data stripes, so its parity is
	# correctly zero there and "expect parity bytes" cannot succeed.
	# - 4M     (exactly one stripe)
	# - 4.5M   (one stripe + partial)
	# - 8.5M   (two stripes + partial)
	local -a test_sizes=($((4 * 1024 * 1024)) \
			     $((4 * 1024 * 1024 + 512 * 1024)) \
			     $((8 * 1024 * 1024 + 512 * 1024)))
	local size
	local i
	local parity_mirror_id

	for size in "${test_sizes[@]}"; do
		echo "Testing with data size: $size bytes"

		rm -f $DIR/$tdir/$tfile
		$LFS setstripe -E -1 -S 4M -c 8 --ec 3+2 \
			$DIR/$tdir/$tfile ||
			error "setstripe --ec 3+2 failed"

		# Write data
		dd if=/dev/urandom of=$DIR/$tdir/$tfile bs=$size \
			count=1 2>/dev/null ||
			error "failed to write $size bytes"

		# Resync to write parity
		$LFS mirror resync $DIR/$tdir/$tfile ||
			error "failed to resync ec mirror"

		# Get parity mirror ID
		parity_mirror_id=$($LFS getstripe $DIR/$tdir/$tfile | \
			grep -B1 "lcme_flags.*parity" | \
			grep "lcme_mirror_id" | awk '{print $2}')

		echo "Parity mirror ID: $parity_mirror_id"

		# Test reads at various offsets
		# For 3+2 EC with 4M stripes, we have 2 parity stripes
		# Each parity stripe should have parity data up to $size,
		# then zeros

		# Test start of first parity stripe (should have data)
		echo "  Checking offset 0 (start of first parity stripe)..."
		check_parity_read $DIR/$tdir/$tfile $parity_mirror_id \
			0 "no"

		# Parity is computed at block granularity, covering
		# only the data range.  Offsets within a parity stripe
		# but past the data range read as zeros.

		# Only check offset 1M if data extends into that range
		if (( size > 1024 * 1024 )); then
			echo "  Checking offset 1M (within first parity stripe)..."
			check_parity_read $DIR/$tdir/$tfile \
				$parity_mirror_id $((1024 * 1024)) "no"
		fi

		# Test start of second parity stripe at 4M: always has
		# data as long as any data was written (each coding stripe
		# gets parity for the same data range)
		echo "  Checking offset 4M (start of second parity stripe)..."
		check_parity_read $DIR/$tdir/$tfile $parity_mirror_id \
			$((4 * 1024 * 1024)) "no"

		# Test past all parity stripes (should be zeros)
		echo "  Checking offset 8M (past all parity)..."
		check_parity_read $DIR/$tdir/$tfile $parity_mirror_id \
			$((8 * 1024 * 1024)) "yes"
	done
}
run_test 34c "test that reading past parity data returns zeros"

test_40a() {
	(( $OSTCOUNT >= 3 )) || skip_env "needs >= 3 OSTs for EC rebuild"

	local tf=$DIR/$tfile

	stack_trap "rm -f $tf"
	enable_ec
	local dstripe=$EC_DSTRIPE
	local cstripe=$EC_CSTRIPE
	# Create a simple EC file with data mirror + parity mirror
	$LFS setstripe -E eof -S 1M -c $dstripe --ec $dstripe+$cstripe $tf ||
		error "setstripe --ec $dstripe+$cstripe failed"

	# Write some data to the file
	dd if=/dev/urandom of=$tf bs=1M count=$dstripe || error "dd failed"

	# Copy data to a temp file
	local tmpfile=$(mktemp)
	stack_trap "rm -f $tmpfile"
	dd if=$tf of=$tmpfile bs=1M count=$dstripe ||
		error "copy to $tmpfile failed"

	# Resync to update parity mirror
	$LFS mirror resync $tf || error "mirror resync failed"

	$LFS find --printf "%Lo\n" $tf

	# collect objects
	local ids=($($LFS getstripe $tf | awk '/lcme_id/{print $2}' |
		tr '\n' ' '))
	local osts=($($LFS getstripe -I${ids[0]} $tf |
		awk '/l_ost_idx:/ { print $5 }' | tr -d "," ))

	# randomly disable 2 OSTs
	local idx1=$((RANDOM % ${#osts[@]}))
	local idx2=$((RANDOM % ${#osts[@]}))
	while (( $idx2 == $idx1 )); do
		idx2=$((RANDOM % ${#osts[@]}))
	done

	cancel_lru_locks osc
	echo "** Disabling OST ${osts[$idx1]}"
	local osc1=$($LCTL dl |
		awk "/OST.*${osts[$idx1]}-osc-[^M]/ {print \$4}")
	$LCTL --device $osc1 deactivate || error "deactivate $osc1 failed"
	stack_trap "$LCTL --device $osc1 activate || true"

	#Verify read still succeeds
	ls -l $tmpfile
	ls -l $tf
	cmp $tmpfile $tf || {
		diff -y --suppress-common-lines <(od -Ax -tx1z $tmpfile) \
			<(od -Ax -tx1z $tf) | head -n 20
		error "data mismatch after OST failure"
	}
	echo "         passed"

	(( cstripe > 1)) || return 0

	cancel_lru_locks osc
	echo "** Disabling OST ${osts[$idx2]}"
	local osc2=$($LCTL dl |
		awk "/OST.*${osts[$idx2]}-osc-[^M]/ {print \$4}")
	$LCTL --device $osc2 deactivate || error "deactivate $osc2 failed"
	stack_trap "$LCTL --device $osc2 activate || true"

	#Verify read still succeeds
	ls -l $tf
	cmp $tmpfile $tf || {
		diff -y --suppress-common-lines <(od -Ax -tx1z $tmpfile) \
			<(od -Ax -tx1z $tf) | head -n 20
		error "data mismatch after OST failure"
	}
	echo "         passed"
}
run_test 40a "test recovery read"

# OBD_FAIL_OST_BRW_READ_BULK is evaluated in tgt_brw_read() on the OSS,
# so fail_loc has to be set on the OST nodes, not the client.
ec_ost_fail_loc() {
	local loc=$1
	local val=${2:-0}

	do_nodes $(osts_nodes) \
		$LCTL set_param -n fail_loc=$loc fail_val=$val > /dev/null
}

# Drop resend_count to 1 so an injected -EIO reaches the LOV layer
# instead of being resent.  0 means "resend forever", so save and
# restore the original rather than assuming a default.
EC_SAVED_RESEND=""
ec_set_resend_count() {
	[[ -n "$EC_SAVED_RESEND" ]] ||
		EC_SAVED_RESEND=$($LCTL get_param -n osc.*.resend_count |
			head -n1)
	$LCTL set_param -n osc.*.resend_count=$1 > /dev/null
}

ec_restore_resend_count() {
	[[ -n "$EC_SAVED_RESEND" ]] || return 0
	$LCTL set_param -n osc.*.resend_count=$EC_SAVED_RESEND > /dev/null
	EC_SAVED_RESEND=""
}

# Simulate OST loss by failing its BRW reads while the OSC stays active,
# so the read really enters CIT_EC_RD reconstruction.  "lctl deactivate"
# would return -EIO before the recovery path and never exercise it.
# In fail_val, 0x10000 means "low bits are an OST index mask".
EC_FAULT_MASK=0
ec_apply_fault() {
	if (( EC_FAULT_MASK == 0 )); then
		ec_ost_fail_loc 0 0
		ec_restore_resend_count
	else
		ec_set_resend_count 1
		ec_ost_fail_loc 0x20f $(( 0x10000 | EC_FAULT_MASK ))
	fi
	# returns non-zero when no unused locks remain; not an error here
	cancel_lru_locks osc || true
}

# Fail an OST by index, pushing the fault-clear onto stack_trap.
ec_deactivate_ost() {
	local ost=$1

	ec_check_fault_index $ost
	EC_FAULT_MASK=$(( EC_FAULT_MASK | (1 << ost) ))
	ec_apply_fault
	stack_trap "EC_FAULT_MASK=\$(( EC_FAULT_MASK & ~(1 << $ost) )); \
		ec_apply_fault"
}

ec_reactivate_ost() {
	local ost=$1

	EC_FAULT_MASK=$(( EC_FAULT_MASK & ~(1 << ost) ))
	ec_apply_fault
}

# Sets data_osts[], parity_osts[] and safe_osts[] in the caller.
# safe_osts = data OSTs not used as parity anywhere.
ec_classify_osts() {
	local tf=$1
	local ids=($($LFS getstripe $tf | awk '/lcme_id/ { print $2 }'))
	local id flags o

	data_osts=()
	parity_osts=()
	safe_osts=()
	for id in "${ids[@]}"; do
		local osts

		flags=$($LFS getstripe -I$id $tf |
			awk '/lcme_flags:/ { print $2 }')
		osts=($($LFS getstripe -I$id $tf |
			awk '/l_ost_idx:/ { print $5 }'))
		if [[ "$flags" == *parity* ]]; then
			parity_osts+=("${osts[@]%,}")
		else
			data_osts+=("${osts[@]%,}")
		fi
	done

	local safe_flag
	for o in "${data_osts[@]}"; do
		local p

		safe_flag=true
		for p in "${parity_osts[@]}"; do
			[[ $o -eq $p ]] && safe_flag=false && break
		done
		$safe_flag && safe_osts+=($o)
	done
	safe_osts=($(printf '%s\n' "${safe_osts[@]}" | sort -un))
}

#
# Inject BRW read failures for EC recovery testing.  Both helpers use
# OBD_FAIL_OST_BRW_READ_BULK (0x20f) and drop resend_count to 1 so an
# injected -EIO reaches the LOV layer instead of being absorbed by the
# OSC.  Each must be paired with its ec_stop_* counterpart (via
# stack_trap or an explicit call).
#
# ec_start_read_fault fails one data OST for as long as the fault is
# armed, which a 4+2 or 2+1 layout can still recover from, so the read
# has to reconstruct from parity.  A one-shot fault (CFS_FAIL_ONCE) does
# not work here: resend_count=1 still permits one OSC resend, and that
# resend lands after the fault has cleared itself, so the read succeeds
# without ever reaching the recovery path.
#
# ec_start_all_reads_fail fails every OST persistently, so the parity
# reads fail too and recovery cannot succeed -- that is what the
# unrecoverable-error tests want.
#
# Use ec_deactivate_ost() when a test needs to name the OSTs itself.
#

# Map data-stripe positions to the OST indices holding them, so a test
# can say "the first and last data stripe" without assuming the
# allocator put the file on OST 0..n.  Index -1 is the last stripe.
# Fails when a wanted stripe shares an OST with a parity object: taking
# that OST out costs two objects rather than one, which would push the
# read past the recovery limit and test something else entirely.
# Usage: ec_data_stripe_osts <file> <stripe>...  -> sets EC_STRIPE_OSTS
EC_STRIPE_OSTS=()
ec_data_stripe_osts() {
	local tf=$1
	local i p

	shift
	ec_classify_osts $tf
	EC_STRIPE_OSTS=()
	for i in "$@"; do
		(( i < 0 )) && i=$(( ${#data_osts[@]} + i ))
		(( i >= 0 && i < ${#data_osts[@]} )) ||
			error "layout has ${#data_osts[@]} data stripes"
		for p in "${parity_osts[@]}"; do
			(( data_osts[i] == p )) &&
				error "data stripe $i shares a parity OST"
		done
		EC_STRIPE_OSTS+=(${data_osts[i]})
	done
}

# Pick the first of the given data stripes whose OST is not also carrying
# parity, and leave it in EC_PICKED_OST.  Stripe order is what matters: a
# file smaller than one raid set holds data on stripe 0 alone, so choosing
# by lowest OST index instead arms the fault on an object the read never
# reaches.  Stripes sharing an OST with a parity object are passed over,
# because failing one costs two objects at once.
# Usage: ec_pick_data_ost <file> [stripe]...   (default: every data stripe)
# Returns non-zero when no candidate is free of parity duty.
EC_PICKED_OST=""
ec_pick_data_ost() {
	local tf=$1
	local stripes=()
	local i p shared

	shift
	ec_classify_osts $tf
	if (( $# > 0 )); then
		stripes=("$@")
	else
		for ((i = 0; i < ${#data_osts[@]}; i++)); do
			stripes+=($i)
		done
	fi

	EC_PICKED_OST=""
	for i in "${stripes[@]}"; do
		(( i >= 0 && i < ${#data_osts[@]} )) || continue
		shared=false
		for p in "${parity_osts[@]}"; do
			(( data_osts[i] == p )) && shared=true && break
		done
		$shared && continue
		EC_PICKED_OST=${data_osts[i]}
		return 0
	done

	return 1
}

# Collect one parity-free OST from every data mirror, so a read cannot be
# served by simply switching to an intact copy.  With several data mirrors
# each holding the whole file, failing a single OST proves nothing: the
# client just reads the other mirror and no reconstruction runs.
# Usage: ec_mirror_victims <file>   -> sets EC_MIRROR_VICTIMS
EC_MIRROR_VICTIMS=()
ec_mirror_victims() {
	local tf=$1
	local comp osts o p shared

	ec_classify_osts $tf
	EC_MIRROR_VICTIMS=()
	for comp in $($LFS getstripe $tf |
		awk '/lcme_id:/ { id = $2 }
		     /lcme_flags:/ { if ($0 !~ /parity/) print id }'); do
		osts=($($LFS getstripe -I$comp $tf |
			awk '/l_ost_idx:/ { print $5 }' | tr -d ","))
		for o in "${osts[@]}"; do
			shared=false
			for p in "${parity_osts[@]}"; do
				(( o == p )) && shared=true && break
			done
			$shared && continue
			EC_MIRROR_VICTIMS+=($o)
			break
		done
	done

	(( ${#EC_MIRROR_VICTIMS[@]} > 0 ))
}

# cfs_fail_index() decodes only bits 0-15 of fail_val in bitmask mode, so a
# mask bit for an OST index >= 16 selects no OST at all: the fault never
# fires and the read succeeds without recovery, which looks exactly like a
# pass.  Fail rather than report success for a fault that was never armed.
ec_check_fault_index() {
	local ost

	for ost in "$@"; do
		(( ost < 16 )) ||
			error "OST index $ost outside fail_val mask (0-15)"
	done
}

# Usage: ec_start_read_fault <file> [ost_index]...
# Defaults to the OST holding the file's first data stripe.  Note this is
# not safe_osts[0]: that array is sorted numerically, so its first element
# is the lowest OST index rather than the OST carrying stripe 0.  A file
# smaller than one raid set has data only on stripe 0, so failing any other
# OST arms a fault the read never reaches.
EC_READ_FAULT_OSTS=()
ec_start_read_fault() {
	local tf=$1
	local mask=0
	local ost

	shift
	if (( $# > 0 )); then
		EC_READ_FAULT_OSTS=("$@")
	else
		ec_pick_data_ost $tf ||
			error "no data OST free of parity duty"
		EC_READ_FAULT_OSTS=($EC_PICKED_OST)
	fi

	ec_check_fault_index "${EC_READ_FAULT_OSTS[@]}"
	for ost in "${EC_READ_FAULT_OSTS[@]}"; do
		mask=$(( mask | (1 << ost) ))
	done

	ec_set_resend_count 1
	ec_ost_fail_loc 0x20f $(( 0x10000 | mask ))
	# cancel_lru_locks forces pages out of the client cache so
	# the next read actually issues a BRW RPC and fires fail_loc.
	# Plain drop_caches is not sufficient: pages from a buffered
	# write remain bulk-pinned for replay until the OST commits
	# the write, and mapping_evict_folio refuses to evict them.
	cancel_lru_locks osc
}

ec_stop_read_fault() {
	EC_READ_FAULT_OSTS=()
	ec_ost_fail_loc 0 0
	ec_restore_resend_count
}

# ec_start_all_reads_fail injects a persistent (non-ONCE) fault so every
# subsequent OST BRW_READ bulk fails on every OST.  EC recovery cannot
# succeed because parity reads also hit the fault.  Use this for tests
# that verify unrecoverable error paths (e.g., EIO propagation when too
# many OSTs fail for the parity count to cover).
ec_start_all_reads_fail() {
	ec_set_resend_count 1
	# 0x20f = OBD_FAIL_OST_BRW_READ_BULK, no CFS_FAIL_ONCE bit.
	# fail_val=0 targets all OSTs.  Fires on every BRW_READ until
	# cleared.
	ec_ost_fail_loc 0x20f 0
	cancel_lru_locks osc
}
ec_stop_all_reads_fail() {
	ec_ost_fail_loc 0 0
	ec_restore_resend_count
}

test_40b() {
	(( OSTCOUNT >= 3 )) || skip_env "needs >= 3 OSTs for 2+1"
	enable_ec

	local tf=$DIR/$tfile

	stack_trap "rm -f $tf"

	$LFS setstripe -E eof -S 1M -c 2 --ec 2+1 $tf ||
		error "setstripe --ec 2+1 failed"

	dd if=/dev/urandom of=$tf bs=1M count=2 || error "dd failed"
	$LFS mirror resync $tf || error "mirror resync failed"

	local sum1=$(md5sum $tf | awk '{print $1}')

	ec_start_read_fault $tf
	stack_trap "ec_stop_read_fault"

	local sum2=$(md5sum $tf | awk '{print $1}')
	[[ "$sum1" == "$sum2" ]] ||
		error "data changed after read fault: $sum1 vs $sum2"
}
run_test 40b "test recovery read with 2+1 EC"

test_40c() {
	(( OSTCOUNT >= 4 )) || skip_env "needs >= 4 OSTs for 2+2"
	enable_ec

	local tf=$DIR/$tfile

	stack_trap "rm -f $tf"

	$LFS setstripe -E eof -S 1M -c 2 --ec 2+2 $tf ||
		error "setstripe --ec 2+2 failed"

	dd if=/dev/urandom of=$tf bs=1M count=2 || error "dd failed"
	$LFS mirror resync $tf || error "mirror resync failed"

	local sum1=$(md5sum $tf | awk '{print $1}')

	ec_start_read_fault $tf
	stack_trap "ec_stop_read_fault"

	local sum2=$(md5sum $tf | awk '{print $1}')
	[[ "$sum1" == "$sum2" ]] ||
		error "data changed after read fault: $sum1 vs $sum2"
}
run_test 40c "test recovery read with 2+2 EC"

test_40d() {
	(( OSTCOUNT >= 5 )) || skip_env "needs >= 5 OSTs for 4+1"
	enable_ec

	local tf=$DIR/$tfile

	stack_trap "rm -f $tf"

	$LFS setstripe -E eof -S 1M -c 4 --ec 4+1 $tf ||
		error "setstripe --ec 4+1 failed"

	dd if=/dev/urandom of=$tf bs=1M count=4 || error "dd failed"
	$LFS mirror resync $tf || error "mirror resync failed"

	local sum1=$(md5sum $tf | awk '{print $1}')

	ec_start_read_fault $tf
	stack_trap "ec_stop_read_fault"

	local sum2=$(md5sum $tf | awk '{print $1}')
	[[ "$sum1" == "$sum2" ]] ||
		error "data changed after read fault: $sum1 vs $sum2"
}
run_test 40d "test recovery read with 4+1 EC"

test_41a() {
	(( OSTCOUNT >= 6 )) || skip_env "needs >= 6 OSTs"
	enable_ec

	local tf=$DIR/$tfile

	stack_trap "rm -f $tf"

	# Create EC file with 4+2 configuration
	$LFS setstripe -E eof -S 1M -c 4 --ec 4+2 $tf ||
		error "setstripe --ec 4+2 failed"

	# Write 8MB of data (2 raid sets)
	dd if=/dev/urandom of=$tf bs=1M count=8 || error "dd failed"

	# Resync to update parity
	$LFS mirror resync $tf || error "mirror resync failed"

	# Get checksums of different portions before failure
	local sum_full=$(md5sum $tf | awk '{print $1}')
	local sum_first_1m=$(dd if=$tf bs=1M count=1 2>/dev/null | md5sum |
		awk '{print $1}')
	local sum_middle_2m=$(dd if=$tf bs=1M skip=3 count=2 2>/dev/null |
		md5sum | awk '{print $1}')
	local sum_last_1m=$(dd if=$tf bs=1M skip=7 count=1 2>/dev/null |
		md5sum | awk '{print $1}')

	ec_start_read_fault $tf
	stack_trap "ec_stop_read_fault"

	# Verify partial reads still work
	local sum_full_after=$(md5sum $tf | awk '{print $1}')
	[[ "$sum_full" == "$sum_full_after" ]] ||
		error "full file checksum changed: $sum_full vs $sum_full_after"

	# The full-file read above left all 8M uptodate in the page cache,
	# so without dropping the locks again each partial read is a cache
	# hit: no BRW RPC, no fault, and nothing to reconstruct.
	cancel_lru_locks osc
	local sum_first_1m_after=$(dd if=$tf bs=1M count=1 2>/dev/null |
		md5sum | awk '{print $1}')
	[[ "$sum_first_1m" == "$sum_first_1m_after" ]] ||
		error "first 1M checksum changed: $sum_first_1m vs" \
			"$sum_first_1m_after"

	cancel_lru_locks osc
	local sum_middle_2m_after=$(dd if=$tf bs=1M skip=3 count=2 \
		2>/dev/null | md5sum | awk '{print $1}')
	[[ "$sum_middle_2m" == "$sum_middle_2m_after" ]] ||
		error "middle 2M checksum changed: $sum_middle_2m vs" \
			"$sum_middle_2m_after"

	cancel_lru_locks osc
	local sum_last_1m_after=$(dd if=$tf bs=1M skip=7 count=1 \
		2>/dev/null | md5sum | awk '{print $1}')
	[[ "$sum_last_1m" == "$sum_last_1m_after" ]] ||
		error "last 1M checksum changed: $sum_last_1m vs" \
			"$sum_last_1m_after"

	echo "** All partial read checksums match after OST failures"
}
run_test 41a "test partial reads with recovery"

test_41b() {
	(( OSTCOUNT >= 6 )) || skip_env "needs >= 6 OSTs"
	enable_ec

	local tf=$DIR/$tfile

	stack_trap "rm -f $tf"

	# Create EC file with 4+2 configuration
	$LFS setstripe -E eof -S 1M -c 4 --ec 4+2 $tf ||
		error "setstripe --ec 4+2 failed"

	# Write 32MB of data (8 raid sets of 4MB each)
	dd if=/dev/urandom of=$tf bs=1M count=32 || error "dd failed"

	# Resync to update parity
	$LFS mirror resync $tf || error "mirror resync failed"

	# Get checksums of reads spanning multiple raid sets
	local sum_full=$(md5sum $tf | awk '{print $1}')
	# Read spanning raid sets 1-2 (0-8MB)
	local sum_span_1_2=$(dd if=$tf bs=1M count=8 2>/dev/null |
		md5sum | awk '{print $1}')
	# Read spanning raid sets 3-5 (8MB-20MB)
	local sum_span_3_5=$(dd if=$tf bs=1M skip=8 count=12 2>/dev/null |
		md5sum | awk '{print $1}')
	# Read spanning raid sets 6-8 (20MB-32MB)
	local sum_span_6_8=$(dd if=$tf bs=1M skip=20 count=12 2>/dev/null |
		md5sum | awk '{print $1}')

	ec_start_read_fault $tf
	stack_trap "ec_stop_read_fault"

	# Verify reads spanning multiple raid sets still work
	local sum_full_after=$(md5sum $tf | awk '{print $1}')
	[[ "$sum_full" == "$sum_full_after" ]] ||
		error "full file checksum changed: $sum_full vs $sum_full_after"

	# Drop the locks between reads: the full-file read above cached the
	# whole 32M, and a cached read issues no BRW RPC and so never hits
	# the injected fault.
	cancel_lru_locks osc
	local sum_span_1_2_after=$(dd if=$tf bs=1M count=8 2>/dev/null |
		md5sum | awk '{print $1}')
	[[ "$sum_span_1_2" == "$sum_span_1_2_after" ]] ||
		error "raid sets 1-2 checksum changed: $sum_span_1_2 vs" \
			"$sum_span_1_2_after"

	cancel_lru_locks osc
	local sum_span_3_5_after=$(dd if=$tf bs=1M skip=8 count=12 \
		2>/dev/null | md5sum | awk '{print $1}')
	[[ "$sum_span_3_5" == "$sum_span_3_5_after" ]] ||
		error "raid sets 3-5 checksum changed: $sum_span_3_5 vs" \
			"$sum_span_3_5_after"

	cancel_lru_locks osc
	local sum_span_6_8_after=$(dd if=$tf bs=1M skip=20 count=12 \
		2>/dev/null | md5sum | awk '{print $1}')
	[[ "$sum_span_6_8" == "$sum_span_6_8_after" ]] ||
		error "raid sets 6-8 checksum changed: $sum_span_6_8 vs" \
			"$sum_span_6_8_after"

	echo "** All multi-raid-set read checksums match after OST failures"
}
run_test 41b "test reads spanning multiple raid sets with recovery"

test_41c() {
	(( OSTCOUNT >= 6 )) || skip_env "needs >= 6 OSTs"
	enable_ec

	local tf=$DIR/$tfile

	stack_trap "rm -f $tf"

	# Create EC file with 4+2 configuration
	$LFS setstripe -E eof -S 1M -c 4 --ec 4+2 $tf ||
		error "setstripe --ec 4+2 failed"

	# Write 8MB of data
	dd if=/dev/urandom of=$tf bs=1M count=8 || error "dd failed"

	# Resync to update parity
	$LFS mirror resync $tf || error "mirror resync failed"

	# Baseline checksums at aligned and unaligned offsets, so the
	# post-failure reads below can be compared against them.

	# Aligned to stripe size (1M) - offset 2M, length 2M
	local sum_aligned=$(dd if=$tf bs=1M skip=2 count=2 2>/dev/null |
		md5sum | awk '{print $1}')
	# Unaligned offset (512K) - offset 512K, length 2M
	local sum_unaligned_512k=$(dd if=$tf bs=512K skip=1 count=4 \
		2>/dev/null | md5sum | awk '{print $1}')
	# Unaligned offset (256K) - offset 1.25M, length 2M
	local sum_unaligned_256k=$(dd if=$tf bs=256K skip=5 count=8 \
		2>/dev/null | md5sum | awk '{print $1}')
	# Small unaligned read (64K offset, 128K size)
	local sum_small_unaligned=$(dd if=$tf bs=64K skip=1 count=2 \
		2>/dev/null | md5sum | awk '{print $1}')

	ec_start_read_fault $tf
	stack_trap "ec_stop_read_fault"

	# Drop caches to force re-read from OSTs (not from cache)
	sync; echo 3 > /proc/sys/vm/drop_caches

	# Verify reads at various offsets still work
	local sum_aligned_after=$(dd if=$tf bs=1M skip=2 count=2 \
		2>/dev/null | md5sum | awk '{print $1}')
	[[ "$sum_aligned" == "$sum_aligned_after" ]] ||
		error "aligned read (offset 2M) checksum changed after" \
			"OST failure: $sum_aligned vs $sum_aligned_after"

	local sum_unaligned_512k_after=$(dd if=$tf bs=512K skip=1 count=4 \
		2>/dev/null | md5sum | awk '{print $1}')
	[[ "$sum_unaligned_512k" == "$sum_unaligned_512k_after" ]] ||
		error "512K unaligned read (offset 512K) checksum" \
			"changed after OST failure: $sum_unaligned_512k vs" \
			"$sum_unaligned_512k_after"

	local sum_unaligned_256k_after=$(dd if=$tf bs=256K skip=5 count=8 \
		2>/dev/null | md5sum | awk '{print $1}')
	[[ "$sum_unaligned_256k" == "$sum_unaligned_256k_after" ]] ||
		error "256K unaligned read (offset 1.25M) checksum" \
			"changed after OST failure: $sum_unaligned_256k vs" \
			"$sum_unaligned_256k_after"

	local sum_small_unaligned_after=$(dd if=$tf bs=64K skip=1 count=2 \
		2>/dev/null | md5sum | awk '{print $1}')
	[[ "$sum_small_unaligned" == "$sum_small_unaligned_after" ]] ||
		error "small unaligned read (offset 64K, size 128K)" \
			"checksum changed after OST failure:" \
			"$sum_small_unaligned vs $sum_small_unaligned_after"

	echo "** aligned and unaligned read checksums match after OST failure"
}
run_test 41c "test reads at different offsets with recovery"

test_41d() {
	(( OSTCOUNT >= 6 )) || skip_env "needs >= 6 OSTs"
	enable_ec

	local tf=$DIR/$tfile

	stack_trap "rm -f $tf"
	local out=$TMP/$tfile.mmap.out
	stack_trap "rm -f $out"

	$LFS setstripe -E eof -S 1M -c 4 --ec 4+2 $tf ||
		error "setstripe --ec 4+2 failed"

	dd if=/dev/urandom of=$tf bs=1M count=8 || error "dd failed"
	$LFS mirror resync $tf || error "mirror resync failed"

	# $MMAP_CAT does mmap(MAP_PRIVATE|MAP_POPULATE) + write.  This
	# forces the page-fault path (CIT_FAULT), unlike md5sum/cat
	# which use read(2).  Baseline first to confirm mmap returns
	# the same bytes as a plain read before any fault injection.
	$MMAP_CAT $tf > $out || error "mmap_cat baseline failed"
	local sum_mmap=$(md5sum $out | awk '{print $1}')
	local sum_read=$(md5sum $tf | awk '{print $1}')
	[[ "$sum_mmap" == "$sum_read" ]] ||
		error "mmap baseline mismatch: $sum_mmap vs $sum_read"

	# Inject a persistent BRW_READ_BULK failure on the OST holding
	# the first data object so every read of that stripe forces EC
	# recovery via parity.  fail_val = 0x10000 | mask, mask bit i =
	# filesystem OST index i, so the index has to come from the
	# layout -- nothing pins the file's objects to OST 0.
	local ids=($($LFS getstripe $tf | awk '/lcme_id/{print $2}'))
	local osts=($($LFS getstripe -I${ids[0]} $tf |
		awk '/l_ost_idx:/ { print $5 }' | tr -d ","))
	local ost=${osts[0]}

	ec_check_fault_index $ost
	ec_set_resend_count 1
	stack_trap "ec_ost_fail_loc 0 0; ec_restore_resend_count"
	#define OBD_FAIL_OST_BRW_READ_BULK 0x20f
	ec_ost_fail_loc 0x20f $(( 0x10000 | (1 << ost) ))
	cancel_lru_locks osc
	sync; echo 3 > /proc/sys/vm/drop_caches

	# CIT_FAULT must route through EC recovery, otherwise the
	# page-fault read returns garbage or kills mmap_cat with
	# SIGBUS (process exits 135).
	$MMAP_CAT $tf > $out
	local rc=$?
	(( rc == 0 )) ||
		error "mmap_cat exited $rc (SIGBUS=135) under OST $ost fault"

	local sum_after=$(md5sum $out | awk '{print $1}')
	[[ "$sum_mmap" == "$sum_after" ]] ||
		error "mmap checksum changed under OST $ost fault:" \
			"$sum_mmap vs $sum_after"

	echo "** mmap read checksum matches with OST $ost persistently failing"
}
run_test 41d "test mmap reads with EC recovery"

test_41e() {
	(( OSTCOUNT >= 6 )) || skip_env "needs >= 6 OSTs"
	enable_ec

	local tf=$DIR/$tfile

	stack_trap "rm -f $tf"

	# Create EC file with 4+2 configuration
	$LFS setstripe -E eof -S 1M -c 4 --ec 4+2 $tf ||
		error "setstripe --ec 4+2 failed"

	# Write 8MB of data using Direct I/O
	dd if=/dev/urandom of=$tf bs=1M count=8 oflag=direct ||
		error "dd with direct I/O failed"

	# Resync to update parity
	$LFS mirror resync $tf || error "mirror resync failed"

	# ll_file_io_generic() sets ci_switch_ec_io and restarts a
	# failed DIO read on an EC file as CIT_EC_RD, so O_DIRECT has a
	# recovery path of its own; check it returns the same bytes as
	# the pre-failure read.

	# Get checksum using Direct I/O read
	local sum_before=$(dd if=$tf bs=1M count=8 iflag=direct 2>/dev/null |
		md5sum | awk '{print $1}')

	ec_start_read_fault $tf
	stack_trap "ec_stop_read_fault"

	# Verify Direct I/O read still works
	local sum_after=$(dd if=$tf bs=1M count=8 iflag=direct 2>/dev/null |
		md5sum | awk '{print $1}')
	[[ "$sum_before" == "$sum_after" ]] ||
		error "Direct I/O read checksum changed:" \
			"$sum_before vs $sum_after"

	echo "** Direct I/O read checksum matches after OST failures"
}
run_test 41e "test Direct I/O reads with recovery"

test_41f() {
	(( OSTCOUNT >= 6 )) || skip_env "needs >= 6 OSTs"
	enable_ec

	local tf=$DIR/$tfile

	stack_trap "rm -f $tf"

	# Create EC file with 4+2 configuration
	$LFS setstripe -E eof -S 1M -c 4 --ec 4+2 $tf ||
		error "setstripe --ec 4+2 failed"

	# Write with buffered I/O, read back with DIO during recovery
	dd if=/dev/urandom of=$tf bs=1M count=4 || error "dd failed"
	$LFS mirror resync $tf || error "mirror resync failed"

	local sum_before=$(md5sum $tf | awk '{print $1}')

	ec_start_read_fault $tf
	stack_trap "ec_stop_read_fault"

	local sum_after=$(dd if=$tf bs=1M count=4 iflag=direct 2>/dev/null |
		md5sum | awk '{print $1}')
	[[ "$sum_before" == "$sum_after" ]] ||
		error "DIO read after buffered write failed: $sum_before" \
			"vs $sum_after"
}
run_test 41f "test DIO read recovery after buffered write"

test_41g() {
	(( OSTCOUNT >= 8 )) || skip_env "needs >= 8 OSTs"
	enable_ec

	local tf=$DIR/$tfile

	stack_trap "rm -f $tf"

	# Create EC file with 2+1 -- smaller stripe count
	$LFS setstripe -E eof -S 1M -c 2 --ec 2+1 $tf ||
		error "setstripe --ec 2+1 failed"

	dd if=/dev/urandom of=$tf bs=1M count=2 oflag=direct ||
		error "dd failed"
	$LFS mirror resync $tf || error "mirror resync failed"

	local sum_before=$(dd if=$tf bs=1M count=2 iflag=direct 2>/dev/null |
		md5sum | awk '{print $1}')

	ec_start_read_fault $tf
	stack_trap "ec_stop_read_fault"

	local sum_after=$(dd if=$tf bs=1M count=2 iflag=direct 2>/dev/null |
		md5sum | awk '{print $1}')
	[[ "$sum_before" == "$sum_after" ]] ||
		error "DIO recovery with 2+1 EC failed: $sum_before vs" \
			"$sum_after"
}
run_test 41g "test DIO recovery with 2+1 EC"

test_41h() {
	(( OSTCOUNT >= 6 )) || skip_env "needs >= 6 OSTs"
	enable_ec

	local tf=$DIR/$tfile

	stack_trap "rm -f $tf"

	$LFS setstripe -E eof -S 1M -c 4 --ec 4+2 $tf ||
		error "setstripe --ec 4+2 failed"

	# Write 16MB with DIO -- spans multiple raid sets
	dd if=/dev/urandom of=$tf bs=1M count=16 oflag=direct ||
		error "dd failed"
	$LFS mirror resync $tf || error "mirror resync failed"

	local sum_before=$(dd if=$tf bs=1M count=16 iflag=direct \
		2>/dev/null | md5sum | awk '{print $1}')

	ec_start_read_fault $tf
	stack_trap "ec_stop_read_fault"

	local sum_after=$(dd if=$tf bs=1M count=16 iflag=direct \
		2>/dev/null | md5sum | awk '{print $1}')
	[[ "$sum_before" == "$sum_after" ]] ||
		error "DIO multi-raidset recovery failed: $sum_before vs" \
			"$sum_after"
}
run_test 41h "test DIO recovery spanning multiple raid sets"

test_41i() {
	(( OSTCOUNT >= 6 )) || skip_env "needs >= 6 OSTs"
	enable_ec

	local tf=$DIR/$tfile

	stack_trap "rm -f $tf"

	$LFS setstripe -E eof -S 1M -c 4 --ec 4+2 $tf ||
		error "setstripe --ec 4+2 failed"

	dd if=/dev/urandom of=$tf bs=1M count=8 || error "dd failed"
	$LFS mirror resync $tf || error "mirror resync failed"

	# Verify buffered recovery works first
	local sum_before=$(md5sum $tf | awk '{print $1}')
	ec_start_read_fault $tf
	local sum_buffered=$(md5sum $tf | awk '{print $1}')
	ec_stop_read_fault
	[[ "$sum_before" == "$sum_buffered" ]] ||
		error "buffered recovery failed: $sum_before vs" \
			"$sum_buffered"

	# Now try the same file with DIO
	ec_start_read_fault $tf
	stack_trap "ec_stop_read_fault"

	echo 3 > /proc/sys/vm/drop_caches
	local sum_dio=$(dd if=$tf bs=1M count=8 iflag=direct 2>/dev/null |
		md5sum | awk '{print $1}')
	[[ "$sum_before" == "$sum_dio" ]] ||
		error "buffered recovery works but DIO fails:" \
			"$sum_before vs $sum_dio"
}
run_test 41i "test DIO vs buffered recovery on same file"

test_41j() {
	(( OSTCOUNT >= 6 )) || skip_env "needs >= 6 OSTs"
	enable_ec
	which aiocp || skip_env "no aiocp installed"

	local tf=$DIR/$tfile

	stack_trap "rm -f $tf"
	local tmp=$DIR/$tfile.aio

	$LFS setstripe -E eof -S 1M -c 4 --ec 4+2 $tf ||
		error "setstripe --ec 4+2 failed"

	dd if=/dev/urandom of=$tf bs=1M count=8 oflag=direct ||
		error "dd write failed"
	$LFS mirror resync $tf || error "mirror resync failed"

	# Baseline: AIO read without faults
	touch $tmp
	aiocp -a $PAGE_SIZE -b 1M -s 8M -f O_DIRECT $tf $tmp ||
		error "aiocp baseline failed"
	local sum_before=$(md5sum $tmp | awk '{print $1}')
	rm -f $tmp

	ec_start_read_fault $tf
	stack_trap "ec_stop_read_fault"
	echo 3 > /proc/sys/vm/drop_caches

	# AIO read during recovery
	touch $tmp
	aiocp -a $PAGE_SIZE -b 1M -s 8M -f O_DIRECT $tf $tmp ||
		error "aiocp with fault failed"
	# $tmp is on Lustre as well, so clear the fault before reading it
	# back: otherwise the verification read is itself taken through the
	# faulted OSTs and a mismatch says nothing about $tf.  The
	# stack_trap above still covers the error paths.
	ec_stop_read_fault
	local sum_after=$(md5sum $tmp | awk '{print $1}')
	rm -f $tmp

	[[ "$sum_before" == "$sum_after" ]] ||
		error "AIO read checksum changed: $sum_before vs $sum_after"
}
run_test 41j "test AIO reads with EC recovery"

test_41k() {
	(( OSTCOUNT >= 8 )) || skip_env "needs >= 8 OSTs"
	enable_ec
	which aiocp || skip_env "no aiocp installed"

	local tf=$DIR/$tfile

	stack_trap "rm -f $tf"
	local tmp=$DIR/$tfile.aio

	# 2+1 EC -- smaller stripe count
	$LFS setstripe -E eof -S 1M -c 2 --ec 2+1 $tf ||
		error "setstripe --ec 2+1 failed"

	dd if=/dev/urandom of=$tf bs=1M count=2 oflag=direct ||
		error "dd write failed"
	$LFS mirror resync $tf || error "mirror resync failed"

	touch $tmp
	aiocp -a $PAGE_SIZE -b 1M -s 2M -f O_DIRECT $tf $tmp ||
		error "aiocp baseline failed"
	local sum_before=$(md5sum $tmp | awk '{print $1}')
	rm -f $tmp

	ec_start_read_fault $tf
	stack_trap "ec_stop_read_fault"
	echo 3 > /proc/sys/vm/drop_caches

	touch $tmp
	aiocp -a $PAGE_SIZE -b 1M -s 2M -f O_DIRECT $tf $tmp ||
		error "aiocp with fault failed"
	# clear the fault before checksumming the copy, which is on Lustre
	ec_stop_read_fault
	local sum_after=$(md5sum $tmp | awk '{print $1}')
	rm -f $tmp

	[[ "$sum_before" == "$sum_after" ]] ||
		error "AIO 2+1 checksum changed: $sum_before vs $sum_after"
}
run_test 41k "test AIO recovery with 2+1 EC"

test_41l() {
	(( OSTCOUNT >= 6 )) || skip_env "needs >= 6 OSTs"
	enable_ec
	which aiocp || skip_env "no aiocp installed"

	local tf=$DIR/$tfile

	stack_trap "rm -f $tf"
	local tmp=$DIR/$tfile.aio

	$LFS setstripe -E eof -S 1M -c 4 --ec 4+2 $tf ||
		error "setstripe --ec 4+2 failed"

	# 16MB spans multiple raid sets
	dd if=/dev/urandom of=$tf bs=1M count=16 oflag=direct ||
		error "dd write failed"
	$LFS mirror resync $tf || error "mirror resync failed"

	touch $tmp
	aiocp -a $PAGE_SIZE -b 1M -s 16M -f O_DIRECT $tf $tmp ||
		error "aiocp baseline failed"
	local sum_before=$(md5sum $tmp | awk '{print $1}')
	rm -f $tmp

	ec_start_read_fault $tf
	stack_trap "ec_stop_read_fault"
	echo 3 > /proc/sys/vm/drop_caches

	touch $tmp
	aiocp -a $PAGE_SIZE -b 1M -s 16M -f O_DIRECT $tf $tmp ||
		error "aiocp multi-raidset failed"
	# clear the fault before checksumming the copy, which is on Lustre
	ec_stop_read_fault
	local sum_after=$(md5sum $tmp | awk '{print $1}')
	rm -f $tmp

	[[ "$sum_before" == "$sum_after" ]] ||
		error "AIO multi-raidset checksum changed: $sum_before vs" \
			"$sum_after"
}
run_test 41l "test AIO recovery spanning multiple raid sets"

test_41m() {
	(( OSTCOUNT >= 6 )) || skip_env "needs >= 6 OSTs"
	enable_ec
	which aiocp || skip_env "no aiocp installed"

	local tf=$DIR/$tfile

	stack_trap "rm -f $tf"
	local tmp=$DIR/$tfile.aio

	$LFS setstripe -E eof -S 1M -c 4 --ec 4+2 $tf ||
		error "setstripe --ec 4+2 failed"

	dd if=/dev/urandom of=$tf bs=1M count=8 || error "dd failed"
	$LFS mirror resync $tf || error "mirror resync failed"

	# Large block size -- single AIO covers full file
	touch $tmp
	aiocp -a $PAGE_SIZE -b 8M -s 8M -f O_DIRECT $tf $tmp ||
		error "aiocp baseline failed"
	local sum_before=$(md5sum $tmp | awk '{print $1}')
	rm -f $tmp

	ec_start_read_fault $tf
	stack_trap "ec_stop_read_fault"
	echo 3 > /proc/sys/vm/drop_caches

	touch $tmp
	aiocp -a $PAGE_SIZE -b 8M -s 8M -f O_DIRECT $tf $tmp ||
		error "aiocp large block failed"
	# clear the fault before checksumming the copy, which is on Lustre
	ec_stop_read_fault
	local sum_after=$(md5sum $tmp | awk '{print $1}')
	rm -f $tmp

	[[ "$sum_before" == "$sum_after" ]] ||
		error "AIO large block checksum changed: $sum_before vs" \
			"$sum_after"
}
run_test 41m "test AIO recovery with large block size"

test_41n() {
	(( OSTCOUNT >= 6 )) || skip_env "needs >= 6 OSTs"
	enable_ec
	which aiocp || skip_env "no aiocp installed"

	local tf=$DIR/$tfile

	stack_trap "rm -f $tf"
	local tmp=$DIR/$tfile.aio

	$LFS setstripe -E eof -S 1M -c 4 --ec 4+2 $tf ||
		error "setstripe --ec 4+2 failed"

	dd if=/dev/urandom of=$tf bs=1M count=8 || error "dd failed"
	$LFS mirror resync $tf || error "mirror resync failed"

	# Verify buffered and sync DIO recovery work first
	local sum_before=$(md5sum $tf | awk '{print $1}')

	ec_start_read_fault $tf
	local sum_buffered=$(md5sum $tf | awk '{print $1}')
	ec_stop_read_fault
	[[ "$sum_before" == "$sum_buffered" ]] ||
		error "buffered recovery failed: $sum_before vs" \
			"$sum_buffered"

	# Now try AIO on the same file
	ec_start_read_fault $tf
	stack_trap "ec_stop_read_fault"
	echo 3 > /proc/sys/vm/drop_caches

	touch $tmp
	aiocp -a $PAGE_SIZE -b 1M -s 8M -f O_DIRECT $tf $tmp ||
		error "aiocp with fault failed"
	# clear the fault before checksumming the copy, which is on Lustre
	ec_stop_read_fault
	local sum_aio=$(md5sum $tmp | awk '{print $1}')
	rm -f $tmp

	[[ "$sum_before" == "$sum_aio" ]] ||
		error "buffered works but AIO fails: $sum_before vs" \
			"$sum_aio"
}
run_test 41n "test AIO vs buffered recovery on same file"

test_42a() {
	(( OSTCOUNT >= 6 )) || skip_env "needs >= 6 OSTs"
	enable_ec

	local tf=$DIR/$tfile

	stack_trap "rm -f $tf"

	# Create EC file with 4+2 configuration, 1M stripe size
	$LFS setstripe -E eof -S 1M -c 4 --ec 4+2 $tf ||
		error "setstripe --ec 4+2 failed"

	# Write file smaller than one stripe (512K with 1M stripe)
	# This tests if recovery works when file doesn't span all data OSTs
	dd if=/dev/urandom of=$tf bs=512K count=1 || error "dd failed"

	# Resync to update parity
	$LFS mirror resync $tf || error "mirror resync failed"

	# Get checksum before failure
	local sum1=$(md5sum $tf | awk '{print $1}')

	ec_start_read_fault $tf
	stack_trap "ec_stop_read_fault"

	# Verify read still succeeds
	local sum2=$(md5sum $tf | awk '{print $1}')
	[[ "$sum1" == "$sum2" ]] ||
		error "data changed after OST failure: $sum1 vs $sum2"
	echo "** File smaller than stripe size: recovery successful"
}
run_test 42a "test recovery with file smaller than stripe size"

test_42b() {
	(( OSTCOUNT >= 6 )) || skip_env "needs >= 6 OSTs"
	enable_ec

	local tf=$DIR/$tfile

	stack_trap "rm -f $tf"

	# Create EC file with 4+2 configuration, 1M stripe size
	$LFS setstripe -E eof -S 1M -c 4 --ec 4+2 $tf ||
		error "setstripe --ec 4+2 failed"

	# Write file with partial stripes (1.5M = 1 full + 0.5 partial)
	# This tests if recovery handles partial stripe reconstruction
	dd if=/dev/urandom of=$tf bs=512K count=3 || error "dd failed"

	# Resync to update parity
	$LFS mirror resync $tf || error "mirror resync failed"

	# Get checksum before failure
	local sum1=$(md5sum $tf | awk '{print $1}')

	ec_start_read_fault $tf
	stack_trap "ec_stop_read_fault"

	# Verify read still succeeds
	local sum2=$(md5sum $tf | awk '{print $1}')
	[[ "$sum1" == "$sum2" ]] ||
		error "data changed after OST failure: $sum1 vs $sum2"
	echo "** File with partial stripes: recovery successful"
}
run_test 42b "test recovery with partial stripes"

test_42c() {
	(( OSTCOUNT >= 6 )) || skip_env "needs >= 6 OSTs"
	enable_ec

	local tf=$DIR/$tfile

	stack_trap "rm -f $tf"

	# Create EC file with 4+2 configuration, 1M stripe size
	# With 4+2 EC, a complete raid set is 4MB (4 data stripes * 1M)
	$LFS setstripe -E eof -S 1M -c 4 --ec 4+2 $tf ||
		error "setstripe --ec 4+2 failed"

	# Write 3M - doesn't complete a raid set (< 4MB)
	# This tests if recovery works with incomplete raid sets
	dd if=/dev/urandom of=$tf bs=1M count=3 || error "dd failed"

	# Resync to update parity
	$LFS mirror resync $tf || error "mirror resync failed"

	# Get checksum before failure
	local sum1=$(md5sum $tf | awk '{print $1}')

	ec_start_read_fault $tf
	stack_trap "ec_stop_read_fault"

	# Verify read still succeeds
	local sum2=$(md5sum $tf | awk '{print $1}')
	[[ "$sum1" == "$sum2" ]] ||
		error "data changed after OST failure: $sum1 vs $sum2"
	echo "** File with incomplete raid set: recovery successful"
}
run_test 42c "test recovery with incomplete raid set"

test_42d() {
	(( OSTCOUNT >= 6 )) || skip_env "needs >= 6 OSTs"
	enable_ec

	local tf=$DIR/$tfile

	stack_trap "rm -f $tf"

	# Create PFL file with EC - two components with different extents
	# Component 1: [0, 4M] with 4+2 EC
	# Component 2: [4M, EOF] with 4+2 EC
	$LFS setstripe -E 4M -S 1M -c 4 --ec 4+2 \
		-E eof -S 1M -c 4 --ec 4+2 $tf ||
		error "setstripe with PFL failed"

	# Write data that spans the component boundary
	# Write 6M total: 4M in first component, 2M in second component
	dd if=/dev/urandom of=$tf bs=1M count=6 || error "dd failed"

	# Resync to update parity
	$LFS mirror resync $tf || error "mirror resync failed"

	# Get checksums before failure
	local sum_full=$(md5sum $tf | awk '{print $1}')
	# Read from first component only (0-4M)
	local sum_comp1=$(dd if=$tf bs=1M count=4 2>/dev/null |
		md5sum | awk '{print $1}')
	# Read from second component only (4M-6M)
	local sum_comp2=$(dd if=$tf bs=1M skip=4 count=2 2>/dev/null |
		md5sum | awk '{print $1}')
	# Read spanning the boundary (2M-6M, crosses at 4M)
	local sum_span=$(dd if=$tf bs=1M skip=2 count=4 2>/dev/null |
		md5sum | awk '{print $1}')

	ec_start_read_fault $tf
	stack_trap "ec_stop_read_fault"

	# Verify all reads still succeed
	local sum_full_after=$(md5sum $tf | awk '{print $1}')
	[[ "$sum_full" == "$sum_full_after" ]] ||
		error "full file checksum changed: $sum_full vs $sum_full_after"

	# Each per-component read needs its own lock cancellation, or it is
	# served from the cache the full-file read just filled and never
	# reaches the faulted OST.
	cancel_lru_locks osc
	local sum_comp1_after=$(dd if=$tf bs=1M count=4 2>/dev/null |
		md5sum | awk '{print $1}')
	[[ "$sum_comp1" == "$sum_comp1_after" ]] ||
		error "component 1 checksum changed: $sum_comp1 vs" \
			"$sum_comp1_after"

	cancel_lru_locks osc
	local sum_comp2_after=$(dd if=$tf bs=1M skip=4 count=2 \
		2>/dev/null | md5sum | awk '{print $1}')
	[[ "$sum_comp2" == "$sum_comp2_after" ]] ||
		error "component 2 checksum changed: $sum_comp2 vs" \
			"$sum_comp2_after"

	cancel_lru_locks osc
	local sum_span_after=$(dd if=$tf bs=1M skip=2 count=4 \
		2>/dev/null | md5sum | awk '{print $1}')
	[[ "$sum_span" == "$sum_span_after" ]] ||
		error "boundary-spanning checksum changed: $sum_span vs" \
			"$sum_span_after"

	echo "** PFL component boundary recovery successful"
}
run_test 42d "test recovery at PFL component boundaries"

test_43a() {
	(( OSTCOUNT >= 6 )) || skip_env "needs >= 6 OSTs"
	enable_ec

	local tf=$DIR/$tfile

	stack_trap "rm -f $tf"

	# Create EC file with 4+2 configuration
	$LFS setstripe -E eof -S 1M -c 4 --ec 4+2 $tf ||
		error "setstripe --ec 4+2 failed"

	# Write 8MB of data
	dd if=/dev/urandom of=$tf bs=1M count=8 || error "dd failed"

	# Resync to update parity
	$LFS mirror resync $tf || error "mirror resync failed"

	# Get checksum before failure
	local sum_before=$(md5sum $tf | awk '{print $1}')

	ec_start_read_fault $tf
	stack_trap "ec_stop_read_fault"

	# Verify read still succeeds with 1 OST failure
	local sum_after=$(md5sum $tf | awk '{print $1}')
	[[ "$sum_before" == "$sum_after" ]] ||
		error "checksum changed after single OST failure: $sum_before" \
			"vs $sum_after"

	echo "** Single OST failure recovery successful"
}
run_test 43a "test recovery with single OST failure (1 of 6 OSTs)"

test_43b() {
	(( OSTCOUNT >= 6 )) || skip_env "needs >= 6 OSTs"
	enable_ec

	local tf=$DIR/$tfile

	stack_trap "rm -f $tf"

	# Create EC file with 4+2 configuration
	$LFS setstripe -E eof -S 1M -c 4 --ec 4+2 $tf ||
		error "setstripe --ec 4+2 failed"

	# Write 8MB of data
	dd if=/dev/urandom of=$tf bs=1M count=8 || error "dd failed"

	# Resync to update parity
	$LFS mirror resync $tf || error "mirror resync failed"

	# Get checksum before failure
	local sum_before=$(md5sum $tf | awk '{print $1}')

	# 4+2 tolerates two lost objects; fail exactly that many.
	ec_data_stripe_osts $tf 0 1
	ec_start_read_fault $tf "${EC_STRIPE_OSTS[@]}"
	stack_trap "ec_stop_read_fault"

	# Verify read still succeeds at the recovery limit
	local sum_after=$(md5sum $tf | awk '{print $1}')
	[[ "$sum_before" == "$sum_after" ]] ||
		error "checksum changed at max failures: $sum_before vs" \
			"$sum_after"

	echo "** Maximum recoverable failure (2 OSTs) recovery successful"
}
run_test 43b "test recovery at maximum (2 OST failures with 4+2 EC)"

test_43c() {
	(( OSTCOUNT >= 6 )) || skip_env "needs >= 6 OSTs"
	enable_ec

	local tf=$DIR/$tfile

	stack_trap "rm -f $tf"

	$LFS setstripe -E eof -S 1M -c 4 --ec 4+2 $tf ||
		error "setstripe --ec 4+2 failed"

	dd if=/dev/urandom of=$tf bs=1M count=8 conv=fsync ||
		error "dd failed"
	$LFS mirror resync $tf || error "mirror resync failed"

	# Fail every OST read persistently: both data and parity
	# reads fail, so EC cannot recover.
	ec_start_all_reads_fail
	stack_trap "ec_stop_all_reads_fail"

	# Verify read fails with -EIO (not corrupt data or crash)
	dd if=$tf of=/dev/null bs=1M 2>&1 | grep -q "Input/output error" ||
		error "Read should fail with EIO when all reads fail"
	echo "** Read correctly failed with I/O error"
}
run_test 43c "test graceful failure with too many OST failures (3+ OSTs)"

test_43d() {
	(( OSTCOUNT >= 6 )) || skip_env "needs >= 6 OSTs"
	enable_ec

	local tf=$DIR/$tfile

	stack_trap "rm -f $tf"

	$LFS setstripe -E eof -S 1M -c 4 --ec 4+2 $tf ||
		error "setstripe --ec 4+2 failed"

	dd if=/dev/urandom of=$tf bs=1M count=8 || error "dd failed"
	$LFS mirror resync $tf || error "mirror resync failed"

	local sum_before=$(md5sum $tf | awk '{print $1}')

	# Round 1: inject faults, read, verify, clear
	ec_start_read_fault $tf
	local sum_after1=$(md5sum $tf | awk '{print $1}')
	ec_stop_read_fault
	[[ "$sum_before" == "$sum_after1" ]] ||
		error "checksum changed: $sum_before vs $sum_after1"
	echo "** Read successful after first fault round"

	# Round 2: fail a second data stripe on top of the first.  4+2
	# tolerates two lost objects, so the read must still reconstruct.
	ec_data_stripe_osts $tf 0 1
	ec_start_read_fault $tf "${EC_STRIPE_OSTS[@]}"
	stack_trap "ec_stop_read_fault"
	local sum_after2=$(md5sum $tf | awk '{print $1}')
	[[ "$sum_before" == "$sum_after2" ]] ||
		error "checksum changed with two OSTs failed:" \
			"$sum_before vs $sum_after2"
	echo "** Read successful with two data stripes failed"
}
run_test 43d "test progressive failures (fail 1, verify, fail 2nd, verify)"

test_44a() {
	(( OSTCOUNT >= 6 )) || skip_env "needs >= 6 OSTs"
	enable_ec

	local tf=$DIR/$tfile

	stack_trap "rm -f $tf"

	$LFS setstripe -E eof -S 1M -c 4 --ec 4+2 $tf ||
		error "setstripe --ec 4+2 failed"

	dd if=/dev/urandom of=$tf bs=1M count=8 conv=fsync ||
		error "dd failed"
	$LFS mirror resync $tf || error "mirror resync failed"

	# Fail every OST read persistently: EC cannot recover.
	ec_start_all_reads_fail
	stack_trap "ec_stop_all_reads_fail"

	# Verify read returns EIO (not corrupt data or crash)
	dd if=$tf of=/dev/null bs=1M 2>&1 | grep -q "Input/output error" ||
		error "Read should return EIO when all reads fail"
	echo "** Read correctly returned -EIO"

	# Clear faults and verify file is accessible again
	ec_stop_all_reads_fail
	cancel_lru_locks osc
	dd if=$tf of=/dev/null bs=1M 2>/dev/null ||
		error "File should be readable after faults cleared"

	echo "** Error handling verified: -EIO returned, file accessible" \
		"after clear"
}
run_test 44a "test error handling with too many OST failures"

test_44b() {
	(( OSTCOUNT >= 6 )) || skip_env "needs >= 6 OSTs"
	enable_ec

	local tf=$DIR/$tfile

	stack_trap "rm -f $tf"

	# Create EC file with 4+2 configuration
	$LFS setstripe -E eof -S 1M -c 4 --ec 4+2 $tf ||
		error "setstripe --ec 4+2 failed"

	# Write 8MB of data
	dd if=/dev/urandom of=$tf bs=1M count=8 || error "dd failed"

	# Resync to update parity
	$LFS mirror resync $tf || error "mirror resync failed"

	# Get checksum before failures
	local sum_before=$(md5sum $tf | awk '{print $1}')

	echo "** Round 1: Inject read faults, verify recovery"
	ec_start_read_fault $tf
	local sum_round1=$(md5sum $tf | awk '{print $1}')
	ec_stop_read_fault
	[[ "$sum_before" == "$sum_round1" ]] ||
		error "checksum changed after round 1: $sum_before vs" \
			"$sum_round1"
	echo "** Round 1 recovery successful"

	echo "** Round 2: Re-inject faults, verify recovery again"
	ec_start_read_fault $tf
	stack_trap "ec_stop_read_fault"
	local sum_round2=$(md5sum $tf | awk '{print $1}')
	[[ "$sum_before" == "$sum_round2" ]] ||
		error "checksum changed after round 2: $sum_before vs" \
			"$sum_round2"
	echo "** Round 2 recovery successful"
}
run_test 44b "test repeated recovery with read fault injection"

test_44c() {
	(( OSTCOUNT >= 6 )) || skip_env "needs >= 6 OSTs"
	enable_ec

	local tf=$DIR/$tfile

	stack_trap "rm -f $tf"

	# Create EC file with 4+2 configuration
	$LFS setstripe -E eof -S 1M -c 4 --ec 4+2 $tf ||
		error "setstripe --ec 4+2 failed"

	# Write 8MB of data
	dd if=/dev/urandom of=$tf bs=1M count=8 conv=fsync ||
		error "dd failed"

	# Resync to update parity
	$LFS mirror resync $tf || error "mirror resync failed"

	# Fail every OST read persistently: both data and recovery
	# reads fail, so EC cannot recover.
	ec_start_all_reads_fail
	stack_trap "ec_stop_all_reads_fail"

	# Read should fail with EIO
	dd if=$tf of=/dev/null bs=1M 2>&1 | grep -q "Input/output error" ||
		error "Read should return EIO when recovery reads also fail"
	echo "** Read correctly returned -EIO"

	# Clear faults and verify file is accessible again
	ec_stop_all_reads_fail
	cancel_lru_locks osc
	dd if=$tf of=/dev/null bs=1M 2>/dev/null ||
		error "File should be readable after faults cleared"

	echo "** Error handling verified: -EIO returned, file accessible" \
		"after clear"
}
run_test 44c "test error handling with read failures during recovery"

test_45a() {
	(( OSTCOUNT >= 6 )) || skip_env "needs >= 6 OSTs"
	enable_ec

	local tf=$DIR/$tfile

	stack_trap "rm -f $tf"

	# Create EC file with 4+2 configuration
	$LFS setstripe -E eof -S 1M -c 4 --ec 4+2 $tf ||
		error "setstripe --ec 4+2 failed"

	# Write 8MB of data
	dd if=/dev/urandom of=$tf bs=1M count=8 || error "dd failed"

	# Resync to update parity
	$LFS mirror resync $tf || error "mirror resync failed"

	# Get checksum before failure
	local sum_before=$(md5sum $tf | awk '{print $1}')

	ec_data_stripe_osts $tf 0 1
	ec_start_read_fault $tf "${EC_STRIPE_OSTS[@]}"
	stack_trap "ec_stop_read_fault"

	# Verify read succeeds with consecutive OST failures
	local sum_after=$(md5sum $tf | awk '{print $1}')
	[[ "$sum_before" == "$sum_after" ]] ||
		error "Consecutive OST failure recovery failed"

	echo "** Consecutive OST failure recovery successful"
}
run_test 45a "test recovery with consecutive OST failures (OST0 and OST1)"

test_45b() {
	(( OSTCOUNT >= 6 )) || skip_env "needs >= 6 OSTs"
	enable_ec

	local tf=$DIR/$tfile

	stack_trap "rm -f $tf"

	# Create EC file with 4+2 configuration
	$LFS setstripe -E eof -S 1M -c 4 --ec 4+2 $tf ||
		error "setstripe --ec 4+2 failed"

	# Write 8MB of data
	dd if=/dev/urandom of=$tf bs=1M count=8 || error "dd failed"

	# Resync to update parity
	$LFS mirror resync $tf || error "mirror resync failed"

	# Get checksum before failure
	local sum_before=$(md5sum $tf | awk '{print $1}')

	ec_data_stripe_osts $tf 0 3
	ec_start_read_fault $tf "${EC_STRIPE_OSTS[@]}"
	stack_trap "ec_stop_read_fault"

	# Verify read succeeds with non-consecutive OST failures
	local sum_after=$(md5sum $tf | awk '{print $1}')
	[[ "$sum_before" == "$sum_after" ]] ||
		error "Non-consecutive OST failure recovery failed"

	echo "** Non-consecutive OST failure recovery successful"
}
run_test 45b "test recovery with non-consecutive OST failures (OST0 and OST3)"

test_45c() {
	(( OSTCOUNT >= 6 )) || skip_env "needs >= 6 OSTs"
	enable_ec

	local tf=$DIR/$tfile

	stack_trap "rm -f $tf"

	# Create EC file with 4+2 configuration
	$LFS setstripe -E eof -S 1M -c 4 --ec 4+2 $tf ||
		error "setstripe --ec 4+2 failed"

	# Write 8MB of data
	dd if=/dev/urandom of=$tf bs=1M count=8 || error "dd failed"

	# Resync to update parity
	$LFS mirror resync $tf || error "mirror resync failed"

	# Get checksum before failure
	local sum_before=$(md5sum $tf | awk '{print $1}')

	# Fail only the parity objects: the data stripes are all intact,
	# so the read never reconstructs and the failure is invisible.
	ec_classify_osts $tf
	(( ${#parity_osts[@]} >= 1 )) || skip_env "no parity OSTs found"
	ec_start_read_fault $tf "${parity_osts[@]}"
	stack_trap "ec_stop_read_fault"

	# Verify read succeeds - should be transparent since data OSTs are fine
	local sum_after=$(md5sum $tf | awk '{print $1}')
	[[ "$sum_before" == "$sum_after" ]] ||
		error "Parity-only OST failure should be transparent"

	echo "** Parity-only OST failure handled correctly (no reconstruction" \
		"needed)"
}
run_test 45c "test recovery with only parity OSTs failed (transparent)"

test_45d() {
	(( OSTCOUNT >= 6 )) || skip_env "needs >= 6 OSTs"
	enable_ec

	local tf=$DIR/$tfile

	stack_trap "rm -f $tf"

	# Create EC file with 4+2 configuration
	$LFS setstripe -E eof -S 1M -c 4 --ec 4+2 $tf ||
		error "setstripe --ec 4+2 failed"

	# Write 8MB of data
	dd if=/dev/urandom of=$tf bs=1M count=8 || error "dd failed"

	# Resync to update parity
	$LFS mirror resync $tf || error "mirror resync failed"

	# Get checksum before failure
	local sum_before=$(md5sum $tf | awk '{print $1}')

	ec_data_stripe_osts $tf 0 2
	ec_start_read_fault $tf "${EC_STRIPE_OSTS[@]}"
	stack_trap "ec_stop_read_fault"

	# Verify read succeeds with specific data OST failures
	local sum_after=$(md5sum $tf | awk '{print $1}')
	[[ "$sum_before" == "$sum_after" ]] ||
		error "Specific data OST failure recovery failed"

	echo "** Specific data OST (0,2) failure recovery successful"
}
run_test 45d "test recovery with specific data OST failures (OST0 and OST2)"

test_46a() {
	(( OSTCOUNT >= 6 )) || skip_env "needs >= 6 OSTs"
	enable_ec

	local tf=$DIR/$tfile

	stack_trap "rm -f $tf"

	# Create EC file with 4+2 configuration and 64K stripe size
	$LFS setstripe -E eof -S 64K -c 4 --ec 4+2 $tf ||
		error "setstripe --ec 4+2 -S 64K failed"

	# Write 1MB of data (16 stripes across 4 OSTs with 64K stripe)
	dd if=/dev/urandom of=$tf bs=1M count=1 || error "dd failed"

	# Resync to update parity
	$LFS mirror resync $tf || error "mirror resync failed"

	# Get checksum before failure
	local sum_before=$(md5sum $tf | awk '{print $1}')

	ec_start_read_fault $tf
	stack_trap "ec_stop_read_fault"

	# Verify read succeeds with correct data
	local sum_after=$(md5sum $tf | awk '{print $1}')
	[[ "$sum_before" == "$sum_after" ]] ||
		error "64K stripe recovery failed: $sum_before vs $sum_after"

	echo "** 64K stripe size recovery successful"
}
run_test 46a "test EC recovery with 64K stripe size"

test_46b() {
	(( OSTCOUNT >= 6 )) || skip_env "needs >= 6 OSTs"
	enable_ec

	local tf=$DIR/$tfile

	stack_trap "rm -f $tf"

	# Create EC file with 4+2 configuration and 256K stripe size
	$LFS setstripe -E eof -S 256K -c 4 --ec 4+2 $tf ||
		error "setstripe --ec 4+2 -S 256K failed"

	# Write 4MB of data (16 stripes across 4 OSTs with 256K stripe)
	dd if=/dev/urandom of=$tf bs=1M count=4 || error "dd failed"

	# Resync to update parity
	$LFS mirror resync $tf || error "mirror resync failed"

	# Get checksum before failure
	local sum_before=$(md5sum $tf | awk '{print $1}')

	ec_start_read_fault $tf
	stack_trap "ec_stop_read_fault"

	# Verify read succeeds with correct data
	local sum_after=$(md5sum $tf | awk '{print $1}')
	[[ "$sum_before" == "$sum_after" ]] ||
		error "256K stripe recovery failed: $sum_before vs $sum_after"

	echo "** 256K stripe size recovery successful"
}
run_test 46b "test EC recovery with 256K stripe size"

test_46c() {
	(( OSTCOUNT >= 6 )) || skip_env "needs >= 6 OSTs"
	enable_ec

	local tf=$DIR/$tfile

	stack_trap "rm -f $tf"

	# Create EC file with 4+2 configuration and 4M stripe size
	$LFS setstripe -E eof -S 4M -c 4 --ec 4+2 $tf ||
		error "setstripe --ec 4+2 -S 4M failed"

	# Write 16MB of data (4 stripes across 4 OSTs with 4M stripe)
	dd if=/dev/urandom of=$tf bs=1M count=16 || error "dd failed"

	# Resync to update parity
	$LFS mirror resync $tf || error "mirror resync failed"

	# Get checksum before failure
	local sum_before=$(md5sum $tf | awk '{print $1}')

	ec_start_read_fault $tf
	stack_trap "ec_stop_read_fault"

	# Verify read succeeds with correct data
	local sum_after=$(md5sum $tf | awk '{print $1}')
	[[ "$sum_before" == "$sum_after" ]] ||
		error "4M stripe recovery failed: $sum_before vs $sum_after"

	echo "** 4M stripe size recovery successful"
}
run_test 46c "test EC recovery with 4M stripe size"

test_46d() {
	(( OSTCOUNT >= 6 )) || skip_env "needs >= 6 OSTs"
	enable_ec

	local tf=$DIR/$tfile

	stack_trap "rm -f $tf"

	# Create PFL file with mixed stripe sizes:
	# Component 1: [0, 4M] with 1M stripe
	# Component 2: [4M, EOF] with 4M stripe
	$LFS setstripe -E 4M -S 1M -c 4 --ec 4+2 \
		-E eof -S 4M -c 4 --ec 4+2 $tf ||
		error "setstripe with mixed stripe sizes failed"

	# Write 12MB of data spanning both components
	dd if=/dev/urandom of=$tf bs=1M count=12 || error "dd failed"

	# Resync to update parity
	$LFS mirror resync $tf || error "mirror resync failed"

	# Get checksums for different regions
	local sum_full=$(md5sum $tf | awk '{print $1}')
	local sum_comp1=$(dd if=$tf bs=1M count=4 2>/dev/null |
		md5sum | awk '{print $1}')
	local sum_comp2=$(dd if=$tf bs=1M skip=4 count=8 2>/dev/null |
		md5sum | awk '{print $1}')

	ec_start_read_fault $tf
	stack_trap "ec_stop_read_fault"

	# Verify all reads succeed
	local sum_full_after=$(md5sum $tf | awk '{print $1}')
	[[ "$sum_full" == "$sum_full_after" ]] ||
		error "Mixed stripe full recovery failed"

	# Without cancelling locks these re-reads come from the page cache
	# the full-file read filled and never issue a BRW RPC.
	cancel_lru_locks osc
	local sum_comp1_after=$(dd if=$tf bs=1M count=4 2>/dev/null |
		md5sum | awk '{print $1}')
	[[ "$sum_comp1" == "$sum_comp1_after" ]] ||
		error "Mixed stripe comp1 recovery failed"

	cancel_lru_locks osc
	local sum_comp2_after=$(dd if=$tf bs=1M skip=4 count=8 2>/dev/null |
		md5sum | awk '{print $1}')
	[[ "$sum_comp2" == "$sum_comp2_after" ]] ||
		error "Mixed stripe comp2 recovery failed"

	echo "** Mixed stripe sizes recovery successful"
}
run_test 46d "test EC recovery with mixed stripe sizes in PFL layout"

test_47a() {
	(( OSTCOUNT >= 6 )) || skip_env "needs >= 6 OSTs"
	enable_ec

	local tf=$DIR/$tfile

	stack_trap "rm -f $tf"

	# Create PFL file with multiple EC components:
	# Component 1: [0, 16M] with 2+1 EC (needs 3 OSTs)
	# Component 2: [16M, EOF] with 4+2 EC (needs 6 OSTs)
	$LFS setstripe -E 16M -S 1M -c 2 --ec 2+1 \
		-E eof -S 1M -c 4 --ec 4+2 $tf ||
		error "setstripe with multiple EC components failed"

	# Write 32MB of data spanning both components
	dd if=/dev/urandom of=$tf bs=1M count=32 || error "dd failed"

	# Resync to update parity
	$LFS mirror resync $tf || error "mirror resync failed"

	# Get checksum before failure
	local sum_before=$(md5sum $tf | awk '{print $1}')
	local sum_comp1=$(dd if=$tf bs=1M count=16 2>/dev/null |
		md5sum | awk '{print $1}')
	local sum_comp2=$(dd if=$tf bs=1M skip=16 count=16 2>/dev/null |
		md5sum | awk '{print $1}')

	ec_start_read_fault $tf
	stack_trap "ec_stop_read_fault"

	# Verify all reads succeed
	local sum_after=$(md5sum $tf | awk '{print $1}')
	[[ "$sum_before" == "$sum_after" ]] ||
		error "Multiple EC components full recovery failed"

	# Without cancelling locks these re-reads come from the page cache
	# the full-file read filled and never issue a BRW RPC.
	cancel_lru_locks osc
	local sum_comp1_after=$(dd if=$tf bs=1M count=16 2>/dev/null |
		md5sum | awk '{print $1}')
	[[ "$sum_comp1" == "$sum_comp1_after" ]] ||
		error "Multiple EC components comp1 recovery failed"

	cancel_lru_locks osc
	local sum_comp2_after=$(dd if=$tf bs=1M skip=16 count=16 2>/dev/null |
		md5sum | awk '{print $1}')
	[[ "$sum_comp2" == "$sum_comp2_after" ]] ||
		error "Multiple EC components comp2 recovery failed"

	echo "** Multiple EC components recovery successful"
}
run_test 47a "test EC recovery with multiple EC components in PFL"

test_47b() {
	(( OSTCOUNT >= 6 )) || skip_env "needs >= 6 OSTs"
	enable_ec

	local tf=$DIR/$tfile

	stack_trap "rm -f $tf"

	# Create PFL file with EC and non-EC components:
	# Component 1: [0, 4M] RAID0 (no EC)
	# Component 2: [4M, EOF] with 4+2 EC
	$LFS setstripe -E 4M -S 1M -c 4 \
		-E eof -S 1M -c 4 --ec 4+2 $tf ||
		error "setstripe with EC and non-EC components failed"

	dd if=/dev/urandom of=$tf bs=1M count=16 || error "dd failed"
	$LFS mirror resync $tf || error "mirror resync failed"

	local sum_comp2=$(dd if=$tf bs=1M skip=4 count=12 2>/dev/null |
		md5sum | awk '{print $1}')

	# ec_classify_osts() lumps every non-parity component into
	# data_osts with comp1 first, so data stripe 4 is the EC
	# component's first stripe.  Failing one of comp1's plain RAID0
	# OSTs instead would leave the 4M-16M read below untouched and its
	# comparison true no matter what recovery does.
	ec_data_stripe_osts $tf 4
	ec_start_read_fault $tf "${EC_STRIPE_OSTS[@]}"
	stack_trap "ec_stop_read_fault"

	# EC component should recover via parity
	local sum_comp2_after=$(dd if=$tf bs=1M skip=4 count=12 \
		2>/dev/null | md5sum | awk '{print $1}')
	[[ "$sum_comp2" == "$sum_comp2_after" ]] ||
		error "EC recovery failed: $sum_comp2 vs $sum_comp2_after"
	echo "** EC+non-EC mixed layout recovery successful"
}
run_test 47b "test EC recovery with EC and non-EC components in PFL"

test_47c() {
	(( OSTCOUNT >= 6 )) || skip_env "needs >= 6 OSTs"
	enable_ec

	local tf=$DIR/$tfile

	stack_trap "rm -f $tf"

	# Create PFL file with different stripe sizes per EC component:
	# Component 1: [0, 8M] with 1M stripes, 4+2 EC
	# Component 2: [8M, EOF] with 4M stripes, 4+2 EC
	$LFS setstripe -E 8M -S 1M -c 4 --ec 4+2 \
		-E eof -S 4M -c 4 --ec 4+2 $tf ||
		error "setstripe with different stripe sizes failed"

	# Write 24MB of data spanning both components
	dd if=/dev/urandom of=$tf bs=1M count=24 || error "dd failed"

	# Resync to update parity
	$LFS mirror resync $tf || error "mirror resync failed"

	# Get checksums before failure
	local sum_full=$(md5sum $tf | awk '{print $1}')
	local sum_comp1=$(dd if=$tf bs=1M count=8 2>/dev/null |
		md5sum | awk '{print $1}')
	local sum_comp2=$(dd if=$tf bs=1M skip=8 count=16 2>/dev/null |
		md5sum | awk '{print $1}')

	ec_start_read_fault $tf
	stack_trap "ec_stop_read_fault"

	# Drop cached locks to force re-read from OSTs via EC recovery
	cancel_lru_locks osc

	# Verify all reads succeed
	local sum_full_after=$(md5sum $tf | awk '{print $1}')
	[[ "$sum_full" == "$sum_full_after" ]] ||
		error "Different stripe sizes full recovery failed"

	local sum_comp1_after=$(dd if=$tf bs=1M count=8 2>/dev/null |
		md5sum | awk '{print $1}')
	[[ "$sum_comp1" == "$sum_comp1_after" ]] ||
		error "Different stripe sizes comp1 (1M) recovery failed"

	local sum_comp2_after=$(dd if=$tf bs=1M skip=8 count=16 2>/dev/null |
		md5sum | awk '{print $1}')
	[[ "$sum_comp2" == "$sum_comp2_after" ]] ||
		error "Different stripe sizes comp2 (4M) recovery failed"

	echo "** Different stripe sizes per component recovery successful"
}
run_test 47c "test EC recovery with different stripe sizes per PFL component"

test_47d() {
	(( OSTCOUNT >= 6 )) || skip_env "needs >= 6 OSTs"
	enable_ec

	local tf=$DIR/$tfile

	stack_trap "rm -f $tf"

	# Create PFL file with boundary at 8M:
	# Component 1: [0, 8M] with 4+2 EC
	# Component 2: [8M, EOF] with 4+2 EC
	$LFS setstripe -E 8M -S 1M -c 4 --ec 4+2 \
		-E eof -S 1M -c 4 --ec 4+2 $tf ||
		error "setstripe for boundary test failed"

	# Write 16MB of data
	dd if=/dev/urandom of=$tf bs=1M count=16 || error "dd failed"

	# Resync to update parity
	$LFS mirror resync $tf || error "mirror resync failed"

	# Get checksum for boundary-spanning region (6M offset, 4M length)
	# This crosses the 8M boundary: reads 6M-8M from comp1,
	# 8M-10M from comp2
	local sum_boundary=$(dd if=$tf bs=1M skip=6 count=4 2>/dev/null |
		md5sum | awk '{print $1}')
	# Also test other boundary-spanning regions
	local sum_boundary2=$(dd if=$tf bs=1M skip=7 count=2 2>/dev/null |
		md5sum | awk '{print $1}')
	local sum_full=$(md5sum $tf | awk '{print $1}')

	ec_start_read_fault $tf
	stack_trap "ec_stop_read_fault"

	# Verify boundary-spanning reads succeed
	local sum_boundary_after=$(dd if=$tf bs=1M skip=6 count=4 2>/dev/null |
		md5sum | awk '{print $1}')
	[[ "$sum_boundary" == "$sum_boundary_after" ]] ||
		error "Boundary read (6M+4M) failed: $sum_boundary vs" \
			"$sum_boundary_after"

	local sum_boundary2_after=$(dd if=$tf bs=1M skip=7 count=2 2>/dev/null |
		md5sum | awk '{print $1}')
	[[ "$sum_boundary2" == "$sum_boundary2_after" ]] ||
		error "Boundary read (7M+2M) failed: $sum_boundary2 vs" \
			"$sum_boundary2_after"

	local sum_full_after=$(md5sum $tf | awk '{print $1}')
	[[ "$sum_full" == "$sum_full_after" ]] ||
		error "Full file read failed after OST failures"

	echo "** Boundary-spanning reads recovery successful"
}
run_test 47d "test EC recovery with reads spanning PFL component boundaries"

test_48a() {
	(( OSTCOUNT >= 6 )) || skip_env "needs >= 6 OSTs"
	enable_ec

	local tf=$DIR/$tfile

	stack_trap "rm -f $tf"

	# Create EC file with 4+2 configuration
	$LFS setstripe -E eof -S 1M -c 4 --ec 4+2 $tf ||
		error "setstripe --ec 4+2 failed"

	# Write data
	dd if=/dev/urandom of=$tf bs=1M count=8 || error "dd failed"

	# Get checksum before any failures
	local sum_before=$(md5sum $tf | awk '{print $1}')

	# Intentionally DO NOT resync - parity is stale
	echo "** Skipping resync - parity remains stale"

	ec_start_read_fault $tf
	stack_trap "ec_stop_read_fault"

	# Parity was never computed, so EC cannot reconstruct the failed
	# stripe.  Either outcome is acceptable -- the read returns the
	# original bytes, or it fails -- but handing back silently wrong
	# data would be a corruption bug, so assert against it.
	local out=$TMP/$tfile.stale
	stack_trap "rm -f $out"

	if dd if=$tf of=$out bs=1M 2>/dev/null; then
		local sum_after=$(md5sum $out | awk '{print $1}')

		[[ "$sum_before" == "$sum_after" ]] ||
			error "stale-parity read returned wrong data:" \
				"$sum_before vs $sum_after"
		echo "** Read with stale parity returned correct data"
	else
		echo "** Read with stale parity failed, as expected"
	fi
}
run_test 48a "test recovery behavior with stale parity"

test_48b() {
	(( OSTCOUNT >= 6 )) || skip_env "needs >= 6 OSTs"
	enable_ec

	local tf=$DIR/$tfile

	stack_trap "rm -f $tf"

	# Create PFL file with two EC components to allow partial resync
	# Component 1: [0, 16M] with 4+2 EC
	# Component 2: [16M, EOF] with 4+2 EC
	$LFS setstripe -E 16M -S 1M -c 4 --ec 4+2 \
		-E eof -S 1M -c 4 --ec 4+2 $tf ||
		error "setstripe for partial resync test failed"

	# Write 32MB spanning both components
	dd if=/dev/urandom of=$tf bs=1M count=32 || error "dd failed"

	# Get checksums for both halves
	local sum_first=$(dd if=$tf bs=1M count=16 2>/dev/null |
		md5sum | awk '{print $1}')
	local sum_second=$(dd if=$tf bs=1M skip=16 count=16 2>/dev/null |
		md5sum | awk '{print $1}')

	# Resync the whole file.  A component-qualified resync is not
	# available here, so both components are brought up to date.
	# Use mirror resync with component ID
	local ids=($($LFS getstripe $tf | awk '/lcme_id/{print $2}' |
		tr '\n' ' '))
	echo "** Component IDs: ${ids[*]}"
	echo "** Resyncing the file (component ${ids[0]} included)"

	# Full resync then write to second half to make it stale
	$LFS mirror resync $tf || error "mirror resync failed"

	# Overwrite second half to make its parity stale
	dd if=/dev/urandom of=$tf bs=1M seek=16 count=16 conv=notrunc ||
		error "overwrite second half failed"

	# Update checksum for second half after overwrite
	sum_second=$(dd if=$tf bs=1M skip=16 count=16 2>/dev/null |
		md5sum | awk '{print $1}')

	# Now first half has valid parity, second half has stale parity
	echo "** First half: valid parity, Second half: stale parity"

	ec_start_read_fault $tf
	stack_trap "ec_stop_read_fault"

	# First half should recover correctly (valid parity)
	local sum_first_after=$(dd if=$tf bs=1M count=16 2>/dev/null |
		md5sum | awk '{print $1}')
	[[ "$sum_first" == "$sum_first_after" ]] ||
		error "First half recovery failed: $sum_first vs" \
			"$sum_first_after"
	echo "** First half (valid parity) recovery successful"

	# The second half's parity is stale, so recovery there may fail;
	# what it must not do is return data that differs from what was
	# written.
	local out=$TMP/$tfile.second
	stack_trap "rm -f $out"

	if dd if=$tf of=$out bs=1M skip=16 count=16 2>/dev/null; then
		local sum_second_after=$(md5sum $out | awk '{print $1}')

		[[ "$sum_second" == "$sum_second_after" ]] ||
			error "second half returned wrong data:" \
				"$sum_second vs $sum_second_after"
		echo "** Second half (stale parity) returned correct data"
	else
		echo "** Second half read failed, as expected with stale parity"
	fi
}
run_test 48b "test recovery with partially stale parity"

test_48c() {
	(( OSTCOUNT >= 6 )) || skip_env "needs >= 6 OSTs"
	enable_ec

	local tf=$DIR/$tfile

	stack_trap "rm -f $tf"

	# Create EC file with 4+2 configuration
	$LFS setstripe -E eof -S 1M -c 4 --ec 4+2 $tf ||
		error "setstripe --ec 4+2 failed"

	# Write initial data
	dd if=/dev/urandom of=$tf bs=1M count=8 || error "initial dd failed"

	# Resync to have valid parity
	$LFS mirror resync $tf || error "mirror resync failed"

	# Get checksum of synced data
	local sum_synced=$(md5sum $tf | awk '{print $1}')
	echo "** Initial data synced, checksum: $sum_synced"

	# Overwrite part of the file WITHOUT resyncing
	# This makes parity stale for the overwritten region
	dd if=/dev/urandom of=$tf bs=1M seek=2 count=4 conv=notrunc ||
		error "overwrite dd failed"

	# Get new checksum after overwrite
	local sum_overwritten=$(md5sum $tf | awk '{print $1}')
	echo "** After overwrite (no resync), checksum: $sum_overwritten"

	# DO NOT resync - parity is now stale for overwritten region
	echo "** Skipping resync after overwrite - parity is stale"

	ec_start_read_fault $tf
	stack_trap "ec_stop_read_fault"

	# Parity still covers the pre-overwrite contents, so recovery of
	# the overwritten range may fail.  Returning the stale, pre-
	# overwrite bytes would be worse than failing: it is a silent
	# rollback of an acknowledged write.
	local out=$TMP/$tfile.overwritten
	local rc=0

	stack_trap "rm -f $out"
	dd if=$tf of=$out bs=1M 2>/dev/null || rc=$?

	if (( rc == 0 )); then
		local sum_after=$(md5sum $out | awk '{print $1}')

		[[ "$sum_synced" != "$sum_after" ]] ||
			error "recovery returned pre-overwrite data"
		[[ "$sum_overwritten" == "$sum_after" ]] ||
			error "recovery returned wrong data:" \
				"$sum_overwritten vs $sum_after"
		echo "** Read after overwrite returned correct data"
	else
		echo "** Read with stale parity failed, as expected"
	fi

	# LCME_FL_STALE is per component, so the overwrite above stales the
	# whole parity mirror and the branch taken there is not guaranteed.
	# Resync and repeat: with parity up to date the degraded read must
	# succeed and return the post-overwrite bytes.  That is the check
	# that can actually fail.
	ec_stop_read_fault
	$LFS mirror resync $tf || error "resync after overwrite failed"

	ec_start_read_fault $tf
	dd if=$tf of=$out bs=1M 2>/dev/null ||
		error "degraded read failed after resync"

	local sum_resynced=$(md5sum $out | awk '{print $1}')

	[[ "$sum_overwritten" == "$sum_resynced" ]] ||
		error "recovery after resync returned wrong data:" \
			"$sum_overwritten vs $sum_resynced"
	echo "** Recovery after resync returned the overwritten data"
}
run_test 48c "test recovery after overwrite without resync"

test_49a() {
	(( OSTCOUNT >= 6 )) || skip_env "needs >= 6 OSTs"
	enable_ec

	local tf=$DIR/$tfile

	stack_trap "rm -f $tf"

	$LFS setstripe -E eof -S 1M -c 4 --ec 4+2 $tf ||
		error "setstripe --ec 4+2 failed"

	dd if=/dev/urandom of=$tf bs=1M count=32 ||
		error "dd failed"

	$LFS mirror resync $tf || error "mirror resync failed"

	local sum_expected=$(md5sum $tf | awk '{print $1}')

	ec_classify_osts $tf
	(( ${#safe_osts[@]} >= 2 )) ||
		skip_env "need >= 2 safe data OSTs"

	echo "** Concurrent reads: Disabling OSTs" \
		"${safe_osts[0]} and ${safe_osts[1]}"
	ec_deactivate_ost ${safe_osts[0]}
	ec_deactivate_ost ${safe_osts[1]}

	cancel_lru_locks osc

	# concurrent readers stress EC recovery page-cache serialization
	local pids=()
	local i
	for i in 1 2 3 4; do
		md5sum $tf > $TMP/ec_read_${i}_$tfile 2>&1 &
		pids+=($!)
	done
	stack_trap "rm -f $TMP/ec_read_*_$tfile"

	local failures=0
	for i in 1 2 3 4; do
		if ! wait ${pids[$((i-1))]}; then
			echo "Reader $i: process failed"
			failures=$((failures + 1))
			continue
		fi
		local sum
		sum=$(awk '{print $1}' \
			$TMP/ec_read_${i}_$tfile)
		if [[ "$sum_expected" != "$sum" ]]; then
			echo "Reader $i: $sum != $sum_expected"
			failures=$((failures + 1))
		fi
	done
	(( failures == 0 )) ||
		error "$failures of 4 concurrent readers failed"

	echo "** All 4 concurrent reads succeeded"
}
run_test 49a "test concurrent reads during EC recovery"

test_49b() {
	(( OSTCOUNT >= 6 )) || skip_env "needs >= 6 OSTs"
	enable_ec

	local tf=$DIR/$tfile

	stack_trap "rm -f $tf"
	local tf_other=$DIR/${tfile}_other

	# Create EC file with 4+2 configuration
	$LFS setstripe -E eof -S 1M -c 4 --ec 4+2 $tf ||
		error "setstripe --ec 4+2 failed"

	# Write data
	dd if=/dev/urandom of=$tf bs=1M count=32 ||
		error "dd failed"

	# Resync parity
	$LFS mirror resync $tf || error "mirror resync failed"

	# Get checksum
	local sum_expected=$(md5sum $tf | awk '{print $1}')

	ec_classify_osts $tf
	(( ${#safe_osts[@]} >= 2 )) ||
		skip_env "need >= 2 safe data OSTs"

	# Pre-allocate other files on a parity OST
	# (which stays active during test)
	local active_ost=${parity_osts[0]}
	local i
	for i in 1 2 3; do
		$LFS setstripe -c 1 -i $active_ost \
			${tf_other}_$i ||
			error "setstripe ${tf_other}_$i"
		dd if=/dev/urandom of=${tf_other}_$i \
			bs=1M count=1 2>/dev/null ||
			error "pre-write ${tf_other}_$i"
	done
	stack_trap "rm -f ${tf_other}_*"

	echo "** Write during recovery: Disabling" \
		"OSTs ${safe_osts[0]} and ${safe_osts[1]}"
	ec_deactivate_ost ${safe_osts[0]}
	ec_deactivate_ost ${safe_osts[1]}

	# Start recovery read in background
	local read_result=$TMP/read_result_$$
	stack_trap "rm -f $read_result"
	cancel_lru_locks osc
	md5sum $tf > $read_result 2>&1 &
	local read_pid=$!
	echo "** Started recovery read (PID $read_pid)"

	# Overwrite the pre-allocated files while
	# EC recovery is in progress
	local write_ok=true
	for i in 1 2 3; do
		dd if=/dev/urandom of=${tf_other}_$i \
			bs=1M count=4 2>/dev/null ||
			write_ok=false
	done

	# Wait for recovery read
	if ! wait $read_pid; then
		error "Recovery read failed"
	fi

	local sum_got=$(awk '{print $1}' $read_result)
	[[ "$sum_expected" == "$sum_got" ]] ||
		error "Recovery read got wrong data:" \
			"$sum_got vs $sum_expected"

	$write_ok ||
		error "Writes to other files failed"

	for i in 1 2 3; do
		local sz=$(stat -c %s ${tf_other}_$i)
		(( sz == 4 * 1048576 )) ||
			error "${tf_other}_$i: $sz != 4M"
	done

	echo "** Recovery read and concurrent writes OK"
}
run_test 49b "test writes to other files during EC recovery"

test_49c() {
	(( OSTCOUNT >= 6 )) || skip_env "needs >= 6 OSTs"
	enable_ec

	local tf=$DIR/$tfile

	stack_trap "rm -f $tf"

	# Create EC file with 4+2 configuration
	$LFS setstripe -E eof -S 1M -c 4 --ec 4+2 $tf ||
		error "setstripe --ec 4+2 failed"

	dd if=/dev/urandom of=$tf bs=1M count=32 ||
		error "dd failed"
	$LFS mirror resync $tf ||
		error "initial mirror resync failed"

	local sum_expected=$(md5sum $tf | awk '{print $1}')

	ec_classify_osts $tf
	(( ${#safe_osts[@]} >= 2 )) ||
		skip_env "need >= 2 safe data OSTs"

	# A resync only does work when a component is stale.  $tf was just
	# resynced, so resyncing it again would fall through to the verify
	# path and the concurrent resync this test is named for would never
	# happen.  Give it a second file whose parity really is stale.
	local tf2=$DIR/$tfile.stale

	$LFS setstripe -E eof -S 1M -c 4 --ec 4+2 $tf2 ||
		error "setstripe $tf2 failed"
	dd if=/dev/urandom of=$tf2 bs=1M count=8 || error "dd $tf2 failed"
	$LFS mirror resync $tf2 || error "resync $tf2 failed"
	dd if=/dev/urandom of=$tf2 bs=1M seek=2 count=4 conv=notrunc ||
		error "overwrite $tf2 failed"
	stack_trap "rm -f $tf2"

	echo "** Resync during recovery: Disabling" \
		"OSTs ${safe_osts[0]} and ${safe_osts[1]}"
	ec_deactivate_ost ${safe_osts[0]}
	ec_deactivate_ost ${safe_osts[1]}
	cancel_lru_locks osc

	# Start recovery read in background
	local read_result=$TMP/resync_read_result_$$
	stack_trap "rm -f $read_result"
	md5sum $tf > $read_result 2>&1 &
	local read_pid=$!
	echo "** Started recovery read (PID $read_pid)"

	# Simultaneously resync the stale file.  It may fail with OSTs
	# down; what matters is that it runs concurrently with the
	# recovery read and neither wedges the other.
	echo "** Attempting mirror resync during recovery..."
	$LFS mirror resync $tf2 2>/dev/null &&
		echo "** Resync succeeded" ||
		echo "** Resync failed (expected with OSTs down)"

	# Two failed OSTs is within a 4+2 layout's tolerance, so the read
	# has to succeed with the right data whether or not the competing
	# resync got anywhere.
	wait $read_pid || error "recovery read failed during resync"

	local sum_got=$(awk '{print $1}' $read_result)

	[[ "$sum_expected" == "$sum_got" ]] ||
		error "recovery read got wrong data: $sum_got vs $sum_expected"
	echo "** Recovery read succeeded"

	echo "** Resync during recovery: no deadlock"
}
run_test 49c "test mirror resync during EC recovery"

test_49d() {
	(( OSTCOUNT >= 6 )) || skip_env "needs >= 6 OSTs"
	enable_ec

	local tf=$DIR/$tfile

	stack_trap "rm -f $tf"

	$LFS setstripe -E eof -S 1M -c 4 --ec 4+2 $tf ||
		error "setstripe --ec 4+2 failed"

	dd if=/dev/urandom of=$tf bs=1M count=64 ||
		error "dd failed"
	$LFS mirror resync $tf || error "mirror resync failed"

	local sum_expected=$(md5sum $tf | awk '{print $1}')

	ec_classify_osts $tf
	(( ${#safe_osts[@]} >= 2 )) ||
		skip_env "need >= 2 safe data OSTs"

	echo "** OST reactivation: Disabling" \
		"OSTs ${safe_osts[0]} and ${safe_osts[1]}"
	ec_deactivate_ost ${safe_osts[0]}
	ec_deactivate_ost ${safe_osts[1]}
	cancel_lru_locks osc

	# Start recovery read in background
	local read_result=$TMP/reactivate_read_result_$$
	stack_trap "rm -f $read_result"
	md5sum $tf > $read_result 2>&1 &
	local read_pid=$!
	echo "** Started recovery read (PID $read_pid)"

	# Brief delay then reactivate OSTs mid-read
	sleep 0.5
	echo "** Reactivating OSTs during read..."
	ec_reactivate_ost ${safe_osts[0]}
	ec_reactivate_ost ${safe_osts[1]}
	echo "** OSTs reactivated"

	# Wait for read to complete
	if ! wait $read_pid; then
		error "Recovery read failed after reactivation"
	fi

	local sum_got=$(awk '{print $1}' $read_result)
	[[ "$sum_expected" == "$sum_got" ]] ||
		error "Read got wrong data:" \
			"$sum_got vs $sum_expected"

	echo "** OST reactivation during recovery OK"
}
run_test 49d "test OST reactivation during EC recovery"

test_50a() {
	(( OSTCOUNT >= 6 )) || skip_env "needs >= 6 OSTs"
	enable_ec

	local tf=$DIR/$tfile

	stack_trap "rm -f $tf"
	local tf2=$DIR2/$tfile

	# Setup second mount
	# check_and_setup_lustre() already mounts $MOUNT2 when the
	# session sets MOUNT_2; only tear down a mount this test made.
	if ! is_mounted $MOUNT2; then
		mkdir -p $MOUNT2
		mount_client $MOUNT2 || error "mount_client $MOUNT2 failed"
		stack_trap "umount_client $MOUNT2"
	fi

	# Create EC file with 4+2 configuration
	$LFS setstripe -E eof -S 1M -c 4 --ec 4+2 $tf ||
		error "setstripe --ec 4+2 failed"

	# Write data
	dd if=/dev/urandom of=$tf bs=1M count=16 || error "dd failed"

	# Resync parity
	$LFS mirror resync $tf || error "mirror resync failed"

	# Get expected checksum
	local sum_expected=$(md5sum $tf | awk '{print $1}')

	ec_classify_osts $tf
	(( ${#safe_osts[@]} >= 2 )) ||
		skip_env "need >= 2 safe data OSTs"
	echo "** Multi-mount test: Disabling OSTs ${safe_osts[0]} and" \
		"${safe_osts[1]}"

	ec_deactivate_ost ${safe_osts[0]}
	ec_deactivate_ost ${safe_osts[1]}
	cancel_lru_locks osc

	# Both mounts read simultaneously
	local result_dir=$TMP/multi_mount_$$
	mkdir -p $result_dir
	stack_trap "rm -rf $result_dir"

	md5sum $tf > $result_dir/mount1 2>&1 &
	local pid1=$!
	md5sum $tf2 > $result_dir/mount2 2>&1 &
	local pid2=$!

	echo "** Started readers on both mounts: PIDs $pid1, $pid2"

	wait $pid1 || error "Mount1 read failed"
	wait $pid2 || error "Mount2 read failed"

	# Verify both got correct data
	local sum1=$(awk '{print $1}' $result_dir/mount1)
	local sum2=$(awk '{print $1}' $result_dir/mount2)

	[[ "$sum_expected" == "$sum1" ]] ||
		error "Mount1 got wrong data: $sum1 vs $sum_expected"
	[[ "$sum_expected" == "$sum2" ]] ||
		error "Mount2 got wrong data: $sum2 vs $sum_expected"
	[[ "$sum1" == "$sum2" ]] ||
		error "Mounts got different data: $sum1 vs $sum2"

	echo "** Both mounts read identical correct data during recovery"
}
run_test 50a "test both mounts read same EC file during recovery"

test_50b() {
	(( OSTCOUNT >= 6 )) || skip_env "needs >= 6 OSTs"
	enable_ec

	local tf=$DIR/$tfile

	stack_trap "rm -f $tf"
	local tf2=$DIR2/$tfile

	# Setup second mount
	# check_and_setup_lustre() already mounts $MOUNT2 when the
	# session sets MOUNT_2; only tear down a mount this test made.
	if ! is_mounted $MOUNT2; then
		mkdir -p $MOUNT2
		mount_client $MOUNT2 || error "mount_client $MOUNT2 failed"
		stack_trap "umount_client $MOUNT2"
	fi

	# Create EC file with 4+2 configuration
	$LFS setstripe -E eof -S 1M -c 4 --ec 4+2 $tf ||
		error "setstripe --ec 4+2 failed"

	# Write data
	dd if=/dev/urandom of=$tf bs=1M count=32 || error "dd failed"

	# Resync parity
	$LFS mirror resync $tf || error "mirror resync failed"

	# Get checksums for different regions
	local sum_first=$(dd if=$tf bs=1M count=8 2>/dev/null |
		md5sum | awk '{print $1}')
	local sum_middle=$(dd if=$tf bs=1M skip=12 count=8 2>/dev/null |
		md5sum | awk '{print $1}')
	local sum_last=$(dd if=$tf bs=1M skip=24 count=8 2>/dev/null |
		md5sum | awk '{print $1}')

	ec_classify_osts $tf
	(( ${#safe_osts[@]} >= 2 )) ||
		skip_env "need >= 2 safe data OSTs"
	echo "** Interleaved reads: Disabling OSTs ${safe_osts[0]} and" \
		"${safe_osts[1]}"

	ec_deactivate_ost ${safe_osts[0]}
	ec_deactivate_ost ${safe_osts[1]}
	cancel_lru_locks osc

	# Interleaved reads from different offsets
	# Mount1 reads first 8M, Mount2 reads middle 8M, then swap
	local result_dir=$TMP/interleaved_$$
	mkdir -p $result_dir
	stack_trap "rm -rf $result_dir"

	# Round 1: Mount1 reads first, Mount2 reads middle
	dd if=$tf bs=1M count=8 2>/dev/null |
		md5sum > $result_dir/m1_first &
	local pid1=$!
	dd if=$tf2 bs=1M skip=12 count=8 2>/dev/null |
		md5sum > $result_dir/m2_middle &
	local pid2=$!

	wait $pid1 || error "Mount1 first region read failed"
	wait $pid2 || error "Mount2 middle region read failed"

	# Round 2: Mount1 reads last, Mount2 reads first
	dd if=$tf bs=1M skip=24 count=8 2>/dev/null |
		md5sum > $result_dir/m1_last &
	pid1=$!
	dd if=$tf2 bs=1M count=8 2>/dev/null |
		md5sum > $result_dir/m2_first &
	pid2=$!

	wait $pid1 || error "Mount1 last region read failed"
	wait $pid2 || error "Mount2 first region read failed"

	# Verify all reads got correct data
	local got
	got=$(awk '{print $1}' $result_dir/m1_first)
	[[ "$sum_first" == "$got" ]] ||
		error "Mount1 first region wrong: $got vs $sum_first"

	got=$(awk '{print $1}' $result_dir/m2_middle)
	[[ "$sum_middle" == "$got" ]] ||
		error "Mount2 middle region wrong: $got vs $sum_middle"

	got=$(awk '{print $1}' $result_dir/m1_last)
	[[ "$sum_last" == "$got" ]] ||
		error "Mount1 last region wrong: $got vs $sum_last"

	got=$(awk '{print $1}' $result_dir/m2_first)
	[[ "$sum_first" == "$got" ]] ||
		error "Mount2 first region wrong: $got vs $sum_first"

	echo "** Interleaved reads from both mounts successful"
}
run_test 50b "test interleaved partial reads from both mounts during recovery"

test_51a() {
	(( OSTCOUNT >= 8 )) || skip_env "needs >= 8 OSTs"
	enable_ec

	local tf=$DIR/$tfile

	stack_trap "rm -f $tf"

	# Single-component 3+1 EC with 6 data stripes.
	# ec_split_stripes(6, 3) → k0=3, n0=2:
	#   2 stripe sets of 3 stripes per RAID set.
	# swidth = 6 * 1M = 6M.
	# RAID set 0: [0, 6M)
	#   SS0: stripes 0,1,2 → [0, 3M)
	#   SS1: stripes 3,4,5 → [3M, 6M)
	# RAID set 1: [6M, 12M)
	#   SS0: stripes 0,1,2 → [6M, 9M)
	#   SS1: stripes 3,4,5 → [9M, 12M)
	#
	# Test EOF at 4 positions:
	#  2M: inside RS0/SS0 (partial first stripe set)
	#  5M: inside RS0/SS1 (partial second stripe set)
	#  8M: inside RS1/SS0 (second RAID set, first SS)
	# 11M: inside RS1/SS1 (second RAID set, second SS)
	local sizes="2 5 8 11"
	local labels=(
		"RS0/SS0"
		"RS0/SS1"
		"RS1/SS0"
		"RS1/SS1"
	)

	local idx=0
	local sz
	for sz in $sizes; do
		echo "** EOF at ${sz}M (${labels[$idx]})"

		rm -f $tf
		$LFS setstripe -E eof -S 1M -c 6 --ec 3+1 \
			$tf ||
			error "setstripe failed for ${sz}M test"

		dd if=/dev/urandom of=$tf bs=1M count=$sz ||
			error "dd ${sz}M failed"

		$LFS mirror resync $tf ||
			error "resync failed for ${sz}M test"

		local sum=$(md5sum $tf | awk '{print $1}')

		# Collect parity and data OSTs to find
		# safe OSTs (data-only, not reused for
		# parity)
		local ids=($($LFS getstripe $tf |
			awk '/lcme_id/{print $2}'))
		local parity_osts=()
		local data_osts=()
		local id flags osts
		for id in "${ids[@]}"; do
			flags=$($LFS getstripe -I$id $tf |
				awk '/lcme_flags:/{print $2}')
			osts=($($LFS getstripe -I$id $tf |
				awk '/l_ost_idx:/{print $5}' |
				tr -d ","))
			if [[ "$flags" == *parity* ]]; then
				parity_osts+=("${osts[@]}")
			elif (( ${#data_osts[@]} == 0 )); then
				data_osts=("${osts[@]}")
			fi
		done

		local safe=()
		local o p safe_flag
		for o in "${data_osts[@]}"; do
			safe_flag=true
			for p in "${parity_osts[@]}"; do
				(( o == p )) &&
					safe_flag=false && break
			done
			$safe_flag && safe+=($o)
		done

		(( ${#safe[@]} >= 1 )) ||
			skip_env "no safe data OSTs for ${sz}M"
		local ost1=${safe[0]}
		echo "** Disabling data OST $ost1"

		ec_deactivate_ost $ost1

		local sum_after=$(md5sum $tf | awk '{print $1}')
		ec_reactivate_ost $ost1

		[[ "$sum" == "$sum_after" ]] ||
			error "EOF ${sz}M ${labels[$idx]}:"\
				" md5 $sum != $sum_after"

		echo "** EOF at ${sz}M (${labels[$idx]}) OK"
		((idx++))
	done
}
run_test 51a "test EC recovery with EOF inside RAID / stripe sets"

test_52a() {
	(( OSTCOUNT >= 8 )) || skip_env "needs >= 8 OSTs"
	enable_ec

	local tf=$DIR/$tfile

	stack_trap "rm -f $tf"

	# 4+2 EC: pcount=2, so max recoverable = 2 data OSTs
	$LFS setstripe -E eof -S 1M -c 4 --ec 4+2 $tf ||
		error "setstripe --ec 4+2 failed"
	dd if=/dev/urandom of=$tf bs=1M count=8 ||
		error "dd failed"
	$LFS mirror resync $tf || error "resync failed"

	local sum_before=$(md5sum $tf | awk '{print $1}')

	ec_classify_osts $tf
	(( ${#safe_osts[@]} >= 2 )) ||
		skip_env "not enough safe data OSTs (${#safe_osts[@]})"

	echo "** Max degradation: disabling 2 data OSTs:" \
		"${safe_osts[0]} and ${safe_osts[1]}"

	ec_deactivate_ost ${safe_osts[0]}
	ec_deactivate_ost ${safe_osts[1]}
	cancel_lru_locks osc

	local sum_after=$(md5sum $tf | awk '{print $1}')
	[[ "$sum_before" == "$sum_after" ]] ||
		error "max degradation recovery failed:" \
			"$sum_before vs $sum_after"

	echo "** Max degradation (2 of 4 data OSTs down) passed"
}
run_test 52a "test EC recovery at max degradation (pcount data OSTs down)"

test_52b() {
	(( OSTCOUNT >= 8 )) || skip_env "needs >= 8 OSTs"
	enable_ec

	local tf=$DIR/$tfile

	stack_trap "rm -f $tf"

	# 4+2 EC: pcount=2, so 3 data OSTs down = unrecoverable
	$LFS setstripe -E eof -S 1M -c 4 --ec 4+2 $tf ||
		error "setstripe --ec 4+2 failed"
	dd if=/dev/urandom of=$tf bs=1M count=8 ||
		error "dd failed"
	$LFS mirror resync $tf || error "resync failed"

	local sum_before=$(md5sum $tf | awk '{print $1}')

	ec_classify_osts $tf
	(( ${#safe_osts[@]} >= 3 )) ||
		skip_env "not enough safe data OSTs (${#safe_osts[@]})"

	echo "** Over-degradation: disabling 3 data OSTs:" \
		"${safe_osts[0]}, ${safe_osts[1]}," \
		"${safe_osts[2]}"

	ec_deactivate_ost ${safe_osts[0]}
	ec_deactivate_ost ${safe_osts[1]}
	ec_deactivate_ost ${safe_osts[2]}
	cancel_lru_locks osc

	# Read must fail: 3 lost data stripes exceed the pcount=2 parity
	# capacity, so there is nothing left to reconstruct from and an
	# error is the only correct answer.  Handing back bytes at all is
	# silent corruption, and handing back the original content means
	# the fault never took effect.  md5sum runs on its own here rather
	# than in a pipeline, so rc is md5sum's status and not awk's.
	local out sum_after rc=0

	out=$(md5sum $tf 2>/dev/null) || rc=$?
	sum_after=$(echo "$out" | awk '{print $1}')

	if (( rc == 0 )); then
		[[ "$sum_before" != "$sum_after" ]] ||
			error "over-degradation returned the original data," \
				"so failing 3 of 4 data OSTs had no effect"
		error "over-degradation returned data ($sum_after) instead" \
			"of an error with 3 of 4 data OSTs down"
	fi
	echo "** Over-degradation correctly returned read error (rc=$rc)"
}
run_test 52b "test EC over-degradation (pcount+1 data OSTs down)"

test_52c() {
	(( OSTCOUNT >= 8 )) || skip_env "needs >= 8 OSTs"
	enable_ec

	local tf=$DIR/$tfile

	stack_trap "rm -f $tf"

	# 4+2 EC: lose 1 data + 1 parity. Still recoverable
	# since 1 parity remains for the 1 lost data stripe.
	$LFS setstripe -E eof -S 1M -c 4 --ec 4+2 $tf ||
		error "setstripe --ec 4+2 failed"
	dd if=/dev/urandom of=$tf bs=1M count=8 ||
		error "dd failed"
	$LFS mirror resync $tf || error "resync failed"

	local sum_before=$(md5sum $tf | awk '{print $1}')

	ec_classify_osts $tf
	(( ${#safe_osts[@]} >= 1 )) ||
		skip_env "not enough safe data OSTs"
	(( ${#parity_osts[@]} >= 1 )) ||
		skip_env "no parity OSTs found"

	# Pick one safe data OST and one parity OST
	local data_victim=${safe_osts[0]}
	# Find a parity OST that's not also a data OST
	local par_victim=""
	local p o in_data
	for p in "${parity_osts[@]}"; do
		in_data=false
		for o in "${data_osts[@]}"; do
			(( p == o )) &&
				in_data=true && break
		done
		if ! $in_data; then
			par_victim=$p
			break
		fi
	done
	[[ -n "$par_victim" ]] ||
		skip_env "no parity-only OST found"

	echo "** Mixed failure: disabling data" \
		"OST$data_victim + parity OST$par_victim"

	ec_deactivate_ost $data_victim
	ec_deactivate_ost $par_victim
	cancel_lru_locks osc

	# With 4+2, losing 1 data + 1 parity = 2 failures
	# total, within pcount tolerance. The remaining
	# parity stripe provides enough data to recover
	# the single lost data stripe.
	local sum_after=$(md5sum $tf | awk '{print $1}')
	[[ "$sum_before" == "$sum_after" ]] ||
		error "mixed parity+data failure recovery" \
			"failed: $sum_before vs $sum_after"

	echo "** Mixed parity+data failure recovery passed"
}
run_test 52c "test EC recovery with mixed parity and data OST failure"

test_53a() {
	(( OSTCOUNT >= 8 )) || skip_env "needs >= 8 OSTs"
	enable_ec

	local tf=$DIR/$tfile

	stack_trap "rm -f $tf"

	# Single-component 3+1 EC with 6 data stripes.
	# swidth = 6M. Write 12M (2 full RAID sets).
	# Then read from various non-zero offsets with
	# one data OST down, forcing EC recovery with
	# crw_pos != 0.
	$LFS setstripe -E eof -S 1M -c 6 --ec 3+1 $tf ||
		error "setstripe failed"
	dd if=/dev/urandom of=$tf bs=1M count=12 ||
		error "dd failed"
	$LFS mirror resync $tf || error "resync failed"

	ec_classify_osts $tf
	(( ${#safe_osts[@]} >= 1 )) ||
		skip_env "no safe data OSTs"
	local victim=${safe_osts[0]}

	# Offsets to test (in 1M blocks):
	#  1M: start of stripe 1 in RS0/SS0
	#  3M: start of SS1 in RS0
	#  5M: start of stripe 5 in RS0/SS1
	#  6M: exact RAID set boundary (RS1 start)
	#  7M: start of stripe 1 of the second row, RS1/SS0
	# 10M: start of stripe 4 of the second row, RS1/SS1
	local offsets="1 3 5 6 7 10"

	local off
	for off in $offsets; do
		local remain=$((12 - off))

		# Get reference read from healthy file
		local ref=$(dd if=$tf bs=1M skip=$off \
			count=$remain 2>/dev/null | md5sum |
			awk '{print $1}')

		echo "** offset=${off}M count=${remain}M"

		ec_deactivate_ost $victim
		cancel_lru_locks osc

		local got=$(dd if=$tf bs=1M skip=$off \
			count=$remain 2>/dev/null | md5sum |
			awk '{print $1}')

		ec_reactivate_ost $victim

		[[ "$ref" == "$got" ]] ||
			error "offset ${off}M: $ref != $got"
	done
}
run_test 53a "test EC recovery with non-zero read offsets"

test_55a() {
	(( OSTCOUNT >= 8 )) || skip_env "needs >= 8 OSTs"
	enable_ec

	local tf=$DIR/$tfile

	stack_trap "rm -f $tf"

	# Tiny files: 1 byte and 4K (one page).
	# With 3+1 EC and 6 data stripes, only stripe 0
	# has any data. All other stripes are beyond-EOF.
	# Tests the i_size cap in lov_ecio_add_data_sub
	# that prevents false LSS_READ_ERR for empty stripes.
	local -a sizes=(1 4096)
	local -a labels=("1-byte" "4K-one-page")

	local i
	for ((i = 0; i < ${#sizes[@]}; i++)); do
		local sz=${sizes[$i]}
		echo "** ${labels[$i]} (${sz} bytes)"

		rm -f $tf
		$LFS setstripe -E eof -S 1M -c 6 --ec 3+1 \
			$tf ||
			error "setstripe failed for ${labels[$i]}"
		dd if=/dev/urandom of=$tf bs=1 count=$sz ||
			error "dd ${labels[$i]} failed"
		$LFS mirror resync $tf ||
			error "resync ${labels[$i]} failed"

		local sum=$(md5sum $tf | awk '{print $1}')

		# Only stripe 0 holds data in a file this small, and
		# safe_osts is sorted by OST index, so its first entry is
		# usually an OST this read never touches.
		ec_data_stripe_osts $tf 0
		local victim=${EC_STRIPE_OSTS[0]}

		ec_deactivate_ost $victim
		cancel_lru_locks osc

		local got=$(md5sum $tf | awk '{print $1}')
		ec_reactivate_ost $victim

		[[ "$sum" == "$got" ]] ||
			error "${labels[$i]}: $sum != $got"

		echo "** ${labels[$i]} OK"
	done
}
run_test 55a "test EC recovery with tiny files (1 byte, one page)"

test_58a() {
	(( OSTCOUNT >= 8 )) || skip_env "needs >= 8 OSTs"
	enable_ec

	# Test parity_used[] with every 2-OST combination in
	# a 4+2 layout. With 2 parity OSTs, degrading 2 data
	# OSTs exercises parity selection and coefficient matrix
	# construction in lov_recover_data().
	local tf=$DIR/$tfile

	stack_trap "rm -f $tf"

	$LFS setstripe -S 1M -c 4 --ec 4+2 $tf ||
		error "setstripe 4+2 failed"
	dd if=/dev/urandom of=$tf bs=1M count=8 ||
		error "dd 8M failed"
	$LFS mirror resync $tf || error "resync failed"

	local sum=$(md5sum $tf | awk '{print $1}')

	ec_classify_osts $tf
	local n=${#safe_osts[@]}
	(( n >= 2 )) || skip_env "need >= 2 safe data OSTs"

	local i j
	for ((i = 0; i < n - 1; i++)); do
		for ((j = i + 1; j < n; j++)); do
			ec_deactivate_ost ${safe_osts[$i]}
			ec_deactivate_ost ${safe_osts[$j]}
			cancel_lru_locks osc

			local got=$(md5sum $tf | awk '{print $1}')
			ec_reactivate_ost ${safe_osts[$j]}
			ec_reactivate_ost ${safe_osts[$i]}

			[[ "$sum" == "$got" ]] ||
				error "2-of-${n} OST${safe_osts[$i]}+" \
					"OST${safe_osts[$j]}:" \
					"$sum != $got"
			echo "  OST${safe_osts[$i]}+" \
				"OST${safe_osts[$j]} OK"
		done
	done
}
run_test 58a "EC parity_used[] with 2-OST combos in 4+2 layout"

test_58b() {
	(( OSTCOUNT >= 8 )) || skip_env "needs >= 8 OSTs"
	enable_ec

	# Combined stress: PFL 4+2 with 2 degraded data stripes
	# and stripe rotation from non-zero e_start. Tests the
	# interaction of parity_eof, err_array, and parity_used[]
	# all together under PFL offset mapping.
	local tf=$DIR/$tfile

	stack_trap "rm -f $tf"
	local sizes="3 5 8 12 17"

	for sz in $sizes; do
		rm -f $tf
		$LFS setstripe \
			-E 4M -S 1M -c 4 --ec 4+2 \
			-E eof -S 1M -c 4 --ec 4+2 $tf ||
			error "setstripe PFL 4+2 failed"
		dd if=/dev/urandom of=$tf bs=1M count=$sz ||
			error "dd ${sz}M failed"
		$LFS mirror resync $tf ||
			error "resync ${sz}M failed"

		local sum=$(md5sum $tf | awk '{print $1}')

		ec_classify_osts $tf
		(( ${#safe_osts[@]} >= 2 )) ||
			skip_env "need >= 2 safe data OSTs (sz=$sz)"
		local v1=${safe_osts[0]}
		local v2=${safe_osts[1]}

		ec_deactivate_ost $v1
		ec_deactivate_ost $v2
		cancel_lru_locks osc

		local got=$(md5sum $tf | awk '{print $1}')
		ec_reactivate_ost $v2
		ec_reactivate_ost $v1

		[[ "$sum" == "$got" ]] ||
			error "PFL 4+2 sz=${sz}M " \
				"OST${v1}+OST${v2}:" \
				"$sum != $got"
		echo "  sz=${sz}M OST${v1}+OST${v2} OK"
	done
}
run_test 58b "EC combined PFL 4+2 with 2 degraded + rotation"

test_59a() {
	(( OSTCOUNT >= 8 )) || skip_env "needs >= 8 OSTs"
	enable_ec

	local tf=$DIR/$tfile

	stack_trap "rm -f $tf"

	# PFL: [0,4M) + [4M,EOF), both 4+2 (2 parity for 2-down)
	$LFS setstripe \
		-E 4M -S 1M -c 4 --ec 4+2 \
		-E eof -S 1M -c 4 --ec 4+2 $tf ||
		error "setstripe PFL failed"

	# Write 2M, skip to 6M, write 4M => spans comp boundary
	dd if=/dev/urandom of=$tf bs=1M count=2 ||
		error "dd first 2M failed"
	dd if=/dev/urandom of=$tf bs=1M count=4 seek=6 ||
		error "dd at offset 6M failed"
	$LFS mirror resync $tf || error "resync failed"

	local sum=$(md5sum $tf | awk '{print $1}')
	ec_classify_osts $tf
	(( ${#safe_osts[@]} >= 1 )) ||
		skip_env "no safe data OSTs"

	local ost
	for ost in "${safe_osts[@]}"; do
		ec_deactivate_ost $ost
		cancel_lru_locks osc
		local got=$(md5sum $tf | awk '{print $1}')
		ec_reactivate_ost $ost
		[[ "$sum" == "$got" ]] ||
			error "PFL sparse OST$ost:" \
				"$sum != $got"
		echo "  OST$ost OK"
	done

	# 2-down test (needs pcount >= 2, so 4+2 layout)
	(( ${#safe_osts[@]} >= 2 )) ||
		skip_env "need >= 2 safe data OSTs for 2-down"

	local v1=${safe_osts[0]}
	local v2=${safe_osts[1]}
	ec_deactivate_ost $v1
	ec_deactivate_ost $v2
	cancel_lru_locks osc
	local got=$(md5sum $tf | awk '{print $1}')
	ec_reactivate_ost $v2
	ec_reactivate_ost $v1
	[[ "$sum" == "$got" ]] ||
		error "PFL sparse 2-down: $sum != $got"
	echo "** PFL sparse with 2 OSTs down OK"
}
run_test 59a "EC recovery with sparse PFL file spanning components"

test_60a() {
	(( OSTCOUNT >= 6 )) || skip_env "needs >= 6 OSTs"
	enable_ec

	local tf=$DIR/$tfile

	stack_trap "rm -f $tf"

	# 4+2 layout, 1M stripes, swidth=4M
	$LFS setstripe -S 1M -c 4 --ec 4+2 $tf ||
		error "setstripe failed"

	# Write 8M (2 full stripe sets), resync parity
	dd if=/dev/urandom of=$tf bs=1M count=8 ||
		error "dd write failed"
	$LFS mirror resync $tf || error "resync failed"

	# Truncate mid-stripe to 2.5M while healthy
	$TRUNCATE $tf $((2 * 1024 * 1024 + 512 * 1024)) ||
		error "truncate to 2.5M failed"
	# Resync so parity reflects truncated state
	$LFS mirror resync $tf || error "resync after trunc failed"

	# Capture reference checksum
	local sum=$(md5sum $tf | awk '{print $1}')

	# Now degrade and verify EC recovery
	ec_classify_osts $tf
	(( ${#safe_osts[@]} >= 1 )) ||
		skip_env "need >= 1 safe data OST"
	local victim=${safe_osts[0]}
	echo "** Deactivating data OST $victim"

	ec_deactivate_ost $victim
	cancel_lru_locks osc

	local sz=$(stat -c %s $tf)
	(( sz == 2 * 1024 * 1024 + 512 * 1024 )) ||
		error "size mismatch: $sz != 2621440"

	local got=$(md5sum $tf | awk '{print $1}')
	[[ "$sum" == "$got" ]] ||
		error "60a: mid-stripe truncate recovery failed:" \
			"$sum != $got"

	echo "** EC recovery after mid-stripe truncate OK"
}
run_test 60a "EC recovery after mid-stripe truncate"

test_60b() {
	(( OSTCOUNT >= 6 )) || skip_env "needs >= 6 OSTs"
	enable_ec

	local tf=$DIR/$tfile

	stack_trap "rm -f $tf"

	$LFS setstripe -S 1M -c 4 --ec 4+2 $tf ||
		error "setstripe failed"

	# Write 12M (3 stripe sets), resync
	dd if=/dev/urandom of=$tf bs=1M count=12 ||
		error "dd write failed"
	$LFS mirror resync $tf || error "resync failed"

	# Save checksum of first 4M (one swidth)
	local sum_4m=$(dd if=$tf bs=1M count=4 2>/dev/null |
		md5sum | awk '{print $1}')

	# Truncate to swidth boundary while healthy
	$TRUNCATE $tf $((4 * 1024 * 1024)) ||
		error "truncate to 4M failed"
	$LFS mirror resync $tf ||
		error "resync after trunc failed"

	local sum=$(md5sum $tf | awk '{print $1}')
	# First 4M should be unchanged
	[[ "$sum_4m" == "$sum" ]] ||
		error "data changed by truncate: $sum_4m != $sum"

	# Degrade and verify recovery
	ec_classify_osts $tf
	(( ${#safe_osts[@]} >= 1 )) ||
		skip_env "need >= 1 safe data OST"
	local victim=${safe_osts[0]}
	echo "** Deactivating data OST $victim"

	ec_deactivate_ost $victim
	cancel_lru_locks osc

	local got=$(md5sum $tf | awk '{print $1}')
	[[ "$sum" == "$got" ]] ||
		error "60b: stripe-boundary truncate recovery" \
			"failed: $sum != $got"

	local sz=$(stat -c %s $tf)
	(( sz == 4 * 1024 * 1024 )) ||
		error "size mismatch: $sz != 4194304"

	echo "** EC recovery after stripe-boundary truncate OK"
}
run_test 60b "EC recovery after stripe-boundary truncate"

test_60c() {
	(( OSTCOUNT >= 6 )) || skip_env "needs >= 6 OSTs"
	enable_ec

	local tf=$DIR/$tfile

	stack_trap "rm -f $tf"

	$LFS setstripe -S 1M -c 4 --ec 4+2 $tf ||
		error "setstripe failed"

	# Write 8M, resync, then truncate to zero
	dd if=/dev/urandom of=$tf bs=1M count=8 ||
		error "dd write failed"
	$LFS mirror resync $tf || error "resync failed"
	$TRUNCATE $tf 0 || error "truncate to 0 failed"

	# Degrade and verify file reads as empty
	ec_classify_osts $tf
	(( ${#safe_osts[@]} >= 1 )) ||
		skip_env "need >= 1 safe data OST"
	local victim=${safe_osts[0]}
	echo "** Deactivating data OST $victim"

	ec_deactivate_ost $victim
	cancel_lru_locks osc

	local sz=$(stat -c %s $tf)
	(( sz == 0 )) || error "size not 0 after truncate: $sz"

	# read through the client rather than asking fstat: wc -c on a
	# regular file takes the stat shortcut and issues no read at all
	local bytes=$(dd if=$tf bs=1M 2>/dev/null | wc -c)
	(( bytes == 0 )) ||
		error "read returned $bytes bytes from truncated file"

	# Re-write while still degraded should fail or succeed
	# depending on whether victim stripe is used; just verify
	# recovery works after reactivate+write+resync
	ec_reactivate_ost $victim
	dd if=/dev/urandom of=$tf bs=1M count=4 ||
		error "dd re-write failed"
	$LFS mirror resync $tf || error "resync failed"

	local sum=$(md5sum $tf | awk '{print $1}')
	ec_deactivate_ost $victim
	cancel_lru_locks osc

	local got=$(md5sum $tf | awk '{print $1}')
	[[ "$sum" == "$got" ]] ||
		error "60c: re-write after truncate recovery" \
			"failed: $sum != $got"

	echo "** EC recovery after truncate-to-zero OK"
}
run_test 60c "EC recovery after truncate to zero and re-write"

test_60d() {
	(( OSTCOUNT >= 6 )) || skip_env "needs >= 6 OSTs"
	enable_ec

	local tf=$DIR/$tfile

	stack_trap "rm -f $tf"

	# PFL: comp1 [0,16M] 2+1, comp2 [16M,EOF] 4+2
	$LFS setstripe -E 16M -S 1M -c 2 --ec 2+1 \
		-E eof -S 1M -c 4 --ec 4+2 $tf ||
		error "setstripe PFL failed"

	# Write 32M spanning both components, resync
	dd if=/dev/urandom of=$tf bs=1M count=32 ||
		error "dd write failed"
	$LFS mirror resync $tf || error "resync failed"

	# Save comp1 checksum before truncate
	local sum_comp1=$(dd if=$tf bs=1M count=16 \
		2>/dev/null | md5sum | awk '{print $1}')

	# Truncate at component boundary (16M) — removes comp2
	$TRUNCATE $tf $((16 * 1024 * 1024)) ||
		error "truncate to 16M failed"
	$LFS mirror resync $tf ||
		error "resync after trunc failed"

	local sum=$(md5sum $tf | awk '{print $1}')
	[[ "$sum_comp1" == "$sum" ]] ||
		error "comp1 changed by truncate: $sum_comp1 != $sum"

	# Degrade and verify comp1 recovers.  The truncate leaves comp2 in
	# the layout, so its OSTs stay in the classification even though
	# nothing reads them now; restrict the choice to comp1's own two
	# data stripes, both of which still hold 8M of the file.  This
	# layout needs 9 objects and rarely gets 9 distinct OSTs, so one
	# of the two usually doubles as parity.
	ec_pick_data_ost $tf 0 1 ||
		skip_env "no parity-free data OST in comp1"
	local victim=$EC_PICKED_OST
	echo "** Deactivating data OST $victim"

	ec_deactivate_ost $victim
	cancel_lru_locks osc

	local got=$(md5sum $tf | awk '{print $1}')
	[[ "$sum" == "$got" ]] ||
		error "60d: comp-boundary truncate recovery" \
			"failed: $sum != $got"

	local sz=$(stat -c %s $tf)
	(( sz == 16 * 1024 * 1024 )) ||
		error "size mismatch: $sz != 16777216"

	# Verify no data beyond comp boundary
	local tail=$(dd if=$tf bs=1M skip=16 count=1 \
		2>/dev/null | wc -c)
	(( tail == 0 )) ||
		error "got $tail bytes beyond comp boundary"

	echo "** EC recovery after PFL comp-boundary truncate OK"
}
run_test 60d "EC recovery after PFL component-boundary truncate"

test_61a() {
	(( OSTCOUNT >= 8 )) || skip_env "needs >= 8 OSTs"
	enable_ec

	local tf=$DIR/$tfile

	stack_trap "rm -f $tf"

	# 4+2 layout, 1M stripes: test various sub-stripe sizes
	local -a sizes=(4096 32768 65536 524288 $((1048576 - 1)))
	local -a labels=(
		"4K" "32K" "64K" "512K" "1M-1"
	)

	local i
	for ((i = 0; i < ${#sizes[@]}; i++)); do
		rm -f $tf
		$LFS setstripe -S 1M -c 4 --ec 4+2 $tf ||
			error "setstripe failed"
		dd if=/dev/urandom of=$tf \
			bs=${sizes[$i]} count=1 ||
			error "dd ${labels[$i]} failed"
		$LFS mirror resync $tf ||
			error "resync ${labels[$i]} failed"
		local sum=$(md5sum $tf | awk '{print $1}')

		# Every size here is under one 1M stripe, so the data lives
		# on stripe 0 alone; safe_osts[0] is the lowest OST index
		# and usually holds none of it.
		ec_data_stripe_osts $tf 0
		local victim=${EC_STRIPE_OSTS[0]}

		ec_deactivate_ost $victim
		cancel_lru_locks osc
		local got=$(md5sum $tf | awk '{print $1}')
		ec_reactivate_ost $victim
		[[ "$sum" == "$got" ]] ||
			error "${labels[$i]} OST$victim:" \
				"$sum != $got"
		echo "  ${labels[$i]} OK"
	done
}
run_test 61a "EC recovery with various sub-stripe file sizes"

test_61b() {
	(( OSTCOUNT >= 8 )) || skip_env "needs >= 8 OSTs"
	enable_ec

	local tf=$DIR/$tfile

	stack_trap "rm -f $tf"

	# 64K stripe: sub-stripe means < 64K
	local -a sizes=(1 4096 $((65536 - 1)))
	local -a labels=("1-byte" "4K" "64K-1")

	local i
	for ((i = 0; i < ${#sizes[@]}; i++)); do
		rm -f $tf
		$LFS setstripe -S 64K -c 4 --ec 4+2 $tf ||
			error "setstripe 64K failed"
		dd if=/dev/urandom of=$tf \
			bs=${sizes[$i]} count=1 ||
			error "dd ${labels[$i]} failed"
		$LFS mirror resync $tf ||
			error "resync ${labels[$i]} failed"
		local sum=$(md5sum $tf | awk '{print $1}')

		ec_classify_osts $tf
		(( ${#safe_osts[@]} >= 1 )) ||
			skip_env "no safe data OSTs"

		local ost
		for ost in "${safe_osts[@]}"; do
			ec_deactivate_ost $ost
			cancel_lru_locks osc
			local got=$(md5sum $tf | awk '{print $1}')
			ec_reactivate_ost $ost
			[[ "$sum" == "$got" ]] ||
				error "${labels[$i]} " \
					"OST$ost:" \
					"$sum != $got"
		done
		echo "  ${labels[$i]} OK (all safe OSTs)"
	done
}
run_test 61b "EC recovery with sub-stripe files and 64K stripe"

test_61c() {
	(( OSTCOUNT >= 8 )) || skip_env "needs >= 8 OSTs"
	enable_ec

	local tf=$DIR/$tfile

	stack_trap "rm -f $tf"

	# PFL: comp1 [0,4M) 4+2 64K stripes (swidth=256K)
	# comp2 [4M,EOF) 4+2 1M stripes
	# File stays within comp1 at sub-stripe size
	$LFS setstripe \
		-E 4M -S 64K -c 4 --ec 4+2 \
		-E eof -S 1M -c 4 --ec 4+2 $tf ||
		error "setstripe PFL failed"

	# Write less than one 64K stripe (32K)
	dd if=/dev/urandom of=$tf bs=32K count=1 ||
		error "dd 32K failed"
	$LFS mirror resync $tf || error "resync failed"

	local sum=$(md5sum $tf | awk '{print $1}')
	ec_classify_osts $tf
	(( ${#safe_osts[@]} >= 1 )) ||
		skip_env "no safe data OSTs"

	local ost
	for ost in "${safe_osts[@]}"; do
		ec_deactivate_ost $ost
		cancel_lru_locks osc
		local got=$(md5sum $tf | awk '{print $1}')
		ec_reactivate_ost $ost
		[[ "$sum" == "$got" ]] ||
			error "PFL sub-stripe OST$ost:" \
				"$sum != $got"
		echo "  OST$ost OK"
	done
}
run_test 61c "EC recovery sub-stripe file in PFL with 64K stripes"

test_61d() {
	(( OSTCOUNT >= 8 )) || skip_env "needs >= 8 OSTs"
	enable_ec

	local tf=$DIR/$tfile

	stack_trap "rm -f $tf"

	# 4+2, 1M stripe: sub-stripe file with 2 OSTs down
	$LFS setstripe -S 1M -c 4 --ec 4+2 $tf ||
		error "setstripe failed"

	dd if=/dev/urandom of=$tf bs=100K count=1 ||
		error "dd 100K failed"
	$LFS mirror resync $tf || error "resync failed"

	local sum=$(md5sum $tf | awk '{print $1}')

	# A 100K file occupies stripe 0 only.  Fail that stripe's OST plus
	# one more, so the 2-down case really loses the stripe being read
	# rather than two OSTs the read never reaches.
	ec_data_stripe_osts $tf 0 1

	local v1=${EC_STRIPE_OSTS[0]}
	local v2=${EC_STRIPE_OSTS[1]}

	ec_deactivate_ost $v1
	ec_deactivate_ost $v2
	cancel_lru_locks osc
	local got=$(md5sum $tf | awk '{print $1}')
	ec_reactivate_ost $v2
	ec_reactivate_ost $v1
	[[ "$sum" == "$got" ]] ||
		error "sub-stripe 2-down: $sum != $got"
	echo "** sub-stripe 100K with 2 OSTs down OK"
}
run_test 61d "EC recovery sub-stripe file with 2 OST failures"

test_62a() {
	(( OSTCOUNT >= 8 )) || skip_env "needs >= 8 OSTs"
	enable_ec

	local tf=$DIR/$tfile

	stack_trap "rm -f $tf"

	# 4+2 layout, 1M stripes
	$LFS setstripe -S 1M -c 4 --ec 4+2 $tf ||
		error "setstripe failed"

	# Append in multiple stages, resync after each
	local stage
	for stage in 1 2 3; do
		dd if=/dev/urandom of=$tf bs=1M count=2 \
			oflag=append conv=notrunc ||
			error "append stage $stage failed"
		$LFS mirror resync $tf ||
			error "resync stage $stage failed"
	done

	# File should be 6M total
	local sz=$(stat -c %s $tf)
	(( sz == 6 * 1048576 )) ||
		error "size mismatch: $sz != 6291456"

	local sum=$(md5sum $tf | awk '{print $1}')
	ec_classify_osts $tf
	(( ${#safe_osts[@]} >= 1 )) ||
		skip_env "no safe data OSTs"

	local ost
	for ost in "${safe_osts[@]}"; do
		ec_deactivate_ost $ost
		cancel_lru_locks osc
		local got=$(md5sum $tf | awk '{print $1}')
		ec_reactivate_ost $ost
		[[ "$sum" == "$got" ]] ||
			error "append OST$ost: $sum != $got"
		echo "  OST$ost OK"
	done
}
run_test 62a "EC recovery after multiple append writes"

test_62b() {
	(( OSTCOUNT >= 8 )) || skip_env "needs >= 8 OSTs"
	enable_ec

	local tf=$DIR/$tfile

	stack_trap "rm -f $tf"

	# 4+2 layout, 1M stripes: append non-aligned amounts
	# with a single resync after the last one
	$LFS setstripe -S 1M -c 4 --ec 4+2 $tf ||
		error "setstripe failed"

	# Append 3M, 1M, 512K, 3M = 7.5M total (unaligned)

	dd if=/dev/urandom of=$tf bs=1M count=3 ||
		error "append 3M failed"
	dd if=/dev/urandom of=$tf bs=1M count=1 \
		oflag=append conv=notrunc ||
		error "append 1M failed"
	dd if=/dev/urandom of=$tf bs=512K count=1 \
		oflag=append conv=notrunc ||
		error "append 512K failed"
	dd if=/dev/urandom of=$tf bs=1M count=3 \
		oflag=append conv=notrunc ||
		error "append last 3M failed"
	$LFS mirror resync $tf || error "resync failed"

	local sum=$(md5sum $tf | awk '{print $1}')
	local sz=$(stat -c %s $tf)
	echo "  file size: $sz"

	ec_classify_osts $tf
	(( ${#safe_osts[@]} >= 2 )) ||
		skip_env "need >= 2 safe data OSTs"

	# 1-down
	local victim=${safe_osts[0]}
	ec_deactivate_ost $victim
	cancel_lru_locks osc
	local got=$(md5sum $tf | awk '{print $1}')
	ec_reactivate_ost $victim
	[[ "$sum" == "$got" ]] ||
		error "unaligned append 1-down:" \
			"$sum != $got"
	echo "  1-down OK"

	# 2-down
	local v1=${safe_osts[0]}
	local v2=${safe_osts[1]}
	ec_deactivate_ost $v1
	ec_deactivate_ost $v2
	cancel_lru_locks osc
	got=$(md5sum $tf | awk '{print $1}')
	ec_reactivate_ost $v2
	ec_reactivate_ost $v1
	[[ "$sum" == "$got" ]] ||
		error "unaligned append 2-down:" \
			"$sum != $got"
	echo "** unaligned append with 2-down OK"
}
run_test 62b "EC recovery after unaligned append writes"

test_62c() {
	(( OSTCOUNT >= 8 )) || skip_env "needs >= 8 OSTs"
	enable_ec

	local tf=$DIR/$tfile

	stack_trap "rm -f $tf"

	# PFL: [0,4M) + [4M,EOF), both 4+2
	$LFS setstripe \
		-E 4M -S 1M -c 4 --ec 4+2 \
		-E eof -S 1M -c 4 --ec 4+2 $tf ||
		error "setstripe PFL failed"

	# Write 2M in comp1, resync
	dd if=/dev/urandom of=$tf bs=1M count=2 ||
		error "dd 2M failed"
	$LFS mirror resync $tf || error "resync 1 failed"

	# Append to cross into comp2
	dd if=/dev/urandom of=$tf bs=1M count=4 \
		oflag=append conv=notrunc ||
		error "append 4M failed"
	$LFS mirror resync $tf || error "resync 2 failed"

	local sum=$(md5sum $tf | awk '{print $1}')
	ec_classify_osts $tf
	(( ${#safe_osts[@]} >= 1 )) ||
		skip_env "no safe data OSTs"

	local ost
	for ost in "${safe_osts[@]}"; do
		ec_deactivate_ost $ost
		cancel_lru_locks osc
		local got=$(md5sum $tf | awk '{print $1}')
		ec_reactivate_ost $ost
		[[ "$sum" == "$got" ]] ||
			error "PFL append OST$ost:" \
				"$sum != $got"
		echo "  OST$ost OK"
	done
}
run_test 62c "EC recovery after append crossing PFL boundary"

test_62d() {
	(( OSTCOUNT >= 8 )) || skip_env "needs >= 8 OSTs"
	enable_ec

	local tf=$DIR/$tfile

	stack_trap "rm -f $tf"

	# 4+2 layout, 1M stripes
	$LFS setstripe -S 1M -c 4 --ec 4+2 $tf ||
		error "setstripe failed"

	# Append sub-stripe amounts repeatedly
	local i
	for ((i = 0; i < 10; i++)); do
		dd if=/dev/urandom of=$tf bs=100K count=1 \
			oflag=append conv=notrunc ||
			error "append $i failed"
	done
	# Total: 1000K, which is still inside the first 1M stripe
	$LFS mirror resync $tf || error "resync failed"

	local sum=$(md5sum $tf | awk '{print $1}')

	# All 1000K sit on stripe 0, so both victims are chosen from the
	# stripe map rather than from the numerically sorted safe_osts,
	# whose first entries usually hold none of this file's data.
	ec_data_stripe_osts $tf 0 1

	# Single degradation
	local victim=${EC_STRIPE_OSTS[0]}
	ec_deactivate_ost $victim
	cancel_lru_locks osc
	local got=$(md5sum $tf | awk '{print $1}')
	ec_reactivate_ost $victim
	[[ "$sum" == "$got" ]] ||
		error "sub-stripe appends 1-down:" \
			"$sum != $got"
	echo "  1-down OK"

	# Double degradation
	local v1=${EC_STRIPE_OSTS[0]}
	local v2=${EC_STRIPE_OSTS[1]}
	ec_deactivate_ost $v1
	ec_deactivate_ost $v2
	cancel_lru_locks osc
	got=$(md5sum $tf | awk '{print $1}')
	ec_reactivate_ost $v2
	ec_reactivate_ost $v1
	[[ "$sum" == "$got" ]] ||
		error "sub-stripe appends 2-down:" \
			"$sum != $got"
	echo "** sub-stripe appends with 2-down OK"
}
run_test 62d "EC recovery after many sub-stripe appends"

test_63a() {
	(( OSTCOUNT >= 8 )) || skip_env "needs >= 8 OSTs"
	enable_ec

	local tf=$DIR/$tfile

	stack_trap "rm -f $tf"

	# 4+2 layout, 1M stripes
	$LFS setstripe -S 1M -c 4 --ec 4+2 $tf ||
		error "setstripe failed"
	dd if=/dev/urandom of=$tf bs=1M count=8 ||
		error "dd 8M failed"
	$LFS mirror resync $tf || error "resync failed"

	local sum=$(md5sum $tf | awk '{print $1}')
	ec_classify_osts $tf
	(( ${#safe_osts[@]} >= 3 )) ||
		skip_env "need >= 3 safe data OSTs"

	# Cycle through each safe OST one at a time
	local ost
	for ost in "${safe_osts[@]}"; do
		ec_deactivate_ost $ost
		cancel_lru_locks osc
		local got=$(md5sum $tf | awk '{print $1}')
		ec_reactivate_ost $ost
		[[ "$sum" == "$got" ]] ||
			error "cycle OST$ost: $sum != $got"
		echo "  OST$ost OK"
	done
	echo "** single-OST cycling OK"
}
run_test 63a "EC recovery cycling through each data OST"

test_63b() {
	(( OSTCOUNT >= 8 )) || skip_env "needs >= 8 OSTs"
	enable_ec

	local tf=$DIR/$tfile

	stack_trap "rm -f $tf"

	# 4+2 layout, 1M stripes
	$LFS setstripe -S 1M -c 4 --ec 4+2 $tf ||
		error "setstripe failed"
	dd if=/dev/urandom of=$tf bs=1M count=8 ||
		error "dd 8M failed"
	$LFS mirror resync $tf || error "resync failed"

	local sum=$(md5sum $tf | awk '{print $1}')
	ec_classify_osts $tf
	(( ${#safe_osts[@]} >= 3 )) ||
		skip_env "need >= 3 safe data OSTs"

	# Cycle through all 2-OST combinations
	local i j
	for ((i = 0; i < ${#safe_osts[@]} - 1; i++)); do
		for ((j = i + 1; j < ${#safe_osts[@]}; j++)); do
			local v1=${safe_osts[$i]}
			local v2=${safe_osts[$j]}
			ec_deactivate_ost $v1
			ec_deactivate_ost $v2
			cancel_lru_locks osc
			local got=$(md5sum $tf |
				awk '{print $1}')
			ec_reactivate_ost $v2
			ec_reactivate_ost $v1
			[[ "$sum" == "$got" ]] ||
				error "pair OST${v1}+" \
					"OST${v2}:" \
					"$sum != $got"
			echo "  OST${v1}+OST${v2} OK"
		done
	done
}
run_test 63b "EC recovery cycling all 2-OST failure pairs"

test_63c() {
	(( OSTCOUNT >= 8 )) || skip_env "needs >= 8 OSTs"
	enable_ec

	local tf=$DIR/$tfile

	stack_trap "rm -f $tf"

	# 4+2 layout, 1M stripes
	$LFS setstripe -S 1M -c 4 --ec 4+2 $tf ||
		error "setstripe failed"
	dd if=/dev/urandom of=$tf bs=1M count=8 ||
		error "dd 8M failed"
	$LFS mirror resync $tf || error "resync failed"

	local sum=$(md5sum $tf | awk '{print $1}')
	ec_classify_osts $tf
	(( ${#safe_osts[@]} >= 2 )) ||
		skip_env "need >= 2 safe data OSTs"

	# Deactivate first, read, deactivate second while
	# first is still down, read again, then recover
	local v1=${safe_osts[0]}
	local v2=${safe_osts[1]}

	ec_deactivate_ost $v1
	cancel_lru_locks osc
	local got1=$(md5sum $tf | awk '{print $1}')
	[[ "$sum" == "$got1" ]] ||
		error "1-down OST$v1: $sum != $got1"
	echo "  1-down OST$v1 OK"

	# Add second failure while first still down
	ec_deactivate_ost $v2
	cancel_lru_locks osc
	local got2=$(md5sum $tf | awk '{print $1}')
	[[ "$sum" == "$got2" ]] ||
		error "2-down OST${v1}+OST${v2}:" \
			"$sum != $got2"
	echo "  2-down OST${v1}+OST${v2} OK"

	# Recover first, read with only second down
	ec_reactivate_ost $v1
	cancel_lru_locks osc
	local got3=$(md5sum $tf | awk '{print $1}')
	ec_reactivate_ost $v2
	[[ "$sum" == "$got3" ]] ||
		error "swap to OST$v2-only:" \
			"$sum != $got3"
	echo "** progressive fail/recover cycle OK"
}
run_test 63c "EC recovery with progressive failure and partial recovery"

test_63d() {
	(( OSTCOUNT >= 8 )) || skip_env "needs >= 8 OSTs"
	enable_ec

	local tf=$DIR/$tfile

	stack_trap "rm -f $tf"

	# PFL: [0,4M) 4+2, [4M,EOF) 4+2
	$LFS setstripe \
		-E 4M -S 1M -c 4 --ec 4+2 \
		-E eof -S 1M -c 4 --ec 4+2 $tf ||
		error "setstripe PFL failed"
	dd if=/dev/urandom of=$tf bs=1M count=12 ||
		error "dd 12M failed"
	$LFS mirror resync $tf || error "resync failed"

	local sum=$(md5sum $tf | awk '{print $1}')
	ec_classify_osts $tf
	(( ${#safe_osts[@]} >= 3 )) ||
		skip_env "need >= 3 safe data OSTs"

	# Cycle: fail OST A, verify, reactivate A,
	# fail OST B, verify, reactivate B, ...
	local prev=""
	local ost
	for ost in "${safe_osts[@]}"; do
		[[ -n "$prev" ]] && ec_reactivate_ost $prev
		ec_deactivate_ost $ost
		cancel_lru_locks osc
		local got=$(md5sum $tf | awk '{print $1}')
		[[ "$sum" == "$got" ]] ||
			error "PFL cycle OST$ost:" \
				"$sum != $got"
		echo "  OST$ost OK"
		prev=$ost
	done
	[[ -n "$prev" ]] && ec_reactivate_ost $prev
	echo "** PFL OST cycling OK"
}
run_test 63d "EC recovery cycling OSTs with PFL layout"

test_64a() {
	(( OSTCOUNT >= 8 )) || skip_env "needs >= 8 OSTs"

	local tf=$DIR/$tfile

	stack_trap "rm -f $tf"
	local tf2=$DIR2/$tfile

	# check_and_setup_lustre() already mounts $MOUNT2 when the
	# session sets MOUNT_2; only tear down a mount this test made.
	if ! is_mounted $MOUNT2; then
		mkdir -p $MOUNT2
		mount_client $MOUNT2 || error "mount_client $MOUNT2 failed"
		stack_trap "umount_client $MOUNT2"
	fi
	enable_ec

	$LFS setstripe -E eof -S 1M -c 4 --ec 4+2 $tf ||
		error "setstripe failed"
	dd if=/dev/urandom of=$tf bs=1M count=8 ||
		error "dd 8M failed"
	$LFS mirror resync $tf || error "resync 1 failed"

	local sum1=$(md5sum $tf | awk '{print $1}')

	ec_classify_osts $tf
	(( ${#safe_osts[@]} >= 1 )) ||
		skip_env "no safe data OSTs"
	local victim=${safe_osts[0]}

	ec_deactivate_ost $victim
	cancel_lru_locks osc

	local got2=$(md5sum $tf2 | awk '{print $1}')
	ec_reactivate_ost $victim

	[[ "$sum1" == "$got2" ]] ||
		error "mount2 after write1: $sum1 != $got2"

	echo "** mount2 reads mount1 data after resync OK"

	cancel_lru_locks osc
	dd if=/dev/urandom of=$tf bs=1M count=8 ||
		error "dd overwrite failed"
	$LFS mirror resync $tf || error "resync 2 failed"

	local sum2=$(md5sum $tf | awk '{print $1}')
	[[ "$sum1" != "$sum2" ]] ||
		error "overwrite produced same checksum"

	ec_deactivate_ost $victim
	cancel_lru_locks osc

	got2=$(md5sum $tf2 | awk '{print $1}')
	ec_reactivate_ost $victim

	[[ "$sum2" == "$got2" ]] ||
		error "mount2 after write2: $sum2 != $got2"

	echo "** mount2 reads updated data after re-resync OK"
}
run_test 64a "mount2 EC recovery after mount1 write+resync cycles"

test_64b() {
	(( OSTCOUNT >= 8 )) || skip_env "needs >= 8 OSTs"
	enable_ec

	local tf=$DIR/$tfile

	stack_trap "rm -f $tf"

	$LFS setstripe -E eof -S 1M -c 4 --ec 4+2 $tf ||
		error "setstripe failed"
	dd if=/dev/urandom of=$tf bs=1M count=8 ||
		error "dd initial failed"
	$LFS mirror resync $tf || error "resync failed"

	local sum_synced=$(md5sum $tf | awk '{print $1}')

	dd if=/dev/urandom of=$tf bs=1M count=8 ||
		error "dd overwrite failed"

	local sum_dirty=$(md5sum $tf | awk '{print $1}')
	[[ "$sum_synced" != "$sum_dirty" ]] ||
		error "overwrite produced same checksum"

	ec_classify_osts $tf
	(( ${#safe_osts[@]} >= 1 )) ||
		skip_env "no safe data OSTs"
	local victim=${safe_osts[0]}

	ec_deactivate_ost $victim
	cancel_lru_locks osc

	# Parity is stale after the un-resynced overwrite, so EC cannot
	# reconstruct the failed stripe.  Either outcome is acceptable --
	# the read returns the current bytes, or it fails -- but silently
	# handing back different data is a corruption bug, so assert
	# against it (same invariant as 48a).
	local out=$TMP/$tfile.stale64b
	local rc=0

	stack_trap "rm -f $out"
	dd if=$tf of=$out bs=1M 2>/dev/null || rc=$?
	ec_reactivate_ost $victim

	if (( rc == 0 )); then
		local got=$(md5sum $out | awk '{print $1}')

		[[ "$sum_dirty" == "$got" ]] ||
			error "stale-parity read returned wrong data:" \
				"$sum_dirty vs $got"
		echo "** stale-parity read returned the current data"
	else
		echo "** stale-parity read failed, as expected"
	fi

	# The overwrite stales the whole parity component, so which branch
	# above runs is not guaranteed.  Resync and repeat: with parity up
	# to date the degraded read must succeed and return the current
	# bytes.  That is the assertion that can actually fail.
	$LFS mirror resync $tf || error "resync after overwrite failed"

	ec_deactivate_ost $victim
	cancel_lru_locks osc
	dd if=$tf of=$out bs=1M 2>/dev/null ||
		error "degraded read failed after resync"
	ec_reactivate_ost $victim

	local sum_resynced=$(md5sum $out | awk '{print $1}')

	[[ "$sum_dirty" == "$sum_resynced" ]] ||
		error "recovery after resync returned wrong data:" \
			"$sum_dirty vs $sum_resynced"
	echo "** Recovery after resync returned the current data"
}
run_test 64b "EC stale-parity read returns current data or fails"

test_64c() {
	(( OSTCOUNT >= 8 )) || skip_env "needs >= 8 OSTs"

	local tf=$DIR/$tfile

	stack_trap "rm -f $tf"
	local tf2=$DIR2/$tfile

	# check_and_setup_lustre() already mounts $MOUNT2 when the
	# session sets MOUNT_2; only tear down a mount this test made.
	if ! is_mounted $MOUNT2; then
		mkdir -p $MOUNT2
		mount_client $MOUNT2 || error "mount_client $MOUNT2 failed"
		stack_trap "umount_client $MOUNT2"
	fi
	enable_ec

	$LFS setstripe -E eof -S 1M -c 4 --ec 4+2 $tf ||
		error "setstripe failed"

	dd if=/dev/urandom of=$tf bs=1M count=8 ||
		error "dd initial failed"
	$LFS mirror resync $tf || error "resync 0 failed"

	ec_classify_osts $tf
	(( ${#safe_osts[@]} >= 1 )) ||
		skip_env "no safe data OSTs"
	local victim=${safe_osts[0]}

	local iter
	for ((iter = 1; iter <= 3; iter++)); do
		echo "** iteration $iter"

		cancel_lru_locks osc
		dd if=/dev/urandom of=$tf bs=1M count=8 ||
			error "dd iter $iter failed"
		$LFS mirror resync $tf ||
			error "resync iter $iter failed"

		local sum=$(md5sum $tf | awk '{print $1}')

		ec_deactivate_ost $victim
		cancel_lru_locks osc

		local got=$(md5sum $tf2 | awk '{print $1}')
		ec_reactivate_ost $victim

		[[ "$sum" == "$got" ]] ||
			error "iter $iter: $sum != $got"

		echo "** iteration $iter OK"
	done
}
run_test 64c "EC recovery stability across write+resync iterations"

test_64d() {
	(( OSTCOUNT >= 8 )) || skip_env "needs >= 8 OSTs"

	local tf=$DIR/$tfile

	stack_trap "rm -f $tf"
	local tf2=$DIR2/$tfile

	# check_and_setup_lustre() already mounts $MOUNT2 when the
	# session sets MOUNT_2; only tear down a mount this test made.
	if ! is_mounted $MOUNT2; then
		mkdir -p $MOUNT2
		mount_client $MOUNT2 || error "mount_client $MOUNT2 failed"
		stack_trap "umount_client $MOUNT2"
	fi
	enable_ec

	$LFS setstripe -E eof -S 1M -c 4 --ec 4+2 $tf ||
		error "setstripe failed"

	dd if=/dev/urandom of=$tf bs=1M count=4 ||
		error "dd mount1 failed"

	dd if=/dev/urandom of=$tf2 bs=1M count=4 \
		oflag=append conv=notrunc ||
		error "dd mount2 append failed"

	local fsize=$(stat -c '%s' $tf)
	(( fsize == 8388608 )) ||
		error "file size $fsize != 8388608"

	$LFS mirror resync $tf || error "resync failed"

	local sum=$(md5sum $tf | awk '{print $1}')

	ec_classify_osts $tf
	(( ${#safe_osts[@]} >= 1 )) ||
		skip_env "no safe data OSTs"
	local victim=${safe_osts[0]}

	ec_deactivate_ost $victim
	cancel_lru_locks osc

	local got1=$(md5sum $tf | awk '{print $1}')
	local got2=$(md5sum $tf2 | awk '{print $1}')

	ec_reactivate_ost $victim

	[[ "$sum" == "$got1" ]] ||
		error "mount1 recovery: $sum != $got1"
	[[ "$sum" == "$got2" ]] ||
		error "mount2 recovery: $sum != $got2"

	echo "** multi-mount append + recovery OK"
}
run_test 64d "EC recovery after multi-mount append"

test_65a() {
	(( OSTCOUNT >= 8 )) || skip_env "needs >= 8 OSTs"
	enable_ec

	local tf=$DIR/$tfile

	stack_trap "rm -f $tf"

	# 3 PFL components: [0,4M) 4+2, [4M,16M) 2+1,
	# [16M,EOF) 4+2
	$LFS setstripe \
		-E 4M -S 1M -c 4 --ec 4+2 \
		-E 16M -S 1M -c 2 --ec 2+1 \
		-E eof -S 1M -c 4 --ec 4+2 $tf ||
		error "setstripe 3-comp PFL failed"

	# Write 24M spanning all three components
	dd if=/dev/urandom of=$tf bs=1M count=24 ||
		error "dd 24M failed"
	$LFS mirror resync $tf || error "resync failed"

	local sum=$(md5sum $tf | awk '{print $1}')
	ec_classify_osts $tf
	(( ${#safe_osts[@]} >= 1 )) ||
		skip_env "no safe data OSTs"

	local ost
	for ost in "${safe_osts[@]}"; do
		ec_deactivate_ost $ost
		cancel_lru_locks osc
		local got=$(md5sum $tf | awk '{print $1}')
		ec_reactivate_ost $ost
		[[ "$sum" == "$got" ]] ||
			error "3-comp OST$ost: $sum != $got"
		echo "  OST$ost OK"
	done
}
run_test 65a "EC recovery with 3-component PFL layout"

test_65b() {
	(( OSTCOUNT >= 8 )) || skip_env "needs >= 8 OSTs"
	enable_ec

	local tf=$DIR/$tfile

	stack_trap "rm -f $tf"

	# 4 PFL components with different EC configs
	$LFS setstripe \
		-E 2M -S 64K -c 4 --ec 4+2 \
		-E 8M -S 256K -c 2 --ec 2+1 \
		-E 32M -S 1M -c 4 --ec 4+2 \
		-E eof -S 1M -c 2 --ec 2+1 $tf ||
		error "setstripe 4-comp PFL failed"

	# Write 48M spanning all four components
	dd if=/dev/urandom of=$tf bs=1M count=48 ||
		error "dd 48M failed"
	$LFS mirror resync $tf || error "resync failed"

	local sum=$(md5sum $tf | awk '{print $1}')
	ec_classify_osts $tf
	(( ${#safe_osts[@]} >= 1 )) ||
		skip_env "no safe data OSTs"

	local ost
	for ost in "${safe_osts[@]}"; do
		ec_deactivate_ost $ost
		cancel_lru_locks osc
		local got=$(md5sum $tf | awk '{print $1}')
		ec_reactivate_ost $ost
		[[ "$sum" == "$got" ]] ||
			error "4-comp OST$ost: $sum != $got"
		echo "  OST$ost OK"
	done
}
run_test 65b "EC recovery with 4-component PFL layout"

test_65c() {
	# The layout below is sparse from 2M to 20M, so resync has to find
	# the holes to know which stripe sets to skip.  ZFS lseek does not
	# report holes reliably for dirty data, as test 12b describes.
	[[ "$ost1_FSTYPE" != "zfs" ]] ||
		skip "LU-14217: lseek holes unreliable on ZFS"

	(( OSTCOUNT >= 8 )) || skip_env "needs >= 8 OSTs"
	enable_ec

	local tf=$DIR/$tfile

	stack_trap "rm -f $tf"

	# 3 PFL components, data only in comp1 and comp3
	$LFS setstripe \
		-E 4M -S 1M -c 4 --ec 4+2 \
		-E 16M -S 1M -c 2 --ec 2+1 \
		-E eof -S 1M -c 4 --ec 4+2 $tf ||
		error "setstripe 3-comp PFL failed"

	# Write 2M in comp1, skip comp2, write 4M in comp3
	dd if=/dev/urandom of=$tf bs=1M count=2 ||
		error "dd comp1 2M failed"
	dd if=/dev/urandom of=$tf bs=1M count=4 seek=20 ||
		error "dd comp3 at 20M failed"
	$LFS mirror resync $tf || error "resync failed"

	local sum=$(md5sum $tf | awk '{print $1}')
	ec_classify_osts $tf
	(( ${#safe_osts[@]} >= 1 )) ||
		skip_env "no safe data OSTs"

	local ost
	for ost in "${safe_osts[@]}"; do
		ec_deactivate_ost $ost
		cancel_lru_locks osc
		local got=$(md5sum $tf | awk '{print $1}')
		ec_reactivate_ost $ost
		[[ "$sum" == "$got" ]] ||
			error "sparse 3-comp OST$ost:" \
				"$sum != $got"
		echo "  OST$ost OK"
	done
}
run_test 65c "EC recovery with sparse 3-component PFL layout"

test_65d() {
	(( OSTCOUNT >= 8 )) || skip_env "needs >= 8 OSTs"
	enable_ec

	local tf=$DIR/$tfile

	stack_trap "rm -f $tf"

	# 3 PFL components, all 4+2 for 2-down testing
	$LFS setstripe \
		-E 4M -S 1M -c 4 --ec 4+2 \
		-E 16M -S 1M -c 4 --ec 4+2 \
		-E eof -S 1M -c 4 --ec 4+2 $tf ||
		error "setstripe 3-comp PFL failed"

	dd if=/dev/urandom of=$tf bs=1M count=24 ||
		error "dd 24M failed"
	$LFS mirror resync $tf || error "resync failed"

	local sum=$(md5sum $tf | awk '{print $1}')
	ec_classify_osts $tf
	(( ${#safe_osts[@]} >= 2 )) ||
		skip_env "need >= 2 safe data OSTs for 2-down"

	local v1=${safe_osts[0]}
	local v2=${safe_osts[1]}

	ec_deactivate_ost $v1
	ec_deactivate_ost $v2
	cancel_lru_locks osc
	local got=$(md5sum $tf | awk '{print $1}')
	ec_reactivate_ost $v2
	ec_reactivate_ost $v1
	[[ "$sum" == "$got" ]] ||
		error "3-comp 2-down: $sum != $got"
	echo "** 3-component PFL with 2 OSTs down OK"
}
run_test 65d "EC recovery with 3-comp PFL and 2 OST failures"

test_66a() {
	(( OSTCOUNT >= 8 )) || skip_env "needs >= 8 OSTs"
	enable_ec

	local tf=$DIR/$tfile

	stack_trap "rm -f $tf"

	# Two data mirrors, both with EC (2+1)
	$LFS setstripe -N -E eof -S 1M -c 2 --ec 2+1 \
		-N -E eof -S 1M -c 2 --ec 2+1 \
		$tf || error "setstripe 2-EC-mirror failed"

	dd if=/dev/urandom of=$tf bs=1M count=8 ||
		error "dd 8M failed"
	$LFS mirror resync $tf || error "resync failed"

	local sum=$(md5sum $tf | awk '{print $1}')

	# Degrade every data mirror at once.  Failing one OST would leave
	# the other mirror intact and the read would be served from it
	# without any reconstruction.
	ec_mirror_victims $tf ||
		skip_env "no parity-free data OST per mirror"

	local ost
	for ost in "${EC_MIRROR_VICTIMS[@]}"; do
		ec_deactivate_ost $ost
	done
	cancel_lru_locks osc

	local got=$(md5sum $tf | awk '{print $1}')

	for ost in "${EC_MIRROR_VICTIMS[@]}"; do
		ec_reactivate_ost $ost
	done

	[[ "$sum" == "$got" ]] ||
		error "2-EC-mirror OSTs ${EC_MIRROR_VICTIMS[*]}:" \
			"$sum != $got"
	echo "  OSTs ${EC_MIRROR_VICTIMS[*]} OK"
}
run_test 66a "EC recovery with two EC data mirrors"

test_66b() {
	(( OSTCOUNT >= 8 )) || skip_env "needs >= 8 OSTs"
	enable_ec

	local tf=$DIR/$tfile

	stack_trap "rm -f $tf"

	# Mixed: one regular data mirror + one EC data mirror
	$LFS setstripe -N -E eof -S 1M -c 2 \
		-N -E eof -S 1M -c 2 --ec 2+1 \
		$tf || error "setstripe mixed failed"

	dd if=/dev/urandom of=$tf bs=1M count=8 ||
		error "dd 8M failed"
	$LFS mirror resync $tf || error "resync failed"

	local sum=$(md5sum $tf | awk '{print $1}')

	# Degrade every data mirror at once.  Failing one OST would leave
	# the other mirror intact and the read would be served from it
	# without any reconstruction.
	ec_mirror_victims $tf ||
		skip_env "no parity-free data OST per mirror"

	local ost
	for ost in "${EC_MIRROR_VICTIMS[@]}"; do
		ec_deactivate_ost $ost
	done
	cancel_lru_locks osc

	local got=$(md5sum $tf | awk '{print $1}')

	for ost in "${EC_MIRROR_VICTIMS[@]}"; do
		ec_reactivate_ost $ost
	done

	[[ "$sum" == "$got" ]] ||
		error "mixed mirror OSTs ${EC_MIRROR_VICTIMS[*]}:" \
			"$sum != $got"
	echo "  OSTs ${EC_MIRROR_VICTIMS[*]} OK"
}
run_test 66b "EC recovery with mixed regular and EC mirrors"

test_66c() {
	(( OSTCOUNT >= 8 )) || skip_env "needs >= 8 OSTs"
	enable_ec

	local tf=$DIR/$tfile

	stack_trap "rm -f $tf"

	# Two EC mirrors with different configs: 2+2 and 2+1
	$LFS setstripe -N -E eof -S 1M -c 2 --ec 2+2 \
		-N -E eof -S 1M -c 2 --ec 2+1 \
		$tf || error "setstripe diff-EC failed"

	dd if=/dev/urandom of=$tf bs=1M count=8 ||
		error "dd 8M failed"
	$LFS mirror resync $tf || error "resync failed"

	local sum=$(md5sum $tf | awk '{print $1}')

	# Degrade every data mirror at once.  Failing one OST would leave
	# the other mirror intact and the read would be served from it
	# without any reconstruction.
	ec_mirror_victims $tf ||
		skip_env "no parity-free data OST per mirror"

	local ost
	for ost in "${EC_MIRROR_VICTIMS[@]}"; do
		ec_deactivate_ost $ost
	done
	cancel_lru_locks osc

	local got=$(md5sum $tf | awk '{print $1}')

	for ost in "${EC_MIRROR_VICTIMS[@]}"; do
		ec_reactivate_ost $ost
	done

	[[ "$sum" == "$got" ]] ||
		error "diff-EC OSTs ${EC_MIRROR_VICTIMS[*]}:" \
			"$sum != $got"
	echo "  OSTs ${EC_MIRROR_VICTIMS[*]} OK"
}
run_test 66c "EC recovery with two different EC configs"

test_66d() {
	(( OSTCOUNT >= 8 )) || skip_env "needs >= 8 OSTs"
	enable_ec

	local tf=$DIR/$tfile

	stack_trap "rm -f $tf"

	# Two EC mirrors (2+2) with 2 OST failures
	$LFS setstripe -N -E eof -S 1M -c 2 --ec 2+2 \
		-N -E eof -S 1M -c 2 --ec 2+2 \
		$tf || error "setstripe 2-EC failed"

	dd if=/dev/urandom of=$tf bs=1M count=8 ||
		error "dd 8M failed"
	$LFS mirror resync $tf || error "resync failed"

	local sum=$(md5sum $tf | awk '{print $1}')

	# One victim per data mirror, so both mirrors are degraded at once.
	# Two victims picked by OST index could land in the same mirror and
	# leave the other one able to answer the read on its own.
	ec_mirror_victims $tf ||
		skip_env "no parity-free data OST per mirror"
	(( ${#EC_MIRROR_VICTIMS[@]} >= 2 )) ||
		skip_env "need a victim in each of the 2 mirrors"

	local ost
	for ost in "${EC_MIRROR_VICTIMS[@]}"; do
		ec_deactivate_ost $ost
	done
	cancel_lru_locks osc

	local got=$(md5sum $tf | awk '{print $1}')

	for ost in "${EC_MIRROR_VICTIMS[@]}"; do
		ec_reactivate_ost $ost
	done

	[[ "$sum" == "$got" ]] ||
		error "2-EC 2-down OSTs ${EC_MIRROR_VICTIMS[*]}:" \
			"$sum != $got"
	echo "** two EC mirrors with 2 OSTs down OK"
}
run_test 66d "EC recovery with two EC mirrors and 2 OST failures"

test_67a() {
	(( OSTCOUNT >= 8 )) || skip_env "needs >= 8 OSTs"
	enable_ec

	local tf=$DIR/$tfile

	stack_trap "rm -f $tf"

	$LFS setstripe -E eof -S 1M -c 4 --ec 4+2 $tf ||
		error "setstripe failed"

	# Write initial data and resync parity
	dd if=/dev/urandom of=$tf bs=1M count=8 ||
		error "dd initial failed"
	$LFS mirror resync $tf || error "resync initial failed"

	# Find which stripe index maps to which OST
	ec_classify_osts $tf
	(( ${#safe_osts[@]} >= 1 )) ||
		skip_env "no safe data OSTs"
	local victim=${safe_osts[0]}

	# Find the stripe index for the victim OST so we
	# can write to other stripes while it's down
	local ids=($($LFS getstripe $tf |
		awk '/lcme_id/{print $2}'))
	local stripe_osts=($($LFS getstripe -I${ids[0]} \
		$tf | awk '/l_ost_idx:/ { print $5 }' |
		tr -d ","))
	local victim_idx=-1 i
	for ((i = 0; i < ${#stripe_osts[@]}; i++)); do
		[[ ${stripe_osts[$i]} -eq $victim ]] &&
			victim_idx=$i && break
	done
	(( victim_idx >= 0 )) ||
		error "victim OST$victim not in stripe map"

	# Pick a safe stripe to write to (not the victim)
	local write_idx=$(( (victim_idx + 1) % 4 ))

	ec_deactivate_ost $victim
	cancel_lru_locks osc

	# Write 1M to a stripe NOT on the degraded OST.  Note the injected
	# fault is OBD_FAIL_OST_BRW_READ_BULK, which fails reads only, so
	# the write itself is not degraded; what this checks is that a
	# write issued while one data object is unreadable still resyncs
	# and reconstructs correctly afterwards.
	dd if=/dev/urandom of=$tf bs=1M count=1 \
		seek=$write_idx conv=notrunc ||
		error "dd to stripe $write_idx failed"

	ec_reactivate_ost $victim
	$LFS mirror resync $tf ||
		error "resync after degraded write failed"

	local sum=$(md5sum $tf | awk '{print $1}')

	# Verify recovery returns correct data
	ec_deactivate_ost $victim
	cancel_lru_locks osc
	local got=$(md5sum $tf | awk '{print $1}')
	ec_reactivate_ost $victim

	[[ "$sum" == "$got" ]] ||
		error "degraded write recovery: $sum != $got"
	echo "** write to non-degraded stripe + recovery OK"
}
run_test 67a "EC write to available stripes with one OST unreadable"

test_67b() {
	(( OSTCOUNT >= 8 )) || skip_env "needs >= 8 OSTs"
	enable_ec

	local tf=$DIR/$tfile

	stack_trap "rm -f $tf"

	$LFS setstripe -E eof -S 1M -c 4 --ec 4+2 $tf ||
		error "setstripe failed"

	# Multiple rewrite + resync cycles, then verify recovery uses
	# the final parity.  Each dd below opens with O_TRUNC (no
	# conv=notrunc, no seek), so a pass replaces the file rather
	# than overwriting it in place.
	local sum
	local iter
	for ((iter = 1; iter <= 3; iter++)); do
		dd if=/dev/urandom of=$tf bs=1M count=8 ||
			error "dd iteration $iter failed"
		$LFS mirror resync $tf ||
			error "resync iteration $iter failed"
	done

	sum=$(md5sum $tf | awk '{print $1}')

	ec_classify_osts $tf
	(( ${#safe_osts[@]} >= 1 )) ||
		skip_env "no safe data OSTs"

	local ost
	for ost in "${safe_osts[@]}"; do
		ec_deactivate_ost $ost
		cancel_lru_locks osc
		local got=$(md5sum $tf | awk '{print $1}')
		ec_reactivate_ost $ost
		[[ "$sum" == "$got" ]] ||
			error "3-cycle OST$ost: $sum != $got"
		echo "  OST$ost OK"
	done
}
run_test 67b "EC recovery after multiple rewrite+resync cycles"

test_67c() {
	(( OSTCOUNT >= 8 )) || skip_env "needs >= 8 OSTs"
	enable_ec

	local tf=$DIR/$tfile

	stack_trap "rm -f $tf"

	# PFL layout
	$LFS setstripe -E 4M -S 1M -c 4 --ec 4+2 \
		-E eof -S 1M -c 4 --ec 4+2 $tf ||
		error "setstripe PFL failed"

	dd if=/dev/urandom of=$tf bs=1M count=8 ||
		error "dd initial failed"
	$LFS mirror resync $tf || error "resync initial failed"

	# Overwrite just comp1 (first 4M), resync, verify
	dd if=/dev/urandom of=$tf bs=1M count=4 \
		conv=notrunc || error "dd overwrite comp1 failed"
	$LFS mirror resync $tf ||
		error "resync after comp1 overwrite failed"

	local sum=$(md5sum $tf | awk '{print $1}')

	ec_classify_osts $tf
	(( ${#safe_osts[@]} >= 1 )) ||
		skip_env "no safe data OSTs"
	local victim=${safe_osts[0]}

	ec_deactivate_ost $victim
	cancel_lru_locks osc
	local got=$(md5sum $tf | awk '{print $1}')
	ec_reactivate_ost $victim

	[[ "$sum" == "$got" ]] ||
		error "PFL partial overwrite recovery:" \
			"$sum != $got"
	echo "** PFL partial overwrite + recovery OK"
}
run_test 67c "EC recovery after partial overwrite of PFL component"

test_67d() {
	(( OSTCOUNT >= 8 )) || skip_env "needs >= 8 OSTs"
	enable_ec

	local tf=$DIR/$tfile

	stack_trap "rm -f $tf"

	$LFS setstripe -E eof -S 1M -c 4 --ec 4+2 $tf ||
		error "setstripe failed"

	dd if=/dev/urandom of=$tf bs=1M count=8 ||
		error "dd initial failed"
	$LFS mirror resync $tf || error "resync initial failed"

	# Overwrite first 2M (partial RAID set), resync
	dd if=/dev/urandom of=$tf bs=1M count=2 \
		conv=notrunc || error "dd overwrite 2M failed"
	$LFS mirror resync $tf ||
		error "resync after partial overwrite failed"

	local sum=$(md5sum $tf | awk '{print $1}')

	ec_classify_osts $tf
	(( ${#safe_osts[@]} >= 2 )) ||
		skip_env "need >= 2 safe data OSTs"
	local v1=${safe_osts[0]}
	local v2=${safe_osts[1]}

	# Single OST down
	ec_deactivate_ost $v1
	cancel_lru_locks osc
	local got=$(md5sum $tf | awk '{print $1}')
	ec_reactivate_ost $v1
	[[ "$sum" == "$got" ]] ||
		error "partial overwrite 1-down: $sum != $got"
	echo "** 1-OST down after partial overwrite OK"

	# Two OSTs down
	ec_deactivate_ost $v1
	ec_deactivate_ost $v2
	cancel_lru_locks osc
	got=$(md5sum $tf | awk '{print $1}')
	ec_reactivate_ost $v2
	ec_reactivate_ost $v1
	[[ "$sum" == "$got" ]] ||
		error "partial overwrite 2-down: $sum != $got"
	echo "** 2-OSTs down after partial overwrite OK"
}
run_test 67d "EC recovery after partial overwrite with 1 and 2 OST failures"

test_68a() {
	(( OSTCOUNT >= 8 )) || skip_env "needs >= 8 OSTs"
	enable_ec

	local tf=$DIR/$tfile

	stack_trap "rm -f $tf"

	# 2+1: 2 data stripes, 1 parity, max 1 failure
	$LFS setstripe -E eof -S 1M -c 2 --ec 2+1 $tf ||
		error "setstripe 2+1 failed"

	# 6M = 3 full RAID sets with k=2
	dd if=/dev/urandom of=$tf bs=1M count=6 ||
		error "dd 6M failed"
	$LFS mirror resync $tf || error "resync failed"

	local sum=$(md5sum $tf | awk '{print $1}')
	ec_classify_osts $tf
	(( ${#safe_osts[@]} >= 1 )) ||
		skip_env "no safe data OSTs for 2+1"

	local ost
	for ost in "${safe_osts[@]}"; do
		ec_deactivate_ost $ost
		cancel_lru_locks osc
		local got=$(md5sum $tf | awk '{print $1}')
		ec_reactivate_ost $ost
		[[ "$sum" == "$got" ]] ||
			error "2+1 OST$ost: $sum != $got"
		echo "  2+1 OST$ost OK"
	done
}
run_test 68a "EC 2+1 recovery cycling all safe data OSTs"

test_68b() {
	(( OSTCOUNT >= 8 )) || skip_env "needs >= 8 OSTs"
	enable_ec

	local tf=$DIR/$tfile

	stack_trap "rm -f $tf"

	# 2+2: 2 data stripes, 2 parity, max 2 failures
	$LFS setstripe -E eof -S 1M -c 2 --ec 2+2 $tf ||
		error "setstripe 2+2 failed"

	dd if=/dev/urandom of=$tf bs=1M count=6 ||
		error "dd 6M failed"
	$LFS mirror resync $tf || error "resync failed"

	local sum=$(md5sum $tf | awk '{print $1}')
	ec_classify_osts $tf
	(( ${#safe_osts[@]} >= 2 )) ||
		skip_env "need >= 2 safe data OSTs for 2+2"

	# Test all 2-OST failure combinations
	local i j
	for ((i = 0; i < ${#safe_osts[@]}; i++)); do
		for ((j = i + 1; j < ${#safe_osts[@]}; j++)); do
			local v1=${safe_osts[$i]}
			local v2=${safe_osts[$j]}
			ec_deactivate_ost $v1
			ec_deactivate_ost $v2
			cancel_lru_locks osc
			local got=$(md5sum $tf | awk '{print $1}')
			ec_reactivate_ost $v2
			ec_reactivate_ost $v1
			[[ "$sum" == "$got" ]] ||
				error "2+2 OST$v1+$v2:" \
					"$sum != $got"
			echo "  2+2 OST$v1+OST$v2 OK"
		done
	done
}
run_test 68b "EC 2+2 recovery with all 2-OST failure pairs"

test_68c() {
	(( OSTCOUNT >= 8 )) || skip_env "needs >= 8 OSTs"
	enable_ec

	local tf=$DIR/$tfile

	stack_trap "rm -f $tf"

	# 4+1: 4 data stripes, 1 parity, max 1 failure
	$LFS setstripe -E eof -S 1M -c 4 --ec 4+1 $tf ||
		error "setstripe 4+1 failed"

	# 16M = 4 full RAID sets with k=4
	dd if=/dev/urandom of=$tf bs=1M count=16 ||
		error "dd 16M failed"
	$LFS mirror resync $tf || error "resync failed"

	local sum=$(md5sum $tf | awk '{print $1}')
	ec_classify_osts $tf
	(( ${#safe_osts[@]} >= 1 )) ||
		skip_env "no safe data OSTs for 4+1"

	local ost
	for ost in "${safe_osts[@]}"; do
		ec_deactivate_ost $ost
		cancel_lru_locks osc
		local got=$(md5sum $tf | awk '{print $1}')
		ec_reactivate_ost $ost
		[[ "$sum" == "$got" ]] ||
			error "4+1 OST$ost: $sum != $got"
		echo "  4+1 OST$ost OK"
	done
}
run_test 68c "EC 4+1 recovery cycling all safe data OSTs"

test_68d() {
	(( OSTCOUNT >= 8 )) || skip_env "needs >= 8 OSTs"
	enable_ec

	local tf=$DIR/$tfile

	stack_trap "rm -f $tf"

	# PFL with different EC geometry per component
	# comp1: 2+1 (3 OSTs), comp2: 4+2 (6 OSTs)
	$LFS setstripe -E 4M -S 1M -c 2 --ec 2+1 \
		-E eof -S 1M -c 4 --ec 4+2 $tf ||
		error "setstripe mixed PFL failed"

	# Write spanning both components
	dd if=/dev/urandom of=$tf bs=1M count=12 ||
		error "dd 12M failed"
	$LFS mirror resync $tf || error "resync failed"

	local sum=$(md5sum $tf | awk '{print $1}')
	ec_classify_osts $tf
	(( ${#safe_osts[@]} >= 1 )) ||
		skip_env "no safe data OSTs for mixed PFL"

	local ost
	for ost in "${safe_osts[@]}"; do
		ec_deactivate_ost $ost
		cancel_lru_locks osc
		local got=$(md5sum $tf | awk '{print $1}')
		ec_reactivate_ost $ost
		[[ "$sum" == "$got" ]] ||
			error "mixed PFL OST$ost:" \
				"$sum != $got"
		echo "  mixed PFL OST$ost OK"
	done
}
run_test 68d "EC recovery with mixed 2+1 and 4+2 PFL components"

test_69a() {
	(( OSTCOUNT >= 8 )) || skip_env "needs >= 8 OSTs"
	enable_ec

	local tf=$DIR/$tfile

	stack_trap "rm -f $tf"

	$LFS setstripe -E eof -S 1M -c 4 --ec 4+2 $tf ||
		error "setstripe failed"

	# Write with O_DIRECT
	dd if=/dev/urandom of=$tf bs=1M count=8 oflag=direct ||
		error "dd DIO write failed"
	$LFS mirror resync $tf || error "resync failed"

	# Read back with buffered I/O for baseline
	local sum=$(md5sum $tf | awk '{print $1}')

	ec_classify_osts $tf
	(( ${#safe_osts[@]} >= 1 )) ||
		skip_env "no safe data OSTs"

	local ost
	for ost in "${safe_osts[@]}"; do
		ec_deactivate_ost $ost
		cancel_lru_locks osc
		local got=$(md5sum $tf | awk '{print $1}')
		ec_reactivate_ost $ost
		[[ "$sum" == "$got" ]] ||
			error "DIO-write OST$ost: $sum != $got"
		echo "  DIO-write OST$ost OK"
	done
}
run_test 69a "EC recovery after O_DIRECT writes"

test_69b() {
	(( OSTCOUNT >= 8 )) || skip_env "needs >= 8 OSTs"
	enable_ec

	local tf=$DIR/$tfile

	stack_trap "rm -f $tf"

	$LFS setstripe -E eof -S 1M -c 4 --ec 4+2 $tf ||
		error "setstripe failed"

	# DIO write with 2-OST failure tolerance
	dd if=/dev/urandom of=$tf bs=1M count=8 oflag=direct ||
		error "dd DIO write failed"
	$LFS mirror resync $tf || error "resync failed"

	local sum=$(md5sum $tf | awk '{print $1}')

	ec_classify_osts $tf
	(( ${#safe_osts[@]} >= 2 )) ||
		skip_env "need >= 2 safe data OSTs"
	local v1=${safe_osts[0]}
	local v2=${safe_osts[1]}

	ec_deactivate_ost $v1
	ec_deactivate_ost $v2
	cancel_lru_locks osc
	local got=$(md5sum $tf | awk '{print $1}')
	ec_reactivate_ost $v2
	ec_reactivate_ost $v1

	[[ "$sum" == "$got" ]] ||
		error "DIO-write 2-down: $sum != $got"
	echo "** DIO write with 2 OSTs down OK"
}
run_test 69b "EC recovery after O_DIRECT write with 2 OST failures"

test_69c() {
	(( OSTCOUNT >= 8 )) || skip_env "needs >= 8 OSTs"
	enable_ec

	local tf=$DIR/$tfile

	stack_trap "rm -f $tf"

	$LFS setstripe -E eof -S 1M -c 4 --ec 4+2 $tf ||
		error "setstripe failed"

	# Initial buffered write + resync
	dd if=/dev/urandom of=$tf bs=1M count=8 ||
		error "dd initial failed"
	$LFS mirror resync $tf || error "resync 1 failed"

	# DIO overwrite + resync
	dd if=/dev/urandom of=$tf bs=1M count=8 \
		oflag=direct conv=notrunc ||
		error "dd DIO overwrite failed"
	$LFS mirror resync $tf || error "resync 2 failed"

	local sum=$(md5sum $tf | awk '{print $1}')

	ec_classify_osts $tf
	(( ${#safe_osts[@]} >= 1 )) ||
		skip_env "no safe data OSTs"
	local victim=${safe_osts[0]}

	ec_deactivate_ost $victim
	cancel_lru_locks osc
	local got=$(md5sum $tf | awk '{print $1}')
	ec_reactivate_ost $victim

	[[ "$sum" == "$got" ]] ||
		error "DIO-overwrite recovery: $sum != $got"
	echo "** buffered write then DIO overwrite + recovery OK"
}
run_test 69c "EC recovery after buffered write then O_DIRECT overwrite"

test_69d() {
	(( OSTCOUNT >= 8 )) || skip_env "needs >= 8 OSTs"
	enable_ec

	local tf=$DIR/$tfile

	stack_trap "rm -f $tf"

	# PFL with DIO writes
	$LFS setstripe -E 4M -S 1M -c 4 --ec 4+2 \
		-E eof -S 1M -c 4 --ec 4+2 $tf ||
		error "setstripe PFL failed"

	dd if=/dev/urandom of=$tf bs=1M count=12 \
		oflag=direct || error "dd DIO PFL failed"
	$LFS mirror resync $tf || error "resync failed"

	local sum=$(md5sum $tf | awk '{print $1}')

	ec_classify_osts $tf
	(( ${#safe_osts[@]} >= 2 )) ||
		skip_env "need >= 2 safe data OSTs"
	local v1=${safe_osts[0]}
	local v2=${safe_osts[1]}

	# 1-down
	ec_deactivate_ost $v1
	cancel_lru_locks osc
	local got=$(md5sum $tf | awk '{print $1}')
	ec_reactivate_ost $v1
	[[ "$sum" == "$got" ]] ||
		error "DIO PFL 1-down: $sum != $got"
	echo "** DIO PFL 1-OST down OK"

	# 2-down
	ec_deactivate_ost $v1
	ec_deactivate_ost $v2
	cancel_lru_locks osc
	got=$(md5sum $tf | awk '{print $1}')
	ec_reactivate_ost $v2
	ec_reactivate_ost $v1
	[[ "$sum" == "$got" ]] ||
		error "DIO PFL 2-down: $sum != $got"
	echo "** DIO PFL 2-OSTs down OK"
}
run_test 69d "EC recovery after O_DIRECT write to PFL layout"

test_70a() {
	(( OSTCOUNT >= 8 )) || skip_env "needs >= 8 OSTs"
	enable_ec

	local tf=$DIR/$tfile

	stack_trap "rm -f $tf"

	$LFS setstripe -E eof -S 1M -c 4 --ec 4+2 $tf ||
		error "setstripe failed"

	# Write 8M, resync, then overwrite middle 2M
	dd if=/dev/urandom of=$tf bs=1M count=8 ||
		error "dd initial failed"
	$LFS mirror resync $tf || error "resync 1 failed"

	dd if=/dev/urandom of=$tf bs=1M count=2 seek=3 \
		conv=notrunc || error "dd mid-overwrite failed"
	$LFS mirror resync $tf || error "resync 2 failed"

	local sum=$(md5sum $tf | awk '{print $1}')

	ec_classify_osts $tf
	(( ${#safe_osts[@]} >= 1 )) ||
		skip_env "no safe data OSTs"

	local ost
	for ost in "${safe_osts[@]}"; do
		ec_deactivate_ost $ost
		cancel_lru_locks osc
		local got=$(md5sum $tf | awk '{print $1}')
		ec_reactivate_ost $ost
		[[ "$sum" == "$got" ]] ||
			error "mid-overwrite OST$ost:" \
				"$sum != $got"
		echo "  mid-overwrite OST$ost OK"
	done
}
run_test 70a "EC recovery after mid-file overwrite"

test_70b() {
	(( OSTCOUNT >= 8 )) || skip_env "needs >= 8 OSTs"
	enable_ec

	local tf=$DIR/$tfile

	stack_trap "rm -f $tf"

	$LFS setstripe -E eof -S 1M -c 4 --ec 4+2 $tf ||
		error "setstripe failed"

	# Write 8M, resync, overwrite with non-aligned size
	dd if=/dev/urandom of=$tf bs=1M count=8 ||
		error "dd initial failed"
	$LFS mirror resync $tf || error "resync 1 failed"

	# Overwrite 512K at offset 1.5M, inside stripe 1, ending
	# exactly on the 2M stripe boundary
	dd if=/dev/urandom of=$tf bs=512K count=1 \
		seek=3 conv=notrunc ||
		error "dd unaligned overwrite failed"
	$LFS mirror resync $tf || error "resync 2 failed"

	local sum=$(md5sum $tf | awk '{print $1}')

	ec_classify_osts $tf
	(( ${#safe_osts[@]} >= 1 )) ||
		skip_env "no safe data OSTs"
	local victim=${safe_osts[0]}

	ec_deactivate_ost $victim
	cancel_lru_locks osc
	local got=$(md5sum $tf | awk '{print $1}')
	ec_reactivate_ost $victim

	[[ "$sum" == "$got" ]] ||
		error "unaligned overwrite: $sum != $got"
	echo "** non-aligned overwrite + recovery OK"
}
run_test 70b "EC recovery after non-stripe-aligned overwrite"

test_70c() {
	(( OSTCOUNT >= 8 )) || skip_env "needs >= 8 OSTs"
	enable_ec

	local tf=$DIR/$tfile

	stack_trap "rm -f $tf"

	$LFS setstripe -E eof -S 1M -c 4 --ec 4+2 $tf ||
		error "setstripe failed"

	# Write 4M (1 RAID set), resync, extend to 12M,
	# resync, then overwrite first 4M again
	dd if=/dev/urandom of=$tf bs=1M count=4 ||
		error "dd 4M failed"
	$LFS mirror resync $tf || error "resync 1 failed"

	dd if=/dev/urandom of=$tf bs=1M count=8 seek=4 ||
		error "dd extend failed"
	$LFS mirror resync $tf || error "resync 2 failed"

	dd if=/dev/urandom of=$tf bs=1M count=4 \
		conv=notrunc ||
		error "dd re-overwrite failed"
	$LFS mirror resync $tf || error "resync 3 failed"

	local sum=$(md5sum $tf | awk '{print $1}')

	ec_classify_osts $tf
	(( ${#safe_osts[@]} >= 2 )) ||
		skip_env "need >= 2 safe data OSTs"
	local v1=${safe_osts[0]}
	local v2=${safe_osts[1]}

	# 1-down
	ec_deactivate_ost $v1
	cancel_lru_locks osc
	local got=$(md5sum $tf | awk '{print $1}')
	ec_reactivate_ost $v1
	[[ "$sum" == "$got" ]] ||
		error "extend+re-overwrite 1-down:" \
			"$sum != $got"
	echo "** 1-down after extend + re-overwrite OK"

	# 2-down
	ec_deactivate_ost $v1
	ec_deactivate_ost $v2
	cancel_lru_locks osc
	got=$(md5sum $tf | awk '{print $1}')
	ec_reactivate_ost $v2
	ec_reactivate_ost $v1
	[[ "$sum" == "$got" ]] ||
		error "extend+re-overwrite 2-down:" \
			"$sum != $got"
	echo "** 2-down after extend + re-overwrite OK"
}
run_test 70c "EC recovery after extend then re-overwrite original data"

test_70d() {
	(( OSTCOUNT >= 8 )) || skip_env "needs >= 8 OSTs"
	enable_ec

	local tf=$DIR/$tfile

	stack_trap "rm -f $tf"

	# PFL: overwrite only comp2, leave comp1 untouched
	$LFS setstripe -E 4M -S 1M -c 4 --ec 4+2 \
		-E eof -S 1M -c 4 --ec 4+2 $tf ||
		error "setstripe PFL failed"

	dd if=/dev/urandom of=$tf bs=1M count=12 ||
		error "dd 12M failed"
	$LFS mirror resync $tf || error "resync 1 failed"

	# Overwrite only in comp2 (offsets 4M-8M)
	dd if=/dev/urandom of=$tf bs=1M count=4 seek=4 \
		conv=notrunc ||
		error "dd overwrite comp2 failed"
	$LFS mirror resync $tf || error "resync 2 failed"

	local sum=$(md5sum $tf | awk '{print $1}')

	ec_classify_osts $tf
	(( ${#safe_osts[@]} >= 1 )) ||
		skip_env "no safe data OSTs"

	local ost
	for ost in "${safe_osts[@]}"; do
		ec_deactivate_ost $ost
		cancel_lru_locks osc
		local got=$(md5sum $tf | awk '{print $1}')
		ec_reactivate_ost $ost
		[[ "$sum" == "$got" ]] ||
			error "PFL comp2-overwrite OST$ost:" \
				"$sum != $got"
		echo "  comp2-overwrite OST$ost OK"
	done
}
run_test 70d "EC recovery after overwriting only PFL comp2"

test_71a() {
	(( OSTCOUNT >= 8 )) || skip_env "needs >= 8 OSTs"
	enable_ec

	stack_trap "rm -f $DIR/$tfile.*"

	# Create 8 EC files with different sizes
	local files=()
	local sums=()
	local i
	for ((i = 0; i < 8; i++)); do
		local f=$DIR/${tfile}.${i}
		$LFS setstripe -E eof -S 1M -c 4 --ec 4+2 $f ||
			error "setstripe file $i failed"
		local sz=$(( (i + 1) * 2 ))
		dd if=/dev/urandom of=$f bs=1M count=$sz ||
			error "dd file $i (${sz}M) failed"
		$LFS mirror resync $f ||
			error "resync file $i failed"
		files+=("$f")
		sums+=($(md5sum $f | awk '{print $1}'))
	done

	# Degrade one OST, verify ALL files
	ec_classify_osts ${files[0]}
	(( ${#safe_osts[@]} >= 1 )) ||
		skip_env "no safe data OSTs"
	local victim=${safe_osts[0]}

	ec_deactivate_ost $victim
	cancel_lru_locks osc

	for ((i = 0; i < 8; i++)); do
		local got=$(md5sum ${files[$i]} | awk '{print $1}')
		[[ "${sums[$i]}" == "$got" ]] ||
			error "file $i OST$victim:" \
				"${sums[$i]} != $got"
		echo "  file $i OK"
	done
	ec_reactivate_ost $victim

	echo "** 8 files all recovered with 1 OST down"
}
run_test 71a "EC batch recovery: 8 files with 1 OST failure"

test_71b() {
	(( OSTCOUNT >= 8 )) || skip_env "needs >= 8 OSTs"
	enable_ec

	stack_trap "rm -f $DIR/$tfile.*"

	# Mix of EC geometries in different files
	local f1=$DIR/${tfile}.2p1
	local f2=$DIR/${tfile}.4p2
	local f3=$DIR/${tfile}.2p2

	$LFS setstripe -E eof -S 1M -c 2 --ec 2+1 $f1 ||
		error "setstripe 2+1 failed"
	$LFS setstripe -E eof -S 1M -c 4 --ec 4+2 $f2 ||
		error "setstripe 4+2 failed"
	$LFS setstripe -E eof -S 1M -c 2 --ec 2+2 $f3 ||
		error "setstripe 2+2 failed"

	dd if=/dev/urandom of=$f1 bs=1M count=6 ||
		error "dd f1 failed"
	dd if=/dev/urandom of=$f2 bs=1M count=8 ||
		error "dd f2 failed"
	dd if=/dev/urandom of=$f3 bs=1M count=6 ||
		error "dd f3 failed"

	$LFS mirror resync $f1 || error "resync f1 failed"
	$LFS mirror resync $f2 || error "resync f2 failed"
	$LFS mirror resync $f3 || error "resync f3 failed"

	local s1=$(md5sum $f1 | awk '{print $1}')
	local s2=$(md5sum $f2 | awk '{print $1}')
	local s3=$(md5sum $f3 | awk '{print $1}')

	# Find an OST that is a safe data OST for all files
	ec_classify_osts $f2
	(( ${#safe_osts[@]} >= 1 )) ||
		skip_env "no safe data OSTs"
	local victim=${safe_osts[0]}

	ec_deactivate_ost $victim
	cancel_lru_locks osc

	local g1=$(md5sum $f1 | awk '{print $1}')
	local g2=$(md5sum $f2 | awk '{print $1}')
	local g3=$(md5sum $f3 | awk '{print $1}')
	ec_reactivate_ost $victim

	[[ "$s1" == "$g1" ]] ||
		error "2+1 file: $s1 != $g1"
	[[ "$s2" == "$g2" ]] ||
		error "4+2 file: $s2 != $g2"
	[[ "$s3" == "$g3" ]] ||
		error "2+2 file: $s3 != $g3"
	echo "** mixed geometry batch recovery OK"
}
run_test 71b "EC batch recovery: mixed geometries with 1 OST failure"

test_71c() {
	(( OSTCOUNT >= 8 )) || skip_env "needs >= 8 OSTs"
	enable_ec

	stack_trap "rm -f $DIR/$tfile.*"

	# 5 files with different sizes, 2-OST failure
	local files=()
	local sums=()
	local sizes=(1 3 5 8 12)
	local i
	for ((i = 0; i < 5; i++)); do
		local f=$DIR/${tfile}.${i}
		$LFS setstripe -E eof -S 1M -c 4 --ec 4+2 $f ||
			error "setstripe file $i failed"
		dd if=/dev/urandom of=$f \
			bs=1M count=${sizes[$i]} ||
			error "dd file $i failed"
		$LFS mirror resync $f ||
			error "resync file $i failed"
		files+=("$f")
		sums+=($(md5sum $f | awk '{print $1}'))
	done

	ec_classify_osts ${files[0]}
	(( ${#safe_osts[@]} >= 2 )) ||
		skip_env "need >= 2 safe data OSTs"
	local v1=${safe_osts[0]}
	local v2=${safe_osts[1]}

	ec_deactivate_ost $v1
	ec_deactivate_ost $v2
	cancel_lru_locks osc

	for ((i = 0; i < 5; i++)); do
		local got=$(md5sum ${files[$i]} | awk '{print $1}')
		[[ "${sums[$i]}" == "$got" ]] ||
			error "file $i 2-down:" \
				"${sums[$i]} != $got"
		echo "  file $i (${sizes[$i]}M) OK"
	done
	ec_reactivate_ost $v2
	ec_reactivate_ost $v1

	echo "** 5 files with 2 OSTs down OK"
}
run_test 71c "EC batch recovery: 5 varied files with 2 OST failures"

test_71d() {
	(( OSTCOUNT >= 8 )) || skip_env "needs >= 8 OSTs"
	enable_ec

	stack_trap "rm -f $DIR/$tfile.*"

	# Mix of plain and PFL files
	local f1=$DIR/${tfile}.plain
	local f2=$DIR/${tfile}.pfl

	$LFS setstripe -E eof -S 1M -c 4 --ec 4+2 $f1 ||
		error "setstripe plain failed"
	$LFS setstripe -E 4M -S 1M -c 4 --ec 4+2 \
		-E eof -S 1M -c 4 --ec 4+2 $f2 ||
		error "setstripe PFL failed"

	dd if=/dev/urandom of=$f1 bs=1M count=8 ||
		error "dd plain failed"
	dd if=/dev/urandom of=$f2 bs=1M count=12 ||
		error "dd PFL failed"

	$LFS mirror resync $f1 || error "resync plain failed"
	$LFS mirror resync $f2 || error "resync PFL failed"

	local s1=$(md5sum $f1 | awk '{print $1}')
	local s2=$(md5sum $f2 | awk '{print $1}')

	ec_classify_osts $f1
	(( ${#safe_osts[@]} >= 1 )) ||
		skip_env "no safe data OSTs"

	local ost
	for ost in "${safe_osts[@]}"; do
		ec_deactivate_ost $ost
		cancel_lru_locks osc
		local g1=$(md5sum $f1 | awk '{print $1}')
		local g2=$(md5sum $f2 | awk '{print $1}')
		ec_reactivate_ost $ost
		[[ "$s1" == "$g1" ]] ||
			error "plain OST$ost: $s1 != $g1"
		[[ "$s2" == "$g2" ]] ||
			error "PFL OST$ost: $s2 != $g2"
		echo "  OST$ost: plain+PFL OK"
	done
}
run_test 71d "EC batch recovery: plain and PFL files cycling OSTs"

test_73a() {
	(( OSTCOUNT >= 8 )) || skip_env "needs >= 8 OSTs"
	enable_ec

	local tf=$DIR/$tfile

	stack_trap "rm -f $tf"
	local sz=16

	$LFS setstripe -E eof -S 1M -c 4 --ec 4+2 $tf ||
		error "setstripe failed"
	dd if=/dev/urandom of=$tf bs=1M count=$sz ||
		error "dd failed"
	$LFS mirror resync $tf || error "resync failed"

	# Compute md5 of whole file and individual 1M blocks
	local sum_full=$(md5sum $tf | awk '{print $1}')
	local block_sums=()
	local i
	for ((i = 0; i < sz; i++)); do
		block_sums+=($(dd if=$tf bs=1M skip=$i count=1 \
			2>/dev/null | md5sum | awk '{print $1}'))
	done

	ec_classify_osts $tf
	(( ${#safe_osts[@]} >= 1 )) ||
		skip_env "no safe data OSTs"

	# Read blocks in random (non-sequential) order while
	# degraded to exercise EC recovery with seeks
	local victim=${safe_osts[0]}
	ec_deactivate_ost $victim
	cancel_lru_locks osc

	# Full sequential read first
	local got_full=$(md5sum $tf | awk '{print $1}')
	# NOTE: if this fails, it may indicate a bug in EC
	# recovery for sequential reads after degradation.
	[[ "$sum_full" == "$got_full" ]] ||
		error "full read OST$victim:" \
			"$sum_full != $got_full"

	cancel_lru_locks osc

	# Random-order single-block reads
	local order=($(shuf -i 0-$((sz - 1))))
	local failures=0
	for i in "${order[@]}"; do
		local got=$(dd if=$tf bs=1M skip=$i count=1 \
			2>/dev/null | md5sum | awk '{print $1}')
		if [[ "${block_sums[$i]}" != "$got" ]]; then
			echo "block $i: ${block_sums[$i]} != $got"
			failures=$((failures + 1))
		fi
	done
	ec_reactivate_ost $victim

	# NOTE: failures here may indicate a bug in EC
	# recovery when reading non-sequential offsets
	# (e.g. parity offset calculation per-stripe).
	(( failures == 0 )) ||
		error "$failures/$sz blocks failed random read"

	echo "** random-order block reads: all $sz OK"
}
run_test 73a "EC random/non-sequential reads during recovery"

test_73b() {
	(( OSTCOUNT >= 8 )) || skip_env "needs >= 8 OSTs"
	enable_ec
	# need allocate-mode fallocate: osd-zfs only implements punch
	check_fallocate_or_skip ost1 alloc
	check_set_fallocate_or_skip

	local tf=$DIR/$tfile

	stack_trap "rm -f $tf"

	$LFS setstripe -E eof -S 1M -c 4 --ec 4+2 $tf ||
		error "setstripe failed"

	# Fallocate 16M, then write 8M into the middle
	fallocate -l 16M $tf ||
		error "fallocate 16M failed"
	dd if=/dev/urandom of=$tf bs=1M count=8 seek=4 \
		conv=notrunc ||
		error "dd into fallocated region failed"
	$LFS mirror resync $tf || error "resync failed"

	local sum=$(md5sum $tf | awk '{print $1}')

	ec_classify_osts $tf
	(( ${#safe_osts[@]} >= 1 )) ||
		skip_env "no safe data OSTs"

	# 1-down recovery
	local victim=${safe_osts[0]}
	ec_deactivate_ost $victim
	cancel_lru_locks osc
	local got=$(md5sum $tf | awk '{print $1}')
	ec_reactivate_ost $victim
	# NOTE: failure may indicate a bug in EC recovery
	# for sparse/fallocated regions (zero-fill vs
	# actual parity mismatch).
	[[ "$sum" == "$got" ]] ||
		error "fallocate 1-down OST$victim:" \
			"$sum != $got"
	echo "  fallocate 1-down OK"

	# Also test pure fallocate (no dd overwrites)
	local tf2=$DIR/${tfile}.pure
	stack_trap "rm -f $tf2"
	$LFS setstripe -E eof -S 1M -c 4 --ec 4+2 $tf2 ||
		error "setstripe tf2 failed"
	fallocate -l 8M $tf2 ||
		error "fallocate tf2 failed"
	$LFS mirror resync $tf2 ||
		error "resync tf2 failed"

	local sum2=$(md5sum $tf2 | awk '{print $1}')
	ec_classify_osts $tf2
	(( ${#safe_osts[@]} >= 1 )) ||
		skip_env "no safe data OSTs for tf2"
	victim=${safe_osts[0]}

	ec_deactivate_ost $victim
	cancel_lru_locks osc
	local got2=$(md5sum $tf2 | awk '{print $1}')
	ec_reactivate_ost $victim
	# NOTE: pure fallocate with no user data tests
	# whether EC recovery handles all-zero stripes.
	[[ "$sum2" == "$got2" ]] ||
		error "pure fallocate 1-down: $sum2 != $got2"

	echo "** fallocate EC recovery OK"
}
run_test 73b "EC recovery after fallocate"

test_73c() {
	(( OSTCOUNT >= 8 )) || skip_env "needs >= 8 OSTs"
	enable_ec

	local tf=$DIR/$tfile

	stack_trap "rm -f $tf"

	$LFS setstripe -E eof -S 1M -c 4 --ec 4+2 $tf ||
		error "setstripe failed"

	# Write 8M, resync
	dd if=/dev/urandom of=$tf bs=1M count=8 ||
		error "dd 8M failed"
	$LFS mirror resync $tf || error "resync 1 failed"

	# Truncate to 2M.  $TRUNCATE is $LUSTRE/tests/truncate, which takes
	# "PATH LENGTH" rather than the coreutils --size option.
	$TRUNCATE $tf 2097152 ||
		error "truncate to 2M failed"
	$LFS mirror resync $tf || error "resync 2 failed"

	# Extend back to 12M with new data
	dd if=/dev/urandom of=$tf bs=1M count=10 seek=2 ||
		error "dd extend to 12M failed"
	$LFS mirror resync $tf || error "resync 3 failed"

	local sum=$(md5sum $tf | awk '{print $1}')

	ec_classify_osts $tf
	(( ${#safe_osts[@]} >= 2 )) ||
		skip_env "need >= 2 safe data OSTs"

	# 1-down
	local v1=${safe_osts[0]}
	ec_deactivate_ost $v1
	cancel_lru_locks osc
	local got=$(md5sum $tf | awk '{print $1}')
	ec_reactivate_ost $v1
	# NOTE: failure may indicate a bug in EC recovery
	# when file size changes between parity syncs
	# (truncate invalidates old parity, extend adds
	# new stripes that need fresh parity).
	[[ "$sum" == "$got" ]] ||
		error "truncate-extend 1-down: $sum != $got"
	echo "  1-down after truncate+extend OK"

	# 2-down
	ec_deactivate_ost ${safe_osts[0]}
	ec_deactivate_ost ${safe_osts[1]}
	cancel_lru_locks osc
	got=$(md5sum $tf | awk '{print $1}')
	ec_reactivate_ost ${safe_osts[1]}
	ec_reactivate_ost ${safe_osts[0]}
	[[ "$sum" == "$got" ]] ||
		error "truncate-extend 2-down: $sum != $got"
	echo "  2-down after truncate+extend OK"

	# Additional: truncate to zero then extend
	local tf2=$DIR/${tfile}.zero
	stack_trap "rm -f $tf2"
	$LFS setstripe -E eof -S 1M -c 4 --ec 4+2 $tf2 ||
		error "setstripe tf2 failed"
	dd if=/dev/urandom of=$tf2 bs=1M count=4 ||
		error "dd tf2 failed"
	$LFS mirror resync $tf2 || error "resync tf2 1 failed"
	$TRUNCATE $tf2 0 ||
		error "truncate to 0 failed"
	dd if=/dev/urandom of=$tf2 bs=1M count=6 ||
		error "dd tf2 re-extend failed"
	$LFS mirror resync $tf2 ||
		error "resync tf2 2 failed"

	local sum2=$(md5sum $tf2 | awk '{print $1}')

	# safe_osts still describes $tf; $tf2 has its own object placement,
	# so classify it before picking a victim or the OST failed here may
	# hold none of $tf2's stripes.
	ec_data_stripe_osts $tf2 0
	ec_deactivate_ost ${EC_STRIPE_OSTS[0]}
	cancel_lru_locks osc
	local got2=$(md5sum $tf2 | awk '{print $1}')
	ec_reactivate_ost ${EC_STRIPE_OSTS[0]}
	# NOTE: truncate-to-zero then extend is a special
	# case — all parity from the first write is gone.
	[[ "$sum2" == "$got2" ]] ||
		error "truncate-zero-extend: $sum2 != $got2"

	echo "** truncate-then-extend recovery OK"
}
run_test 73c "EC truncate-then-extend recovery"

test_73d() {
	(( OSTCOUNT >= 8 )) || skip_env "needs >= 8 OSTs"
	enable_ec

	local tf=$DIR/$tfile

	stack_trap "rm -f $tf"

	$LFS setstripe -E eof -S 1M -c 4 --ec 4+2 $tf ||
		error "setstripe failed"

	# Seed the file with initial data, resync parity
	dd if=/dev/urandom of=$tf bs=1M count=4 ||
		error "dd seed failed"
	$LFS mirror resync $tf || error "resync 1 failed"

	# Append 8M using O_APPEND via dd
	dd if=/dev/urandom of=$tf bs=1M count=8 \
		oflag=append conv=notrunc ||
		error "dd O_APPEND failed"
	$LFS mirror resync $tf || error "resync 2 failed"

	local sum=$(md5sum $tf | awk '{print $1}')
	local fsize=$(stat -c '%s' $tf)
	(( fsize == 12 * 1048576 )) ||
		error "expected 12M, got $fsize bytes"

	ec_classify_osts $tf
	(( ${#safe_osts[@]} >= 2 )) ||
		skip_env "need >= 2 safe data OSTs"

	# 1-down degraded read
	local v1=${safe_osts[0]}
	ec_deactivate_ost $v1
	cancel_lru_locks osc
	local got=$(md5sum $tf | awk '{print $1}')
	ec_reactivate_ost $v1
	# NOTE: failure may indicate a bug in EC recovery
	# for appended regions where parity was computed
	# over the extended file range.
	[[ "$sum" == "$got" ]] ||
		error "O_APPEND 1-down: $sum != $got"
	echo "  O_APPEND 1-down OK"

	# 2-down degraded read
	ec_deactivate_ost ${safe_osts[0]}
	ec_deactivate_ost ${safe_osts[1]}
	cancel_lru_locks osc
	got=$(md5sum $tf | awk '{print $1}')
	ec_reactivate_ost ${safe_osts[1]}
	ec_reactivate_ost ${safe_osts[0]}
	[[ "$sum" == "$got" ]] ||
		error "O_APPEND 2-down: $sum != $got"
	echo "  O_APPEND 2-down OK"

	# Append in degraded mode. The BRW_READ_BULK fault
	# leaves the OSC import active and fails only reads,
	# so (unlike an inactive import) the append itself
	# succeeds; the new data lands in the data mirror
	# with parity left stale until the next resync.
	ec_deactivate_ost ${safe_osts[0]}
	dd if=/dev/urandom of=$tf bs=1M count=4 \
		oflag=append conv=notrunc ||
		error "degraded O_APPEND write failed"
	ec_reactivate_ost ${safe_osts[0]}

	fsize=$(stat -c '%s' $tf)
	(( fsize == 16 * 1048576 )) ||
		error "expected 16M after degraded append, " \
			"got $fsize bytes"

	# Restore parity over the extended range, then confirm
	# the whole file still reads back through parity
	# reconstruction with a data OST down.
	$LFS mirror resync $tf || error "resync 3 failed"
	sum=$(md5sum $tf | awk '{print $1}')

	ec_deactivate_ost ${safe_osts[0]}
	cancel_lru_locks osc
	local got3=$(md5sum $tf | awk '{print $1}')
	ec_reactivate_ost ${safe_osts[0]}
	[[ "$sum" == "$got3" ]] ||
		error "degraded-append recovery mismatch:" \
			"$sum != $got3"
	echo "  degraded O_APPEND succeeded and recovers"

	echo "** O_APPEND degraded mode: all OK"
}
run_test 73d "EC O_APPEND during degraded mode"

test_73e() {
	(( OSTCOUNT >= 8 )) || skip_env "needs >= 8 OSTs"
	enable_ec

	local tf=$DIR/$tfile
	local sz=128

	$LFS setstripe -E eof -S 1M -c 4 --ec 4+2 $tf ||
		error "setstripe failed"
	stack_trap "rm -f $tf"

	# Write 128M
	dd if=/dev/urandom of=$tf bs=1M count=$sz ||
		error "dd ${sz}M failed"
	$LFS mirror resync $tf || error "resync failed"

	local sum=$(md5sum $tf | awk '{print $1}')
	local fsize=$(stat -c '%s' $tf)
	(( fsize == sz * 1048576 )) ||
		error "size mismatch: expected ${sz}M, " \
			"got $fsize"

	ec_classify_osts $tf
	(( ${#safe_osts[@]} >= 2 )) ||
		skip_env "need >= 2 safe data OSTs"

	# 1-down: single data OST failure
	local v1=${safe_osts[0]}
	ec_deactivate_ost $v1
	cancel_lru_locks osc
	local got=$(md5sum $tf | awk '{print $1}')
	ec_reactivate_ost $v1
	# NOTE: large file recovery exercises many RAID
	# sets (128M / 4M swidth = 32 sets). Failure may
	# indicate offset calculation bugs that only
	# manifest at higher offsets.
	[[ "$sum" == "$got" ]] ||
		error "large ${sz}M 1-down: $sum != $got"
	echo "  ${sz}M 1-down OK"

	# 2-down: two data OST failures
	ec_deactivate_ost ${safe_osts[0]}
	ec_deactivate_ost ${safe_osts[1]}
	cancel_lru_locks osc
	got=$(md5sum $tf | awk '{print $1}')
	ec_reactivate_ost ${safe_osts[1]}
	ec_reactivate_ost ${safe_osts[0]}
	[[ "$sum" == "$got" ]] ||
		error "large ${sz}M 2-down: $sum != $got"
	echo "  ${sz}M 2-down OK"

	# Spot-check: read a few individual blocks at
	# high offsets to verify per-stripe recovery
	local offsets=(0 31 64 96 127)
	local block_sums=()
	local i
	for i in "${offsets[@]}"; do
		block_sums+=($(dd if=$tf bs=1M skip=$i \
			count=1 2>/dev/null |
			md5sum | awk '{print $1}'))
	done

	ec_deactivate_ost ${safe_osts[0]}
	cancel_lru_locks osc

	local failures=0
	local j=0
	for i in "${offsets[@]}"; do
		local got_blk=$(dd if=$tf bs=1M skip=$i \
			count=1 2>/dev/null |
			md5sum | awk '{print $1}')
		if [[ "${block_sums[$j]}" != "$got_blk" ]]; then
			echo "block $i: ${block_sums[$j]}" \
				"!= $got_blk"
			failures=$((failures + 1))
		fi
		j=$((j + 1))
	done
	ec_reactivate_ost ${safe_osts[0]}

	(( failures == 0 )) ||
		error "$failures/${#offsets[@]} blocks " \
			"failed at high offsets"

	echo "** large file (${sz}M) recovery: all OK"
}
run_test 73e "EC large file recovery (128M)"

test_74a() {
	[[ $OSTCOUNT -ge 8 ]] || skip_env "needs >= 8 OSTs"
	enable_ec

	local tf=$DIR/$tfile

	stack_trap "rm -f $tf"
	local sz=64
	local rounds=20

	$LFS setstripe -E eof -S 1M -c 4 --ec 4+2 $tf ||
		error "setstripe failed"
	dd if=/dev/urandom of=$tf bs=1M count=$sz ||
		error "dd failed"
	$LFS mirror resync $tf || error "resync failed"

	local sum=$(md5sum $tf | awk '{print $1}')

	ec_classify_osts $tf
	(( ${#safe_osts[@]} >= 2 )) ||
		skip_env "need >= 2 safe data OSTs"

	# Race deactivation against active reads so the
	# failure lands mid-I/O.
	local failures=0
	local reads_done=0
	local i

	for ((i = 1; i <= rounds; i++)); do
		local pids=()
		local j
		for ((j = 0; j < 4; j++)); do
			md5sum $tf \
				> $TMP/ec_race_${j}_$tfile \
				2>&1 &
			pids+=($!)
		done

		# deactivate while readers are likely in flight
		sleep 0.$((RANDOM % 3))
		local victim=${safe_osts[$((i % 2))]}
		ec_deactivate_ost $victim

		for ((j = 0; j < 4; j++)); do
			if ! wait ${pids[$j]}; then
				echo "round $i reader $j:" \
					"process failed"
				failures=$((failures + 1))
				continue
			fi
			local got
			got=$(awk '{print $1}' \
				$TMP/ec_race_${j}_$tfile)
			if [[ "$sum" != "$got" ]]; then
				echo "round $i reader $j:" \
					"$sum != $got"
				failures=$((failures + 1))
			fi
			reads_done=$((reads_done + 1))
		done

		ec_reactivate_ost $victim
		cancel_lru_locks osc
	done
	stack_trap "rm -f $TMP/ec_race_*_$tfile"

	echo "** $reads_done reads across $rounds rounds," \
		"$failures failures"
	(( failures == 0 )) ||
		error "$failures reads failed racing with" \
			"OST deactivation"
}
run_test 74a "EC reads racing with OST deactivation"

test_75a() {
	(( OSTCOUNT >= 6 )) || skip_env "needs >= 6 OSTs"
	enable_ec

	local tf=$DIR/$tfile

	stack_trap "rm -f $tf"

	$LFS setstripe -E eof -S 1M -c 4 --ec 4+2 $tf ||
		error "setstripe --ec 4+2 failed"
	dd if=/dev/urandom of=$tf bs=1M count=8 ||
		error "dd failed"
	$LFS mirror resync $tf || error "resync failed"

	ec_classify_osts $tf
	(( ${#safe_osts[@]} >= 1 )) ||
		skip_env "need >= 1 safe data OST"

	# Baseline for the degraded reads below.  Without it the reads
	# only prove that md5sum exited 0, so a reconstruction returning
	# correct-length but wrong bytes would pass.
	local sum=$(md5sum $tf | awk '{print $1}')

	# stat/getattr on healthy file.  These are served from the MDS and
	# the inode LVB, so the injected BRW read fault does not affect
	# them; they assert that degrading an OST does not disturb the
	# size, not that stat itself goes through recovery.
	local sz
	sz=$(stat -c %s $tf)
	(( sz == 8 * 1024 * 1024 )) ||
		error "size $sz != 8M before degrade"

	# degrade 1 data OST
	ec_deactivate_ost ${safe_osts[0]}
	cancel_lru_locks osc

	# lfs getstripe must always work (MDS-only)
	$LFS getstripe $tf > /dev/null ||
		error "getstripe failed with OST down"

	# Data reads via EC recovery must work and return the same bytes
	local got
	got=$(md5sum $tf | awk '{print $1}') ||
		error "md5sum failed with OST down"
	[[ "$sum" == "$got" ]] ||
		error "degraded read returned wrong data: $sum vs $got"

	echo "** data read during degraded mode: OK"

	# Degrade a parity OST instead
	ec_reactivate_ost ${safe_osts[0]}
	(( ${#parity_osts[@]} >= 1 )) ||
		skip_env "need >= 1 parity OST"

	ec_deactivate_ost ${parity_osts[0]}
	cancel_lru_locks osc

	# Stat should succeed when only parity is down
	sz=$(stat -c %s $tf)
	(( sz == 8 * 1024 * 1024 )) ||
		error "size $sz != 8M after parity degrade"

	$LFS getstripe $tf > /dev/null ||
		error "getstripe failed with parity down"

	# With only parity unreadable the data stripes are all intact, so
	# the read must succeed without reconstructing anything.
	got=$(md5sum $tf | awk '{print $1}') ||
		error "md5sum failed with parity down"
	[[ "$sum" == "$got" ]] ||
		error "read with parity down returned wrong data:" \
			"$sum vs $got"

	echo "** stat/getattr during degraded mode: OK"
}
run_test 75a "EC stat/getattr during degraded mode"

test_75b() {
	(( OSTCOUNT >= 6 )) || skip_env "needs >= 6 OSTs"
	enable_ec

	local tf=$DIR/$tfile

	stack_trap "rm -f $tf"
	local iterations=50
	local fail_count=0

	$LFS setstripe -E eof -S 1M -c 4 --ec 4+2 $tf ||
		error "setstripe --ec 4+2 failed"

	ec_classify_osts $tf
	(( ${#safe_osts[@]} >= 1 )) ||
		skip_env "need >= 1 safe data OST"

	local i sum got
	for ((i = 0; i < iterations; i++)); do
		dd if=/dev/urandom of=$tf bs=1M count=4 \
			conv=notrunc 2>/dev/null ||
			error "dd failed iter $i"
		$LFS mirror resync $tf 2>/dev/null ||
			error "mirror resync failed"

		sum=$(md5sum $tf | awk '{print $1}')

		ec_deactivate_ost ${safe_osts[0]}
		cancel_lru_locks osc

		got=$(md5sum $tf | awk '{print $1}')
		ec_reactivate_ost ${safe_osts[0]}

		if [[ "$sum" != "$got" ]]; then
			((fail_count++))
			echo "FAIL iteration $i: $sum != $got"
		fi
	done

	(( fail_count == 0 )) ||
		error "$fail_count/$iterations iterations " \
			"had checksum mismatch"
	echo "** $iterations stress iterations passed"
}
run_test 75b "EC stress/repeatability degraded read loop"

test_75c() {
	(( OSTCOUNT >= 8 )) || skip_env "needs >= 8 OSTs"
	enable_ec

	local tdir=$DIR/$tdir
	test_mkdir $tdir
	stack_trap "rm -rf $tdir"

	# Create a PFL file with 2 EC components that
	# use overlapping OSTs via stripe rotation
	local tf=$tdir/$tfile
	$LFS setstripe \
		-E 4M -S 1M -c 4 --ec 4+2 \
		-E eof -S 1M -c 4 --ec 4+2 $tf ||
		error "PFL setstripe failed"

	dd if=/dev/urandom of=$tf bs=1M count=12 ||
		error "dd failed"
	$LFS mirror resync $tf 2>/dev/null ||
		error "mirror resync failed"

	# Verify both components are present
	local ncomp
	ncomp=$($LFS getstripe --component-count $tf)
	(( ncomp >= 4 )) ||
		error "expected >= 4 components, got $ncomp"

	# Degrade: pick an OST in the first component
	ec_classify_osts $tf
	(( ${#safe_osts[@]} >= 1 )) ||
		skip_env "need >= 1 safe data OST"

	local sum
	sum=$(md5sum $tf | awk '{print $1}')

	ec_deactivate_ost ${safe_osts[0]}
	cancel_lru_locks osc

	local got
	got=$(md5sum $tf | awk '{print $1}')
	[[ "$sum" == "$got" ]] ||
		error "PFL rotation collision: $sum != $got"

	echo "** PFL stripe rotation collision: data OK"
}
run_test 75c "EC stripe rotation collision in PFL"

test_75d() {
	(( OSTCOUNT >= 6 )) || skip_env "needs >= 6 OSTs"
	enable_ec

	local tf=$DIR/$tfile

	stack_trap "rm -f $tf"

	$LFS setstripe -E eof -S 1M -c 4 --ec 4+2 $tf ||
		error "setstripe --ec 4+2 failed"
	dd if=/dev/urandom of=$tf bs=1M count=8 ||
		error "dd failed"
	$LFS mirror resync $tf ||
		error "mirror resync failed"

	# mirror verify must succeed on a healthy file
	$LFS mirror verify $tf ||
		error "mirror verify failed on healthy file"

	local sum=$(md5sum $tf | awk '{print $1}')

	ec_classify_osts $tf
	(( ${#parity_osts[@]} >= 1 )) ||
		skip_env "need >= 1 parity OST"
	(( ${#safe_osts[@]} >= 1 )) ||
		skip_env "need >= 1 safe data OST"

	# Test 1: SEEK_DATA/SEEK_HOLE with a data OST down.  This asserts
	# that LSEEK still returns the right answers while a stripe is
	# unreadable; it does not by itself prove the lov_lock_enqueue
	# degraded-stripe path ran, since a fully allocated file can be
	# answered without consulting every stripe.  A sparse file is used
	# so the offsets below have to come from the OSTs, and the seek
	# starts past stripe 0 so it crosses the degraded stripe.
	#
	# ZFS lseek does not report holes reliably for dirty data, as test
	# 12b describes, so neither the resync nor the offsets below can be
	# asserted there and only the mirror verify below runs (LU-14217).
	if [[ "$ost1_FSTYPE" != "zfs" ]]; then
		local tfs=$DIR/$tfile.sparse

		$LFS setstripe -E eof -S 1M -c 4 --ec 4+2 $tfs ||
			error "setstripe sparse failed"
		stack_trap "rm -f $tfs"
		dd if=/dev/urandom of=$tfs bs=1M count=1 seek=5 \
			conv=notrunc || error "sparse dd failed"
		$LFS mirror resync $tfs || error "resync sparse failed"

		echo "** Test SEEK_DATA with data OST down"
		ec_deactivate_ost ${safe_osts[0]}
		cancel_lru_locks osc

		local data_off hole_off

		data_off=$($LUSTRE/tests/lseek_test -d 1048576 $tfs) ||
			error "SEEK_DATA failed with data OST down"
		(( data_off == 5 * 1024 * 1024 )) ||
			error "SEEK_DATA returned $data_off, expected 5M"

		hole_off=$($LUSTRE/tests/lseek_test -l 0 $tfs) ||
			error "SEEK_HOLE failed with data OST down"
		(( hole_off == 0 )) ||
			error "SEEK_HOLE returned $hole_off, expected 0"

		ec_reactivate_ost ${safe_osts[0]}
	fi

	# Test 2: parity OST down — verify should
	# detect unreadable parity but not crash/hang
	echo "** Test mirror verify with parity OST down"
	ec_deactivate_ost ${parity_osts[0]}
	cancel_lru_locks osc

	# Verify cannot read the parity objects, so it is allowed to fail;
	# rc is reported rather than asserted on.  What must hold is that it
	# returns instead of hanging, and leaves the file itself intact --
	# that is what the checksum below actually checks.
	local out rc
	out=$($LFS mirror verify $tf 2>&1)
	rc=$?
	echo "mirror verify (parity down): rc=$rc"
	echo "$out" | head -5

	ec_reactivate_ost ${parity_osts[0]}
	cancel_lru_locks osc

	local sum_after=$(md5sum $tf | awk '{print $1}')
	[[ "$sum" == "$sum_after" ]] ||
		error "mirror verify with parity down changed the data:" \
			"$sum vs $sum_after"

	# Note: mirror verify with data OST down is
	# currently blocked by an O_DIRECT bug
	# (LBUGs in osc_req_attr_set for uncovered
	# pages). When that is fixed, add a test here.
	echo "** mirror verify degraded tests: done"
}
run_test 75d "EC lfs mirror verify in degraded mode"

test_75e() {
	(( OSTCOUNT >= 6 )) || skip_env "needs >= 6 OSTs"
	enable_ec

	local tdir=$DIR/$tdir
	test_mkdir $tdir
	stack_trap "rm -rf $tdir"

	# Set EC default on directory
	$LFS setstripe -E eof -S 1M -c 4 --ec 4+2 $tdir ||
		error "setstripe on dir failed"

	# Create files that inherit directory layout
	local i tf sum got
	for ((i = 0; i < 5; i++)); do
		tf=$tdir/file.$i
		dd if=/dev/urandom of=$tf bs=1M count=4 \
			2>/dev/null ||
			error "dd failed for file.$i"
	done

	# Verify files inherited EC layout
	for ((i = 0; i < 5; i++)); do
		tf=$tdir/file.$i
		local cstripe
		cstripe=$($LFS getstripe $tf |
			awk '/lcme_cstripe_count:/ {print $2}')
		(( cstripe == 2 )) ||
			error "file.$i: cstripe $cstripe != 2"
	done

	# Resync all
	for ((i = 0; i < 5; i++)); do
		$LFS mirror resync $tdir/file.$i 2>/dev/null ||
			error "mirror resync failed"
	done

	# Degrade and verify all files still readable
	ec_classify_osts $tdir/file.0
	(( ${#safe_osts[@]} >= 1 )) ||
		skip_env "need >= 1 safe data OST"

	ec_deactivate_ost ${safe_osts[0]}
	cancel_lru_locks osc

	for ((i = 0; i < 5; i++)); do
		tf=$tdir/file.$i
		md5sum $tf > /dev/null 2>&1 ||
			error "read failed on file.$i " \
				"with OST down"
	done

	echo "** dir-inherited EC recovery: all OK"
}
run_test 75e "EC directory-inherited layout recovery"

test_75f() {
	(( OSTCOUNT >= 6 )) || skip_env "needs >= 6 OSTs"
	enable_ec

	local tdir=$DIR/$tdir
	test_mkdir $tdir
	stack_trap "rm -rf $tdir"

	# Create multiple EC files — stripe rotation
	# means each file starts on a different OST
	local nfiles=8
	local i tf
	for ((i = 0; i < nfiles; i++)); do
		tf=$tdir/file.$i
		$LFS setstripe -E eof -S 1M -c 4 \
			--ec 4+2 $tf ||
			error "setstripe file.$i failed"
		dd if=/dev/urandom of=$tf bs=1M count=4 \
			2>/dev/null ||
			error "dd file.$i failed"
		$LFS mirror resync $tf 2>/dev/null ||
			error "mirror resync failed"
	done

	# Collect md5sums before degrade
	local -A sums
	for ((i = 0; i < nfiles; i++)); do
		sums[$i]=$(md5sum $tdir/file.$i |
			awk '{print $1}')
	done

	# Degrade an OST
	ec_classify_osts $tdir/file.0
	(( ${#safe_osts[@]} >= 1 )) ||
		skip_env "need >= 1 safe data OST"

	ec_deactivate_ost ${safe_osts[0]}
	cancel_lru_locks osc

	# All files should be readable via EC recovery
	local fail=0
	for ((i = 0; i < nfiles; i++)); do
		local got
		got=$(md5sum $tdir/file.$i 2>&1 |
			awk '{print $1}')
		if [[ "${sums[$i]}" != "$got" ]]; then
			echo "FAIL file.$i: expected " \
				"${sums[$i]} got $got"
			((fail++))
		fi
	done

	(( fail == 0 )) ||
		error "$fail/$nfiles files had " \
			"checksum mismatch"
	echo "** stripe rotation across $nfiles files: OK"
}
run_test 75f "EC stripe rotation across files"

complete_test $SECONDS
check_and_cleanup_lustre
exit_status