Browse Source
Merge
Merge edbb237e7a into 51841a2e04
committed by
GitHub
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
34 changed files with 5218 additions and 28 deletions
-
753.github/workflows/posix-compliance.yml
-
654test/fuse_integration/POSIX_COMPLIANCE.md
-
271test/fuse_integration/POSIX_IMPLEMENTATION_ROADMAP.md
-
12test/fuse_integration/atime_linux.go
-
12test/fuse_integration/atime_nonlinux.go
-
12test/fuse_integration/atime_windows.go
-
5test/fuse_integration/concurrent_operations_test.go
-
39test/fuse_integration/directio_darwin.go
-
21test/fuse_integration/directio_linux.go
-
29test/fuse_integration/directio_unsupported.go
-
2test/fuse_integration/directory_operations_test.go
-
79test/fuse_integration/fallocate_darwin.go
-
38test/fuse_integration/fallocate_linux.go
-
30test/fuse_integration/fallocate_unsupported.go
-
58test/fuse_integration/framework.go
-
11test/fuse_integration/go.mod
-
10test/fuse_integration/go.sum
-
2test/fuse_integration/minimal_test.go
-
85test/fuse_integration/mmap_unix.go
-
47test/fuse_integration/mmap_unsupported.go
-
509test/fuse_integration/posix_Makefile
-
710test/fuse_integration/posix_compliance_test.go
-
758test/fuse_integration/posix_extended_test.go
-
562test/fuse_integration/posix_external_test.go
-
43test/fuse_integration/sendfile_darwin.go
-
33test/fuse_integration/sendfile_linux.go
-
19test/fuse_integration/sendfile_unsupported.go
-
2test/fuse_integration/simple_test.go
-
126test/fuse_integration/vectored_io_unix.go
-
41test/fuse_integration/vectored_io_unsupported.go
-
2test/fuse_integration/working_demo_test.go
-
125test/fuse_integration/xattr_darwin.go
-
115test/fuse_integration/xattr_linux.go
-
31test/fuse_integration/xattr_unsupported.go
@ -0,0 +1,753 @@ |
|||||
|
name: POSIX Compliance Tests |
||||
|
|
||||
|
on: |
||||
|
push: |
||||
|
branches: [ main, master, develop ] |
||||
|
paths: |
||||
|
- 'weed/mount/**' |
||||
|
- 'weed/command/mount*' |
||||
|
- 'weed/command/fuse*' |
||||
|
- 'test/fuse_integration/**' |
||||
|
pull_request: |
||||
|
branches: [ main, master, develop ] |
||||
|
paths: |
||||
|
- 'weed/mount/**' |
||||
|
- 'weed/command/mount*' |
||||
|
- 'weed/command/fuse*' |
||||
|
- 'test/fuse_integration/**' |
||||
|
workflow_dispatch: |
||||
|
inputs: |
||||
|
test_type: |
||||
|
description: 'Type of POSIX tests to run (default: full for comprehensive testing)' |
||||
|
required: true |
||||
|
default: 'full' |
||||
|
type: choice |
||||
|
options: |
||||
|
- critical |
||||
|
- basic |
||||
|
- extended |
||||
|
- full |
||||
|
enable_external_tests: |
||||
|
description: 'Run external test suites (slower)' |
||||
|
required: false |
||||
|
default: false |
||||
|
type: boolean |
||||
|
|
||||
|
env: |
||||
|
GO_VERSION: '1.24' |
||||
|
TIMEOUT: '45m' |
||||
|
|
||||
|
jobs: |
||||
|
posix-compliance-ubuntu: |
||||
|
runs-on: ubuntu-latest |
||||
|
timeout-minutes: 60 |
||||
|
|
||||
|
strategy: |
||||
|
matrix: |
||||
|
fuse-version: ['2.9', '3.0'] |
||||
|
fail-fast: false |
||||
|
|
||||
|
steps: |
||||
|
- name: Checkout code |
||||
|
uses: actions/checkout@v4 |
||||
|
with: |
||||
|
fetch-depth: 1 |
||||
|
|
||||
|
- name: Set up Go ${{ env.GO_VERSION }} |
||||
|
uses: actions/setup-go@v4 |
||||
|
with: |
||||
|
go-version: ${{ env.GO_VERSION }} |
||||
|
cache: true |
||||
|
cache-dependency-path: | |
||||
|
go.sum |
||||
|
test/fuse_integration/go.sum |
||||
|
|
||||
|
- name: Install system dependencies |
||||
|
run: | |
||||
|
sudo apt-get update |
||||
|
sudo apt-get install -y \ |
||||
|
attr \ |
||||
|
acl \ |
||||
|
build-essential \ |
||||
|
git \ |
||||
|
python3-pip |
||||
|
|
||||
|
# Install FUSE version specific packages |
||||
|
if [ "${{ matrix.fuse-version }}" = "2.9" ]; then |
||||
|
echo "Installing FUSE 2.9..." |
||||
|
sudo apt-get install -y fuse libfuse-dev |
||||
|
elif [ "${{ matrix.fuse-version }}" = "3.0" ]; then |
||||
|
echo "Installing FUSE 3.0..." |
||||
|
sudo apt-get install -y fuse3 libfuse3-dev |
||||
|
# Also install fuse2 for compatibility if needed |
||||
|
sudo apt-get install -y fuse libfuse-dev |
||||
|
fi |
||||
|
|
||||
|
- name: Set up user permissions for FUSE |
||||
|
run: | |
||||
|
# Create fuse group if it doesn't exist |
||||
|
sudo groupadd -f fuse |
||||
|
|
||||
|
# Add user to fuse group |
||||
|
sudo usermod -a -G fuse $USER |
||||
|
|
||||
|
# Set permissions on FUSE device |
||||
|
sudo chmod 666 /dev/fuse |
||||
|
|
||||
|
# Ensure fuse module is loaded |
||||
|
sudo modprobe fuse || true |
||||
|
|
||||
|
# Verify setup |
||||
|
echo "FUSE setup verification:" |
||||
|
ls -la /dev/fuse |
||||
|
groups $USER | grep fuse || echo "User not in fuse group yet (will be effective after login)" |
||||
|
lsmod | grep fuse || echo "FUSE module not loaded" |
||||
|
|
||||
|
- name: Install external test tools |
||||
|
if: ${{ github.event.inputs.enable_external_tests == 'true' }} |
||||
|
run: | |
||||
|
# Install nfstest for POSIX API verification |
||||
|
pip3 install --user nfstest |
||||
|
|
||||
|
# Install FIO for performance testing |
||||
|
sudo apt-get install -y fio |
||||
|
|
||||
|
# Install additional test utilities |
||||
|
sudo apt-get install -y stress-ng |
||||
|
|
||||
|
- name: Build SeaweedFS |
||||
|
run: | |
||||
|
make |
||||
|
# Verify binary exists and is executable (it's installed to GOPATH/bin) |
||||
|
weed version |
||||
|
|
||||
|
# Make weed binary available in PATH (it's already there from go install) |
||||
|
sudo cp $(which weed) /usr/local/bin/weed |
||||
|
which weed |
||||
|
weed version |
||||
|
|
||||
|
- name: Set up SeaweedFS cluster |
||||
|
run: | |
||||
|
start_and_wait() { |
||||
|
local name=$1 |
||||
|
local url=$2 |
||||
|
local pidfile=$3 |
||||
|
shift 3 |
||||
|
local cmd="$@" |
||||
|
|
||||
|
echo "Starting $name..." |
||||
|
$cmd > "/tmp/seaweedfs/$name.log" 2>&1 & |
||||
|
local pid=$! |
||||
|
echo $pid > "$pidfile" |
||||
|
|
||||
|
echo "Waiting for $name to start..." |
||||
|
for i in {1..30}; do |
||||
|
if curl -sf "$url" > /dev/null 2>&1; then |
||||
|
echo "$name is ready" |
||||
|
return 0 |
||||
|
fi |
||||
|
if [ $i -eq 30 ]; then |
||||
|
echo "$name failed to start" |
||||
|
cat "/tmp/seaweedfs/$name.log" |
||||
|
exit 1 |
||||
|
fi |
||||
|
sleep 2 |
||||
|
done |
||||
|
} |
||||
|
|
||||
|
mkdir -p /tmp/seaweedfs/{master,volume,filer,mount} |
||||
|
|
||||
|
start_and_wait "master" "http://127.0.0.1:9333/cluster/status" "/tmp/seaweedfs/master.pid" \ |
||||
|
"weed master -ip=127.0.0.1 -port=9333 -mdir=/tmp/seaweedfs/master -raftBootstrap=true" |
||||
|
|
||||
|
start_and_wait "volume" "http://127.0.0.1:8080/status" "/tmp/seaweedfs/volume.pid" \ |
||||
|
"weed volume -mserver=127.0.0.1:9333 -ip=127.0.0.1 -port=8080 -dir=/tmp/seaweedfs/volume -max=100" |
||||
|
|
||||
|
start_and_wait "filer" "http://127.0.0.1:8888/" "/tmp/seaweedfs/filer.pid" \ |
||||
|
"weed filer -master=127.0.0.1:9333 -ip=127.0.0.1 -port=8888" |
||||
|
|
||||
|
echo "SeaweedFS cluster status:" |
||||
|
curl -s http://127.0.0.1:9333/cluster/status || true |
||||
|
|
||||
|
- name: Set up FUSE mount |
||||
|
run: | |
||||
|
# Create mount point |
||||
|
MOUNT_POINT="/tmp/seaweedfs/mount" |
||||
|
mkdir -p $MOUNT_POINT |
||||
|
|
||||
|
echo "Mounting SeaweedFS FUSE filesystem..." |
||||
|
# Mount SeaweedFS FUSE filesystem in background |
||||
|
weed mount \ |
||||
|
-filer=127.0.0.1:8888 \ |
||||
|
-dir=$MOUNT_POINT \ |
||||
|
-filer.path=/ \ |
||||
|
-dirAutoCreate=true \ |
||||
|
-allowOthers=true \ |
||||
|
-nonempty=true \ |
||||
|
> /tmp/seaweedfs/mount.log 2>&1 & |
||||
|
MOUNT_PID=$! |
||||
|
echo $MOUNT_PID > /tmp/seaweedfs/mount.pid |
||||
|
|
||||
|
# Wait for mount to be ready |
||||
|
echo "Waiting for FUSE mount to be ready..." |
||||
|
for i in {1..30}; do |
||||
|
if mountpoint -q $MOUNT_POINT 2>/dev/null; then |
||||
|
echo "FUSE mount is ready" |
||||
|
break |
||||
|
fi |
||||
|
# Alternative check - try to list the directory |
||||
|
if ls -la $MOUNT_POINT > /dev/null 2>&1; then |
||||
|
echo "FUSE mount is ready (directory accessible)" |
||||
|
break |
||||
|
fi |
||||
|
if [ $i -eq 30 ]; then |
||||
|
echo "FUSE mount failed to be ready" |
||||
|
cat /tmp/seaweedfs/mount.log |
||||
|
echo "Mount status:" |
||||
|
mount | grep fuse || echo "No FUSE mounts found" |
||||
|
ps aux | grep weed || echo "No weed processes found" |
||||
|
exit 1 |
||||
|
fi |
||||
|
sleep 2 |
||||
|
done |
||||
|
|
||||
|
# Verify mount is working |
||||
|
echo "Testing FUSE mount functionality..." |
||||
|
echo "test" > $MOUNT_POINT/test_file.txt |
||||
|
if [ "$(cat $MOUNT_POINT/test_file.txt)" = "test" ]; then |
||||
|
echo "FUSE mount is working correctly" |
||||
|
rm $MOUNT_POINT/test_file.txt |
||||
|
else |
||||
|
echo "FUSE mount is not working properly" |
||||
|
exit 1 |
||||
|
fi |
||||
|
|
||||
|
# Export mount point for tests |
||||
|
echo "SEAWEEDFS_MOUNT_POINT=$MOUNT_POINT" >> $GITHUB_ENV |
||||
|
|
||||
|
- name: Set up test environment |
||||
|
run: | |
||||
|
cd test/fuse_integration |
||||
|
|
||||
|
# Initialize Go module for tests |
||||
|
if [ ! -f go.mod ]; then |
||||
|
go mod init seaweedfs-posix-tests |
||||
|
go mod tidy |
||||
|
fi |
||||
|
|
||||
|
# Create reports directory |
||||
|
mkdir -p reports |
||||
|
|
||||
|
# Set up external tools if requested |
||||
|
# Note: External tools setup is now optional and won't fail the build |
||||
|
if [ "${{ github.event.inputs.enable_external_tests }}" = "true" ]; then |
||||
|
echo "Setting up external POSIX testing tools..." |
||||
|
if make -f posix_Makefile setup-external-tools; then |
||||
|
# Make external tools mandatory when explicitly requested and successfully set up |
||||
|
export POSIX_REQUIRE_EXTERNAL_TOOLS="true" |
||||
|
echo "POSIX_REQUIRE_EXTERNAL_TOOLS=true" >> $GITHUB_ENV |
||||
|
echo "✅ External tools setup completed successfully" |
||||
|
else |
||||
|
echo "⚠️ External tools setup failed - continuing with basic tests only" |
||||
|
echo "External tools will be skipped but tests will continue" |
||||
|
echo "This is expected behavior when build tools are not available" |
||||
|
fi |
||||
|
fi |
||||
|
|
||||
|
# Verify SeaweedFS cluster is accessible |
||||
|
echo "Verifying SeaweedFS cluster accessibility..." |
||||
|
curl -sf http://127.0.0.1:9333/cluster/status |
||||
|
curl -sf http://127.0.0.1:8080/status |
||||
|
curl -sf http://127.0.0.1:8888/ |
||||
|
|
||||
|
# Verify mount point |
||||
|
echo "Mount point: $SEAWEEDFS_MOUNT_POINT" |
||||
|
ls -la $SEAWEEDFS_MOUNT_POINT |
||||
|
|
||||
|
# Verify mount is actually working by testing file operations |
||||
|
echo "Verifying SeaweedFS mount functionality..." |
||||
|
TEST_FILE="$SEAWEEDFS_MOUNT_POINT/.mount_verification_test" |
||||
|
echo "SeaweedFS mount test" > "$TEST_FILE" |
||||
|
if [ "$(cat "$TEST_FILE")" = "SeaweedFS mount test" ]; then |
||||
|
echo "✅ Mount verification successful" |
||||
|
rm -f "$TEST_FILE" |
||||
|
else |
||||
|
echo "❌ Mount verification failed - mount may not be working properly" |
||||
|
exit 1 |
||||
|
fi |
||||
|
|
||||
|
# Check if this is actually a FUSE mount |
||||
|
if mount | grep -q "$SEAWEEDFS_MOUNT_POINT"; then |
||||
|
echo "✅ FUSE mount detected:" |
||||
|
mount | grep "$SEAWEEDFS_MOUNT_POINT" |
||||
|
else |
||||
|
echo "⚠️ Warning: Mount point not found in mount table" |
||||
|
fi |
||||
|
|
||||
|
- name: Run POSIX compliance tests |
||||
|
id: posix-tests |
||||
|
env: |
||||
|
SEAWEEDFS_MOUNT_POINT: ${{ env.SEAWEEDFS_MOUNT_POINT }} |
||||
|
POSIX_REQUIRE_EXTERNAL_TOOLS: ${{ env.POSIX_REQUIRE_EXTERNAL_TOOLS }} |
||||
|
run: | |
||||
|
cd test/fuse_integration |
||||
|
|
||||
|
# Determine which tests to run |
||||
|
TEST_TYPE="${{ github.event.inputs.test_type }}" |
||||
|
if [ -z "$TEST_TYPE" ]; then |
||||
|
TEST_TYPE="full" |
||||
|
fi |
||||
|
|
||||
|
echo "Running POSIX tests: $TEST_TYPE" |
||||
|
if [ "$TEST_TYPE" = "full" ]; then |
||||
|
echo "🚀 Running COMPREHENSIVE POSIX test suite (including external test suites)" |
||||
|
else |
||||
|
echo "Using mount point: $SEAWEEDFS_MOUNT_POINT" |
||||
|
fi |
||||
|
|
||||
|
# Set test configuration to use our mounted filesystem |
||||
|
export TEST_MOUNT_POINT="$SEAWEEDFS_MOUNT_POINT" |
||||
|
export TEST_SKIP_CLUSTER_SETUP="true" |
||||
|
|
||||
|
# Debug: Show environment variables |
||||
|
echo "🔍 Test Environment:" |
||||
|
echo " TEST_MOUNT_POINT=$TEST_MOUNT_POINT" |
||||
|
echo " TEST_SKIP_CLUSTER_SETUP=$TEST_SKIP_CLUSTER_SETUP" |
||||
|
echo " SEAWEEDFS_MOUNT_POINT=$SEAWEEDFS_MOUNT_POINT" |
||||
|
|
||||
|
case "$TEST_TYPE" in |
||||
|
"critical") |
||||
|
echo "Running critical POSIX tests (basic compliance only)..." |
||||
|
make -f posix_Makefile test-posix-critical |
||||
|
;; |
||||
|
"basic") |
||||
|
echo "Running basic POSIX tests (core file operations)..." |
||||
|
make -f posix_Makefile test-posix-basic |
||||
|
;; |
||||
|
"extended") |
||||
|
echo "Running extended POSIX tests (including advanced features)..." |
||||
|
make -f posix_Makefile test-posix-extended |
||||
|
;; |
||||
|
"full") |
||||
|
echo "🚀 Running COMPREHENSIVE POSIX test suite (all tests including external)..." |
||||
|
echo " This includes: basic, extended, and external test suites" |
||||
|
echo " External suites: pjdfstest, nfstest_posix, and custom stress tests" |
||||
|
make -f posix_Makefile test-posix-full |
||||
|
;; |
||||
|
*) |
||||
|
echo "Unknown test type: $TEST_TYPE, defaulting to full suite" |
||||
|
echo "🚀 Running full POSIX test suite (all tests including external)..." |
||||
|
make -f posix_Makefile test-posix-full |
||||
|
;; |
||||
|
esac |
||||
|
|
||||
|
- name: Run external test suites |
||||
|
if: ${{ github.event.inputs.enable_external_tests == 'true' }} |
||||
|
continue-on-error: true |
||||
|
run: | |
||||
|
cd test/fuse_integration |
||||
|
|
||||
|
# Run external tests (may fail on some systems) |
||||
|
make -f posix_Makefile test-nfstest-posix || echo "nfstest failed or not available" |
||||
|
make -f posix_Makefile test-fio-posix || echo "FIO tests failed or not available" |
||||
|
|
||||
|
- name: Generate compliance report |
||||
|
if: always() |
||||
|
run: | |
||||
|
cd test/fuse_integration |
||||
|
make -f posix_Makefile generate-report |
||||
|
|
||||
|
- name: Cleanup SeaweedFS cluster and FUSE mount |
||||
|
if: always() |
||||
|
run: | |
||||
|
stop_process() { |
||||
|
local name=$1 |
||||
|
local pidfile=$2 |
||||
|
if [ -f "$pidfile" ]; then |
||||
|
local pid=$(cat "$pidfile") |
||||
|
if kill -0 $pid 2>/dev/null; then |
||||
|
echo "Stopping $name process (PID: $pid)..." |
||||
|
kill -TERM $pid || true |
||||
|
sleep 2 |
||||
|
kill -KILL $pid 2>/dev/null || true |
||||
|
fi |
||||
|
fi |
||||
|
} |
||||
|
|
||||
|
echo "Cleaning up SeaweedFS cluster and FUSE mount..." |
||||
|
|
||||
|
MOUNT_POINT="/tmp/seaweedfs/mount" |
||||
|
if mountpoint -q $MOUNT_POINT 2>/dev/null; then |
||||
|
echo "Unmounting FUSE filesystem..." |
||||
|
fusermount -u $MOUNT_POINT || umount $MOUNT_POINT || true |
||||
|
fi |
||||
|
|
||||
|
stop_process "mount" "/tmp/seaweedfs/mount.pid" |
||||
|
stop_process "filer" "/tmp/seaweedfs/filer.pid" |
||||
|
stop_process "volume" "/tmp/seaweedfs/volume.pid" |
||||
|
stop_process "master" "/tmp/seaweedfs/master.pid" |
||||
|
|
||||
|
pkill -f "weed " || true |
||||
|
|
||||
|
fusermount -u $MOUNT_POINT 2>/dev/null || true |
||||
|
umount $MOUNT_POINT 2>/dev/null || true |
||||
|
|
||||
|
rm -rf /tmp/seaweedfs || true |
||||
|
|
||||
|
echo "Cleanup completed" |
||||
|
|
||||
|
- name: Collect system information for debugging |
||||
|
if: failure() |
||||
|
run: | |
||||
|
cd test/fuse_integration |
||||
|
mkdir -p reports/debug |
||||
|
|
||||
|
echo "System Information" > reports/debug/system_info.txt |
||||
|
uname -a >> reports/debug/system_info.txt |
||||
|
echo "" >> reports/debug/system_info.txt |
||||
|
|
||||
|
echo "Mount Information" >> reports/debug/system_info.txt |
||||
|
mount >> reports/debug/system_info.txt |
||||
|
echo "" >> reports/debug/system_info.txt |
||||
|
|
||||
|
echo "Disk Space" >> reports/debug/system_info.txt |
||||
|
df -h >> reports/debug/system_info.txt |
||||
|
echo "" >> reports/debug/system_info.txt |
||||
|
|
||||
|
echo "Memory Information" >> reports/debug/system_info.txt |
||||
|
free -h >> reports/debug/system_info.txt |
||||
|
echo "" >> reports/debug/system_info.txt |
||||
|
|
||||
|
echo "FUSE Information" >> reports/debug/system_info.txt |
||||
|
ls -la /dev/fuse >> reports/debug/system_info.txt |
||||
|
lsmod | grep fuse >> reports/debug/system_info.txt |
||||
|
echo "" >> reports/debug/system_info.txt |
||||
|
|
||||
|
echo "SeaweedFS Version" >> reports/debug/system_info.txt |
||||
|
weed version >> reports/debug/system_info.txt |
||||
|
echo "" >> reports/debug/system_info.txt |
||||
|
|
||||
|
echo "Go Version" >> reports/debug/system_info.txt |
||||
|
go version >> reports/debug/system_info.txt |
||||
|
echo "" >> reports/debug/system_info.txt |
||||
|
|
||||
|
echo "Running Processes" >> reports/debug/system_info.txt |
||||
|
ps aux | grep -E "(weed|fuse)" >> reports/debug/system_info.txt |
||||
|
echo "" >> reports/debug/system_info.txt |
||||
|
|
||||
|
# Collect SeaweedFS service logs |
||||
|
echo "=== SeaweedFS Service Logs ===" >> reports/debug/system_info.txt |
||||
|
if [ -f /tmp/seaweedfs/master.log ]; then |
||||
|
echo "Master Log:" >> reports/debug/system_info.txt |
||||
|
tail -50 /tmp/seaweedfs/master.log >> reports/debug/system_info.txt |
||||
|
echo "" >> reports/debug/system_info.txt |
||||
|
fi |
||||
|
|
||||
|
if [ -f /tmp/seaweedfs/volume.log ]; then |
||||
|
echo "Volume Log:" >> reports/debug/system_info.txt |
||||
|
tail -50 /tmp/seaweedfs/volume.log >> reports/debug/system_info.txt |
||||
|
echo "" >> reports/debug/system_info.txt |
||||
|
fi |
||||
|
|
||||
|
if [ -f /tmp/seaweedfs/filer.log ]; then |
||||
|
echo "Filer Log:" >> reports/debug/system_info.txt |
||||
|
tail -50 /tmp/seaweedfs/filer.log >> reports/debug/system_info.txt |
||||
|
echo "" >> reports/debug/system_info.txt |
||||
|
fi |
||||
|
|
||||
|
if [ -f /tmp/seaweedfs/mount.log ]; then |
||||
|
echo "Mount Log:" >> reports/debug/system_info.txt |
||||
|
tail -50 /tmp/seaweedfs/mount.log >> reports/debug/system_info.txt |
||||
|
echo "" >> reports/debug/system_info.txt |
||||
|
fi |
||||
|
|
||||
|
# Copy full logs as separate files |
||||
|
if [ -f /tmp/seaweedfs/master.log ]; then |
||||
|
cp /tmp/seaweedfs/master.log reports/debug/master.log |
||||
|
fi |
||||
|
if [ -f /tmp/seaweedfs/volume.log ]; then |
||||
|
cp /tmp/seaweedfs/volume.log reports/debug/volume.log |
||||
|
fi |
||||
|
if [ -f /tmp/seaweedfs/filer.log ]; then |
||||
|
cp /tmp/seaweedfs/filer.log reports/debug/filer.log |
||||
|
fi |
||||
|
if [ -f /tmp/seaweedfs/mount.log ]; then |
||||
|
cp /tmp/seaweedfs/mount.log reports/debug/mount.log |
||||
|
fi |
||||
|
|
||||
|
- name: Upload test results |
||||
|
uses: actions/upload-artifact@v4 |
||||
|
if: always() |
||||
|
with: |
||||
|
name: posix-test-results-ubuntu-fuse${{ matrix.fuse-version }} |
||||
|
path: | |
||||
|
test/fuse_integration/reports/ |
||||
|
test/fuse_integration/*.log |
||||
|
retention-days: 30 |
||||
|
|
||||
|
- name: Upload test coverage |
||||
|
uses: actions/upload-artifact@v4 |
||||
|
if: always() && (github.event.inputs.test_type == 'full' || github.event.inputs.test_type == 'extended') |
||||
|
with: |
||||
|
name: posix-coverage-ubuntu |
||||
|
path: test/fuse_integration/reports/posix_coverage.html |
||||
|
retention-days: 14 |
||||
|
|
||||
|
- name: Comment PR with results |
||||
|
if: github.event_name == 'pull_request' && always() |
||||
|
uses: actions/github-script@v7 |
||||
|
with: |
||||
|
script: | |
||||
|
const fs = require('fs'); |
||||
|
const path = require('path'); |
||||
|
|
||||
|
const reportPath = 'test/fuse_integration/reports/posix_compliance_summary.txt'; |
||||
|
|
||||
|
if (fs.existsSync(reportPath)) { |
||||
|
const report = fs.readFileSync(reportPath, 'utf8'); |
||||
|
const comment = `## POSIX Compliance Test Results |
||||
|
|
||||
|
**Go Version:** ${{ env.GO_VERSION }} |
||||
|
**FUSE Version:** ${{ matrix.fuse-version }} |
||||
|
**Test Type:** ${{ github.event.inputs.test_type || 'full' }} |
||||
|
|
||||
|
<details> |
||||
|
<summary>Test Summary</summary> |
||||
|
|
||||
|
\`\`\` |
||||
|
${report} |
||||
|
\`\`\` |
||||
|
</details> |
||||
|
|
||||
|
📊 Full results available in [test artifacts](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}) |
||||
|
`; |
||||
|
|
||||
|
github.rest.issues.createComment({ |
||||
|
issue_number: context.issue.number, |
||||
|
owner: context.repo.owner, |
||||
|
repo: context.repo.repo, |
||||
|
body: comment |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
posix-compliance-macos: |
||||
|
runs-on: macos-latest |
||||
|
timeout-minutes: 60 |
||||
|
|
||||
|
steps: |
||||
|
- name: Checkout code |
||||
|
uses: actions/checkout@v4 |
||||
|
|
||||
|
- name: Set up Go |
||||
|
uses: actions/setup-go@v4 |
||||
|
with: |
||||
|
go-version: ${{ env.GO_VERSION }} |
||||
|
cache: true |
||||
|
|
||||
|
- name: Install macFUSE |
||||
|
run: | |
||||
|
# Install macFUSE |
||||
|
brew install macfuse |
||||
|
|
||||
|
# Note: macFUSE may require system extension approval on macOS |
||||
|
# This step may need manual intervention in some cases |
||||
|
|
||||
|
- name: Build SeaweedFS |
||||
|
run: | |
||||
|
make |
||||
|
# Verify binary exists and is executable (it's installed to GOPATH/bin) |
||||
|
weed version |
||||
|
|
||||
|
- name: Run critical POSIX tests (macOS) |
||||
|
continue-on-error: true # macOS FUSE can be more restrictive |
||||
|
run: | |
||||
|
cd test/fuse_integration |
||||
|
|
||||
|
if [ ! -f go.mod ]; then |
||||
|
go mod init seaweedfs-posix-tests |
||||
|
go mod tidy |
||||
|
fi |
||||
|
|
||||
|
mkdir -p reports |
||||
|
|
||||
|
# Run basic tests only on macOS due to FUSE limitations |
||||
|
make -f posix_Makefile test-posix-critical |
||||
|
|
||||
|
- name: Upload macOS test results |
||||
|
uses: actions/upload-artifact@v4 |
||||
|
if: always() |
||||
|
with: |
||||
|
name: posix-test-results-macos |
||||
|
path: | |
||||
|
test/fuse_integration/reports/ |
||||
|
test/fuse_integration/*.log |
||||
|
retention-days: 30 |
||||
|
|
||||
|
security-analysis: |
||||
|
runs-on: ubuntu-latest |
||||
|
if: github.event_name == 'pull_request' |
||||
|
|
||||
|
steps: |
||||
|
- name: Checkout code |
||||
|
uses: actions/checkout@v4 |
||||
|
|
||||
|
- name: Set up Go |
||||
|
uses: actions/setup-go@v4 |
||||
|
with: |
||||
|
go-version: ${{ env.GO_VERSION }} |
||||
|
|
||||
|
- name: Install security analysis tools |
||||
|
run: | |
||||
|
echo "Installing staticcheck for security analysis..." |
||||
|
go install honnef.co/go/tools/cmd/staticcheck@latest |
||||
|
|
||||
|
- name: Run security analysis on FUSE code |
||||
|
run: | |
||||
|
# Analyze mount and FUSE-related code for security and quality issues |
||||
|
echo "Running staticcheck security analysis..." |
||||
|
|
||||
|
# Run staticcheck on FUSE-related code |
||||
|
echo "Analyzing FUSE mount code..." |
||||
|
staticcheck ./weed/mount/... > staticcheck-report.txt 2>&1 || true |
||||
|
staticcheck ./weed/command/mount* ./weed/command/fuse* >> staticcheck-report.txt 2>&1 || true |
||||
|
|
||||
|
# Convert to JSON format for consistency |
||||
|
echo "{" > gosec-report.json |
||||
|
echo " \"tool\": \"staticcheck\"," >> gosec-report.json |
||||
|
echo " \"timestamp\": \"$(date -Iseconds)\"," >> gosec-report.json |
||||
|
echo " \"issues\": [" >> gosec-report.json |
||||
|
|
||||
|
if [ -s staticcheck-report.txt ]; then |
||||
|
echo " \"$(cat staticcheck-report.txt | head -20 | sed 's/"/\\"/g' | tr '\n' ' ')\"" >> gosec-report.json |
||||
|
fi |
||||
|
|
||||
|
echo " ]," >> gosec-report.json |
||||
|
echo " \"stats\": {" >> gosec-report.json |
||||
|
echo " \"files_analyzed\": $(find ./weed/mount ./weed/command -name '*.go' | wc -l)," >> gosec-report.json |
||||
|
echo " \"issues_found\": $(wc -l < staticcheck-report.txt 2>/dev/null || echo 0)" >> gosec-report.json |
||||
|
echo " }" >> gosec-report.json |
||||
|
echo "}" >> gosec-report.json |
||||
|
|
||||
|
echo "Security analysis completed" |
||||
|
echo "Issues found: $(wc -l < staticcheck-report.txt 2>/dev/null || echo 0)" |
||||
|
|
||||
|
- name: Upload security analysis results |
||||
|
uses: actions/upload-artifact@v4 |
||||
|
if: always() |
||||
|
with: |
||||
|
name: security-analysis-results |
||||
|
path: gosec-report.json |
||||
|
retention-days: 30 |
||||
|
|
||||
|
performance-baseline: |
||||
|
runs-on: ubuntu-latest |
||||
|
if: github.event.inputs.test_type == 'full' || github.event_name == 'schedule' |
||||
|
|
||||
|
steps: |
||||
|
- name: Checkout code |
||||
|
uses: actions/checkout@v4 |
||||
|
|
||||
|
- name: Set up Go |
||||
|
uses: actions/setup-go@v4 |
||||
|
with: |
||||
|
go-version: ${{ env.GO_VERSION }} |
||||
|
|
||||
|
- name: Install dependencies |
||||
|
run: | |
||||
|
sudo apt-get update |
||||
|
sudo apt-get install -y fuse libfuse-dev fio |
||||
|
sudo usermod -a -G fuse $USER |
||||
|
sudo chmod 666 /dev/fuse |
||||
|
|
||||
|
- name: Build SeaweedFS |
||||
|
run: make |
||||
|
|
||||
|
- name: Run performance baseline tests |
||||
|
run: | |
||||
|
cd test/fuse_integration |
||||
|
|
||||
|
if [ ! -f go.mod ]; then |
||||
|
go mod init seaweedfs-posix-tests |
||||
|
go mod tidy |
||||
|
fi |
||||
|
|
||||
|
mkdir -p reports |
||||
|
|
||||
|
# Run performance benchmarks |
||||
|
make -f posix_Makefile benchmark-posix |
||||
|
make -f posix_Makefile test-fio-posix |
||||
|
|
||||
|
- name: Store performance baseline |
||||
|
uses: actions/upload-artifact@v4 |
||||
|
with: |
||||
|
name: performance-baseline-results |
||||
|
path: | |
||||
|
test/fuse_integration/reports/posix_benchmark_results.log |
||||
|
test/fuse_integration/reports/fio_*_results.log |
||||
|
retention-days: 90 |
||||
|
|
||||
|
# Schedule regular compliance checks |
||||
|
scheduled-compliance-check: |
||||
|
runs-on: ubuntu-latest |
||||
|
if: github.event_name == 'schedule' |
||||
|
|
||||
|
steps: |
||||
|
- name: Checkout code |
||||
|
uses: actions/checkout@v4 |
||||
|
|
||||
|
- name: Set up Go |
||||
|
uses: actions/setup-go@v4 |
||||
|
with: |
||||
|
go-version: ${{ env.GO_VERSION }} |
||||
|
|
||||
|
- name: Install dependencies |
||||
|
run: | |
||||
|
sudo apt-get update |
||||
|
sudo apt-get install -y fuse libfuse-dev |
||||
|
pip3 install --user nfstest |
||||
|
|
||||
|
- name: Build SeaweedFS |
||||
|
run: make |
||||
|
|
||||
|
- name: Run comprehensive compliance check |
||||
|
run: | |
||||
|
cd test/fuse_integration |
||||
|
make -f posix_Makefile test-posix-full |
||||
|
make -f posix_Makefile generate-report |
||||
|
|
||||
|
- name: Create compliance issue if tests fail |
||||
|
if: failure() |
||||
|
uses: actions/github-script@v7 |
||||
|
with: |
||||
|
script: | |
||||
|
const issue = await github.rest.issues.create({ |
||||
|
owner: context.repo.owner, |
||||
|
repo: context.repo.repo, |
||||
|
title: 'POSIX Compliance Check Failed - ' + new Date().toISOString().split('T')[0], |
||||
|
body: `## POSIX Compliance Check Failure |
||||
|
|
||||
|
The scheduled POSIX compliance check has failed. This may indicate: |
||||
|
|
||||
|
- Regression in FUSE mount functionality |
||||
|
- Changes affecting POSIX compatibility |
||||
|
- Infrastructure issues with the test environment |
||||
|
|
||||
|
**Action Required:** Review the test results and investigate any failures. |
||||
|
|
||||
|
**Test Run:** [Workflow Run](https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}) |
||||
|
|
||||
|
**Date:** ${new Date().toISOString()} |
||||
|
|
||||
|
--- |
||||
|
This issue was automatically created by the POSIX compliance workflow. |
||||
|
`, |
||||
|
labels: ['bug', 'posix-compliance', 'automated-issue'] |
||||
|
}); |
||||
|
|
||||
|
console.log('Created issue:', issue.data.number); |
||||
|
|
||||
|
# Schedule this workflow to run weekly |
||||
|
# Uncomment the following lines to enable scheduled runs: |
||||
|
# schedule: |
||||
|
# - cron: '0 2 * * 1' # Every Monday at 2 AM UTC |
||||
|
|
||||
@ -0,0 +1,654 @@ |
|||||
|
# SeaweedFS FUSE POSIX Compliance Testing |
||||
|
|
||||
|
## Overview |
||||
|
|
||||
|
This comprehensive test suite provides full POSIX compliance testing for SeaweedFS FUSE mounts. It includes basic POSIX operations, extended features, external test suite integration, and performance benchmarking to ensure SeaweedFS meets filesystem standards. |
||||
|
|
||||
|
## 🎯 Key Features |
||||
|
|
||||
|
### ✅ **Comprehensive Test Coverage** |
||||
|
- **Basic POSIX Operations**: File/directory create, read, write, delete, rename |
||||
|
- **Advanced Features**: Extended attributes, file locking, memory mapping *(see roadmap)* |
||||
|
- **I/O Operations**: Synchronous/asynchronous I/O, direct I/O, vectored I/O *(see roadmap)* |
||||
|
- **Error Handling**: Comprehensive error condition testing |
||||
|
- **Concurrent Operations**: Multi-threaded stress testing |
||||
|
- **External Integration**: pjdfstest, nfstest, FIO integration |
||||
|
|
||||
|
> **📋 Implementation Status**: Some advanced features are currently skipped pending platform-specific implementations. See [`POSIX_IMPLEMENTATION_ROADMAP.md`](./POSIX_IMPLEMENTATION_ROADMAP.md) for a detailed implementation plan and current status. |
||||
|
|
||||
|
### 📊 **Performance Analysis** |
||||
|
- **Benchmarking**: Built-in performance benchmarks |
||||
|
- **Profiling**: CPU and memory profiling support |
||||
|
- **Coverage Analysis**: Code coverage reporting |
||||
|
- **Stress Testing**: High-load concurrent operation testing |
||||
|
|
||||
|
### 🔧 **External Tool Integration** |
||||
|
- **pjdfstest**: Industry-standard POSIX filesystem test suite |
||||
|
- **nfstest_posix**: Network filesystem POSIX API verification |
||||
|
- **FIO**: Flexible I/O performance testing |
||||
|
- **Custom Litmus Tests**: Focused edge case testing |
||||
|
|
||||
|
## 📋 Test Categories |
||||
|
|
||||
|
### 1. Basic POSIX Compliance (`posix_compliance_test.go`) |
||||
|
|
||||
|
#### File Operations |
||||
|
- ✅ File creation with O_CREAT, O_EXCL flags |
||||
|
- ✅ File truncation and size management |
||||
|
- ✅ File deletion and unlinking |
||||
|
- ✅ Atomic rename operations |
||||
|
- ✅ File permission management |
||||
|
|
||||
|
#### Directory Operations |
||||
|
- ✅ Directory creation and removal |
||||
|
- ✅ Directory listing and traversal |
||||
|
- ✅ Directory rename operations |
||||
|
- ✅ Non-empty directory handling |
||||
|
|
||||
|
#### Symlink Operations |
||||
|
- ✅ Symbolic link creation and resolution |
||||
|
- ✅ Broken symlink handling |
||||
|
- ✅ Symlink target verification |
||||
|
- ✅ Symlink permission handling |
||||
|
|
||||
|
#### Permission Tests |
||||
|
- ✅ File permission setting and verification |
||||
|
- ✅ Directory permission enforcement |
||||
|
- ✅ Permission inheritance |
||||
|
- ✅ Access control validation |
||||
|
|
||||
|
#### Timestamp Tests |
||||
|
- ✅ Access time (atime) updates |
||||
|
- ✅ Modification time (mtime) tracking |
||||
|
- ✅ Change time (ctime) management |
||||
|
- ✅ Timestamp precision and setting |
||||
|
|
||||
|
#### I/O Operations |
||||
|
- ✅ Read/write operations |
||||
|
- ✅ File seeking and positioning |
||||
|
- ✅ Append mode operations |
||||
|
- ✅ Buffer management |
||||
|
|
||||
|
#### File Descriptors |
||||
|
- ✅ File descriptor duplication |
||||
|
- ✅ File descriptor flags |
||||
|
- ✅ Close-on-exec handling |
||||
|
- ✅ File descriptor limits |
||||
|
|
||||
|
#### Atomic Operations |
||||
|
- ✅ Atomic file operations |
||||
|
- ✅ Rename atomicity |
||||
|
- ✅ Link/unlink atomicity |
||||
|
|
||||
|
#### Concurrent Access |
||||
|
- ✅ Multi-reader scenarios |
||||
|
- ✅ Concurrent write handling |
||||
|
- ✅ Reader-writer coordination |
||||
|
- ✅ Race condition prevention |
||||
|
|
||||
|
#### Error Handling |
||||
|
- ✅ ENOENT (file not found) handling |
||||
|
- ✅ EACCES (permission denied) handling |
||||
|
- ✅ EBADF (bad file descriptor) handling |
||||
|
- ✅ ENOTEMPTY (directory not empty) handling |
||||
|
- ✅ Error code compliance |
||||
|
|
||||
|
### 2. Extended POSIX Features (`posix_extended_test.go`) |
||||
|
|
||||
|
#### Extended Attributes |
||||
|
- ✅ Setting extended attributes (setxattr) |
||||
|
- ✅ Getting extended attributes (getxattr) |
||||
|
- ✅ Listing extended attributes (listxattr) |
||||
|
- ✅ Removing extended attributes (removexattr) |
||||
|
- ✅ Extended attribute namespaces |
||||
|
|
||||
|
#### File Locking |
||||
|
- ✅ Advisory locking (fcntl) |
||||
|
- ✅ Exclusive locks (F_WRLCK) |
||||
|
- ✅ Shared locks (F_RDLCK) |
||||
|
- ✅ Lock conflict detection |
||||
|
- ✅ Lock release and cleanup |
||||
|
|
||||
|
#### Advanced I/O |
||||
|
- ✅ Vectored I/O (readv/writev) |
||||
|
- ✅ Positioned I/O (pread/pwrite) |
||||
|
- ✅ Asynchronous I/O patterns |
||||
|
- ✅ Scatter-gather operations |
||||
|
|
||||
|
#### Sparse Files |
||||
|
- ✅ Sparse file creation |
||||
|
- ✅ Hole detection and handling |
||||
|
- ✅ Sparse file efficiency |
||||
|
- ✅ Seek beyond EOF |
||||
|
|
||||
|
#### Large Files (>2GB) |
||||
|
- ✅ Large file operations |
||||
|
- ✅ 64-bit offset handling |
||||
|
- ✅ Large file seeking |
||||
|
- ✅ Large file truncation |
||||
|
|
||||
|
#### Memory Mapping |
||||
|
- ✅ File memory mapping (mmap) |
||||
|
- ✅ Memory-mapped I/O |
||||
|
- ✅ Memory synchronization (msync) |
||||
|
- ✅ Memory unmapping (munmap) |
||||
|
- ✅ Shared/private mappings |
||||
|
|
||||
|
#### Direct I/O |
||||
|
- ✅ Direct I/O operations (O_DIRECT) |
||||
|
- ✅ Buffer alignment requirements |
||||
|
- ✅ Direct I/O performance |
||||
|
- ✅ Bypass buffer cache |
||||
|
|
||||
|
#### File Preallocation |
||||
|
- ✅ Space preallocation (fallocate) |
||||
|
- ✅ File hole punching |
||||
|
- ✅ Space reservation |
||||
|
- ✅ Allocation efficiency |
||||
|
|
||||
|
#### Zero-Copy Operations |
||||
|
- ✅ Zero-copy file transfer (sendfile) |
||||
|
- ✅ Efficient data movement |
||||
|
- ✅ Cross-filesystem transfers |
||||
|
|
||||
|
### 3. External Test Suite Integration (`posix_external_test.go`) |
||||
|
|
||||
|
#### pjdfstest Integration |
||||
|
- ✅ Comprehensive POSIX filesystem test suite |
||||
|
- ✅ Industry-standard compliance verification |
||||
|
- ✅ Automated test execution |
||||
|
- ✅ Result analysis and reporting |
||||
|
|
||||
|
#### nfstest_posix Integration |
||||
|
- ✅ POSIX API verification |
||||
|
- ✅ Network filesystem compliance |
||||
|
- ✅ API behavior validation |
||||
|
- ✅ Comprehensive coverage |
||||
|
|
||||
|
#### Custom Litmus Tests |
||||
|
- ✅ Atomic rename verification |
||||
|
- ✅ Link count accuracy |
||||
|
- ✅ Symlink cycle detection |
||||
|
- ✅ Concurrent operation safety |
||||
|
- ✅ Directory consistency |
||||
|
|
||||
|
#### Stress Testing |
||||
|
- ✅ High-frequency operations |
||||
|
- ✅ Concurrent access patterns |
||||
|
- ✅ Resource exhaustion scenarios |
||||
|
- ✅ Error recovery testing |
||||
|
|
||||
|
#### Edge Case Testing |
||||
|
- ✅ Empty filenames |
||||
|
- ✅ Very long filenames |
||||
|
- ✅ Special characters |
||||
|
- ✅ Deep directory nesting |
||||
|
- ✅ Path length limits |
||||
|
|
||||
|
## 🚀 Quick Start |
||||
|
|
||||
|
### Prerequisites |
||||
|
|
||||
|
```bash |
||||
|
# Install required tools |
||||
|
sudo apt-get install fuse # Linux |
||||
|
brew install macfuse # macOS |
||||
|
|
||||
|
# Install Go 1.21+ |
||||
|
go version |
||||
|
|
||||
|
# Build SeaweedFS |
||||
|
cd /path/to/seaweedfs |
||||
|
make |
||||
|
``` |
||||
|
|
||||
|
### Basic Usage |
||||
|
|
||||
|
```bash |
||||
|
# Navigate to test directory |
||||
|
cd test/fuse_integration |
||||
|
|
||||
|
# Check prerequisites |
||||
|
make -f posix_Makefile check-prereqs |
||||
|
|
||||
|
# Run basic POSIX compliance tests |
||||
|
make -f posix_Makefile test-posix-basic |
||||
|
|
||||
|
# Run complete test suite |
||||
|
make -f posix_Makefile test-posix-full |
||||
|
|
||||
|
# Generate compliance report |
||||
|
make -f posix_Makefile generate-report |
||||
|
``` |
||||
|
|
||||
|
## 🔧 Advanced Usage |
||||
|
|
||||
|
### Test Categories |
||||
|
|
||||
|
#### Critical Tests Only |
||||
|
```bash |
||||
|
# Run only critical POSIX compliance tests (faster) |
||||
|
make -f posix_Makefile test-posix-critical |
||||
|
``` |
||||
|
|
||||
|
#### Extended Feature Tests |
||||
|
```bash |
||||
|
# Test advanced POSIX features |
||||
|
make -f posix_Makefile test-posix-extended |
||||
|
``` |
||||
|
|
||||
|
#### External Tool Integration |
||||
|
```bash |
||||
|
# Setup and run external test suites |
||||
|
make -f posix_Makefile setup-external-tools |
||||
|
make -f posix_Makefile test-posix-external |
||||
|
``` |
||||
|
|
||||
|
#### Stress Testing |
||||
|
```bash |
||||
|
# Run stress tests for concurrent operations |
||||
|
make -f posix_Makefile test-posix-stress |
||||
|
``` |
||||
|
|
||||
|
### Performance Analysis |
||||
|
|
||||
|
#### Benchmarking |
||||
|
```bash |
||||
|
# Run performance benchmarks |
||||
|
make -f posix_Makefile benchmark-posix |
||||
|
``` |
||||
|
|
||||
|
#### Profiling |
||||
|
```bash |
||||
|
# Profile CPU and memory usage |
||||
|
make -f posix_Makefile profile-posix |
||||
|
|
||||
|
# View CPU profile |
||||
|
go tool pprof reports/posix.cpu.prof |
||||
|
``` |
||||
|
|
||||
|
#### Coverage Analysis |
||||
|
```bash |
||||
|
# Generate test coverage report |
||||
|
make -f posix_Makefile coverage-posix |
||||
|
# View: reports/posix_coverage.html |
||||
|
``` |
||||
|
|
||||
|
#### FIO Performance Testing |
||||
|
```bash |
||||
|
# Run FIO-based I/O performance tests |
||||
|
make -f posix_Makefile test-fio-posix |
||||
|
``` |
||||
|
|
||||
|
### External Test Suites |
||||
|
|
||||
|
#### pjdfstest |
||||
|
```bash |
||||
|
# Setup and run pjdfstest |
||||
|
make -f posix_Makefile setup-pjdfstest |
||||
|
make -f posix_Makefile test-pjdfstest |
||||
|
``` |
||||
|
|
||||
|
#### nfstest_posix |
||||
|
```bash |
||||
|
# Install and run nfstest_posix |
||||
|
make -f posix_Makefile setup-nfstest |
||||
|
make -f posix_Makefile test-nfstest-posix |
||||
|
``` |
||||
|
|
||||
|
## 📊 Understanding Results |
||||
|
|
||||
|
### Test Status Indicators |
||||
|
|
||||
|
- ✅ **PASS**: Test completed successfully |
||||
|
- ❌ **FAIL**: Test failed - indicates non-compliance |
||||
|
- ⚠️ **SKIP**: Test skipped - feature not supported |
||||
|
- 🔄 **RETRY**: Test retried due to transient failure |
||||
|
|
||||
|
### Report Formats |
||||
|
|
||||
|
#### HTML Report (`reports/posix_compliance_report.html`) |
||||
|
- Interactive web-based report |
||||
|
- Test category breakdown |
||||
|
- Detailed results with navigation |
||||
|
- Visual status indicators |
||||
|
|
||||
|
#### Text Summary (`reports/posix_compliance_summary.txt`) |
||||
|
- Concise text-based summary |
||||
|
- Key findings and recommendations |
||||
|
- Command-line friendly format |
||||
|
|
||||
|
#### JSON Report (`reports/posix_compliance_report.json`) |
||||
|
- Machine-readable results |
||||
|
- Integration with CI/CD pipelines |
||||
|
- Automated result processing |
||||
|
|
||||
|
### Log Files |
||||
|
|
||||
|
- `posix_basic_results.log`: Basic POSIX test detailed output |
||||
|
- `posix_extended_results.log`: Extended feature test output |
||||
|
- `posix_external_results.log`: External test suite output |
||||
|
- `posix_benchmark_results.log`: Performance benchmark results |
||||
|
|
||||
|
## 🐛 Debugging Failed Tests |
||||
|
|
||||
|
### Common Issues |
||||
|
|
||||
|
#### Permission Denied Errors |
||||
|
```bash |
||||
|
# Ensure proper FUSE permissions |
||||
|
sudo usermod -a -G fuse $USER |
||||
|
# Re-login or run: newgrp fuse |
||||
|
|
||||
|
# Check mount options |
||||
|
make -f posix_Makefile test-posix-basic MOUNT_OPTIONS="-allowOthers" |
||||
|
``` |
||||
|
|
||||
|
#### Extended Attributes Not Supported |
||||
|
```bash |
||||
|
# Check if xattr is supported by underlying filesystem |
||||
|
getfattr --version |
||||
|
# May need to skip xattr tests on certain filesystems |
||||
|
``` |
||||
|
|
||||
|
#### File Locking Issues |
||||
|
```bash |
||||
|
# Verify file locking support |
||||
|
# Some network filesystems may not support full locking |
||||
|
``` |
||||
|
|
||||
|
#### Memory Issues |
||||
|
```bash |
||||
|
# Increase available memory for tests |
||||
|
# Large file tests may require significant RAM |
||||
|
``` |
||||
|
|
||||
|
### Debug Mode |
||||
|
|
||||
|
```bash |
||||
|
# Run tests with verbose output |
||||
|
make -f posix_Makefile test-posix-basic VERBOSE=1 |
||||
|
|
||||
|
# Keep test files for inspection |
||||
|
make -f posix_Makefile test-posix-basic CLEANUP=false |
||||
|
|
||||
|
# Enable debug logging |
||||
|
make -f posix_Makefile test-posix-basic DEBUG=true |
||||
|
``` |
||||
|
|
||||
|
### Manual Test Execution |
||||
|
|
||||
|
```bash |
||||
|
# Run specific test |
||||
|
cd test/fuse_integration |
||||
|
go test -v -run TestPOSIXCompliance/FileOperations posix_compliance_test.go |
||||
|
|
||||
|
# Run with custom timeout |
||||
|
go test -v -timeout 30m -run TestPOSIXExtended posix_extended_test.go |
||||
|
``` |
||||
|
|
||||
|
## 🔄 CI/CD Integration |
||||
|
|
||||
|
### GitHub Actions |
||||
|
|
||||
|
```yaml |
||||
|
name: POSIX Compliance Tests |
||||
|
|
||||
|
on: [push, pull_request] |
||||
|
|
||||
|
jobs: |
||||
|
posix-compliance: |
||||
|
runs-on: ubuntu-latest |
||||
|
steps: |
||||
|
- uses: actions/checkout@v4 |
||||
|
- uses: actions/setup-go@v4 |
||||
|
with: |
||||
|
go-version: '1.21' |
||||
|
|
||||
|
- name: Install FUSE |
||||
|
run: sudo apt-get update && sudo apt-get install -y fuse |
||||
|
|
||||
|
- name: Build SeaweedFS |
||||
|
run: make |
||||
|
|
||||
|
- name: Run POSIX Compliance Tests |
||||
|
run: | |
||||
|
cd test/fuse_integration |
||||
|
make -f posix_Makefile ci-posix-tests |
||||
|
|
||||
|
- name: Upload Test Results |
||||
|
uses: actions/upload-artifact@v4 |
||||
|
if: always() |
||||
|
with: |
||||
|
name: posix-test-results |
||||
|
path: test/fuse_integration/reports/ |
||||
|
``` |
||||
|
|
||||
|
### Jenkins Pipeline |
||||
|
|
||||
|
```groovy |
||||
|
pipeline { |
||||
|
agent any |
||||
|
stages { |
||||
|
stage('Setup') { |
||||
|
steps { |
||||
|
sh 'make' |
||||
|
} |
||||
|
} |
||||
|
stage('POSIX Compliance') { |
||||
|
steps { |
||||
|
dir('test/fuse_integration') { |
||||
|
sh 'make -f posix_Makefile ci-posix-tests' |
||||
|
} |
||||
|
} |
||||
|
post { |
||||
|
always { |
||||
|
archiveArtifacts 'test/fuse_integration/reports/**' |
||||
|
publishHTML([ |
||||
|
allowMissing: false, |
||||
|
alwaysLinkToLastBuild: true, |
||||
|
keepAll: true, |
||||
|
reportDir: 'test/fuse_integration/reports', |
||||
|
reportFiles: 'posix_compliance_report.html', |
||||
|
reportName: 'POSIX Compliance Report' |
||||
|
]) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
### Docker-based Testing |
||||
|
|
||||
|
```bash |
||||
|
# Build Docker image for testing |
||||
|
cd test/fuse_integration |
||||
|
make -f posix_Makefile docker-test-posix |
||||
|
``` |
||||
|
|
||||
|
## 🎯 POSIX Compliance Checklist |
||||
|
|
||||
|
### Critical POSIX Features |
||||
|
- [ ] File creation, reading, writing, deletion |
||||
|
- [ ] Directory operations (create, remove, list) |
||||
|
- [ ] File permissions and ownership |
||||
|
- [ ] Symbolic links |
||||
|
- [ ] Hard links |
||||
|
- [ ] File timestamps |
||||
|
- [ ] Error handling compliance |
||||
|
- [ ] Atomic operations |
||||
|
|
||||
|
### Advanced POSIX Features |
||||
|
- [ ] Extended attributes |
||||
|
- [ ] File locking (advisory) |
||||
|
- [ ] Memory-mapped I/O |
||||
|
- [ ] Vectored I/O (readv/writev) |
||||
|
- [ ] Positioned I/O (pread/pwrite) |
||||
|
- [ ] Direct I/O |
||||
|
- [ ] File preallocation |
||||
|
- [ ] Sparse files |
||||
|
|
||||
|
### Performance Requirements |
||||
|
- [ ] Reasonable performance under load |
||||
|
- [ ] Concurrent access handling |
||||
|
- [ ] Memory usage optimization |
||||
|
- [ ] CPU efficiency |
||||
|
- [ ] I/O throughput |
||||
|
|
||||
|
### Reliability Requirements |
||||
|
- [ ] Data consistency |
||||
|
- [ ] Error recovery |
||||
|
- [ ] Race condition prevention |
||||
|
- [ ] Resource cleanup |
||||
|
- [ ] Crash recovery |
||||
|
|
||||
|
## 📈 Performance Benchmarks |
||||
|
|
||||
|
### Expected Performance Characteristics |
||||
|
|
||||
|
| Operation | Expected Range | Notes | |
||||
|
|-----------|----------------|-------| |
||||
|
| File Create | 1000-10000 ops/sec | Depends on chunk size | |
||||
|
| File Read | 100-1000 MB/sec | Network limited | |
||||
|
| File Write | 50-500 MB/sec | Depends on replication | |
||||
|
| Directory Ops | 500-5000 ops/sec | Metadata operations | |
||||
|
| Concurrent Ops | Scales with cores | Up to hardware limits | |
||||
|
|
||||
|
### Benchmarking Commands |
||||
|
|
||||
|
```bash |
||||
|
# Basic I/O performance |
||||
|
make -f posix_Makefile benchmark-posix |
||||
|
|
||||
|
# FIO comprehensive testing |
||||
|
make -f posix_Makefile test-fio-posix |
||||
|
|
||||
|
# Custom benchmark with specific parameters |
||||
|
go test -v -bench=. -benchtime=30s posix_compliance_test.go |
||||
|
``` |
||||
|
|
||||
|
## 🔍 Troubleshooting |
||||
|
|
||||
|
### Common Test Failures |
||||
|
|
||||
|
#### 1. Mount Permission Issues |
||||
|
```bash |
||||
|
# Error: Permission denied mounting |
||||
|
# Solution: Check FUSE permissions |
||||
|
sudo chmod 666 /dev/fuse |
||||
|
sudo usermod -a -G fuse $USER |
||||
|
``` |
||||
|
|
||||
|
#### 2. Extended Attribute Tests Fail |
||||
|
```bash |
||||
|
# Error: Operation not supported (ENOTSUP) |
||||
|
# Solution: Check filesystem xattr support |
||||
|
mount | grep $(df . | tail -1 | awk '{print $1}') |
||||
|
# May need to remount with xattr support |
||||
|
``` |
||||
|
|
||||
|
#### 3. File Locking Tests Fail |
||||
|
```bash |
||||
|
# Error: Function not implemented (ENOSYS) |
||||
|
# Solution: Some network filesystems don't support locking |
||||
|
# This may be expected behavior |
||||
|
``` |
||||
|
|
||||
|
#### 4. Large File Tests Fail |
||||
|
```bash |
||||
|
# Error: No space left on device |
||||
|
# Solution: Ensure sufficient disk space |
||||
|
df -h /tmp # Check temp directory space |
||||
|
# May need to clean up or use different temp directory |
||||
|
``` |
||||
|
|
||||
|
#### 5. Performance Tests Timeout |
||||
|
```bash |
||||
|
# Error: Test timeout exceeded |
||||
|
# Solution: Increase timeout or reduce test scope |
||||
|
go test -timeout 60m -run TestPOSIXCompliance |
||||
|
``` |
||||
|
|
||||
|
### Debug Information Collection |
||||
|
|
||||
|
```bash |
||||
|
# Collect system information |
||||
|
uname -a > debug_info.txt |
||||
|
mount >> debug_info.txt |
||||
|
df -h >> debug_info.txt |
||||
|
free -h >> debug_info.txt |
||||
|
|
||||
|
# Collect SeaweedFS information |
||||
|
./weed version >> debug_info.txt |
||||
|
ps aux | grep weed >> debug_info.txt |
||||
|
|
||||
|
# Collect test logs |
||||
|
cp reports/*.log debug_logs/ |
||||
|
``` |
||||
|
|
||||
|
## 📚 Additional Resources |
||||
|
|
||||
|
### POSIX Standards |
||||
|
- [POSIX.1-2017 (IEEE Std 1003.1-2017)](https://pubs.opengroup.org/onlinepubs/9699919799/) |
||||
|
- [Single UNIX Specification](https://www.opengroup.org/openbrand/register/) |
||||
|
|
||||
|
### External Test Suites |
||||
|
- [pjdfstest](https://github.com/pjd/pjdfstest) - Comprehensive POSIX filesystem tests |
||||
|
- [nfstest](https://nfstest.readthedocs.io/) - Network filesystem testing |
||||
|
- [FIO](https://fio.readthedocs.io/) - Flexible I/O tester |
||||
|
|
||||
|
### SeaweedFS Documentation |
||||
|
- [SeaweedFS Wiki](https://github.com/seaweedfs/seaweedfs/wiki) |
||||
|
- [FUSE Mount Documentation](https://github.com/seaweedfs/seaweedfs/wiki/FUSE-Mount) |
||||
|
- [Performance Tuning Guide](https://github.com/seaweedfs/seaweedfs/wiki/Optimization) |
||||
|
|
||||
|
## 🤝 Contributing |
||||
|
|
||||
|
### Adding New Tests |
||||
|
|
||||
|
1. **Basic Tests**: Add to `posix_compliance_test.go` |
||||
|
2. **Extended Tests**: Add to `posix_extended_test.go` |
||||
|
3. **External Integration**: Add to `posix_external_test.go` |
||||
|
|
||||
|
### Test Guidelines |
||||
|
|
||||
|
```go |
||||
|
func (s *POSIXTestSuite) TestNewFeature(t *testing.T) { |
||||
|
mountPoint := s.framework.GetMountPoint() |
||||
|
|
||||
|
t.Run("SubTestName", func(t *testing.T) { |
||||
|
// Test setup |
||||
|
testFile := filepath.Join(mountPoint, "test.txt") |
||||
|
|
||||
|
// Test execution |
||||
|
err := someOperation(testFile) |
||||
|
|
||||
|
// Assertions |
||||
|
require.NoError(t, err) |
||||
|
|
||||
|
// Cleanup (if needed) |
||||
|
defer os.Remove(testFile) |
||||
|
}) |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
### Submitting Improvements |
||||
|
|
||||
|
1. Fork the repository |
||||
|
2. Create a feature branch |
||||
|
3. Add comprehensive tests |
||||
|
4. Ensure all existing tests pass |
||||
|
5. Submit a pull request with detailed description |
||||
|
|
||||
|
## 📞 Support |
||||
|
|
||||
|
For questions, issues, or contributions: |
||||
|
|
||||
|
- **GitHub Issues**: [SeaweedFS Issues](https://github.com/seaweedfs/seaweedfs/issues) |
||||
|
- **Discussions**: [SeaweedFS Discussions](https://github.com/seaweedfs/seaweedfs/discussions) |
||||
|
- **Documentation**: [SeaweedFS Wiki](https://github.com/seaweedfs/seaweedfs/wiki) |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
*This comprehensive POSIX compliance test suite ensures SeaweedFS FUSE mounts meet industry standards for filesystem behavior, performance, and reliability.* |
||||
@ -0,0 +1,271 @@ |
|||||
|
# POSIX Compliance Implementation Roadmap |
||||
|
|
||||
|
This document tracks the implementation status of POSIX features in the SeaweedFS FUSE mount compliance test suite and provides a roadmap for completing comprehensive POSIX compliance testing. |
||||
|
|
||||
|
## Overview |
||||
|
|
||||
|
The POSIX compliance test suite currently has several tests that are skipped due to requiring platform-specific implementations. This roadmap outlines the steps needed to implement these tests and achieve comprehensive POSIX compliance validation. |
||||
|
|
||||
|
## Current Status |
||||
|
|
||||
|
### ✅ Implemented Features |
||||
|
- Basic file operations (create, read, write, delete) |
||||
|
- Directory operations (mkdir, rmdir, rename) |
||||
|
- File permissions and ownership |
||||
|
- Symbolic and hard links |
||||
|
- Basic I/O operations (seek, append, positioned I/O) |
||||
|
- File descriptors and atomic operations |
||||
|
- Concurrent access patterns |
||||
|
- Error handling compliance |
||||
|
- Timestamp operations |
||||
|
|
||||
|
### 🚧 Partially Implemented |
||||
|
- Cross-platform compatibility (basic implementation with platform-specific access time handling) |
||||
|
|
||||
|
### ❌ Missing Implementations |
||||
|
|
||||
|
#### 1. Extended Attributes (xattr) |
||||
|
**Priority: High** |
||||
|
**Platforms: Linux, macOS, FreeBSD** |
||||
|
|
||||
|
**Current Status:** All xattr tests are skipped |
||||
|
- `TestExtendedAttributes/SetExtendedAttribute` |
||||
|
- `TestExtendedAttributes/ListExtendedAttributes` |
||||
|
- `TestExtendedAttributes/RemoveExtendedAttribute` |
||||
|
|
||||
|
**Implementation Plan:** |
||||
|
```go |
||||
|
// Linux implementation |
||||
|
//go:build linux |
||||
|
func setXattr(path, name string, value []byte) error { |
||||
|
return syscall.Setxattr(path, name, value, 0) |
||||
|
} |
||||
|
|
||||
|
// macOS implementation |
||||
|
//go:build darwin |
||||
|
func setXattr(path, name string, value []byte) error { |
||||
|
return syscall.Setxattr(path, name, value, 0, 0) |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
**Required Files:** |
||||
|
- `xattr_linux.go` - Linux syscall implementations |
||||
|
- `xattr_darwin.go` - macOS syscall implementations |
||||
|
- `xattr_freebsd.go` - FreeBSD syscall implementations |
||||
|
- `xattr_unsupported.go` - Fallback for unsupported platforms |
||||
|
|
||||
|
#### 2. Memory Mapping (mmap) |
||||
|
**Priority: High** |
||||
|
**Platforms: All POSIX systems** |
||||
|
|
||||
|
**Current Status:** All mmap tests are skipped |
||||
|
- `TestMemoryMapping/MemoryMappedRead` |
||||
|
- `TestMemoryMapping/MemoryMappedWrite` |
||||
|
|
||||
|
**Implementation Plan:** |
||||
|
```go |
||||
|
func testMmap(t *testing.T, filePath string) { |
||||
|
fd, err := syscall.Open(filePath, syscall.O_RDWR, 0) |
||||
|
require.NoError(t, err) |
||||
|
defer syscall.Close(fd) |
||||
|
|
||||
|
// Platform-specific mmap implementation |
||||
|
data, err := syscall.Mmap(fd, 0, size, syscall.PROT_READ|syscall.PROT_WRITE, syscall.MAP_SHARED) |
||||
|
require.NoError(t, err) |
||||
|
defer syscall.Munmap(data) |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
**Required Files:** |
||||
|
- `mmap_unix.go` - Unix-like systems implementation |
||||
|
- `mmap_windows.go` - Windows implementation (if needed) |
||||
|
|
||||
|
#### 3. Vectored I/O (readv/writev) |
||||
|
**Priority: Medium** |
||||
|
**Platforms: All POSIX systems** |
||||
|
|
||||
|
**Current Status:** Vectored I/O test is skipped |
||||
|
- `TestAdvancedIO/VectoredIO` |
||||
|
|
||||
|
**Implementation Plan:** |
||||
|
```go |
||||
|
func testVectoredIO(t *testing.T, filePath string) { |
||||
|
// Use syscall.Syscall for readv/writev |
||||
|
// Linux: SYS_READV, SYS_WRITEV |
||||
|
// Other platforms: platform-specific syscall numbers |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
#### 4. Direct I/O |
||||
|
**Priority: Medium** |
||||
|
**Platforms: Linux, some Unix variants** |
||||
|
|
||||
|
**Current Status:** Direct I/O test is skipped |
||||
|
- `TestDirectIO/DirectIO` |
||||
|
|
||||
|
**Implementation Plan:** |
||||
|
```go |
||||
|
//go:build linux |
||||
|
func testDirectIO(t *testing.T, filePath string) { |
||||
|
fd, err := syscall.Open(filePath, syscall.O_RDWR|syscall.O_DIRECT, 0) |
||||
|
// Test with aligned buffers |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
#### 5. File Preallocation (fallocate) |
||||
|
**Priority: Medium** |
||||
|
**Platforms: Linux, some Unix variants** |
||||
|
|
||||
|
**Current Status:** fallocate test is skipped |
||||
|
- `TestFilePreallocation/Fallocate` |
||||
|
|
||||
|
**Implementation Plan:** |
||||
|
```go |
||||
|
//go:build linux |
||||
|
func testFallocate(t *testing.T, filePath string) { |
||||
|
fd, err := syscall.Open(filePath, syscall.O_RDWR, 0) |
||||
|
err = syscall.Syscall6(syscall.SYS_FALLOCATE, uintptr(fd), 0, 0, uintptr(size), 0, 0) |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
#### 6. Zero-Copy Transfer (sendfile) |
||||
|
**Priority: Low** |
||||
|
**Platforms: Linux, FreeBSD, some Unix variants** |
||||
|
|
||||
|
**Current Status:** sendfile test is skipped |
||||
|
- `TestZeroCopyTransfer/Sendfile` |
||||
|
|
||||
|
**Implementation Plan:** |
||||
|
```go |
||||
|
//go:build linux |
||||
|
func testSendfile(t *testing.T, srcPath, dstPath string) { |
||||
|
// Use syscall.Syscall for sendfile |
||||
|
// Platform-specific implementations |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
#### 7. File Sealing (Linux-specific) |
||||
|
**Priority: Low** |
||||
|
**Platforms: Linux only** |
||||
|
|
||||
|
**Current Status:** File sealing test is skipped |
||||
|
- `TestFileSealing` |
||||
|
|
||||
|
**Implementation Plan:** |
||||
|
```go |
||||
|
//go:build linux |
||||
|
func testFileSealing(t *testing.T) { |
||||
|
// Use fcntl with F_ADD_SEALS, F_GET_SEALS |
||||
|
// Test various seal types: F_SEAL_WRITE, F_SEAL_SHRINK, etc. |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
## Implementation Strategy |
||||
|
|
||||
|
### Phase 1: Core Features (High Priority) |
||||
|
1. **Extended Attributes** - Essential for many applications |
||||
|
2. **Memory Mapping** - Critical for performance-sensitive applications |
||||
|
|
||||
|
### Phase 2: Advanced I/O (Medium Priority) |
||||
|
3. **Vectored I/O** - Important for efficient bulk operations |
||||
|
4. **Direct I/O** - Needed for database and high-performance applications |
||||
|
5. **File Preallocation** - Important for preventing fragmentation |
||||
|
|
||||
|
### Phase 3: Specialized Features (Low Priority) |
||||
|
6. **Zero-Copy Transfer** - Performance optimization feature |
||||
|
7. **File Sealing** - Security feature, Linux-specific |
||||
|
|
||||
|
## Platform Support Matrix |
||||
|
|
||||
|
| Feature | Linux | macOS | FreeBSD | Windows | Notes | |
||||
|
|---------|-------|-------|---------|---------|-------| |
||||
|
| Extended Attributes | ✅ | ✅ | ✅ | ❌ | Different syscall signatures | |
||||
|
| Memory Mapping | ✅ | ✅ | ✅ | ✅ | Standard POSIX | |
||||
|
| Vectored I/O | ✅ | ✅ | ✅ | ❌ | readv/writev syscalls | |
||||
|
| Direct I/O | ✅ | ❌ | ✅ | ❌ | O_DIRECT flag | |
||||
|
| fallocate | ✅ | ❌ | ❌ | ❌ | Linux-specific | |
||||
|
| sendfile | ✅ | ❌ | ✅ | ❌ | Platform-specific | |
||||
|
| File Sealing | ✅ | ❌ | ❌ | ❌ | Linux-only | |
||||
|
|
||||
|
## Development Guidelines |
||||
|
|
||||
|
### 1. Platform-Specific Implementation Pattern |
||||
|
```go |
||||
|
// feature_linux.go |
||||
|
//go:build linux |
||||
|
package fuse |
||||
|
func platformSpecificFunction() { /* Linux implementation */ } |
||||
|
|
||||
|
// feature_darwin.go |
||||
|
//go:build darwin |
||||
|
package fuse |
||||
|
func platformSpecificFunction() { /* macOS implementation */ } |
||||
|
|
||||
|
// feature_unsupported.go |
||||
|
//go:build !linux && !darwin |
||||
|
package fuse |
||||
|
func platformSpecificFunction() { /* Skip or error */ } |
||||
|
``` |
||||
|
|
||||
|
### 2. Test Structure Pattern |
||||
|
```go |
||||
|
func (s *POSIXExtendedTestSuite) TestFeature(t *testing.T) { |
||||
|
if !isFeatureSupported() { |
||||
|
t.Skip("Feature not supported on this platform") |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
t.Run("FeatureTest", func(t *testing.T) { |
||||
|
// Actual test implementation |
||||
|
}) |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
### 3. Error Handling |
||||
|
- Use platform-specific error checking |
||||
|
- Provide meaningful error messages |
||||
|
- Gracefully handle unsupported features |
||||
|
|
||||
|
## Testing Strategy |
||||
|
|
||||
|
### 1. Continuous Integration |
||||
|
- Run tests on multiple platforms (Linux, macOS) |
||||
|
- Use build tags to enable/disable platform-specific tests |
||||
|
- Ensure graceful degradation on unsupported platforms |
||||
|
|
||||
|
### 2. Feature Detection |
||||
|
- Implement runtime feature detection where possible |
||||
|
- Skip tests gracefully when features are unavailable |
||||
|
- Log clear messages about skipped functionality |
||||
|
|
||||
|
### 3. Documentation |
||||
|
- Document platform-specific behavior |
||||
|
- Provide examples for each implemented feature |
||||
|
- Maintain compatibility matrices |
||||
|
|
||||
|
## Contributing |
||||
|
|
||||
|
When implementing these features: |
||||
|
|
||||
|
1. **Start with high-priority items** (Extended Attributes, Memory Mapping) |
||||
|
2. **Follow the platform-specific pattern** outlined above |
||||
|
3. **Add comprehensive tests** for each feature |
||||
|
4. **Update this roadmap** as features are implemented |
||||
|
5. **Document any platform-specific quirks** or limitations |
||||
|
|
||||
|
## Future Enhancements |
||||
|
|
||||
|
Beyond the current roadmap, consider: |
||||
|
- **File locking** (flock, fcntl locks) |
||||
|
- **Asynchronous I/O** (aio_read, aio_write) |
||||
|
- **File change notifications** (inotify, kqueue) |
||||
|
- **POSIX ACLs** (Access Control Lists) |
||||
|
- **Sparse file operations** |
||||
|
- **File hole punching** |
||||
|
|
||||
|
## References |
||||
|
|
||||
|
- [POSIX.1-2017 Standard](https://pubs.opengroup.org/onlinepubs/9699919799/) |
||||
|
- [Linux Programmer's Manual](https://man7.org/linux/man-pages/) |
||||
|
- [FreeBSD System Calls](https://www.freebsd.org/cgi/man.cgi) |
||||
|
- [macOS System Calls](https://developer.apple.com/library/archive/documentation/System/Conceptual/ManPages_iPhoneOS/) |
||||
@ -0,0 +1,12 @@ |
|||||
|
//go:build linux
|
||||
|
|
||||
|
package fuse |
||||
|
|
||||
|
import ( |
||||
|
"syscall" |
||||
|
) |
||||
|
|
||||
|
// getAtimeNano returns the access time in nanoseconds for Linux systems
|
||||
|
func getAtimeNano(stat *syscall.Stat_t) int64 { |
||||
|
return stat.Atim.Sec*1e9 + stat.Atim.Nsec |
||||
|
} |
||||
@ -0,0 +1,12 @@ |
|||||
|
//go:build !linux && !windows
|
||||
|
|
||||
|
package fuse |
||||
|
|
||||
|
import ( |
||||
|
"syscall" |
||||
|
) |
||||
|
|
||||
|
// getAtimeNano returns the access time in nanoseconds for non-Linux systems (macOS, BSD, etc.)
|
||||
|
func getAtimeNano(stat *syscall.Stat_t) int64 { |
||||
|
return stat.Atimespec.Sec*1e9 + stat.Atimespec.Nsec |
||||
|
} |
||||
@ -0,0 +1,12 @@ |
|||||
|
//go:build windows
|
||||
|
|
||||
|
package fuse |
||||
|
|
||||
|
import ( |
||||
|
"syscall" |
||||
|
) |
||||
|
|
||||
|
// getAtimeNano returns the access time in nanoseconds for Windows systems
|
||||
|
func getAtimeNano(stat *syscall.Win32FileAttributeData) int64 { |
||||
|
return stat.LastAccessTime.Nanoseconds() |
||||
|
} |
||||
@ -0,0 +1,39 @@ |
|||||
|
//go:build darwin
|
||||
|
|
||||
|
package fuse |
||||
|
|
||||
|
import ( |
||||
|
"syscall" |
||||
|
) |
||||
|
|
||||
|
// Direct I/O support for macOS
|
||||
|
|
||||
|
const ( |
||||
|
// macOS doesn't have O_DIRECT, but we can use fcntl with F_NOCACHE
|
||||
|
F_NOCACHE = 48 |
||||
|
) |
||||
|
|
||||
|
func openDirectIO(path string, flags int, mode uint32) (int, error) { |
||||
|
// Open file normally first
|
||||
|
fd, err := syscall.Open(path, flags, mode) |
||||
|
if err != nil { |
||||
|
return -1, err |
||||
|
} |
||||
|
|
||||
|
// Set F_NOCACHE to bypass buffer cache (similar to O_DIRECT)
|
||||
|
_, _, errno := syscall.Syscall(syscall.SYS_FCNTL, |
||||
|
uintptr(fd), |
||||
|
F_NOCACHE, |
||||
|
1) // enable
|
||||
|
|
||||
|
if errno != 0 { |
||||
|
syscall.Close(fd) |
||||
|
return -1, errno |
||||
|
} |
||||
|
|
||||
|
return fd, nil |
||||
|
} |
||||
|
|
||||
|
func isDirectIOSupported() bool { |
||||
|
return true |
||||
|
} |
||||
@ -0,0 +1,21 @@ |
|||||
|
//go:build linux
|
||||
|
|
||||
|
package fuse |
||||
|
|
||||
|
import ( |
||||
|
"syscall" |
||||
|
) |
||||
|
|
||||
|
// Direct I/O support for Linux
|
||||
|
|
||||
|
const ( |
||||
|
O_DIRECT = 0x4000 // Direct I/O flag for Linux
|
||||
|
) |
||||
|
|
||||
|
func openDirectIO(path string, flags int, mode uint32) (int, error) { |
||||
|
return syscall.Open(path, flags|O_DIRECT, mode) |
||||
|
} |
||||
|
|
||||
|
func isDirectIOSupported() bool { |
||||
|
return true |
||||
|
} |
||||
@ -0,0 +1,29 @@ |
|||||
|
//go:build !linux && !darwin
|
||||
|
|
||||
|
package fuse |
||||
|
|
||||
|
import ( |
||||
|
"errors" |
||||
|
"syscall" |
||||
|
) |
||||
|
|
||||
|
// Direct I/O support for unsupported platforms
|
||||
|
|
||||
|
var ErrDirectIONotSupported = errors.New("direct I/O not supported on this platform") |
||||
|
|
||||
|
const ( |
||||
|
O_DIRECT = 0x4000 // Dummy flag for compatibility
|
||||
|
) |
||||
|
|
||||
|
func openDirectIO(path string, flags int, mode uint32) (int, error) { |
||||
|
// Fall back to regular open
|
||||
|
fd, err := syscall.Open(path, flags, mode) |
||||
|
if err != nil { |
||||
|
return -1, err |
||||
|
} |
||||
|
return int(fd), nil |
||||
|
} |
||||
|
|
||||
|
func isDirectIOSupported() bool { |
||||
|
return false |
||||
|
} |
||||
@ -0,0 +1,79 @@ |
|||||
|
//go:build darwin
|
||||
|
|
||||
|
package fuse |
||||
|
|
||||
|
import ( |
||||
|
"syscall" |
||||
|
"unsafe" |
||||
|
) |
||||
|
|
||||
|
// File allocation support for macOS using fcntl
|
||||
|
|
||||
|
const ( |
||||
|
// macOS doesn't have fallocate, but we can use fcntl with F_PREALLOCATE
|
||||
|
F_ALLOCATECONTIG = 0x02 |
||||
|
F_ALLOCATEALL = 0x04 |
||||
|
|
||||
|
// Dummy flags for compatibility
|
||||
|
FALLOC_FL_KEEP_SIZE = 0x01 |
||||
|
FALLOC_FL_PUNCH_HOLE = 0x02 |
||||
|
FALLOC_FL_NO_HIDE_STALE = 0x04 |
||||
|
FALLOC_FL_COLLAPSE_RANGE = 0x08 |
||||
|
FALLOC_FL_ZERO_RANGE = 0x10 |
||||
|
FALLOC_FL_INSERT_RANGE = 0x20 |
||||
|
FALLOC_FL_UNSHARE_RANGE = 0x40 |
||||
|
) |
||||
|
|
||||
|
// fstore_t structure for F_PREALLOCATE
|
||||
|
type fstore struct { |
||||
|
flags uint32 |
||||
|
posmode int16 |
||||
|
offset int64 |
||||
|
length int64 |
||||
|
bytesalloc int64 |
||||
|
} |
||||
|
|
||||
|
func fallocateFile(fd int, mode int, offset int64, length int64) error { |
||||
|
// Check for unsupported modes on macOS
|
||||
|
unsupportedModes := FALLOC_FL_PUNCH_HOLE | FALLOC_FL_NO_HIDE_STALE | FALLOC_FL_COLLAPSE_RANGE | FALLOC_FL_ZERO_RANGE | FALLOC_FL_INSERT_RANGE | FALLOC_FL_UNSHARE_RANGE |
||||
|
if mode&unsupportedModes != 0 { |
||||
|
return syscall.ENOTSUP // Operation not supported
|
||||
|
} |
||||
|
|
||||
|
// On macOS, we use fcntl with F_PREALLOCATE
|
||||
|
store := fstore{ |
||||
|
flags: F_ALLOCATECONTIG, |
||||
|
posmode: syscall.F_PEOFPOSMODE, // Allocate from EOF
|
||||
|
offset: 0, |
||||
|
length: length, |
||||
|
} |
||||
|
|
||||
|
_, _, errno := syscall.Syscall(syscall.SYS_FCNTL, |
||||
|
uintptr(fd), |
||||
|
syscall.F_PREALLOCATE, |
||||
|
uintptr(unsafe.Pointer(&store))) |
||||
|
|
||||
|
if errno != 0 { |
||||
|
// If contiguous allocation fails, try non-contiguous
|
||||
|
store.flags = F_ALLOCATEALL |
||||
|
_, _, errno = syscall.Syscall(syscall.SYS_FCNTL, |
||||
|
uintptr(fd), |
||||
|
syscall.F_PREALLOCATE, |
||||
|
uintptr(unsafe.Pointer(&store))) |
||||
|
|
||||
|
if errno != 0 { |
||||
|
return errno |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Set file size if not keeping size
|
||||
|
if mode&FALLOC_FL_KEEP_SIZE == 0 { |
||||
|
return syscall.Ftruncate(fd, offset+length) |
||||
|
} |
||||
|
|
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
func isFallocateSupported() bool { |
||||
|
return true |
||||
|
} |
||||
@ -0,0 +1,38 @@ |
|||||
|
//go:build linux
|
||||
|
|
||||
|
package fuse |
||||
|
|
||||
|
import ( |
||||
|
"syscall" |
||||
|
) |
||||
|
|
||||
|
// File allocation support for Linux
|
||||
|
|
||||
|
const ( |
||||
|
// Fallocate flags
|
||||
|
FALLOC_FL_KEEP_SIZE = 0x01 |
||||
|
FALLOC_FL_PUNCH_HOLE = 0x02 |
||||
|
FALLOC_FL_NO_HIDE_STALE = 0x04 |
||||
|
FALLOC_FL_COLLAPSE_RANGE = 0x08 |
||||
|
FALLOC_FL_ZERO_RANGE = 0x10 |
||||
|
FALLOC_FL_INSERT_RANGE = 0x20 |
||||
|
FALLOC_FL_UNSHARE_RANGE = 0x40 |
||||
|
) |
||||
|
|
||||
|
func fallocateFile(fd int, mode int, offset int64, length int64) error { |
||||
|
_, _, errno := syscall.Syscall6(syscall.SYS_FALLOCATE, |
||||
|
uintptr(fd), |
||||
|
uintptr(mode), |
||||
|
uintptr(offset), |
||||
|
uintptr(length), |
||||
|
0, 0) |
||||
|
|
||||
|
if errno != 0 { |
||||
|
return errno |
||||
|
} |
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
func isFallocateSupported() bool { |
||||
|
return true |
||||
|
} |
||||
@ -0,0 +1,30 @@ |
|||||
|
//go:build !linux && !darwin
|
||||
|
|
||||
|
package fuse |
||||
|
|
||||
|
import ( |
||||
|
"errors" |
||||
|
) |
||||
|
|
||||
|
// File allocation support for unsupported platforms
|
||||
|
|
||||
|
var ErrFallocateNotSupported = errors.New("fallocate not supported on this platform") |
||||
|
|
||||
|
const ( |
||||
|
// Dummy flags for compatibility
|
||||
|
FALLOC_FL_KEEP_SIZE = 0x01 |
||||
|
FALLOC_FL_PUNCH_HOLE = 0x02 |
||||
|
FALLOC_FL_NO_HIDE_STALE = 0x04 |
||||
|
FALLOC_FL_COLLAPSE_RANGE = 0x08 |
||||
|
FALLOC_FL_ZERO_RANGE = 0x10 |
||||
|
FALLOC_FL_INSERT_RANGE = 0x20 |
||||
|
FALLOC_FL_UNSHARE_RANGE = 0x40 |
||||
|
) |
||||
|
|
||||
|
func fallocateFile(fd int, mode int, offset int64, length int64) error { |
||||
|
return ErrFallocateNotSupported |
||||
|
} |
||||
|
|
||||
|
func isFallocateSupported() bool { |
||||
|
return false |
||||
|
} |
||||
@ -1,11 +0,0 @@ |
|||||
module seaweedfs-fuse-tests |
|
||||
|
|
||||
go 1.21 |
|
||||
|
|
||||
require github.com/stretchr/testify v1.8.4 |
|
||||
|
|
||||
require ( |
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect |
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect |
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect |
|
||||
) |
|
||||
@ -1,10 +0,0 @@ |
|||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= |
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= |
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= |
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= |
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= |
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= |
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= |
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= |
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= |
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= |
|
||||
@ -0,0 +1,85 @@ |
|||||
|
//go:build unix
|
||||
|
|
||||
|
package fuse |
||||
|
|
||||
|
import ( |
||||
|
"syscall" |
||||
|
"unsafe" |
||||
|
) |
||||
|
|
||||
|
// Memory mapping support for Unix-like systems
|
||||
|
|
||||
|
func mmapFile(fd int, offset int64, length int, prot int, flags int) ([]byte, error) { |
||||
|
addr, _, errno := syscall.Syscall6(syscall.SYS_MMAP, |
||||
|
0, // addr (let kernel choose)
|
||||
|
uintptr(length), |
||||
|
uintptr(prot), |
||||
|
uintptr(flags), |
||||
|
uintptr(fd), |
||||
|
uintptr(offset)) |
||||
|
|
||||
|
if errno != 0 { |
||||
|
return nil, errno |
||||
|
} |
||||
|
|
||||
|
// Convert the address to a byte slice
|
||||
|
return (*[1 << 30]byte)(unsafe.Pointer(addr))[:length:length], nil |
||||
|
} |
||||
|
|
||||
|
func munmapFile(data []byte) error { |
||||
|
if len(data) == 0 { |
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
_, _, errno := syscall.Syscall(syscall.SYS_MUNMAP, |
||||
|
uintptr(unsafe.Pointer(&data[0])), |
||||
|
uintptr(len(data)), |
||||
|
0) |
||||
|
|
||||
|
if errno != 0 { |
||||
|
return errno |
||||
|
} |
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
func msyncFile(data []byte, flags int) error { |
||||
|
if len(data) == 0 { |
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
_, _, errno := syscall.Syscall(syscall.SYS_MSYNC, |
||||
|
uintptr(unsafe.Pointer(&data[0])), |
||||
|
uintptr(len(data)), |
||||
|
uintptr(flags)) |
||||
|
|
||||
|
if errno != 0 { |
||||
|
return errno |
||||
|
} |
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
func isMmapSupported() bool { |
||||
|
return true |
||||
|
} |
||||
|
|
||||
|
// Memory protection flags
|
||||
|
const ( |
||||
|
PROT_READ = syscall.PROT_READ |
||||
|
PROT_WRITE = syscall.PROT_WRITE |
||||
|
PROT_EXEC = syscall.PROT_EXEC |
||||
|
PROT_NONE = syscall.PROT_NONE |
||||
|
) |
||||
|
|
||||
|
// Memory mapping flags
|
||||
|
const ( |
||||
|
MAP_SHARED = syscall.MAP_SHARED |
||||
|
MAP_PRIVATE = syscall.MAP_PRIVATE |
||||
|
MAP_ANONYMOUS = syscall.MAP_ANON |
||||
|
) |
||||
|
|
||||
|
// Memory sync flags
|
||||
|
const ( |
||||
|
MS_ASYNC = syscall.MS_ASYNC |
||||
|
MS_SYNC = syscall.MS_SYNC |
||||
|
MS_INVALIDATE = syscall.MS_INVALIDATE |
||||
|
) |
||||
@ -0,0 +1,47 @@ |
|||||
|
//go:build !unix
|
||||
|
|
||||
|
package fuse |
||||
|
|
||||
|
import ( |
||||
|
"errors" |
||||
|
) |
||||
|
|
||||
|
// Memory mapping support for unsupported platforms
|
||||
|
|
||||
|
var ErrMmapNotSupported = errors.New("memory mapping not supported on this platform") |
||||
|
|
||||
|
func mmapFile(fd int, offset int64, length int, prot int, flags int) ([]byte, error) { |
||||
|
return nil, ErrMmapNotSupported |
||||
|
} |
||||
|
|
||||
|
func munmapFile(data []byte) error { |
||||
|
return ErrMmapNotSupported |
||||
|
} |
||||
|
|
||||
|
func msyncFile(data []byte, flags int) error { |
||||
|
return ErrMmapNotSupported |
||||
|
} |
||||
|
|
||||
|
func isMmapSupported() bool { |
||||
|
return false |
||||
|
} |
||||
|
|
||||
|
// Dummy constants for unsupported platforms
|
||||
|
const ( |
||||
|
PROT_READ = 0x1 |
||||
|
PROT_WRITE = 0x2 |
||||
|
PROT_EXEC = 0x4 |
||||
|
PROT_NONE = 0x0 |
||||
|
) |
||||
|
|
||||
|
const ( |
||||
|
MAP_SHARED = 0x01 |
||||
|
MAP_PRIVATE = 0x02 |
||||
|
MAP_ANONYMOUS = 0x20 |
||||
|
) |
||||
|
|
||||
|
const ( |
||||
|
MS_ASYNC = 0x1 |
||||
|
MS_SYNC = 0x4 |
||||
|
MS_INVALIDATE = 0x2 |
||||
|
) |
||||
@ -0,0 +1,509 @@ |
|||||
|
# SeaweedFS POSIX Compliance Testing Makefile |
||||
|
|
||||
|
# Configuration |
||||
|
WEED_BINARY := $(shell which weed 2>/dev/null || echo "../../weed") |
||||
|
GO_VERSION := 1.21 |
||||
|
TEST_TIMEOUT := 45m |
||||
|
COVERAGE_FILE := posix_coverage.out |
||||
|
REPORT_DIR := reports |
||||
|
EXTERNAL_TOOLS_DIR := external_tools |
||||
|
MOUNT_POINT ?= /tmp/seaweedfs_mount |
||||
|
|
||||
|
# Go test command prefix for external mount points |
||||
|
GO_TEST_PREFIX := |
||||
|
ifneq ($(TEST_MOUNT_POINT),) |
||||
|
GO_TEST_PREFIX := TEST_MOUNT_POINT="$(TEST_MOUNT_POINT)" TEST_SKIP_CLUSTER_SETUP="true" |
||||
|
endif |
||||
|
|
||||
|
# Test categories |
||||
|
POSIX_BASIC_TESTS := posix_compliance_test.go |
||||
|
POSIX_EXTENDED_TESTS := posix_extended_test.go |
||||
|
POSIX_EXTERNAL_TESTS := posix_external_test.go |
||||
|
|
||||
|
# Colors for output |
||||
|
RED := \033[31m |
||||
|
GREEN := \033[32m |
||||
|
YELLOW := \033[33m |
||||
|
BLUE := \033[34m |
||||
|
MAGENTA := \033[35m |
||||
|
CYAN := \033[36m |
||||
|
WHITE := \033[37m |
||||
|
RESET := \033[0m |
||||
|
|
||||
|
.DEFAULT_GOAL := help |
||||
|
|
||||
|
# Prerequisites checks |
||||
|
check-binary: |
||||
|
@if [ ! -f "$(WEED_BINARY)" ]; then \ |
||||
|
echo "$(RED)❌ SeaweedFS binary not found at $(WEED_BINARY)$(RESET)"; \ |
||||
|
echo " Please run 'make' in the root directory first"; \ |
||||
|
exit 1; \ |
||||
|
fi |
||||
|
@echo "$(GREEN)[OK] SeaweedFS binary found at $(WEED_BINARY)$(RESET)" |
||||
|
|
||||
|
check-fuse: |
||||
|
@if command -v fusermount >/dev/null 2>&1; then \ |
||||
|
echo "$(GREEN)[OK] FUSE is installed (Linux)$(RESET)"; \ |
||||
|
elif command -v umount >/dev/null 2>&1 && [ "$$(uname)" = "Darwin" ]; then \ |
||||
|
echo "$(GREEN)[OK] FUSE is available (macOS)$(RESET)"; \ |
||||
|
else \ |
||||
|
echo "$(RED)❌ FUSE not found. Please install:$(RESET)"; \ |
||||
|
echo " Ubuntu/Debian: sudo apt-get install fuse"; \ |
||||
|
echo " CentOS/RHEL: sudo yum install fuse"; \ |
||||
|
echo " macOS: brew install macfuse"; \ |
||||
|
exit 1; \ |
||||
|
fi |
||||
|
|
||||
|
check-go: |
||||
|
@go version | grep -q "go1\.[2-9][0-9]" || \ |
||||
|
go version | grep -q "go1\.2[1-9]" || \ |
||||
|
(echo "$(RED)❌ Go $(GO_VERSION)+ required. Current: $$(go version)$(RESET)" && exit 1) |
||||
|
@echo "$(GREEN)[OK] Go version check passed$(RESET)" |
||||
|
|
||||
|
check-prereqs: check-go check-fuse check-binary |
||||
|
@echo "$(GREEN)[OK] All prerequisites satisfied$(RESET)" |
||||
|
|
||||
|
# Setup and initialization |
||||
|
|
||||
|
|
||||
|
setup-reports: |
||||
|
@mkdir -p $(REPORT_DIR) |
||||
|
@mkdir -p $(EXTERNAL_TOOLS_DIR) |
||||
|
|
||||
|
setup-external-tools: setup-reports |
||||
|
@echo "$(BLUE)🛠️ Setting up external POSIX test tools...$(RESET)" |
||||
|
@$(MAKE) setup-pjdfstest || echo "$(YELLOW)[WARNING] pjdfstest setup failed - continuing without it$(RESET)" |
||||
|
@$(MAKE) setup-nfstest || echo "$(YELLOW)[WARNING] nfstest setup failed - continuing without it$(RESET)" |
||||
|
@$(MAKE) setup-fio || echo "$(YELLOW)[WARNING] FIO setup failed - continuing without it$(RESET)" |
||||
|
|
||||
|
# External tools setup |
||||
|
setup-pjdfstest: |
||||
|
@if [ ! -d "$(EXTERNAL_TOOLS_DIR)/pjdfstest" ]; then \ |
||||
|
echo "$(BLUE)[SETUP] Setting up pjdfstest...$(RESET)"; \ |
||||
|
if ! command -v autoreconf >/dev/null 2>&1; then \ |
||||
|
echo "$(YELLOW)[WARNING] autoreconf not found - installing build-essential...$(RESET)"; \ |
||||
|
if command -v apt-get >/dev/null 2>&1; then \ |
||||
|
sudo apt-get update && sudo apt-get install -y build-essential autoconf automake libtool; \ |
||||
|
elif command -v yum >/dev/null 2>&1; then \ |
||||
|
sudo yum install -y gcc make autoconf automake libtool; \ |
||||
|
else \ |
||||
|
echo "$(YELLOW)[WARNING] Please install build tools manually$(RESET)"; \ |
||||
|
exit 1; \ |
||||
|
fi; \ |
||||
|
fi; \ |
||||
|
cd $(EXTERNAL_TOOLS_DIR) && \ |
||||
|
git clone https://github.com/pjd/pjdfstest.git && \ |
||||
|
cd pjdfstest && \ |
||||
|
autoreconf -ifs && \ |
||||
|
./configure && \ |
||||
|
make; \ |
||||
|
else \ |
||||
|
echo "$(GREEN)[OK] pjdfstest already setup$(RESET)"; \ |
||||
|
fi |
||||
|
|
||||
|
setup-nfstest: |
||||
|
@echo "$(BLUE)[INSTALL] Installing nfstest...$(RESET)" |
||||
|
@pip3 install --user nfstest || \ |
||||
|
echo "$(YELLOW)[WARNING] nfstest installation failed. Install manually: pip3 install nfstest$(RESET)" |
||||
|
|
||||
|
setup-fio: |
||||
|
@if ! command -v fio >/dev/null 2>&1; then \ |
||||
|
echo "$(BLUE)[SETUP] Installing FIO...$(RESET)"; \ |
||||
|
if command -v apt-get >/dev/null 2>&1; then \ |
||||
|
sudo apt-get update && sudo apt-get install -y fio; \ |
||||
|
elif command -v yum >/dev/null 2>&1; then \ |
||||
|
sudo yum install -y fio; \ |
||||
|
elif command -v brew >/dev/null 2>&1; then \ |
||||
|
brew install fio; \ |
||||
|
else \ |
||||
|
echo "$(YELLOW)[WARNING] Please install FIO manually$(RESET)"; \ |
||||
|
fi; \ |
||||
|
else \ |
||||
|
echo "$(GREEN)[OK] FIO already installed$(RESET)"; \ |
||||
|
fi |
||||
|
|
||||
|
# Core test execution |
||||
|
test-posix-basic: check-prereqs setup-reports |
||||
|
@echo "$(CYAN)[TEST] Running basic POSIX compliance tests...$(RESET)" |
||||
|
ifneq ($(TEST_MOUNT_POINT),) |
||||
|
@echo "$(BLUE)Using external mount point: $(TEST_MOUNT_POINT)$(RESET)" |
||||
|
endif |
||||
|
@$(GO_TEST_PREFIX) go test -v -timeout $(TEST_TIMEOUT) -failfast=false -run TestPOSIXCompliance . 2>&1 | \ |
||||
|
tee $(REPORT_DIR)/posix_basic_results.log |
||||
|
|
||||
|
test-posix-extended: check-prereqs setup-reports |
||||
|
@echo "$(CYAN)[TEST] Running extended POSIX compliance tests...$(RESET)" |
||||
|
ifneq ($(TEST_MOUNT_POINT),) |
||||
|
@echo "$(BLUE)Using external mount point: $(TEST_MOUNT_POINT)$(RESET)" |
||||
|
endif |
||||
|
@$(GO_TEST_PREFIX) go test -v -timeout $(TEST_TIMEOUT) -failfast=false -run TestPOSIXExtended . 2>&1 | \ |
||||
|
tee $(REPORT_DIR)/posix_extended_results.log |
||||
|
|
||||
|
test-posix-external: check-prereqs setup-reports |
||||
|
@echo "$(CYAN)[TEST] Running external POSIX test suite integration...$(RESET)" |
||||
|
ifneq ($(TEST_MOUNT_POINT),) |
||||
|
@echo "$(BLUE)Using external mount point: $(TEST_MOUNT_POINT)$(RESET)" |
||||
|
endif |
||||
|
@if [ -d "$(EXTERNAL_TOOLS_DIR)/pjdfstest" ] || command -v nfstest_posix >/dev/null 2>&1; then \ |
||||
|
echo "$(GREEN)[OK] External tools available - running external tests$(RESET)"; \ |
||||
|
$(GO_TEST_PREFIX) go test -v -timeout $(TEST_TIMEOUT) -failfast=false -run TestExternalPOSIXSuites . 2>&1 | \ |
||||
|
tee $(REPORT_DIR)/posix_external_results.log; \ |
||||
|
else \ |
||||
|
echo "$(YELLOW)[WARNING] External tools not available - skipping external tests$(RESET)"; \ |
||||
|
echo "External tests skipped - tools not available" > $(REPORT_DIR)/posix_external_results.log; \ |
||||
|
fi |
||||
|
|
||||
|
# Comprehensive test suites |
||||
|
test-posix-full: test-posix-basic test-posix-extended test-posix-external |
||||
|
@echo "$(GREEN)[OK] Full POSIX compliance test suite completed$(RESET)" |
||||
|
@$(MAKE) generate-report |
||||
|
|
||||
|
test-posix-critical: check-prereqs setup-reports |
||||
|
@echo "$(CYAN)[TEST] Running critical POSIX compliance tests...$(RESET)" |
||||
|
ifneq ($(TEST_MOUNT_POINT),) |
||||
|
@echo "$(BLUE)Using external mount point: $(TEST_MOUNT_POINT)$(RESET)" |
||||
|
endif |
||||
|
@$(GO_TEST_PREFIX) go test -v -timeout 15m -failfast=false \ |
||||
|
-run "TestPOSIXCompliance/(FileOperations|DirectoryOperations|PermissionTests|IOOperations)" \ |
||||
|
. 2>&1 | tee $(REPORT_DIR)/posix_critical_results.log |
||||
|
|
||||
|
test-posix-stress: check-prereqs setup-reports |
||||
|
@echo "$(CYAN)[TEST] Running POSIX stress tests...$(RESET)" |
||||
|
ifneq ($(TEST_MOUNT_POINT),) |
||||
|
@echo "$(BLUE)Using external mount point: $(TEST_MOUNT_POINT)$(RESET)" |
||||
|
endif |
||||
|
@if [ -d "$(EXTERNAL_TOOLS_DIR)/pjdfstest" ] || command -v nfstest_posix >/dev/null 2>&1; then \ |
||||
|
echo "$(GREEN)[OK] External tools available - running stress tests$(RESET)"; \ |
||||
|
$(GO_TEST_PREFIX) go test -v -timeout $(TEST_TIMEOUT) -failfast=false \ |
||||
|
-run "TestExternalPOSIXSuites/CustomPOSIXTests" \ |
||||
|
. 2>&1 | tee $(REPORT_DIR)/posix_stress_results.log; \ |
||||
|
else \ |
||||
|
echo "$(YELLOW)[WARNING] External tools not available - skipping stress tests$(RESET)"; \ |
||||
|
echo "Stress tests skipped - tools not available" > $(REPORT_DIR)/posix_stress_results.log; \ |
||||
|
fi |
||||
|
|
||||
|
# Performance and benchmarks |
||||
|
benchmark-posix: check-prereqs setup-reports |
||||
|
@echo "$(CYAN)📈 Running POSIX performance benchmarks...$(RESET)" |
||||
|
ifneq ($(TEST_MOUNT_POINT),) |
||||
|
@echo "$(BLUE)Using external mount point: $(TEST_MOUNT_POINT)$(RESET)" |
||||
|
endif |
||||
|
@$(GO_TEST_PREFIX) go test -v -timeout $(TEST_TIMEOUT) -bench=. -benchmem \ |
||||
|
. 2>&1 | \ |
||||
|
tee $(REPORT_DIR)/posix_benchmark_results.log |
||||
|
|
||||
|
profile-posix: check-prereqs setup-reports |
||||
|
@echo "$(CYAN)[PROFILE] Running POSIX tests with profiling...$(RESET)" |
||||
|
ifneq ($(TEST_MOUNT_POINT),) |
||||
|
@echo "$(BLUE)Using external mount point: $(TEST_MOUNT_POINT)$(RESET)" |
||||
|
endif |
||||
|
@$(GO_TEST_PREFIX) go test -v -timeout $(TEST_TIMEOUT) -cpuprofile $(REPORT_DIR)/posix.cpu.prof \ |
||||
|
-memprofile $(REPORT_DIR)/posix.mem.prof -run TestPOSIXCompliance . |
||||
|
@echo "$(GREEN)[PROFILE] Profiles generated:$(RESET)" |
||||
|
@echo " CPU: $(REPORT_DIR)/posix.cpu.prof" |
||||
|
@echo " Memory: $(REPORT_DIR)/posix.mem.prof" |
||||
|
@echo "$(BLUE)View with: go tool pprof $(REPORT_DIR)/posix.cpu.prof$(RESET)" |
||||
|
|
||||
|
# Coverage analysis |
||||
|
coverage-posix: check-prereqs setup-reports |
||||
|
@echo "$(CYAN)[COVERAGE] Running POSIX tests with coverage analysis...$(RESET)" |
||||
|
ifneq ($(TEST_MOUNT_POINT),) |
||||
|
@echo "$(BLUE)Using external mount point: $(TEST_MOUNT_POINT)$(RESET)" |
||||
|
endif |
||||
|
@$(GO_TEST_PREFIX) go test -v -timeout $(TEST_TIMEOUT) -coverprofile=$(REPORT_DIR)/$(COVERAGE_FILE) \ |
||||
|
. |
||||
|
@go tool cover -html=$(REPORT_DIR)/$(COVERAGE_FILE) -o $(REPORT_DIR)/posix_coverage.html |
||||
|
@echo "$(GREEN)[COVERAGE] Coverage report generated: $(REPORT_DIR)/posix_coverage.html$(RESET)" |
||||
|
|
||||
|
# External tool tests |
||||
|
test-pjdfstest: setup-pjdfstest setup-reports |
||||
|
@echo "$(CYAN)[TEST] Running pjdfstest suite...$(RESET)" |
||||
|
@if [ -d "$(EXTERNAL_TOOLS_DIR)/pjdfstest" ]; then \ |
||||
|
mkdir -p $(MOUNT_POINT)/pjdfstest_workdir; \ |
||||
|
cd $(MOUNT_POINT)/pjdfstest_workdir && \ |
||||
|
prove -r $(CURDIR)/$(EXTERNAL_TOOLS_DIR)/pjdfstest/tests/ 2>&1 | tee $(CURDIR)/$(REPORT_DIR)/pjdfstest_results.log; \ |
||||
|
else \ |
||||
|
echo "$(RED)❌ pjdfstest not setup$(RESET)"; \ |
||||
|
exit 1; \ |
||||
|
fi |
||||
|
|
||||
|
test-nfstest-posix: setup-nfstest setup-reports |
||||
|
@echo "$(CYAN)[TEST] Running nfstest_posix...$(RESET)" |
||||
|
@if command -v nfstest_posix >/dev/null 2>&1; then \ |
||||
|
mkdir -p $(MOUNT_POINT); \ |
||||
|
nfstest_posix --path $(MOUNT_POINT) --verbose 2>&1 | \ |
||||
|
tee $(REPORT_DIR)/nfstest_results.log; \ |
||||
|
else \ |
||||
|
echo "$(YELLOW)[WARNING] nfstest_posix not available$(RESET)"; \ |
||||
|
fi |
||||
|
|
||||
|
# FIO-based performance tests |
||||
|
test-fio-posix: setup-reports |
||||
|
@echo "$(CYAN)[TEST] Running FIO-based POSIX I/O tests...$(RESET)" |
||||
|
@$(MAKE) create-fio-configs |
||||
|
@if command -v fio >/dev/null 2>&1; then \ |
||||
|
for config in $(REPORT_DIR)/fio_*.conf; do \ |
||||
|
echo "$(BLUE)Running FIO config: $$config$(RESET)"; \ |
||||
|
fio $$config --output=$(REPORT_DIR)/$$(basename $$config .conf)_results.log; \ |
||||
|
done; \ |
||||
|
else \ |
||||
|
echo "$(YELLOW)[WARNING] FIO not available$(RESET)"; \ |
||||
|
fi |
||||
|
|
||||
|
create-fio-configs: setup-reports |
||||
|
@echo "$(BLUE)[CONFIG] Creating FIO test configurations...$(RESET)" |
||||
|
@echo "[global]" > $(REPORT_DIR)/fio_random_rw.conf |
||||
|
@echo "name=posix_random_rw" >> $(REPORT_DIR)/fio_random_rw.conf |
||||
|
@echo "ioengine=sync" >> $(REPORT_DIR)/fio_random_rw.conf |
||||
|
@echo "iodepth=1" >> $(REPORT_DIR)/fio_random_rw.conf |
||||
|
@echo "rw=randrw" >> $(REPORT_DIR)/fio_random_rw.conf |
||||
|
@echo "bs=4k" >> $(REPORT_DIR)/fio_random_rw.conf |
||||
|
@echo "direct=0" >> $(REPORT_DIR)/fio_random_rw.conf |
||||
|
@echo "size=100m" >> $(REPORT_DIR)/fio_random_rw.conf |
||||
|
@echo "numjobs=4" >> $(REPORT_DIR)/fio_random_rw.conf |
||||
|
@echo "runtime=30" >> $(REPORT_DIR)/fio_random_rw.conf |
||||
|
@echo "time_based" >> $(REPORT_DIR)/fio_random_rw.conf |
||||
|
@echo "" >> $(REPORT_DIR)/fio_random_rw.conf |
||||
|
@echo "[random_rw_test]" >> $(REPORT_DIR)/fio_random_rw.conf |
||||
|
@echo "directory=$(MOUNT_POINT)" >> $(REPORT_DIR)/fio_random_rw.conf |
||||
|
@echo "[global]" > $(REPORT_DIR)/fio_sequential.conf |
||||
|
@echo "name=posix_sequential" >> $(REPORT_DIR)/fio_sequential.conf |
||||
|
@echo "ioengine=sync" >> $(REPORT_DIR)/fio_sequential.conf |
||||
|
@echo "iodepth=1" >> $(REPORT_DIR)/fio_sequential.conf |
||||
|
@echo "bs=1m" >> $(REPORT_DIR)/fio_sequential.conf |
||||
|
@echo "direct=0" >> $(REPORT_DIR)/fio_sequential.conf |
||||
|
@echo "size=500m" >> $(REPORT_DIR)/fio_sequential.conf |
||||
|
@echo "runtime=60" >> $(REPORT_DIR)/fio_sequential.conf |
||||
|
@echo "time_based" >> $(REPORT_DIR)/fio_sequential.conf |
||||
|
@echo "" >> $(REPORT_DIR)/fio_sequential.conf |
||||
|
@echo "[seq_write]" >> $(REPORT_DIR)/fio_sequential.conf |
||||
|
@echo "rw=write" >> $(REPORT_DIR)/fio_sequential.conf |
||||
|
@echo "directory=$(MOUNT_POINT)" >> $(REPORT_DIR)/fio_sequential.conf |
||||
|
@echo "" >> $(REPORT_DIR)/fio_sequential.conf |
||||
|
@echo "[seq_read]" >> $(REPORT_DIR)/fio_sequential.conf |
||||
|
@echo "rw=read" >> $(REPORT_DIR)/fio_sequential.conf |
||||
|
@echo "directory=$(MOUNT_POINT)" >> $(REPORT_DIR)/fio_sequential.conf |
||||
|
|
||||
|
# Reporting and analysis |
||||
|
generate-report: setup-reports |
||||
|
@echo "$(BLUE)[REPORT] Generating comprehensive POSIX compliance report...$(RESET)" |
||||
|
@$(MAKE) -f posix_Makefile generate-html-report |
||||
|
@$(MAKE) -f posix_Makefile generate-text-report |
||||
|
@$(MAKE) -f posix_Makefile generate-json-report |
||||
|
|
||||
|
generate-html-report: |
||||
|
@echo "$(BLUE)[REPORT] Generating HTML report...$(RESET)" |
||||
|
@echo "<!DOCTYPE html>" > $(REPORT_DIR)/posix_compliance_report.html |
||||
|
@echo "<html><head><title>SeaweedFS POSIX Compliance Report</title>" >> $(REPORT_DIR)/posix_compliance_report.html |
||||
|
@echo "<style>body{font-family:Arial;margin:20px}.pass{color:green}.fail{color:red}.summary{background:#f5f5f5;padding:10px;margin:10px 0}.test-section{margin:20px 0;border-left:3px solid #ddd;padding-left:15px}</style>" >> $(REPORT_DIR)/posix_compliance_report.html |
||||
|
@echo "</head><body><h1>SeaweedFS POSIX Compliance Report</h1>" >> $(REPORT_DIR)/posix_compliance_report.html |
||||
|
@echo "<p>Generated on: $$(date)</p>" >> $(REPORT_DIR)/posix_compliance_report.html |
||||
|
@echo "<div class='summary'>" >> $(REPORT_DIR)/posix_compliance_report.html |
||||
|
@echo "<h2>Test Summary</h2>" >> $(REPORT_DIR)/posix_compliance_report.html |
||||
|
@if [ -f "$(REPORT_DIR)/posix_basic_results.log" ]; then \ |
||||
|
TOTAL=$$(grep -c "RUN\|PASS\|FAIL" $(REPORT_DIR)/posix_basic_results.log 2>/dev/null || echo "0"); \ |
||||
|
PASSED=$$(grep -c "PASS:" $(REPORT_DIR)/posix_basic_results.log 2>/dev/null || echo "0"); \ |
||||
|
FAILED=$$(grep -c "FAIL:" $(REPORT_DIR)/posix_basic_results.log 2>/dev/null || echo "0"); \ |
||||
|
echo "<p>Basic Tests: <span class='pass'>$$PASSED passed</span>, <span class='fail'>$$FAILED failed</span></p>" >> $(REPORT_DIR)/posix_compliance_report.html; \ |
||||
|
fi |
||||
|
@if [ -f "$(REPORT_DIR)/posix_critical_results.log" ]; then \ |
||||
|
TOTAL=$$(grep -c "RUN\|PASS\|FAIL" $(REPORT_DIR)/posix_critical_results.log 2>/dev/null || echo "0"); \ |
||||
|
PASSED=$$(grep -c "PASS:" $(REPORT_DIR)/posix_critical_results.log 2>/dev/null || echo "0"); \ |
||||
|
FAILED=$$(grep -c "FAIL:" $(REPORT_DIR)/posix_critical_results.log 2>/dev/null || echo "0"); \ |
||||
|
echo "<p>Critical Tests: <span class='pass'>$$PASSED passed</span>, <span class='fail'>$$FAILED failed</span></p>" >> $(REPORT_DIR)/posix_compliance_report.html; \ |
||||
|
fi |
||||
|
@echo "</div>" >> $(REPORT_DIR)/posix_compliance_report.html |
||||
|
@echo "<h2>Detailed Results</h2>" >> $(REPORT_DIR)/posix_compliance_report.html |
||||
|
@for logfile in $(REPORT_DIR)/*.log; do \ |
||||
|
if [ -f "$$logfile" ]; then \ |
||||
|
basename=$$(basename "$$logfile" .log); \ |
||||
|
echo "<div class='test-section'>" >> $(REPORT_DIR)/posix_compliance_report.html; \ |
||||
|
echo "<h3>$$basename</h3>" >> $(REPORT_DIR)/posix_compliance_report.html; \ |
||||
|
echo "<pre>" >> $(REPORT_DIR)/posix_compliance_report.html; \ |
||||
|
tail -50 "$$logfile" | sed 's/&/\&/g; s/</\</g; s/>/\>/g' >> $(REPORT_DIR)/posix_compliance_report.html; \ |
||||
|
echo "</pre></div>" >> $(REPORT_DIR)/posix_compliance_report.html; \ |
||||
|
fi; \ |
||||
|
done |
||||
|
@echo "</body></html>" >> $(REPORT_DIR)/posix_compliance_report.html |
||||
|
@echo "$(GREEN)[OK] HTML report generated: $(REPORT_DIR)/posix_compliance_report.html$(RESET)" |
||||
|
|
||||
|
generate-text-report: |
||||
|
@echo "$(BLUE)[REPORT] Generating text report...$(RESET)" |
||||
|
@echo "SeaweedFS POSIX Compliance Report" > $(REPORT_DIR)/posix_compliance_summary.txt |
||||
|
@echo "=================================" >> $(REPORT_DIR)/posix_compliance_summary.txt |
||||
|
@echo "" >> $(REPORT_DIR)/posix_compliance_summary.txt |
||||
|
@echo "Generated: $$(date)" >> $(REPORT_DIR)/posix_compliance_summary.txt |
||||
|
@echo "SeaweedFS Version: $$($(WEED_BINARY) version 2>/dev/null || echo 'Unknown')" >> $(REPORT_DIR)/posix_compliance_summary.txt |
||||
|
@echo "" >> $(REPORT_DIR)/posix_compliance_summary.txt |
||||
|
@echo "Test Summary: See individual log files for detailed results" >> $(REPORT_DIR)/posix_compliance_summary.txt |
||||
|
@echo "$(GREEN)[OK] Text report generated: $(REPORT_DIR)/posix_compliance_summary.txt$(RESET)" |
||||
|
|
||||
|
generate-json-report: |
||||
|
@echo "$(BLUE)[REPORT] Generating JSON report...$(RESET)" |
||||
|
@echo "{" > $(REPORT_DIR)/posix_compliance_report.json |
||||
|
@echo " \"report_type\": \"posix_compliance\"," >> $(REPORT_DIR)/posix_compliance_report.json |
||||
|
@echo " \"timestamp\": \"$$(date -u +"%Y-%m-%dT%H:%M:%SZ")\"," >> $(REPORT_DIR)/posix_compliance_report.json |
||||
|
@echo " \"seaweedfs_version\": \"$$($(WEED_BINARY) version 2>/dev/null | head -n1 | sed 's/\\/\\\\/g; s/\"/\\\"/g' || echo 'Unknown')\"," >> $(REPORT_DIR)/posix_compliance_report.json |
||||
|
@echo " \"test_environment\": { \"os\": \"$$(uname -s)\", \"arch\": \"$$(uname -m)\" }," >> $(REPORT_DIR)/posix_compliance_report.json |
||||
|
@echo " \"test_suites\": {" >> $(REPORT_DIR)/posix_compliance_report.json |
||||
|
@FIRST=true; \ |
||||
|
for logfile in $(REPORT_DIR)/*.log; do \ |
||||
|
if [ -f "$$logfile" ]; then \ |
||||
|
basename=$$(basename "$$logfile" .log); \ |
||||
|
if [ "$$FIRST" = "true" ]; then \ |
||||
|
FIRST=false; \ |
||||
|
else \ |
||||
|
echo " ," >> $(REPORT_DIR)/posix_compliance_report.json; \ |
||||
|
fi; \ |
||||
|
PASSED=$$(grep -c "PASS:" "$$logfile" 2>/dev/null || echo 0); \ |
||||
|
FAILED=$$(grep -c "FAIL:" "$$logfile" 2>/dev/null || echo 0); \ |
||||
|
TOTAL=$$(expr "$$PASSED" + "$$FAILED" 2>/dev/null || echo 0); \ |
||||
|
echo " \"$$basename\": {" >> $(REPORT_DIR)/posix_compliance_report.json; \ |
||||
|
echo " \"total_tests\": $$TOTAL," >> $(REPORT_DIR)/posix_compliance_report.json; \ |
||||
|
echo " \"passed\": $$PASSED," >> $(REPORT_DIR)/posix_compliance_report.json; \ |
||||
|
echo " \"failed\": $$FAILED," >> $(REPORT_DIR)/posix_compliance_report.json; \ |
||||
|
if [ "$$TOTAL" -gt 0 ] 2>/dev/null; then \ |
||||
|
SUCCESS_RATE=$$(awk "BEGIN {printf \"%.2f\", ($$PASSED/$$TOTAL)*100}" 2>/dev/null || echo "0.00"); \ |
||||
|
else \ |
||||
|
SUCCESS_RATE="0.00"; \ |
||||
|
fi; \ |
||||
|
echo " \"success_rate\": \"$$SUCCESS_RATE%\"" >> $(REPORT_DIR)/posix_compliance_report.json; \ |
||||
|
echo " }" >> $(REPORT_DIR)/posix_compliance_report.json; \ |
||||
|
fi; \ |
||||
|
done |
||||
|
@echo " }," >> $(REPORT_DIR)/posix_compliance_report.json |
||||
|
@TOTAL_PASSED=0; TOTAL_FAILED=0; \ |
||||
|
for logfile in $(REPORT_DIR)/*.log; do \ |
||||
|
if [ -f "$$logfile" ]; then \ |
||||
|
PASSED=$$(grep -c "PASS:" "$$logfile" 2>/dev/null || echo 0); \ |
||||
|
FAILED=$$(grep -c "FAIL:" "$$logfile" 2>/dev/null || echo 0); \ |
||||
|
TOTAL_PASSED=$$(expr "$$TOTAL_PASSED" + "$$PASSED" 2>/dev/null || echo "$$TOTAL_PASSED"); \ |
||||
|
TOTAL_FAILED=$$(expr "$$TOTAL_FAILED" + "$$FAILED" 2>/dev/null || echo "$$TOTAL_FAILED"); \ |
||||
|
fi; \ |
||||
|
done; \ |
||||
|
GRAND_TOTAL=$$(expr "$$TOTAL_PASSED" + "$$TOTAL_FAILED" 2>/dev/null || echo 0); \ |
||||
|
if [ "$$GRAND_TOTAL" -gt 0 ] 2>/dev/null; then \ |
||||
|
OVERALL_SUCCESS=$$(awk "BEGIN {printf \"%.2f\", ($$TOTAL_PASSED/$$GRAND_TOTAL)*100}" 2>/dev/null || echo "0.00"); \ |
||||
|
else \ |
||||
|
OVERALL_SUCCESS="0.00"; \ |
||||
|
fi; \ |
||||
|
echo " \"summary\": {" >> $(REPORT_DIR)/posix_compliance_report.json; \ |
||||
|
echo " \"total_tests\": $$GRAND_TOTAL," >> $(REPORT_DIR)/posix_compliance_report.json; \ |
||||
|
echo " \"total_passed\": $$TOTAL_PASSED," >> $(REPORT_DIR)/posix_compliance_report.json; \ |
||||
|
echo " \"total_failed\": $$TOTAL_FAILED," >> $(REPORT_DIR)/posix_compliance_report.json; \ |
||||
|
echo " \"overall_success_rate\": \"$$OVERALL_SUCCESS%\"" >> $(REPORT_DIR)/posix_compliance_report.json; \ |
||||
|
echo " }" >> $(REPORT_DIR)/posix_compliance_report.json |
||||
|
@echo "}" >> $(REPORT_DIR)/posix_compliance_report.json |
||||
|
@echo "$(GREEN)[OK] JSON report generated: $(REPORT_DIR)/posix_compliance_report.json$(RESET)" |
||||
|
|
||||
|
# Cleanup and maintenance |
||||
|
clean: |
||||
|
@echo "$(YELLOW)🧹 Cleaning up test artifacts...$(RESET)" |
||||
|
@rm -rf $(REPORT_DIR) |
||||
|
@rm -rf /tmp/seaweedfs_*_test_* |
||||
|
@go clean -testcache |
||||
|
@echo "$(GREEN)[OK] Cleanup complete$(RESET)" |
||||
|
|
||||
|
clean-external-tools: |
||||
|
@echo "$(YELLOW)🧹 Cleaning up external tools...$(RESET)" |
||||
|
@rm -rf $(EXTERNAL_TOOLS_DIR) |
||||
|
|
||||
|
clean-all: clean clean-external-tools |
||||
|
|
||||
|
# Development and debugging |
||||
|
validate: |
||||
|
@echo "$(BLUE)[OK] Validating test files...$(RESET)" |
||||
|
@go build -o /dev/null ./... |
||||
|
@echo "$(GREEN)[OK] All test files compile successfully$(RESET)" |
||||
|
|
||||
|
fmt: |
||||
|
@echo "$(BLUE)🎨 Formatting Go code...$(RESET)" |
||||
|
@go fmt ./... |
||||
|
|
||||
|
lint: |
||||
|
@echo "$(BLUE)🔍 Running linter...$(RESET)" |
||||
|
@if command -v golangci-lint >/dev/null 2>&1; then \ |
||||
|
golangci-lint run; \ |
||||
|
else \ |
||||
|
echo "$(YELLOW)[WARNING] golangci-lint not found, running go vet instead$(RESET)"; \ |
||||
|
go vet ./...; \ |
||||
|
fi |
||||
|
|
||||
|
# CI/CD integration |
||||
|
ci-posix-tests: check-prereqs setup-external-tools |
||||
|
@echo "$(CYAN)🚀 Running POSIX tests for CI/CD...$(RESET)" |
||||
|
@$(MAKE) test-posix-critical |
||||
|
@$(MAKE) generate-report |
||||
|
|
||||
|
|
||||
|
|
||||
|
# Documentation and help |
||||
|
list-tests: |
||||
|
@echo "$(BLUE)📋 Available POSIX test functions:$(RESET)" |
||||
|
@grep -r "^func Test" *.go 2>/dev/null | sed 's/.*func \(Test[^(]*\).*/ \1/' | sort || echo "No test files found" |
||||
|
|
||||
|
test-info: |
||||
|
@echo "$(BLUE)[INFO] POSIX Test Information:$(RESET)" |
||||
|
@echo "Test files:" |
||||
|
@echo " - $(POSIX_BASIC_TESTS): Core POSIX compliance tests" |
||||
|
@echo " - $(POSIX_EXTENDED_TESTS): Extended POSIX feature tests" |
||||
|
@echo " - $(POSIX_EXTERNAL_TESTS): External test suite integration" |
||||
|
@echo "" |
||||
|
@echo "External tools:" |
||||
|
@echo " - pjdfstest: Comprehensive POSIX filesystem test suite" |
||||
|
@echo " - nfstest: Network filesystem POSIX API verification" |
||||
|
@echo " - FIO: Flexible I/O performance testing" |
||||
|
|
||||
|
help: |
||||
|
@echo "$(CYAN)SeaweedFS POSIX Compliance Testing$(RESET)" |
||||
|
@echo "==================================" |
||||
|
@echo "" |
||||
|
@echo "$(WHITE)Prerequisites:$(RESET)" |
||||
|
@echo " $(GREEN)make check-prereqs$(RESET) - Check all prerequisites" |
||||
|
@echo " $(GREEN)make setup-external-tools$(RESET) - Setup external POSIX test tools" |
||||
|
@echo "" |
||||
|
@echo "$(WHITE)Core POSIX Tests:$(RESET)" |
||||
|
@echo " $(GREEN)make test-posix-basic$(RESET) - Run basic POSIX compliance tests" |
||||
|
@echo " $(GREEN)make test-posix-extended$(RESET) - Run extended POSIX feature tests" |
||||
|
@echo " $(GREEN)make test-posix-external$(RESET) - Run external test suite integration" |
||||
|
@echo " $(GREEN)make test-posix-full$(RESET) - Run complete POSIX test suite" |
||||
|
@echo "" |
||||
|
@echo "$(WHITE)Focused Tests:$(RESET)" |
||||
|
@echo " $(GREEN)make test-posix-critical$(RESET) - Run critical POSIX compliance tests" |
||||
|
@echo " $(GREEN)make test-posix-stress$(RESET) - Run POSIX stress tests" |
||||
|
@echo "" |
||||
|
@echo "$(WHITE)Performance & Analysis:$(RESET)" |
||||
|
@echo " $(GREEN)make benchmark-posix$(RESET) - Run POSIX performance benchmarks" |
||||
|
@echo " $(GREEN)make profile-posix$(RESET) - Run tests with performance profiling" |
||||
|
@echo " $(GREEN)make coverage-posix$(RESET) - Run tests with coverage analysis" |
||||
|
@echo " $(GREEN)make test-fio-posix$(RESET) - Run FIO-based I/O performance tests" |
||||
|
@echo "" |
||||
|
@echo "$(WHITE)External Test Suites:$(RESET)" |
||||
|
@echo " $(GREEN)make test-pjdfstest$(RESET) - Run pjdfstest suite" |
||||
|
@echo " $(GREEN)make test-nfstest-posix$(RESET) - Run nfstest_posix" |
||||
|
@echo "" |
||||
|
@echo "$(WHITE)Reporting:$(RESET)" |
||||
|
@echo " $(GREEN)make generate-report$(RESET) - Generate comprehensive compliance report" |
||||
|
@echo "" |
||||
|
@echo "$(WHITE)Development:$(RESET)" |
||||
|
@echo " $(GREEN)make validate$(RESET) - Validate test file compilation" |
||||
|
@echo " $(GREEN)make fmt$(RESET) - Format Go code" |
||||
|
@echo " $(GREEN)make lint$(RESET) - Run linter" |
||||
|
@echo " $(GREEN)make list-tests$(RESET) - List available test functions" |
||||
|
@echo "" |
||||
|
@echo "$(WHITE)Maintenance:$(RESET)" |
||||
|
@echo " $(GREEN)make clean$(RESET) - Clean up test artifacts" |
||||
|
@echo " $(GREEN)make clean-all$(RESET) - Clean everything including external tools" |
||||
|
@echo "" |
||||
|
@echo "$(WHITE)CI/CD Integration:$(RESET)" |
||||
|
@echo " $(GREEN)make ci-posix-tests$(RESET) - Run POSIX tests optimized for CI/CD" |
||||
|
|
||||
|
|
||||
|
.PHONY: help check-prereqs check-binary check-fuse check-go setup-reports \ |
||||
|
setup-external-tools setup-pjdfstest setup-nfstest setup-fio \ |
||||
|
test-posix-basic test-posix-extended test-posix-external test-posix-full \ |
||||
|
test-posix-critical test-posix-stress benchmark-posix profile-posix coverage-posix \ |
||||
|
test-pjdfstest test-nfstest-posix test-fio-posix create-fio-configs \ |
||||
|
generate-report generate-html-report generate-text-report generate-json-report \ |
||||
|
clean clean-external-tools clean-all validate fmt lint ci-posix-tests \ |
||||
|
list-tests test-info |
||||
@ -0,0 +1,710 @@ |
|||||
|
//go:build !windows
|
||||
|
|
||||
|
package fuse |
||||
|
|
||||
|
import ( |
||||
|
"fmt" |
||||
|
"os" |
||||
|
"path/filepath" |
||||
|
"syscall" |
||||
|
"testing" |
||||
|
"time" |
||||
|
|
||||
|
"github.com/stretchr/testify/require" |
||||
|
) |
||||
|
|
||||
|
// POSIXComplianceTestSuite provides comprehensive POSIX compliance testing for FUSE mounts
|
||||
|
type POSIXComplianceTestSuite struct { |
||||
|
framework *FuseTestFramework |
||||
|
t *testing.T |
||||
|
} |
||||
|
|
||||
|
// NewPOSIXComplianceTestSuite creates a new POSIX compliance test suite
|
||||
|
func NewPOSIXComplianceTestSuite(t *testing.T, framework *FuseTestFramework) *POSIXComplianceTestSuite { |
||||
|
return &POSIXComplianceTestSuite{ |
||||
|
framework: framework, |
||||
|
t: t, |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// TestPOSIXCompliance runs all POSIX compliance tests
|
||||
|
func TestPOSIXCompliance(t *testing.T) { |
||||
|
config := DefaultTestConfig() |
||||
|
config.EnableDebug = true |
||||
|
config.MountOptions = []string{"-allowOthers", "-nonempty"} |
||||
|
|
||||
|
framework := NewFuseTestFramework(t, config) |
||||
|
defer framework.Cleanup() |
||||
|
require.NoError(t, framework.Setup(config)) |
||||
|
|
||||
|
suite := NewPOSIXComplianceTestSuite(t, framework) |
||||
|
|
||||
|
// Run all POSIX compliance test categories
|
||||
|
t.Run("FileOperations", suite.TestFileOperations) |
||||
|
t.Run("DirectoryOperations", suite.TestDirectoryOperations) |
||||
|
t.Run("SymlinkOperations", suite.TestSymlinkOperations) |
||||
|
t.Run("PermissionTests", suite.TestPermissions) |
||||
|
t.Run("TimestampTests", suite.TestTimestamps) |
||||
|
t.Run("IOOperations", suite.TestIOOperations) |
||||
|
t.Run("FileDescriptorTests", suite.TestFileDescriptors) |
||||
|
t.Run("AtomicOperations", suite.TestAtomicOperations) |
||||
|
t.Run("ConcurrentAccess", suite.TestConcurrentAccess) |
||||
|
t.Run("ErrorHandling", suite.TestErrorHandling) |
||||
|
} |
||||
|
|
||||
|
// TestFileOperations tests POSIX file operation compliance
|
||||
|
func (s *POSIXComplianceTestSuite) TestFileOperations(t *testing.T) { |
||||
|
mountPoint := s.framework.GetMountPoint() |
||||
|
|
||||
|
t.Run("CreateFile", func(t *testing.T) { |
||||
|
filepath := filepath.Join(mountPoint, "test_create.txt") |
||||
|
|
||||
|
// Test file creation with O_CREAT
|
||||
|
fd, err := syscall.Open(filepath, syscall.O_CREAT|syscall.O_WRONLY, 0644) |
||||
|
require.NoError(t, err) |
||||
|
require.Greater(t, fd, 0) |
||||
|
|
||||
|
err = syscall.Close(fd) |
||||
|
require.NoError(t, err) |
||||
|
|
||||
|
// Verify file exists
|
||||
|
_, err = os.Stat(filepath) |
||||
|
require.NoError(t, err) |
||||
|
}) |
||||
|
|
||||
|
t.Run("CreateExclusiveFile", func(t *testing.T) { |
||||
|
filepath := filepath.Join(mountPoint, "test_excl.txt") |
||||
|
|
||||
|
// First creation should succeed
|
||||
|
fd, err := syscall.Open(filepath, syscall.O_CREAT|syscall.O_EXCL|syscall.O_WRONLY, 0644) |
||||
|
require.NoError(t, err) |
||||
|
syscall.Close(fd) |
||||
|
|
||||
|
// Second creation should fail with EEXIST
|
||||
|
_, err = syscall.Open(filepath, syscall.O_CREAT|syscall.O_EXCL|syscall.O_WRONLY, 0644) |
||||
|
require.Error(t, err) |
||||
|
require.Equal(t, syscall.EEXIST, err) |
||||
|
}) |
||||
|
|
||||
|
t.Run("TruncateFile", func(t *testing.T) { |
||||
|
filepath := filepath.Join(mountPoint, "test_truncate.txt") |
||||
|
content := []byte("Hello, World! This is a test file for truncation.") |
||||
|
|
||||
|
// Create file with content
|
||||
|
err := os.WriteFile(filepath, content, 0644) |
||||
|
require.NoError(t, err) |
||||
|
|
||||
|
// Truncate to 5 bytes
|
||||
|
err = syscall.Truncate(filepath, 5) |
||||
|
require.NoError(t, err) |
||||
|
|
||||
|
// Verify truncation
|
||||
|
readContent, err := os.ReadFile(filepath) |
||||
|
require.NoError(t, err) |
||||
|
require.Equal(t, []byte("Hello"), readContent) |
||||
|
}) |
||||
|
|
||||
|
t.Run("UnlinkFile", func(t *testing.T) { |
||||
|
filepath := filepath.Join(mountPoint, "test_unlink.txt") |
||||
|
|
||||
|
// Create file
|
||||
|
err := os.WriteFile(filepath, []byte("test"), 0644) |
||||
|
require.NoError(t, err) |
||||
|
|
||||
|
// Unlink file
|
||||
|
err = syscall.Unlink(filepath) |
||||
|
require.NoError(t, err) |
||||
|
|
||||
|
// Verify file no longer exists
|
||||
|
_, err = os.Stat(filepath) |
||||
|
require.True(t, os.IsNotExist(err)) |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
// TestDirectoryOperations tests POSIX directory operation compliance
|
||||
|
func (s *POSIXComplianceTestSuite) TestDirectoryOperations(t *testing.T) { |
||||
|
mountPoint := s.framework.GetMountPoint() |
||||
|
|
||||
|
t.Run("CreateDirectory", func(t *testing.T) { |
||||
|
dirPath := filepath.Join(mountPoint, "test_mkdir") |
||||
|
|
||||
|
err := syscall.Mkdir(dirPath, 0755) |
||||
|
require.NoError(t, err) |
||||
|
|
||||
|
// Verify directory exists and has correct type
|
||||
|
stat, err := os.Stat(dirPath) |
||||
|
require.NoError(t, err) |
||||
|
require.True(t, stat.IsDir()) |
||||
|
}) |
||||
|
|
||||
|
t.Run("RemoveDirectory", func(t *testing.T) { |
||||
|
dirPath := filepath.Join(mountPoint, "test_rmdir") |
||||
|
|
||||
|
// Create directory
|
||||
|
err := os.Mkdir(dirPath, 0755) |
||||
|
require.NoError(t, err) |
||||
|
|
||||
|
// Remove directory
|
||||
|
err = syscall.Rmdir(dirPath) |
||||
|
require.NoError(t, err) |
||||
|
|
||||
|
// Verify directory no longer exists
|
||||
|
_, err = os.Stat(dirPath) |
||||
|
require.True(t, os.IsNotExist(err)) |
||||
|
}) |
||||
|
|
||||
|
t.Run("RemoveNonEmptyDirectory", func(t *testing.T) { |
||||
|
dirPath := filepath.Join(mountPoint, "test_rmdir_nonempty") |
||||
|
filePath := filepath.Join(dirPath, "file.txt") |
||||
|
|
||||
|
// Create directory and file
|
||||
|
err := os.Mkdir(dirPath, 0755) |
||||
|
require.NoError(t, err) |
||||
|
err = os.WriteFile(filePath, []byte("test"), 0644) |
||||
|
require.NoError(t, err) |
||||
|
|
||||
|
// Attempt to remove non-empty directory should fail
|
||||
|
err = syscall.Rmdir(dirPath) |
||||
|
require.Error(t, err) |
||||
|
require.Equal(t, syscall.ENOTEMPTY, err) |
||||
|
}) |
||||
|
|
||||
|
t.Run("RenameDirectory", func(t *testing.T) { |
||||
|
oldPath := filepath.Join(mountPoint, "old_dir") |
||||
|
newPath := filepath.Join(mountPoint, "new_dir") |
||||
|
|
||||
|
// Create directory
|
||||
|
err := os.Mkdir(oldPath, 0755) |
||||
|
require.NoError(t, err) |
||||
|
|
||||
|
// Rename directory
|
||||
|
err = os.Rename(oldPath, newPath) |
||||
|
require.NoError(t, err) |
||||
|
|
||||
|
// Verify old path doesn't exist and new path does
|
||||
|
_, err = os.Stat(oldPath) |
||||
|
require.True(t, os.IsNotExist(err)) |
||||
|
stat, err := os.Stat(newPath) |
||||
|
require.NoError(t, err) |
||||
|
require.True(t, stat.IsDir()) |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
// TestSymlinkOperations tests POSIX symlink operation compliance
|
||||
|
func (s *POSIXComplianceTestSuite) TestSymlinkOperations(t *testing.T) { |
||||
|
mountPoint := s.framework.GetMountPoint() |
||||
|
|
||||
|
t.Run("CreateSymlink", func(t *testing.T) { |
||||
|
targetFile := filepath.Join(mountPoint, "target.txt") |
||||
|
linkFile := filepath.Join(mountPoint, "link.txt") |
||||
|
|
||||
|
// Create target file
|
||||
|
err := os.WriteFile(targetFile, []byte("target content"), 0644) |
||||
|
require.NoError(t, err) |
||||
|
|
||||
|
// Create symlink
|
||||
|
err = os.Symlink(targetFile, linkFile) |
||||
|
require.NoError(t, err) |
||||
|
|
||||
|
// Verify symlink properties
|
||||
|
linkStat, err := os.Lstat(linkFile) |
||||
|
require.NoError(t, err) |
||||
|
require.Equal(t, os.ModeSymlink, linkStat.Mode()&os.ModeType) |
||||
|
|
||||
|
// Verify symlink content
|
||||
|
linkTarget, err := os.Readlink(linkFile) |
||||
|
require.NoError(t, err) |
||||
|
require.Equal(t, targetFile, linkTarget) |
||||
|
|
||||
|
// Verify following symlink works
|
||||
|
content, err := os.ReadFile(linkFile) |
||||
|
require.NoError(t, err) |
||||
|
require.Equal(t, []byte("target content"), content) |
||||
|
}) |
||||
|
|
||||
|
t.Run("BrokenSymlink", func(t *testing.T) { |
||||
|
nonexistentTarget := filepath.Join(mountPoint, "nonexistent.txt") |
||||
|
linkFile := filepath.Join(mountPoint, "broken_link.txt") |
||||
|
|
||||
|
// Create symlink to nonexistent file
|
||||
|
err := os.Symlink(nonexistentTarget, linkFile) |
||||
|
require.NoError(t, err) |
||||
|
|
||||
|
// Lstat should work (doesn't follow symlink)
|
||||
|
_, err = os.Lstat(linkFile) |
||||
|
require.NoError(t, err) |
||||
|
|
||||
|
// Stat should fail (follows symlink to nonexistent target)
|
||||
|
_, err = os.Stat(linkFile) |
||||
|
require.Error(t, err) |
||||
|
require.True(t, os.IsNotExist(err)) |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
// TestPermissions tests POSIX permission compliance
|
||||
|
func (s *POSIXComplianceTestSuite) TestPermissions(t *testing.T) { |
||||
|
mountPoint := s.framework.GetMountPoint() |
||||
|
|
||||
|
t.Run("FilePermissions", func(t *testing.T) { |
||||
|
testFile := filepath.Join(mountPoint, "perm_test.txt") |
||||
|
|
||||
|
// Create file with specific permissions
|
||||
|
fd, err := syscall.Open(testFile, syscall.O_CREAT|syscall.O_WRONLY, 0642) |
||||
|
require.NoError(t, err) |
||||
|
syscall.Close(fd) |
||||
|
|
||||
|
// Verify permissions
|
||||
|
stat, err := os.Stat(testFile) |
||||
|
require.NoError(t, err) |
||||
|
|
||||
|
// Note: The final file permissions are affected by the system's umask.
|
||||
|
// We check against the requested mode and the mode as affected by a common umask (e.g. 0022).
|
||||
|
actualMode := stat.Mode() & os.ModePerm |
||||
|
expectedMode := os.FileMode(0642) |
||||
|
expectedModeWithUmask := os.FileMode(0640) // e.g., 0642 with umask 0002 or 0022
|
||||
|
|
||||
|
// Accept either the exact permissions or permissions as modified by umask
|
||||
|
if actualMode != expectedMode && actualMode != expectedModeWithUmask { |
||||
|
t.Errorf("Expected file permissions %o or %o, but got %o", |
||||
|
expectedMode, expectedModeWithUmask, actualMode) |
||||
|
} |
||||
|
}) |
||||
|
|
||||
|
t.Run("ChangeFilePermissions", func(t *testing.T) { |
||||
|
testFile := filepath.Join(mountPoint, "chmod_test.txt") |
||||
|
|
||||
|
// Create file
|
||||
|
err := os.WriteFile(testFile, []byte("test"), 0644) |
||||
|
require.NoError(t, err) |
||||
|
|
||||
|
// Change permissions
|
||||
|
err = os.Chmod(testFile, 0755) |
||||
|
require.NoError(t, err) |
||||
|
|
||||
|
// Verify new permissions
|
||||
|
stat, err := os.Stat(testFile) |
||||
|
require.NoError(t, err) |
||||
|
require.Equal(t, os.FileMode(0755), stat.Mode()&os.ModePerm) |
||||
|
}) |
||||
|
|
||||
|
t.Run("DirectoryPermissions", func(t *testing.T) { |
||||
|
testDir := filepath.Join(mountPoint, "perm_dir") |
||||
|
|
||||
|
// Create directory with specific permissions
|
||||
|
err := syscall.Mkdir(testDir, 0750) |
||||
|
require.NoError(t, err) |
||||
|
|
||||
|
// Verify permissions
|
||||
|
stat, err := os.Stat(testDir) |
||||
|
require.NoError(t, err) |
||||
|
require.Equal(t, os.FileMode(0750)|os.ModeDir, stat.Mode()) |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
// TestTimestamps tests POSIX timestamp compliance
|
||||
|
func (s *POSIXComplianceTestSuite) TestTimestamps(t *testing.T) { |
||||
|
mountPoint := s.framework.GetMountPoint() |
||||
|
|
||||
|
t.Run("AccessTime", func(t *testing.T) { |
||||
|
testFile := filepath.Join(mountPoint, "atime_test.txt") |
||||
|
|
||||
|
// Create file
|
||||
|
err := os.WriteFile(testFile, []byte("test content"), 0644) |
||||
|
require.NoError(t, err) |
||||
|
|
||||
|
// Get initial timestamps
|
||||
|
stat1, err := os.Stat(testFile) |
||||
|
require.NoError(t, err) |
||||
|
|
||||
|
// Wait a bit to ensure time difference
|
||||
|
time.Sleep(100 * time.Millisecond) |
||||
|
|
||||
|
// Read file (should update access time)
|
||||
|
_, err = os.ReadFile(testFile) |
||||
|
require.NoError(t, err) |
||||
|
|
||||
|
// Get new timestamps
|
||||
|
stat2, err := os.Stat(testFile) |
||||
|
require.NoError(t, err) |
||||
|
|
||||
|
// Access time should have been updated, and modification time should be unchanged.
|
||||
|
stat1Sys := stat1.Sys().(*syscall.Stat_t) |
||||
|
stat2Sys := stat2.Sys().(*syscall.Stat_t) |
||||
|
require.True(t, getAtimeNano(stat2Sys) >= getAtimeNano(stat1Sys), "access time should be updated or stay the same") |
||||
|
require.Equal(t, stat1.ModTime(), stat2.ModTime(), "modification time should not change on read") |
||||
|
}) |
||||
|
|
||||
|
t.Run("ModificationTime", func(t *testing.T) { |
||||
|
testFile := filepath.Join(mountPoint, "mtime_test.txt") |
||||
|
|
||||
|
// Create file
|
||||
|
err := os.WriteFile(testFile, []byte("initial content"), 0644) |
||||
|
require.NoError(t, err) |
||||
|
|
||||
|
// Get initial timestamp
|
||||
|
stat1, err := os.Stat(testFile) |
||||
|
require.NoError(t, err) |
||||
|
|
||||
|
// Wait to ensure time difference
|
||||
|
time.Sleep(100 * time.Millisecond) |
||||
|
|
||||
|
// Modify file
|
||||
|
err = os.WriteFile(testFile, []byte("modified content"), 0644) |
||||
|
require.NoError(t, err) |
||||
|
|
||||
|
// Get new timestamp
|
||||
|
stat2, err := os.Stat(testFile) |
||||
|
require.NoError(t, err) |
||||
|
|
||||
|
// Modification time should have changed
|
||||
|
require.True(t, stat2.ModTime().After(stat1.ModTime())) |
||||
|
}) |
||||
|
|
||||
|
t.Run("SetTimestamps", func(t *testing.T) { |
||||
|
testFile := filepath.Join(mountPoint, "utime_test.txt") |
||||
|
|
||||
|
// Create file
|
||||
|
err := os.WriteFile(testFile, []byte("test"), 0644) |
||||
|
require.NoError(t, err) |
||||
|
|
||||
|
// Set specific timestamps
|
||||
|
atime := time.Now().Add(-24 * time.Hour) |
||||
|
mtime := time.Now().Add(-12 * time.Hour) |
||||
|
|
||||
|
err = os.Chtimes(testFile, atime, mtime) |
||||
|
require.NoError(t, err) |
||||
|
|
||||
|
// Verify timestamps were set
|
||||
|
stat, err := os.Stat(testFile) |
||||
|
require.NoError(t, err) |
||||
|
require.True(t, stat.ModTime().Equal(mtime)) |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
// TestIOOperations tests POSIX I/O operation compliance
|
||||
|
func (s *POSIXComplianceTestSuite) TestIOOperations(t *testing.T) { |
||||
|
mountPoint := s.framework.GetMountPoint() |
||||
|
|
||||
|
t.Run("ReadWrite", func(t *testing.T) { |
||||
|
testFile := filepath.Join(mountPoint, "rw_test.txt") |
||||
|
testData := []byte("Hello, POSIX World!") |
||||
|
|
||||
|
// Write data
|
||||
|
fd, err := syscall.Open(testFile, syscall.O_CREAT|syscall.O_WRONLY, 0644) |
||||
|
require.NoError(t, err) |
||||
|
|
||||
|
n, err := syscall.Write(fd, testData) |
||||
|
require.NoError(t, err) |
||||
|
require.Equal(t, len(testData), n) |
||||
|
|
||||
|
err = syscall.Close(fd) |
||||
|
require.NoError(t, err) |
||||
|
|
||||
|
// Read data back
|
||||
|
fd, err = syscall.Open(testFile, syscall.O_RDONLY, 0) |
||||
|
require.NoError(t, err) |
||||
|
|
||||
|
readBuffer := make([]byte, len(testData)) |
||||
|
n, err = syscall.Read(fd, readBuffer) |
||||
|
require.NoError(t, err) |
||||
|
require.Equal(t, len(testData), n) |
||||
|
require.Equal(t, testData, readBuffer) |
||||
|
|
||||
|
err = syscall.Close(fd) |
||||
|
require.NoError(t, err) |
||||
|
}) |
||||
|
|
||||
|
t.Run("Seek", func(t *testing.T) { |
||||
|
testFile := filepath.Join(mountPoint, "seek_test.txt") |
||||
|
testData := []byte("0123456789ABCDEF") |
||||
|
|
||||
|
// Create file with test data
|
||||
|
err := os.WriteFile(testFile, testData, 0644) |
||||
|
require.NoError(t, err) |
||||
|
|
||||
|
// Open for reading
|
||||
|
fd, err := syscall.Open(testFile, syscall.O_RDONLY, 0) |
||||
|
require.NoError(t, err) |
||||
|
defer syscall.Close(fd) |
||||
|
|
||||
|
// Seek to position 5
|
||||
|
pos, err := syscall.Seek(fd, 5, 0) // SEEK_SET
|
||||
|
require.NoError(t, err) |
||||
|
require.Equal(t, int64(5), pos) |
||||
|
|
||||
|
// Read 3 bytes
|
||||
|
buffer := make([]byte, 3) |
||||
|
n, err := syscall.Read(fd, buffer) |
||||
|
require.NoError(t, err) |
||||
|
require.Equal(t, 3, n) |
||||
|
require.Equal(t, []byte("567"), buffer) |
||||
|
|
||||
|
// Seek from current position
|
||||
|
pos, err = syscall.Seek(fd, 2, 1) // SEEK_CUR
|
||||
|
require.NoError(t, err) |
||||
|
require.Equal(t, int64(10), pos) |
||||
|
|
||||
|
// Read 1 byte
|
||||
|
buffer = make([]byte, 1) |
||||
|
n, err = syscall.Read(fd, buffer) |
||||
|
require.NoError(t, err) |
||||
|
require.Equal(t, 1, n) |
||||
|
require.Equal(t, []byte("A"), buffer) |
||||
|
|
||||
|
// Test positioned I/O operations (pread/pwrite)
|
||||
|
syscall.Close(fd) |
||||
|
|
||||
|
// Open for read/write to test pwrite
|
||||
|
fd, err = syscall.Open(testFile, syscall.O_RDWR, 0) |
||||
|
require.NoError(t, err) |
||||
|
defer syscall.Close(fd) |
||||
|
|
||||
|
// Positioned write test
|
||||
|
writeData := []byte("XYZ") |
||||
|
n, err = syscall.Pwrite(fd, writeData, 5) // pwrite at offset 5
|
||||
|
require.NoError(t, err) |
||||
|
require.Equal(t, len(writeData), n) |
||||
|
|
||||
|
// Verify file position is unchanged by pwrite
|
||||
|
currentPos, err := syscall.Seek(fd, 0, 1) // SEEK_CUR
|
||||
|
require.NoError(t, err) |
||||
|
require.Equal(t, int64(0), currentPos, "file offset should not be changed by pwrite") |
||||
|
|
||||
|
// Read back with pread
|
||||
|
readBuffer := make([]byte, len(writeData)) |
||||
|
n, err = syscall.Pread(fd, readBuffer, 5) // pread at offset 5
|
||||
|
require.NoError(t, err) |
||||
|
require.Equal(t, len(writeData), n) |
||||
|
require.Equal(t, writeData, readBuffer) |
||||
|
|
||||
|
// Verify file position is still unchanged by pread
|
||||
|
currentPos, err = syscall.Seek(fd, 0, 1) // SEEK_CUR
|
||||
|
require.NoError(t, err) |
||||
|
require.Equal(t, int64(0), currentPos, "file offset should not be changed by pread") |
||||
|
}) |
||||
|
|
||||
|
t.Run("AppendMode", func(t *testing.T) { |
||||
|
testFile := filepath.Join(mountPoint, "append_test.txt") |
||||
|
|
||||
|
// Create file with initial content
|
||||
|
err := os.WriteFile(testFile, []byte("initial"), 0644) |
||||
|
require.NoError(t, err) |
||||
|
|
||||
|
// Open in append mode
|
||||
|
fd, err := syscall.Open(testFile, syscall.O_WRONLY|syscall.O_APPEND, 0) |
||||
|
require.NoError(t, err) |
||||
|
|
||||
|
// Write additional content
|
||||
|
appendData := []byte(" appended") |
||||
|
n, err := syscall.Write(fd, appendData) |
||||
|
require.NoError(t, err) |
||||
|
require.Equal(t, len(appendData), n) |
||||
|
|
||||
|
err = syscall.Close(fd) |
||||
|
require.NoError(t, err) |
||||
|
|
||||
|
// Verify content
|
||||
|
content, err := os.ReadFile(testFile) |
||||
|
require.NoError(t, err) |
||||
|
require.Equal(t, []byte("initial appended"), content) |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
// TestFileDescriptors tests POSIX file descriptor behavior
|
||||
|
func (s *POSIXComplianceTestSuite) TestFileDescriptors(t *testing.T) { |
||||
|
mountPoint := s.framework.GetMountPoint() |
||||
|
|
||||
|
t.Run("DuplicateFileDescriptors", func(t *testing.T) { |
||||
|
testFile := filepath.Join(mountPoint, "dup_test.txt") |
||||
|
testData := []byte("duplicate test") |
||||
|
|
||||
|
// Create and open file
|
||||
|
fd, err := syscall.Open(testFile, syscall.O_CREAT|syscall.O_RDWR, 0644) |
||||
|
require.NoError(t, err) |
||||
|
defer syscall.Close(fd) |
||||
|
|
||||
|
// Write initial data
|
||||
|
n, err := syscall.Write(fd, testData) |
||||
|
require.NoError(t, err) |
||||
|
require.Equal(t, len(testData), n) |
||||
|
|
||||
|
// Duplicate file descriptor
|
||||
|
dupFd, err := syscall.Dup(fd) |
||||
|
require.NoError(t, err) |
||||
|
defer syscall.Close(dupFd) |
||||
|
|
||||
|
// Both descriptors should refer to the same file
|
||||
|
// Seek on one should affect the other
|
||||
|
pos, err := syscall.Seek(fd, 0, 0) // SEEK_SET
|
||||
|
require.NoError(t, err) |
||||
|
require.Equal(t, int64(0), pos) |
||||
|
|
||||
|
// Read from duplicate descriptor should start from position 0
|
||||
|
buffer := make([]byte, 9) |
||||
|
n, err = syscall.Read(dupFd, buffer) |
||||
|
require.NoError(t, err) |
||||
|
require.Equal(t, 9, n) |
||||
|
require.Equal(t, []byte("duplicate"), buffer) |
||||
|
}) |
||||
|
|
||||
|
t.Run("FileDescriptorFlags", func(t *testing.T) { |
||||
|
testFile := filepath.Join(mountPoint, "flags_test.txt") |
||||
|
|
||||
|
// Create file
|
||||
|
err := os.WriteFile(testFile, []byte("test"), 0644) |
||||
|
require.NoError(t, err) |
||||
|
|
||||
|
// Open with close-on-exec flag
|
||||
|
fd, err := syscall.Open(testFile, syscall.O_RDONLY|syscall.O_CLOEXEC, 0) |
||||
|
require.NoError(t, err) |
||||
|
defer syscall.Close(fd) |
||||
|
|
||||
|
// Verify close-on-exec flag is set
|
||||
|
// Note: FcntlInt is not available on all platforms, this test may need platform-specific implementation
|
||||
|
t.Skip("FcntlInt not available on this platform") |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
// TestAtomicOperations tests POSIX atomic operation compliance
|
||||
|
func (s *POSIXComplianceTestSuite) TestAtomicOperations(t *testing.T) { |
||||
|
mountPoint := s.framework.GetMountPoint() |
||||
|
|
||||
|
t.Run("AtomicRename", func(t *testing.T) { |
||||
|
oldFile := filepath.Join(mountPoint, "atomic_old.txt") |
||||
|
newFile := filepath.Join(mountPoint, "atomic_new.txt") |
||||
|
testData := []byte("atomic test data") |
||||
|
|
||||
|
// Create source file
|
||||
|
err := os.WriteFile(oldFile, testData, 0644) |
||||
|
require.NoError(t, err) |
||||
|
|
||||
|
// Create existing target file
|
||||
|
err = os.WriteFile(newFile, []byte("old content"), 0644) |
||||
|
require.NoError(t, err) |
||||
|
|
||||
|
// Atomic rename should replace target
|
||||
|
err = os.Rename(oldFile, newFile) |
||||
|
require.NoError(t, err) |
||||
|
|
||||
|
// Verify source no longer exists
|
||||
|
_, err = os.Stat(oldFile) |
||||
|
require.True(t, os.IsNotExist(err)) |
||||
|
|
||||
|
// Verify target has new content
|
||||
|
content, err := os.ReadFile(newFile) |
||||
|
require.NoError(t, err) |
||||
|
require.Equal(t, testData, content) |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
// TestConcurrentAccess tests concurrent access patterns
|
||||
|
func (s *POSIXComplianceTestSuite) TestConcurrentAccess(t *testing.T) { |
||||
|
mountPoint := s.framework.GetMountPoint() |
||||
|
|
||||
|
t.Run("ConcurrentReads", func(t *testing.T) { |
||||
|
testFile := filepath.Join(mountPoint, "concurrent_read.txt") |
||||
|
testData := []byte("concurrent read test data") |
||||
|
|
||||
|
// Create test file
|
||||
|
err := os.WriteFile(testFile, testData, 0644) |
||||
|
require.NoError(t, err) |
||||
|
|
||||
|
// Launch multiple concurrent readers
|
||||
|
const numReaders = 10 |
||||
|
results := make(chan error, numReaders) |
||||
|
|
||||
|
for i := 0; i < numReaders; i++ { |
||||
|
go func() { |
||||
|
content, err := os.ReadFile(testFile) |
||||
|
if err != nil { |
||||
|
results <- err |
||||
|
return |
||||
|
} |
||||
|
if string(content) != string(testData) { |
||||
|
results <- fmt.Errorf("content mismatch") |
||||
|
return |
||||
|
} |
||||
|
results <- nil |
||||
|
}() |
||||
|
} |
||||
|
|
||||
|
// Check all readers succeeded
|
||||
|
for i := 0; i < numReaders; i++ { |
||||
|
err := <-results |
||||
|
require.NoError(t, err) |
||||
|
} |
||||
|
}) |
||||
|
|
||||
|
t.Run("ConcurrentFileCreations", func(t *testing.T) { |
||||
|
testFile := filepath.Join(mountPoint, "concurrent_write.txt") |
||||
|
|
||||
|
// Launch multiple concurrent writers
|
||||
|
const numWriters = 5 |
||||
|
results := make(chan error, numWriters) |
||||
|
|
||||
|
for i := 0; i < numWriters; i++ { |
||||
|
go func(id int) { |
||||
|
content := fmt.Sprintf("writer %d data", id) |
||||
|
err := os.WriteFile(fmt.Sprintf("%s_%d", testFile, id), []byte(content), 0644) |
||||
|
results <- err |
||||
|
}(i) |
||||
|
} |
||||
|
|
||||
|
// Check all writers succeeded
|
||||
|
for i := 0; i < numWriters; i++ { |
||||
|
err := <-results |
||||
|
require.NoError(t, err) |
||||
|
} |
||||
|
|
||||
|
// Verify all files were created
|
||||
|
for i := 0; i < numWriters; i++ { |
||||
|
fileName := fmt.Sprintf("%s_%d", testFile, i) |
||||
|
_, err := os.Stat(fileName) |
||||
|
require.NoError(t, err) |
||||
|
} |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
// TestErrorHandling tests POSIX error handling compliance
|
||||
|
func (s *POSIXComplianceTestSuite) TestErrorHandling(t *testing.T) { |
||||
|
mountPoint := s.framework.GetMountPoint() |
||||
|
|
||||
|
t.Run("AccessNonexistentFile", func(t *testing.T) { |
||||
|
nonexistentFile := filepath.Join(mountPoint, "does_not_exist.txt") |
||||
|
|
||||
|
// Reading nonexistent file should return ENOENT
|
||||
|
_, err := os.ReadFile(nonexistentFile) |
||||
|
require.Error(t, err) |
||||
|
require.True(t, os.IsNotExist(err)) |
||||
|
}) |
||||
|
|
||||
|
t.Run("CreateFileInNonexistentDirectory", func(t *testing.T) { |
||||
|
fileInNonexistentDir := filepath.Join(mountPoint, "nonexistent_dir", "file.txt") |
||||
|
|
||||
|
// Creating file in nonexistent directory should fail
|
||||
|
err := os.WriteFile(fileInNonexistentDir, []byte("test"), 0644) |
||||
|
require.Error(t, err) |
||||
|
require.True(t, os.IsNotExist(err)) |
||||
|
}) |
||||
|
|
||||
|
t.Run("InvalidFileDescriptor", func(t *testing.T) { |
||||
|
// Using an invalid file descriptor should return appropriate error
|
||||
|
buffer := make([]byte, 10) |
||||
|
_, err := syscall.Read(999, buffer) // 999 is likely an invalid fd
|
||||
|
require.Error(t, err) |
||||
|
require.Equal(t, syscall.EBADF, err) |
||||
|
}) |
||||
|
|
||||
|
t.Run("PermissionDenied", func(t *testing.T) { |
||||
|
testFile := filepath.Join(mountPoint, "readonly.txt") |
||||
|
|
||||
|
// Create read-only file
|
||||
|
err := os.WriteFile(testFile, []byte("readonly"), 0444) |
||||
|
require.NoError(t, err) |
||||
|
|
||||
|
// Attempting to write to read-only file should fail
|
||||
|
err = os.WriteFile(testFile, []byte("modified"), 0444) |
||||
|
require.Error(t, err) |
||||
|
require.True(t, os.IsPermission(err)) |
||||
|
}) |
||||
|
} |
||||
@ -0,0 +1,758 @@ |
|||||
|
//go:build !windows
|
||||
|
|
||||
|
package fuse |
||||
|
|
||||
|
import ( |
||||
|
"fmt" |
||||
|
"os" |
||||
|
"path/filepath" |
||||
|
"strings" |
||||
|
"syscall" |
||||
|
"testing" |
||||
|
|
||||
|
"github.com/stretchr/testify/assert" |
||||
|
"github.com/stretchr/testify/require" |
||||
|
) |
||||
|
|
||||
|
// POSIXExtendedTestSuite provides additional POSIX compliance tests
|
||||
|
// covering extended attributes, file locking, and advanced features.
|
||||
|
//
|
||||
|
// NOTE: Some tests in this suite may be skipped or may have different behavior
|
||||
|
// depending on the FUSE implementation and platform support. This is expected
|
||||
|
// behavior for comprehensive POSIX compliance testing.
|
||||
|
//
|
||||
|
// See POSIX_IMPLEMENTATION_ROADMAP.md for a comprehensive plan to implement
|
||||
|
// these missing features.
|
||||
|
type POSIXExtendedTestSuite struct { |
||||
|
framework *FuseTestFramework |
||||
|
t *testing.T |
||||
|
} |
||||
|
|
||||
|
// ErrorCollector helps collect multiple test errors without stopping execution
|
||||
|
type ErrorCollector struct { |
||||
|
errors []string |
||||
|
t *testing.T |
||||
|
} |
||||
|
|
||||
|
// NewErrorCollector creates a new error collector
|
||||
|
func NewErrorCollector(t *testing.T) *ErrorCollector { |
||||
|
return &ErrorCollector{ |
||||
|
errors: make([]string, 0), |
||||
|
t: t, |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Add adds an error to the collection
|
||||
|
func (ec *ErrorCollector) Add(format string, args ...interface{}) { |
||||
|
ec.errors = append(ec.errors, fmt.Sprintf(format, args...)) |
||||
|
} |
||||
|
|
||||
|
// Check checks a condition and adds an error if it fails
|
||||
|
func (ec *ErrorCollector) Check(condition bool, format string, args ...interface{}) { |
||||
|
if !condition { |
||||
|
ec.Add(format, args...) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// CheckError checks if an error is nil and adds it if not
|
||||
|
func (ec *ErrorCollector) CheckError(err error, format string, args ...interface{}) { |
||||
|
if err != nil { |
||||
|
ec.Add(format+": %v", append(args, err)...) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// HasErrors returns true if any errors were collected
|
||||
|
func (ec *ErrorCollector) HasErrors() bool { |
||||
|
return len(ec.errors) > 0 |
||||
|
} |
||||
|
|
||||
|
// Report reports all collected errors to the test
|
||||
|
func (ec *ErrorCollector) Report() { |
||||
|
if len(ec.errors) > 0 { |
||||
|
ec.t.Errorf("Test completed with %d errors:\n%s", len(ec.errors), strings.Join(ec.errors, "\n")) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// NewPOSIXExtendedTestSuite creates a new extended POSIX compliance test suite
|
||||
|
func NewPOSIXExtendedTestSuite(t *testing.T, framework *FuseTestFramework) *POSIXExtendedTestSuite { |
||||
|
return &POSIXExtendedTestSuite{ |
||||
|
framework: framework, |
||||
|
t: t, |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// TestPOSIXExtended runs extended POSIX compliance tests
|
||||
|
func TestPOSIXExtended(t *testing.T) { |
||||
|
config := DefaultTestConfig() |
||||
|
config.EnableDebug = true |
||||
|
config.MountOptions = []string{"-allowOthers", "-nonempty"} |
||||
|
|
||||
|
framework := NewFuseTestFramework(t, config) |
||||
|
defer framework.Cleanup() |
||||
|
|
||||
|
// Setup framework with better error handling
|
||||
|
err := framework.Setup(config) |
||||
|
if err != nil { |
||||
|
t.Logf("Framework setup failed: %v", err) |
||||
|
t.Skip("Skipping extended tests due to framework setup failure") |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
// Verify framework is working
|
||||
|
mountPoint := framework.GetMountPoint() |
||||
|
if mountPoint == "" { |
||||
|
t.Log("Framework mount point is empty") |
||||
|
t.Skip("Skipping extended tests due to invalid mount point") |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
suite := NewPOSIXExtendedTestSuite(t, framework) |
||||
|
|
||||
|
// Run extended POSIX compliance test categories
|
||||
|
// Use a resilient approach that continues even if individual tests fail
|
||||
|
testCategories := []struct { |
||||
|
name string |
||||
|
fn func(*testing.T) |
||||
|
}{ |
||||
|
{"ExtendedAttributes", suite.TestExtendedAttributes}, |
||||
|
{"FileLocking", suite.TestFileLocking}, |
||||
|
{"AdvancedIO", suite.TestAdvancedIO}, |
||||
|
{"SparseFiles", suite.TestSparseFiles}, |
||||
|
{"LargeFiles", suite.TestLargeFiles}, |
||||
|
{"MMap", suite.TestMemoryMapping}, |
||||
|
{"DirectIO", suite.TestDirectIO}, |
||||
|
{"FileSealing", suite.TestFileSealing}, |
||||
|
{"Fallocate", suite.TestFallocate}, |
||||
|
{"Sendfile", suite.TestSendfile}, |
||||
|
} |
||||
|
|
||||
|
// Run all test categories and collect results
|
||||
|
var failedCategories []string |
||||
|
for _, category := range testCategories { |
||||
|
t.Run(category.name, func(subT *testing.T) { |
||||
|
// Capture panics and convert them to test failures
|
||||
|
defer func() { |
||||
|
if r := recover(); r != nil { |
||||
|
subT.Errorf("Test category %s panicked: %v", category.name, r) |
||||
|
failedCategories = append(failedCategories, category.name) |
||||
|
} |
||||
|
}() |
||||
|
|
||||
|
category.fn(subT) |
||||
|
if subT.Failed() { |
||||
|
failedCategories = append(failedCategories, category.name) |
||||
|
} |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
// Report overall results
|
||||
|
if len(failedCategories) > 0 { |
||||
|
t.Logf("Extended POSIX tests completed. Failed categories: %v", failedCategories) |
||||
|
} else { |
||||
|
t.Logf("All extended POSIX test categories passed!") |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// TestExtendedAttributes tests POSIX extended attribute support
|
||||
|
func (s *POSIXExtendedTestSuite) TestExtendedAttributes(t *testing.T) { |
||||
|
mountPoint := s.framework.GetMountPoint() |
||||
|
|
||||
|
t.Run("SetAndGetXattr", func(t *testing.T) { |
||||
|
if !isXattrSupported() { |
||||
|
t.Skip("Extended attributes not supported on this platform") |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
testFile := filepath.Join(mountPoint, "xattr_test.txt") |
||||
|
|
||||
|
// Create test file
|
||||
|
err := os.WriteFile(testFile, []byte("xattr test"), 0644) |
||||
|
if !assert.NoError(t, err, "Failed to create test file") { |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
// Set extended attribute
|
||||
|
attrName := "user.test_attr" |
||||
|
attrValue := []byte("test_value") |
||||
|
err = setXattr(testFile, attrName, attrValue, 0) |
||||
|
assert.NoError(t, err, "Failed to set extended attribute") |
||||
|
|
||||
|
// Verify attribute was set
|
||||
|
readValue := make([]byte, 256) |
||||
|
size, err := getXattr(testFile, attrName, readValue) |
||||
|
assert.NoError(t, err, "Failed to get extended attribute") |
||||
|
assert.Equal(t, len(attrValue), size, "Extended attribute size mismatch") |
||||
|
assert.Equal(t, attrValue, readValue[:size], "Extended attribute value mismatch") |
||||
|
}) |
||||
|
|
||||
|
t.Run("ListXattrs", func(t *testing.T) { |
||||
|
if !isXattrSupported() { |
||||
|
t.Skip("Extended attributes not supported on this platform") |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
testFile := filepath.Join(mountPoint, "xattr_list_test.txt") |
||||
|
|
||||
|
// Create test file
|
||||
|
err := os.WriteFile(testFile, []byte("list xattr test"), 0644) |
||||
|
if !assert.NoError(t, err, "Failed to create test file") { |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
// Set multiple extended attributes
|
||||
|
attrs := map[string][]byte{ |
||||
|
"user.attr1": []byte("value1"), |
||||
|
"user.attr2": []byte("value2"), |
||||
|
"user.attr3": []byte("value3"), |
||||
|
} |
||||
|
|
||||
|
for name, value := range attrs { |
||||
|
err = setXattr(testFile, name, value, 0) |
||||
|
assert.NoError(t, err, "Failed to set extended attribute %s", name) |
||||
|
} |
||||
|
|
||||
|
// List all attributes
|
||||
|
listBuf := make([]byte, 1024) |
||||
|
size, err := listXattr(testFile, listBuf) |
||||
|
assert.NoError(t, err, "Failed to list extended attributes") |
||||
|
assert.Greater(t, size, 0, "No extended attributes found") |
||||
|
|
||||
|
// Parse the null-separated list and verify attributes
|
||||
|
if size > 0 { |
||||
|
attrList := parseXattrList(listBuf[:size]) |
||||
|
expectedAttrs := []string{"user.attr1", "user.attr2", "user.attr3"} |
||||
|
for _, expectedAttr := range expectedAttrs { |
||||
|
assert.Contains(t, attrList, expectedAttr, "Expected attribute %s should be in the list", expectedAttr) |
||||
|
} |
||||
|
} |
||||
|
}) |
||||
|
|
||||
|
t.Run("RemoveXattr", func(t *testing.T) { |
||||
|
if !isXattrSupported() { |
||||
|
t.Skip("Extended attributes not supported on this platform") |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
testFile := filepath.Join(mountPoint, "xattr_remove_test.txt") |
||||
|
|
||||
|
// Create test file
|
||||
|
err := os.WriteFile(testFile, []byte("remove xattr test"), 0644) |
||||
|
require.NoError(t, err) |
||||
|
|
||||
|
// Set extended attribute
|
||||
|
attrName := "user.remove_test" |
||||
|
attrValue := []byte("to_be_removed") |
||||
|
err = setXattr(testFile, attrName, attrValue, 0) |
||||
|
require.NoError(t, err) |
||||
|
|
||||
|
// Verify attribute exists
|
||||
|
readValue := make([]byte, 256) |
||||
|
size, err := getXattr(testFile, attrName, readValue) |
||||
|
require.NoError(t, err) |
||||
|
require.Equal(t, len(attrValue), size) |
||||
|
|
||||
|
// Remove the attribute
|
||||
|
err = removeXattr(testFile, attrName) |
||||
|
require.NoError(t, err) |
||||
|
|
||||
|
// Verify attribute is gone
|
||||
|
_, err = getXattr(testFile, attrName, readValue) |
||||
|
require.Error(t, err) // Should fail with ENODATA or similar
|
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
// TestFileLocking tests POSIX file locking mechanisms
|
||||
|
// Note: File locking behavior may vary between FUSE implementations.
|
||||
|
// Some implementations may not enforce locks between file descriptors
|
||||
|
// from the same process, which is a known limitation.
|
||||
|
func (s *POSIXExtendedTestSuite) TestFileLocking(t *testing.T) { |
||||
|
mountPoint := s.framework.GetMountPoint() |
||||
|
|
||||
|
t.Run("AdvisoryLocking", func(t *testing.T) { |
||||
|
testFile := filepath.Join(mountPoint, "lock_test.txt") |
||||
|
|
||||
|
// Create test file
|
||||
|
err := os.WriteFile(testFile, []byte("locking test"), 0644) |
||||
|
if !assert.NoError(t, err, "Failed to create test file") { |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
// Open file
|
||||
|
file, err := os.OpenFile(testFile, os.O_RDWR, 0644) |
||||
|
if !assert.NoError(t, err, "Failed to open test file") { |
||||
|
return |
||||
|
} |
||||
|
defer file.Close() |
||||
|
|
||||
|
// Apply exclusive lock
|
||||
|
flock := syscall.Flock_t{ |
||||
|
Type: syscall.F_WRLCK, |
||||
|
Whence: 0, |
||||
|
Start: 0, |
||||
|
Len: 0, // Lock entire file
|
||||
|
} |
||||
|
|
||||
|
err = syscall.FcntlFlock(file.Fd(), syscall.F_SETLK, &flock) |
||||
|
assert.NoError(t, err, "Failed to acquire exclusive lock") |
||||
|
|
||||
|
// Try to lock from another file descriptor (should fail)
|
||||
|
file2, err := os.OpenFile(testFile, os.O_RDWR, 0644) |
||||
|
if !assert.NoError(t, err, "Failed to open second file descriptor") { |
||||
|
return |
||||
|
} |
||||
|
defer file2.Close() |
||||
|
|
||||
|
flock2 := syscall.Flock_t{ |
||||
|
Type: syscall.F_WRLCK, |
||||
|
Whence: 0, |
||||
|
Start: 0, |
||||
|
Len: 0, |
||||
|
} |
||||
|
|
||||
|
// Note: Some FUSE implementations may not enforce file locking between file descriptors
|
||||
|
// from the same process. This is a known limitation in some FUSE implementations.
|
||||
|
err = syscall.FcntlFlock(file2.Fd(), syscall.F_SETLK, &flock2) |
||||
|
if err == syscall.EAGAIN { |
||||
|
// Lock was properly enforced
|
||||
|
t.Log("File locking properly enforced between file descriptors") |
||||
|
} else if err == nil { |
||||
|
// Lock was not enforced (common in some FUSE implementations)
|
||||
|
t.Log("File locking not enforced between file descriptors from same process (FUSE limitation)") |
||||
|
} else { |
||||
|
// Some other error occurred
|
||||
|
t.Logf("File locking attempt resulted in error: %v", err) |
||||
|
} |
||||
|
|
||||
|
// Log the actual error for debugging
|
||||
|
t.Logf("File locking test completed with result: %v", err) |
||||
|
|
||||
|
// Release lock
|
||||
|
flock.Type = syscall.F_UNLCK |
||||
|
err = syscall.FcntlFlock(file.Fd(), syscall.F_SETLK, &flock) |
||||
|
assert.NoError(t, err, "Failed to release lock") |
||||
|
|
||||
|
// Now second lock should succeed
|
||||
|
err = syscall.FcntlFlock(file2.Fd(), syscall.F_SETLK, &flock2) |
||||
|
assert.NoError(t, err, "Failed to acquire lock after release") |
||||
|
}) |
||||
|
|
||||
|
t.Run("SharedLocking", func(t *testing.T) { |
||||
|
testFile := filepath.Join(mountPoint, "shared_lock_test.txt") |
||||
|
|
||||
|
// Create test file
|
||||
|
err := os.WriteFile(testFile, []byte("shared locking test"), 0644) |
||||
|
if !assert.NoError(t, err, "Failed to create test file") { |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
// Open file for reading
|
||||
|
file1, err := os.Open(testFile) |
||||
|
if !assert.NoError(t, err, "Failed to open first file descriptor") { |
||||
|
return |
||||
|
} |
||||
|
defer file1.Close() |
||||
|
|
||||
|
file2, err := os.Open(testFile) |
||||
|
if !assert.NoError(t, err, "Failed to open second file descriptor") { |
||||
|
return |
||||
|
} |
||||
|
defer file2.Close() |
||||
|
|
||||
|
// Apply shared locks (should both succeed)
|
||||
|
flock1 := syscall.Flock_t{ |
||||
|
Type: syscall.F_RDLCK, |
||||
|
Whence: 0, |
||||
|
Start: 0, |
||||
|
Len: 0, |
||||
|
} |
||||
|
|
||||
|
flock2 := syscall.Flock_t{ |
||||
|
Type: syscall.F_RDLCK, |
||||
|
Whence: 0, |
||||
|
Start: 0, |
||||
|
Len: 0, |
||||
|
} |
||||
|
|
||||
|
err = syscall.FcntlFlock(file1.Fd(), syscall.F_SETLK, &flock1) |
||||
|
assert.NoError(t, err, "Failed to acquire first shared lock") |
||||
|
|
||||
|
err = syscall.FcntlFlock(file2.Fd(), syscall.F_SETLK, &flock2) |
||||
|
assert.NoError(t, err, "Failed to acquire second shared lock") |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
// TestAdvancedIO tests advanced I/O operations
|
||||
|
// Note: Vectored I/O support may vary between FUSE implementations.
|
||||
|
// Some implementations may not properly support readv/writev operations.
|
||||
|
func (s *POSIXExtendedTestSuite) TestAdvancedIO(t *testing.T) { |
||||
|
mountPoint := s.framework.GetMountPoint() |
||||
|
|
||||
|
t.Run("ReadWriteV", func(t *testing.T) { |
||||
|
if !isVectoredIOSupported() { |
||||
|
t.Skip("Vectored I/O not supported on this platform") |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
testFile := filepath.Join(mountPoint, "readwritev_test.txt") |
||||
|
|
||||
|
// Create file
|
||||
|
fd, err := syscall.Open(testFile, syscall.O_CREAT|syscall.O_RDWR, 0644) |
||||
|
if !assert.NoError(t, err, "Failed to create test file") { |
||||
|
return |
||||
|
} |
||||
|
defer syscall.Close(fd) |
||||
|
|
||||
|
// Prepare test data in multiple buffers
|
||||
|
writeBuffers := [][]byte{ |
||||
|
[]byte("Hello "), |
||||
|
[]byte("vectored "), |
||||
|
[]byte("I/O "), |
||||
|
[]byte("world!"), |
||||
|
} |
||||
|
|
||||
|
// Write using writev
|
||||
|
writeIOVs := makeIOVecs(writeBuffers) |
||||
|
t.Logf("Attempting vectored I/O write with %d buffers", len(writeIOVs)) |
||||
|
totalWritten, err := writevFile(fd, writeIOVs) |
||||
|
if err != nil { |
||||
|
// Some FUSE implementations may not support vectored I/O properly
|
||||
|
t.Logf("Vectored I/O write failed: %v - this may be a FUSE limitation", err) |
||||
|
t.Skip("Vectored I/O not properly supported by this FUSE implementation") |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
expectedTotal := 0 |
||||
|
for _, buf := range writeBuffers { |
||||
|
expectedTotal += len(buf) |
||||
|
} |
||||
|
assert.Equal(t, expectedTotal, totalWritten, "Vectored write size mismatch") |
||||
|
|
||||
|
// Seek back to beginning
|
||||
|
_, err = syscall.Seek(fd, 0, 0) |
||||
|
if !assert.NoError(t, err, "Failed to seek to beginning") { |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
// Read using readv into multiple buffers
|
||||
|
readBuffers := [][]byte{ |
||||
|
make([]byte, 6), // "Hello "
|
||||
|
make([]byte, 9), // "vectored "
|
||||
|
make([]byte, 3), // "I/O "
|
||||
|
make([]byte, 6), // "world!"
|
||||
|
} |
||||
|
|
||||
|
readIOVs := makeIOVecs(readBuffers) |
||||
|
t.Logf("Attempting vectored I/O read with %d buffers", len(readIOVs)) |
||||
|
totalRead, err := readvFile(fd, readIOVs) |
||||
|
if err != nil { |
||||
|
t.Logf("Vectored I/O read failed: %v - this may be a FUSE limitation", err) |
||||
|
t.Skip("Vectored I/O not properly supported by this FUSE implementation") |
||||
|
return |
||||
|
} |
||||
|
assert.Equal(t, expectedTotal, totalRead, "Vectored read size mismatch") |
||||
|
|
||||
|
// Verify data matches
|
||||
|
for i, expected := range writeBuffers { |
||||
|
assert.Equal(t, expected, readBuffers[i], "Vectored I/O data mismatch at buffer %d", i) |
||||
|
} |
||||
|
}) |
||||
|
|
||||
|
} |
||||
|
|
||||
|
// TestSparseFiles tests sparse file handling
|
||||
|
func (s *POSIXExtendedTestSuite) TestSparseFiles(t *testing.T) { |
||||
|
mountPoint := s.framework.GetMountPoint() |
||||
|
|
||||
|
t.Run("CreateSparseFile", func(t *testing.T) { |
||||
|
testFile := filepath.Join(mountPoint, "sparse_test.txt") |
||||
|
|
||||
|
// Open file
|
||||
|
fd, err := syscall.Open(testFile, syscall.O_CREAT|syscall.O_RDWR, 0644) |
||||
|
require.NoError(t, err) |
||||
|
defer syscall.Close(fd) |
||||
|
|
||||
|
// Create a sparse file by seeking beyond end and writing
|
||||
|
const sparseSize = 1024 * 1024 // 1MB
|
||||
|
_, err = syscall.Seek(fd, sparseSize, 0) |
||||
|
require.NoError(t, err) |
||||
|
|
||||
|
// Write at the end
|
||||
|
endData := []byte("end") |
||||
|
n, err := syscall.Write(fd, endData) |
||||
|
require.NoError(t, err) |
||||
|
require.Equal(t, len(endData), n) |
||||
|
|
||||
|
// Verify file size
|
||||
|
stat, err := os.Stat(testFile) |
||||
|
require.NoError(t, err) |
||||
|
require.Equal(t, int64(sparseSize+len(endData)), stat.Size()) |
||||
|
|
||||
|
// Read from the beginning (should be zeros)
|
||||
|
_, err = syscall.Seek(fd, 0, 0) |
||||
|
require.NoError(t, err) |
||||
|
|
||||
|
buffer := make([]byte, 100) |
||||
|
n, err = syscall.Read(fd, buffer) |
||||
|
require.NoError(t, err) |
||||
|
require.Equal(t, 100, n) |
||||
|
|
||||
|
// Should be all zeros
|
||||
|
for _, b := range buffer { |
||||
|
require.Equal(t, byte(0), b) |
||||
|
} |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
// TestLargeFiles tests large file handling (>2GB)
|
||||
|
func (s *POSIXExtendedTestSuite) TestLargeFiles(t *testing.T) { |
||||
|
mountPoint := s.framework.GetMountPoint() |
||||
|
|
||||
|
t.Run("LargeFileOperations", func(t *testing.T) { |
||||
|
testFile := filepath.Join(mountPoint, "large_file_test.txt") |
||||
|
|
||||
|
// Open file
|
||||
|
fd, err := syscall.Open(testFile, syscall.O_CREAT|syscall.O_RDWR, 0644) |
||||
|
require.NoError(t, err) |
||||
|
defer syscall.Close(fd) |
||||
|
|
||||
|
// Seek to position > 2GB
|
||||
|
const largeOffset = 3 * 1024 * 1024 * 1024 // 3GB
|
||||
|
pos, err := syscall.Seek(fd, largeOffset, 0) |
||||
|
require.NoError(t, err) |
||||
|
require.Equal(t, int64(largeOffset), pos) |
||||
|
|
||||
|
// Write at large offset
|
||||
|
testData := []byte("large file data") |
||||
|
n, err := syscall.Write(fd, testData) |
||||
|
require.NoError(t, err) |
||||
|
require.Equal(t, len(testData), n) |
||||
|
|
||||
|
// Read back from large offset
|
||||
|
_, err = syscall.Seek(fd, largeOffset, 0) |
||||
|
require.NoError(t, err) |
||||
|
|
||||
|
readBuffer := make([]byte, len(testData)) |
||||
|
n, err = syscall.Read(fd, readBuffer) |
||||
|
require.NoError(t, err) |
||||
|
require.Equal(t, len(testData), n) |
||||
|
require.Equal(t, testData, readBuffer) |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
// TestMemoryMapping tests memory mapping functionality
|
||||
|
func (s *POSIXExtendedTestSuite) TestMemoryMapping(t *testing.T) { |
||||
|
mountPoint := s.framework.GetMountPoint() |
||||
|
|
||||
|
t.Run("MmapFile", func(t *testing.T) { |
||||
|
if !isMmapSupported() { |
||||
|
t.Skip("Memory mapping not supported on this platform") |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
testFile := filepath.Join(mountPoint, "mmap_test.txt") |
||||
|
testData := make([]byte, 4096) |
||||
|
for i := range testData { |
||||
|
testData[i] = byte(i % 256) |
||||
|
} |
||||
|
|
||||
|
// Create test file
|
||||
|
err := os.WriteFile(testFile, testData, 0644) |
||||
|
require.NoError(t, err) |
||||
|
|
||||
|
// Open file for reading
|
||||
|
fd, err := syscall.Open(testFile, syscall.O_RDONLY, 0) |
||||
|
require.NoError(t, err) |
||||
|
defer syscall.Close(fd) |
||||
|
|
||||
|
// Memory map the file
|
||||
|
mappedData, err := mmapFile(fd, 0, len(testData), PROT_READ, MAP_SHARED) |
||||
|
require.NoError(t, err) |
||||
|
defer munmapFile(mappedData) |
||||
|
|
||||
|
// Verify mapped content matches original
|
||||
|
require.Equal(t, testData, mappedData) |
||||
|
}) |
||||
|
|
||||
|
t.Run("MmapWrite", func(t *testing.T) { |
||||
|
if !isMmapSupported() { |
||||
|
t.Skip("Memory mapping not supported on this platform") |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
testFile := filepath.Join(mountPoint, "mmap_write_test.txt") |
||||
|
size := 4096 |
||||
|
|
||||
|
// Create empty file of specific size
|
||||
|
fd, err := syscall.Open(testFile, syscall.O_CREAT|syscall.O_RDWR, 0644) |
||||
|
require.NoError(t, err) |
||||
|
defer syscall.Close(fd) |
||||
|
|
||||
|
err = syscall.Ftruncate(fd, int64(size)) |
||||
|
require.NoError(t, err) |
||||
|
|
||||
|
// Memory map the file for writing
|
||||
|
mappedData, err := mmapFile(fd, 0, size, PROT_READ|PROT_WRITE, MAP_SHARED) |
||||
|
require.NoError(t, err) |
||||
|
defer munmapFile(mappedData) |
||||
|
|
||||
|
// Write test pattern to mapped memory
|
||||
|
testPattern := []byte("Hello, mmap world!") |
||||
|
copy(mappedData, testPattern) |
||||
|
|
||||
|
// Sync changes to disk
|
||||
|
err = msyncFile(mappedData, MS_SYNC) |
||||
|
require.NoError(t, err) |
||||
|
|
||||
|
// Verify changes were written by reading file directly
|
||||
|
readData, err := os.ReadFile(testFile) |
||||
|
require.NoError(t, err) |
||||
|
require.True(t, len(readData) >= len(testPattern)) |
||||
|
require.Equal(t, testPattern, readData[:len(testPattern)]) |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
// TestDirectIO tests direct I/O operations
|
||||
|
func (s *POSIXExtendedTestSuite) TestDirectIO(t *testing.T) { |
||||
|
mountPoint := s.framework.GetMountPoint() |
||||
|
|
||||
|
t.Run("DirectIO", func(t *testing.T) { |
||||
|
if !isDirectIOSupported() { |
||||
|
t.Skip("Direct I/O not supported on this platform") |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
testFile := filepath.Join(mountPoint, "directio_test.txt") |
||||
|
|
||||
|
// Open file with direct I/O
|
||||
|
fd, err := openDirectIO(testFile, syscall.O_CREAT|syscall.O_RDWR, 0644) |
||||
|
require.NoError(t, err) |
||||
|
defer syscall.Close(fd) |
||||
|
|
||||
|
// For direct I/O, data must be aligned to sector boundaries
|
||||
|
// Use 4KB aligned buffer (common sector size)
|
||||
|
blockSize := 4096 |
||||
|
testData := make([]byte, blockSize) |
||||
|
for i := range testData { |
||||
|
testData[i] = byte(i % 256) |
||||
|
} |
||||
|
|
||||
|
// Write data using direct I/O
|
||||
|
n, err := syscall.Write(fd, testData) |
||||
|
require.NoError(t, err) |
||||
|
require.Equal(t, len(testData), n) |
||||
|
|
||||
|
// Seek back to beginning
|
||||
|
_, err = syscall.Seek(fd, 0, 0) |
||||
|
require.NoError(t, err) |
||||
|
|
||||
|
// Read data using direct I/O
|
||||
|
readData := make([]byte, blockSize) |
||||
|
n, err = syscall.Read(fd, readData) |
||||
|
require.NoError(t, err) |
||||
|
require.Equal(t, len(testData), n) |
||||
|
require.Equal(t, testData, readData) |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
// TestFileSealing tests file sealing mechanisms (Linux-specific)
|
||||
|
func (s *POSIXExtendedTestSuite) TestFileSealing(t *testing.T) { |
||||
|
// This test is Linux-specific and may not be applicable to all systems
|
||||
|
t.Skip("File sealing tests require Linux-specific features") |
||||
|
} |
||||
|
|
||||
|
// TestFallocate tests file preallocation
|
||||
|
func (s *POSIXExtendedTestSuite) TestFallocate(t *testing.T) { |
||||
|
mountPoint := s.framework.GetMountPoint() |
||||
|
|
||||
|
t.Run("FallocateSpace", func(t *testing.T) { |
||||
|
if !isFallocateSupported() { |
||||
|
t.Skip("fallocate not supported on this platform") |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
testFile := filepath.Join(mountPoint, "fallocate_test.txt") |
||||
|
|
||||
|
// Open file
|
||||
|
fd, err := syscall.Open(testFile, syscall.O_CREAT|syscall.O_RDWR, 0644) |
||||
|
require.NoError(t, err) |
||||
|
defer syscall.Close(fd) |
||||
|
|
||||
|
// Preallocate 1MB of space
|
||||
|
allocSize := int64(1024 * 1024) |
||||
|
err = fallocateFile(fd, 0, 0, allocSize) |
||||
|
require.NoError(t, err) |
||||
|
|
||||
|
// Verify file size was extended
|
||||
|
var stat syscall.Stat_t |
||||
|
err = syscall.Fstat(fd, &stat) |
||||
|
require.NoError(t, err) |
||||
|
require.GreaterOrEqual(t, stat.Size, allocSize) |
||||
|
|
||||
|
// Write some data and verify it works
|
||||
|
testData := []byte("fallocate test data") |
||||
|
n, err := syscall.Write(fd, testData) |
||||
|
require.NoError(t, err) |
||||
|
require.Equal(t, len(testData), n) |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
// TestSendfile tests zero-copy file transfer
|
||||
|
func (s *POSIXExtendedTestSuite) TestSendfile(t *testing.T) { |
||||
|
mountPoint := s.framework.GetMountPoint() |
||||
|
|
||||
|
t.Run("SendfileCopy", func(t *testing.T) { |
||||
|
sourceFile := filepath.Join(mountPoint, "sendfile_source.txt") |
||||
|
targetFile := filepath.Join(mountPoint, "sendfile_target.txt") |
||||
|
|
||||
|
testData := make([]byte, 8192) |
||||
|
for i := range testData { |
||||
|
testData[i] = byte(i % 256) |
||||
|
} |
||||
|
|
||||
|
// Create source file
|
||||
|
err := os.WriteFile(sourceFile, testData, 0644) |
||||
|
require.NoError(t, err) |
||||
|
|
||||
|
// Open source for reading
|
||||
|
srcFd, err := syscall.Open(sourceFile, syscall.O_RDONLY, 0) |
||||
|
require.NoError(t, err) |
||||
|
defer syscall.Close(srcFd) |
||||
|
|
||||
|
// Create target file
|
||||
|
dstFd, err := syscall.Open(targetFile, syscall.O_CREAT|syscall.O_WRONLY, 0644) |
||||
|
require.NoError(t, err) |
||||
|
defer syscall.Close(dstFd) |
||||
|
|
||||
|
// Sendfile test
|
||||
|
if !isSendfileSupported() { |
||||
|
t.Skip("sendfile not supported on this platform") |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
// Use sendfile to copy data
|
||||
|
var offset int64 = 0 |
||||
|
transferred, err := sendfileTransfer(dstFd, srcFd, &offset, len(testData)) |
||||
|
require.NoError(t, err) |
||||
|
require.Equal(t, len(testData), transferred) |
||||
|
|
||||
|
// Verify copy
|
||||
|
copiedData, err := os.ReadFile(targetFile) |
||||
|
require.NoError(t, err) |
||||
|
require.Equal(t, testData, copiedData) |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
// Helper function to parse null-separated xattr list
|
||||
|
func parseXattrList(data []byte) []string { |
||||
|
var attrs []string |
||||
|
start := 0 |
||||
|
for i, b := range data { |
||||
|
if b == 0 { |
||||
|
if i > start { |
||||
|
attrs = append(attrs, string(data[start:i])) |
||||
|
} |
||||
|
start = i + 1 |
||||
|
} |
||||
|
} |
||||
|
return attrs |
||||
|
} |
||||
@ -0,0 +1,562 @@ |
|||||
|
package fuse |
||||
|
|
||||
|
import ( |
||||
|
"fmt" |
||||
|
"os" |
||||
|
"os/exec" |
||||
|
"path/filepath" |
||||
|
"strings" |
||||
|
"syscall" |
||||
|
"testing" |
||||
|
"time" |
||||
|
|
||||
|
"github.com/stretchr/testify/assert" |
||||
|
"github.com/stretchr/testify/require" |
||||
|
) |
||||
|
|
||||
|
// ExternalPOSIXTestSuite manages integration with external POSIX test suites
|
||||
|
type ExternalPOSIXTestSuite struct { |
||||
|
framework *FuseTestFramework |
||||
|
t *testing.T |
||||
|
workDir string |
||||
|
} |
||||
|
|
||||
|
// NewExternalPOSIXTestSuite creates a new external POSIX test suite runner
|
||||
|
func NewExternalPOSIXTestSuite(t *testing.T, framework *FuseTestFramework) *ExternalPOSIXTestSuite { |
||||
|
workDir := filepath.Join(os.TempDir(), fmt.Sprintf("posix_external_tests_%d", time.Now().Unix())) |
||||
|
os.MkdirAll(workDir, 0755) |
||||
|
|
||||
|
return &ExternalPOSIXTestSuite{ |
||||
|
framework: framework, |
||||
|
t: t, |
||||
|
workDir: workDir, |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Cleanup removes temporary test directories
|
||||
|
func (s *ExternalPOSIXTestSuite) Cleanup() { |
||||
|
os.RemoveAll(s.workDir) |
||||
|
} |
||||
|
|
||||
|
// TestExternalPOSIXSuites runs integration tests with external POSIX test suites
|
||||
|
func TestExternalPOSIXSuites(t *testing.T) { |
||||
|
config := DefaultTestConfig() |
||||
|
config.EnableDebug = true |
||||
|
config.MountOptions = []string{"-allowOthers", "-nonempty"} |
||||
|
|
||||
|
framework := NewFuseTestFramework(t, config) |
||||
|
defer framework.Cleanup() |
||||
|
require.NoError(t, framework.Setup(config)) |
||||
|
|
||||
|
suite := NewExternalPOSIXTestSuite(t, framework) |
||||
|
defer suite.Cleanup() |
||||
|
|
||||
|
// Run various external POSIX test suites
|
||||
|
t.Run("PjdFsTest", suite.TestPjdFsTest) |
||||
|
t.Run("NFSTestPOSIX", suite.TestNFSTestPOSIX) |
||||
|
t.Run("LitmusTests", suite.TestLitmusTests) |
||||
|
t.Run("CustomPOSIXTests", suite.TestCustomPOSIXTests) |
||||
|
} |
||||
|
|
||||
|
// TestPjdFsTest runs the comprehensive pjdfstest POSIX filesystem test suite
|
||||
|
func (s *ExternalPOSIXTestSuite) TestPjdFsTest(t *testing.T) { |
||||
|
mountPoint := s.framework.GetMountPoint() |
||||
|
|
||||
|
// Check if pjdfstest is available
|
||||
|
_, err := exec.LookPath("pjdfstest") |
||||
|
if err != nil { |
||||
|
// For comprehensive POSIX compliance testing, external tools should be mandatory
|
||||
|
if os.Getenv("POSIX_REQUIRE_EXTERNAL_TOOLS") == "true" { |
||||
|
t.Fatalf("pjdfstest is required for comprehensive POSIX compliance testing but not found. Install from: https://github.com/pjd/pjdfstest") |
||||
|
} |
||||
|
t.Skip("pjdfstest not found. Install from: https://github.com/pjd/pjdfstest") |
||||
|
} |
||||
|
|
||||
|
// Create test directory within mount point
|
||||
|
testDir := filepath.Join(mountPoint, "pjdfstest") |
||||
|
err = os.MkdirAll(testDir, 0755) |
||||
|
require.NoError(t, err) |
||||
|
|
||||
|
// List of critical POSIX operations to test
|
||||
|
pjdTests := []struct { |
||||
|
name string |
||||
|
testPath string |
||||
|
critical bool |
||||
|
}{ |
||||
|
{"chflags", "tests/chflags", false}, |
||||
|
{"chmod", "tests/chmod", true}, |
||||
|
{"chown", "tests/chown", true}, |
||||
|
{"create", "tests/create", true}, |
||||
|
{"link", "tests/link", true}, |
||||
|
{"mkdir", "tests/mkdir", true}, |
||||
|
{"mkfifo", "tests/mkfifo", false}, |
||||
|
{"mknod", "tests/mknod", false}, |
||||
|
{"open", "tests/open", true}, |
||||
|
{"rename", "tests/rename", true}, |
||||
|
{"rmdir", "tests/rmdir", true}, |
||||
|
{"symlink", "tests/symlink", true}, |
||||
|
{"truncate", "tests/truncate", true}, |
||||
|
{"unlink", "tests/unlink", true}, |
||||
|
} |
||||
|
|
||||
|
// Download and setup pjdfstest if needed
|
||||
|
pjdDir := filepath.Join(s.workDir, "pjdfstest") |
||||
|
if _, err := os.Stat(pjdDir); os.IsNotExist(err) { |
||||
|
t.Logf("Setting up pjdfstest...") |
||||
|
err = s.setupPjdFsTest(pjdDir) |
||||
|
if err != nil { |
||||
|
t.Skipf("Failed to setup pjdfstest: %v", err) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Run each test category
|
||||
|
for _, test := range pjdTests { |
||||
|
t.Run(test.name, func(t *testing.T) { |
||||
|
s.runPjdTest(t, pjdDir, test.testPath, testDir, test.critical) |
||||
|
}) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// TestNFSTestPOSIX runs nfstest_posix for POSIX API verification
|
||||
|
func (s *ExternalPOSIXTestSuite) TestNFSTestPOSIX(t *testing.T) { |
||||
|
mountPoint := s.framework.GetMountPoint() |
||||
|
|
||||
|
// Check if nfstest_posix is available
|
||||
|
_, err := exec.LookPath("nfstest_posix") |
||||
|
if err != nil { |
||||
|
// For comprehensive POSIX compliance testing, external tools should be mandatory
|
||||
|
if os.Getenv("POSIX_REQUIRE_EXTERNAL_TOOLS") == "true" { |
||||
|
t.Fatalf("nfstest_posix is required for comprehensive POSIX compliance testing but not found. Install via: pip install nfstest") |
||||
|
} |
||||
|
t.Skip("nfstest_posix not found. Install via: pip install nfstest") |
||||
|
} |
||||
|
|
||||
|
testDir := filepath.Join(mountPoint, "nfstest") |
||||
|
err = os.MkdirAll(testDir, 0755) |
||||
|
require.NoError(t, err) |
||||
|
|
||||
|
// Run nfstest_posix with comprehensive API testing
|
||||
|
cmd := exec.Command("nfstest_posix", |
||||
|
"--path", testDir, |
||||
|
"--verbose", |
||||
|
"--createlog", |
||||
|
"--runid", fmt.Sprintf("seaweedfs_%d", time.Now().Unix()), |
||||
|
) |
||||
|
|
||||
|
output, err := cmd.CombinedOutput() |
||||
|
t.Logf("nfstest_posix output:\n%s", string(output)) |
||||
|
|
||||
|
if err != nil { |
||||
|
t.Logf("nfstest_posix failed: %v", err) |
||||
|
// Don't fail the test completely, just log the failure
|
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// TestLitmusTests runs focused POSIX compliance litmus tests
|
||||
|
func (s *ExternalPOSIXTestSuite) TestLitmusTests(t *testing.T) { |
||||
|
mountPoint := s.framework.GetMountPoint() |
||||
|
|
||||
|
// Create litmus test scripts for critical POSIX behaviors
|
||||
|
litmusTests := []struct { |
||||
|
name string |
||||
|
script string |
||||
|
}{ |
||||
|
{ |
||||
|
name: "AtomicRename", |
||||
|
script: `#!/bin/bash |
||||
|
set -e |
||||
|
cd "$1" |
||||
|
echo "test data" > temp_file |
||||
|
echo "original" > target_file |
||||
|
mv temp_file target_file |
||||
|
[ "$(cat target_file)" = "test data" ] |
||||
|
echo "PASS: Atomic rename works" |
||||
|
`, |
||||
|
}, |
||||
|
{ |
||||
|
name: "LinkCount", |
||||
|
script: `#!/bin/bash |
||||
|
set -e |
||||
|
cd "$1" |
||||
|
echo "test" > original |
||||
|
ln original hardlink |
||||
|
if [[ "$(uname)" == "Darwin" ]]; then |
||||
|
[ $(stat -f %l original) -eq 2 ] |
||||
|
else |
||||
|
[ $(stat -c %h original) -eq 2 ] |
||||
|
fi |
||||
|
rm hardlink |
||||
|
if [[ "$(uname)" == "Darwin" ]]; then |
||||
|
[ $(stat -f %l original) -eq 1 ] |
||||
|
else |
||||
|
[ $(stat -c %h original) -eq 1 ] |
||||
|
fi |
||||
|
echo "PASS: Hard link counting works" |
||||
|
`, |
||||
|
}, |
||||
|
{ |
||||
|
name: "SymlinkCycles", |
||||
|
script: `#!/bin/bash |
||||
|
set -e |
||||
|
cd "$1" |
||||
|
ln -s link1 link2 |
||||
|
ln -s link2 link1 |
||||
|
# Try to access the file, which should fail due to too many symbolic links |
||||
|
if cat link1 2>/dev/null; then |
||||
|
echo "FAIL: Symlink cycle not detected" |
||||
|
exit 1 |
||||
|
fi |
||||
|
echo "PASS: Symlink cycle handling works" |
||||
|
`, |
||||
|
}, |
||||
|
{ |
||||
|
name: "ConcurrentCreate", |
||||
|
script: `#!/bin/bash |
||||
|
set -e |
||||
|
cd "$1" |
||||
|
for i in {1..10}; do |
||||
|
(echo "process $i" > "file_$i") & |
||||
|
done |
||||
|
wait |
||||
|
[ $(ls file_* | wc -l) -eq 10 ] |
||||
|
echo "PASS: Concurrent file creation works" |
||||
|
`, |
||||
|
}, |
||||
|
{ |
||||
|
name: "DirectoryConsistency", |
||||
|
script: `#!/bin/bash |
||||
|
set -e |
||||
|
cd "$1" |
||||
|
mkdir testdir |
||||
|
cd testdir |
||||
|
touch file1 file2 file3 |
||||
|
cd .. |
||||
|
entries=$(ls testdir | wc -l) |
||||
|
[ $entries -eq 3 ] |
||||
|
rmdir testdir 2>/dev/null && echo "FAIL: Non-empty directory removed" && exit 1 |
||||
|
rm testdir/* |
||||
|
rmdir testdir |
||||
|
echo "PASS: Directory consistency works" |
||||
|
`, |
||||
|
}, |
||||
|
} |
||||
|
|
||||
|
testDir := filepath.Join(mountPoint, "litmus") |
||||
|
err := os.MkdirAll(testDir, 0755) |
||||
|
require.NoError(t, err) |
||||
|
|
||||
|
// Run each litmus test
|
||||
|
for _, test := range litmusTests { |
||||
|
t.Run(test.name, func(t *testing.T) { |
||||
|
s.runLitmusTest(t, test.name, test.script, testDir) |
||||
|
}) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// TestCustomPOSIXTests runs custom POSIX compliance tests
|
||||
|
func (s *ExternalPOSIXTestSuite) TestCustomPOSIXTests(t *testing.T) { |
||||
|
mountPoint := s.framework.GetMountPoint() |
||||
|
|
||||
|
// Custom stress tests for POSIX compliance
|
||||
|
t.Run("StressRename", func(t *testing.T) { |
||||
|
s.stressTestRename(t, mountPoint) |
||||
|
}) |
||||
|
|
||||
|
t.Run("StressCreate", func(t *testing.T) { |
||||
|
s.stressTestCreate(t, mountPoint) |
||||
|
}) |
||||
|
|
||||
|
t.Run("StressDirectory", func(t *testing.T) { |
||||
|
s.stressTestDirectory(t, mountPoint) |
||||
|
}) |
||||
|
|
||||
|
t.Run("EdgeCases", func(t *testing.T) { |
||||
|
s.testEdgeCases(t, mountPoint) |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
// setupPjdFsTest downloads and sets up the pjdfstest suite
|
||||
|
func (s *ExternalPOSIXTestSuite) setupPjdFsTest(targetDir string) error { |
||||
|
// Clone pjdfstest repository
|
||||
|
cmd := exec.Command("git", "clone", "https://github.com/pjd/pjdfstest.git", targetDir) |
||||
|
if err := cmd.Run(); err != nil { |
||||
|
return fmt.Errorf("failed to clone pjdfstest: %w", err) |
||||
|
} |
||||
|
|
||||
|
// Build pjdfstest
|
||||
|
cmd = exec.Command("make", "-C", targetDir) |
||||
|
if err := cmd.Run(); err != nil { |
||||
|
return fmt.Errorf("failed to build pjdfstest: %w", err) |
||||
|
} |
||||
|
|
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
// runPjdTest executes a specific pjdfstest
|
||||
|
func (s *ExternalPOSIXTestSuite) runPjdTest(t *testing.T, pjdDir, testPath, mountDir string, critical bool) { |
||||
|
fullTestPath := filepath.Join(pjdDir, testPath) |
||||
|
|
||||
|
// Check if test path exists
|
||||
|
if _, err := os.Stat(fullTestPath); os.IsNotExist(err) { |
||||
|
t.Skipf("Test path does not exist: %s", fullTestPath) |
||||
|
} |
||||
|
|
||||
|
cmd := exec.Command("prove", "-r", fullTestPath) |
||||
|
cmd.Dir = mountDir |
||||
|
cmd.Env = append(os.Environ(), "FSTEST="+filepath.Join(pjdDir, "pjdfstest")) |
||||
|
|
||||
|
output, err := cmd.CombinedOutput() |
||||
|
t.Logf("pjdfstest %s output:\n%s", testPath, string(output)) |
||||
|
|
||||
|
if err != nil { |
||||
|
if critical { |
||||
|
t.Errorf("Critical pjdfstest failed: %s - %v", testPath, err) |
||||
|
} else { |
||||
|
t.Logf("Non-critical pjdfstest failed: %s - %v", testPath, err) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// runLitmusTest executes a litmus test script
|
||||
|
func (s *ExternalPOSIXTestSuite) runLitmusTest(t *testing.T, name, script, testDir string) { |
||||
|
scriptPath := filepath.Join(s.workDir, name+".sh") |
||||
|
|
||||
|
// Write script to file
|
||||
|
err := os.WriteFile(scriptPath, []byte(script), 0755) |
||||
|
require.NoError(t, err) |
||||
|
|
||||
|
// Create isolated test directory
|
||||
|
isolatedDir := filepath.Join(testDir, name) |
||||
|
err = os.MkdirAll(isolatedDir, 0755) |
||||
|
require.NoError(t, err) |
||||
|
defer os.RemoveAll(isolatedDir) |
||||
|
|
||||
|
// Execute script
|
||||
|
cmd := exec.Command("bash", scriptPath, isolatedDir) |
||||
|
output, err := cmd.CombinedOutput() |
||||
|
|
||||
|
t.Logf("Litmus test %s output:\n%s", name, string(output)) |
||||
|
|
||||
|
if err != nil { |
||||
|
t.Errorf("Litmus test %s failed: %v", name, err) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// stressTestRename tests rename operations under stress
|
||||
|
func (s *ExternalPOSIXTestSuite) stressTestRename(t *testing.T, mountPoint string) { |
||||
|
testDir := filepath.Join(mountPoint, "stress_rename") |
||||
|
err := os.MkdirAll(testDir, 0755) |
||||
|
require.NoError(t, err) |
||||
|
defer os.RemoveAll(testDir) |
||||
|
|
||||
|
// Create files and rename them concurrently
|
||||
|
const numFiles = 100 |
||||
|
const numWorkers = 10 |
||||
|
|
||||
|
// Create initial files
|
||||
|
for i := 0; i < numFiles; i++ { |
||||
|
fileName := filepath.Join(testDir, fmt.Sprintf("file_%d.txt", i)) |
||||
|
err := os.WriteFile(fileName, []byte(fmt.Sprintf("content_%d", i)), 0644) |
||||
|
if !assert.NoError(t, err, "Failed to create initial file %d", i) { |
||||
|
return // Skip test if setup fails
|
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Concurrent rename operations
|
||||
|
results := make(chan error, numWorkers) |
||||
|
for w := 0; w < numWorkers; w++ { |
||||
|
go func(worker int) { |
||||
|
for i := worker; i < numFiles; i += numWorkers { |
||||
|
oldName := filepath.Join(testDir, fmt.Sprintf("file_%d.txt", i)) |
||||
|
newName := filepath.Join(testDir, fmt.Sprintf("renamed_%d.txt", i)) |
||||
|
err := os.Rename(oldName, newName) |
||||
|
if err != nil { |
||||
|
results <- err |
||||
|
return |
||||
|
} |
||||
|
} |
||||
|
results <- nil |
||||
|
}(w) |
||||
|
} |
||||
|
|
||||
|
// Wait for all workers and collect errors
|
||||
|
var errorCount int |
||||
|
for w := 0; w < numWorkers; w++ { |
||||
|
err := <-results |
||||
|
if err != nil { |
||||
|
assert.NoError(t, err, "Worker %d failed", w) |
||||
|
errorCount++ |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Verify all files were renamed (if no errors occurred)
|
||||
|
if errorCount == 0 { |
||||
|
files, err := filepath.Glob(filepath.Join(testDir, "renamed_*.txt")) |
||||
|
assert.NoError(t, err, "Failed to list renamed files") |
||||
|
assert.Equal(t, numFiles, len(files), "Not all files were renamed successfully") |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// stressTestCreate tests file creation under stress
|
||||
|
func (s *ExternalPOSIXTestSuite) stressTestCreate(t *testing.T, mountPoint string) { |
||||
|
testDir := filepath.Join(mountPoint, "stress_create") |
||||
|
err := os.MkdirAll(testDir, 0755) |
||||
|
require.NoError(t, err) |
||||
|
defer os.RemoveAll(testDir) |
||||
|
|
||||
|
const numFiles = 1000 |
||||
|
const numWorkers = 20 |
||||
|
|
||||
|
results := make(chan error, numWorkers) |
||||
|
|
||||
|
for w := 0; w < numWorkers; w++ { |
||||
|
go func(worker int) { |
||||
|
for i := worker; i < numFiles; i += numWorkers { |
||||
|
fileName := filepath.Join(testDir, fmt.Sprintf("stress_%d_%d.txt", worker, i)) |
||||
|
err := os.WriteFile(fileName, []byte(fmt.Sprintf("data_%d_%d", worker, i)), 0644) |
||||
|
if err != nil { |
||||
|
results <- err |
||||
|
return |
||||
|
} |
||||
|
} |
||||
|
results <- nil |
||||
|
}(w) |
||||
|
} |
||||
|
|
||||
|
// Wait for all workers
|
||||
|
for w := 0; w < numWorkers; w++ { |
||||
|
err := <-results |
||||
|
require.NoError(t, err) |
||||
|
} |
||||
|
|
||||
|
// Verify all files were created
|
||||
|
files, err := filepath.Glob(filepath.Join(testDir, "stress_*.txt")) |
||||
|
require.NoError(t, err) |
||||
|
require.Equal(t, numFiles, len(files)) |
||||
|
} |
||||
|
|
||||
|
// stressTestDirectory tests directory operations under stress
|
||||
|
func (s *ExternalPOSIXTestSuite) stressTestDirectory(t *testing.T, mountPoint string) { |
||||
|
testDir := filepath.Join(mountPoint, "stress_directory") |
||||
|
err := os.MkdirAll(testDir, 0755) |
||||
|
require.NoError(t, err) |
||||
|
defer os.RemoveAll(testDir) |
||||
|
|
||||
|
const numDirs = 500 |
||||
|
const numWorkers = 10 |
||||
|
|
||||
|
results := make(chan error, numWorkers) |
||||
|
|
||||
|
// Create directories concurrently
|
||||
|
for w := 0; w < numWorkers; w++ { |
||||
|
go func(worker int) { |
||||
|
for i := worker; i < numDirs; i += numWorkers { |
||||
|
dirName := filepath.Join(testDir, fmt.Sprintf("dir_%d_%d", worker, i)) |
||||
|
err := os.MkdirAll(dirName, 0755) |
||||
|
if err != nil { |
||||
|
results <- err |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
// Create a file in each directory
|
||||
|
fileName := filepath.Join(dirName, "test.txt") |
||||
|
err = os.WriteFile(fileName, []byte("test"), 0644) |
||||
|
if err != nil { |
||||
|
results <- err |
||||
|
return |
||||
|
} |
||||
|
} |
||||
|
results <- nil |
||||
|
}(w) |
||||
|
} |
||||
|
|
||||
|
// Wait for all workers
|
||||
|
for w := 0; w < numWorkers; w++ { |
||||
|
err := <-results |
||||
|
require.NoError(t, err) |
||||
|
} |
||||
|
|
||||
|
// Verify directory structure
|
||||
|
dirs, err := filepath.Glob(filepath.Join(testDir, "dir_*")) |
||||
|
require.NoError(t, err) |
||||
|
require.Equal(t, numDirs, len(dirs)) |
||||
|
} |
||||
|
|
||||
|
// testEdgeCases tests various POSIX edge cases
|
||||
|
func (s *ExternalPOSIXTestSuite) testEdgeCases(t *testing.T, mountPoint string) { |
||||
|
testDir := filepath.Join(mountPoint, "edge_cases") |
||||
|
err := os.MkdirAll(testDir, 0755) |
||||
|
require.NoError(t, err) |
||||
|
defer os.RemoveAll(testDir) |
||||
|
|
||||
|
t.Run("WriteToDirectoryAsFile", func(t *testing.T) { |
||||
|
// Test writing to a directory as if it were a file (should fail)
|
||||
|
// Note: filepath.Join(testDir, "") returns testDir itself
|
||||
|
err := os.WriteFile(testDir, []byte("test"), 0644) |
||||
|
require.Error(t, err) |
||||
|
// Verify the error is specifically about the target being a directory
|
||||
|
var pathErr *os.PathError |
||||
|
require.ErrorAs(t, err, &pathErr) |
||||
|
require.Equal(t, syscall.EISDIR, pathErr.Err) |
||||
|
}) |
||||
|
|
||||
|
t.Run("VeryLongFileName", func(t *testing.T) { |
||||
|
// Test very long file names
|
||||
|
longName := strings.Repeat("a", 255) // NAME_MAX is typically 255
|
||||
|
longFile := filepath.Join(testDir, longName) |
||||
|
|
||||
|
err := os.WriteFile(longFile, []byte("long name test"), 0644) |
||||
|
// This might succeed or fail depending on filesystem limits
|
||||
|
if err == nil { |
||||
|
defer os.Remove(longFile) |
||||
|
t.Logf("Very long filename accepted") |
||||
|
} else { |
||||
|
t.Logf("Very long filename rejected: %v", err) |
||||
|
} |
||||
|
}) |
||||
|
|
||||
|
t.Run("SpecialCharacters", func(t *testing.T) { |
||||
|
// Test files with special characters
|
||||
|
specialFiles := []string{ |
||||
|
"file with spaces.txt", |
||||
|
"file-with-dashes.txt", |
||||
|
"file_with_underscores.txt", |
||||
|
"file.with.dots.txt", |
||||
|
} |
||||
|
|
||||
|
for _, fileName := range specialFiles { |
||||
|
filePath := filepath.Join(testDir, fileName) |
||||
|
err := os.WriteFile(filePath, []byte("special char test"), 0644) |
||||
|
require.NoError(t, err, "Failed to create file with special chars: %s", fileName) |
||||
|
|
||||
|
// Verify we can read it back
|
||||
|
_, err = os.ReadFile(filePath) |
||||
|
require.NoError(t, err, "Failed to read file with special chars: %s", fileName) |
||||
|
} |
||||
|
}) |
||||
|
|
||||
|
t.Run("DeepDirectoryNesting", func(t *testing.T) { |
||||
|
// Test deep directory nesting
|
||||
|
deepPath := testDir |
||||
|
for i := 0; i < 100; i++ { |
||||
|
deepPath = filepath.Join(deepPath, fmt.Sprintf("level_%d", i)) |
||||
|
} |
||||
|
|
||||
|
err := os.MkdirAll(deepPath, 0755) |
||||
|
// This might fail due to PATH_MAX limits
|
||||
|
if err != nil { |
||||
|
t.Logf("Deep directory nesting failed at some level: %v", err) |
||||
|
} else { |
||||
|
t.Logf("Deep directory nesting succeeded") |
||||
|
|
||||
|
// Test creating a file in the deep path
|
||||
|
testFile := filepath.Join(deepPath, "deep_file.txt") |
||||
|
err = os.WriteFile(testFile, []byte("deep test"), 0644) |
||||
|
if err == nil { |
||||
|
t.Logf("File creation in deep path succeeded") |
||||
|
} else { |
||||
|
t.Logf("File creation in deep path failed: %v", err) |
||||
|
} |
||||
|
} |
||||
|
}) |
||||
|
} |
||||
@ -0,0 +1,43 @@ |
|||||
|
//go:build darwin
|
||||
|
|
||||
|
package fuse |
||||
|
|
||||
|
import ( |
||||
|
"syscall" |
||||
|
"unsafe" |
||||
|
) |
||||
|
|
||||
|
// Sendfile support for macOS
|
||||
|
|
||||
|
func sendfileTransfer(outFd int, inFd int, offset *int64, count int) (int, error) { |
||||
|
// macOS sendfile has different signature: sendfile(in_fd, out_fd, offset, len, hdtr, flags)
|
||||
|
var off int64 |
||||
|
if offset != nil { |
||||
|
off = *offset |
||||
|
} |
||||
|
|
||||
|
length := int64(count) |
||||
|
|
||||
|
_, _, errno := syscall.Syscall6(syscall.SYS_SENDFILE, |
||||
|
uintptr(inFd), // input fd
|
||||
|
uintptr(outFd), // output fd
|
||||
|
uintptr(off), // offset
|
||||
|
uintptr(unsafe.Pointer(&length)), // length (in/out parameter)
|
||||
|
0, // hdtr (headers/trailers)
|
||||
|
0) // flags
|
||||
|
|
||||
|
if errno != 0 { |
||||
|
return 0, errno |
||||
|
} |
||||
|
|
||||
|
// Update offset if provided
|
||||
|
if offset != nil { |
||||
|
*offset += length |
||||
|
} |
||||
|
|
||||
|
return int(length), nil |
||||
|
} |
||||
|
|
||||
|
func isSendfileSupported() bool { |
||||
|
return true |
||||
|
} |
||||
@ -0,0 +1,33 @@ |
|||||
|
//go:build linux
|
||||
|
|
||||
|
package fuse |
||||
|
|
||||
|
import ( |
||||
|
"syscall" |
||||
|
"unsafe" |
||||
|
) |
||||
|
|
||||
|
// Sendfile support for Linux
|
||||
|
|
||||
|
func sendfileTransfer(outFd int, inFd int, offset *int64, count int) (int, error) { |
||||
|
var offsetPtr uintptr |
||||
|
if offset != nil { |
||||
|
offsetPtr = uintptr(unsafe.Pointer(offset)) |
||||
|
} |
||||
|
|
||||
|
n, _, errno := syscall.Syscall6(syscall.SYS_SENDFILE, |
||||
|
uintptr(outFd), |
||||
|
uintptr(inFd), |
||||
|
offsetPtr, |
||||
|
uintptr(count), |
||||
|
0, 0) |
||||
|
|
||||
|
if errno != 0 { |
||||
|
return 0, errno |
||||
|
} |
||||
|
return int(n), nil |
||||
|
} |
||||
|
|
||||
|
func isSendfileSupported() bool { |
||||
|
return true |
||||
|
} |
||||
@ -0,0 +1,19 @@ |
|||||
|
//go:build !linux && !darwin
|
||||
|
|
||||
|
package fuse |
||||
|
|
||||
|
import ( |
||||
|
"errors" |
||||
|
) |
||||
|
|
||||
|
// Sendfile support for unsupported platforms
|
||||
|
|
||||
|
var ErrSendfileNotSupported = errors.New("sendfile not supported on this platform") |
||||
|
|
||||
|
func sendfileTransfer(outFd int, inFd int, offset *int64, count int) (int, error) { |
||||
|
return 0, ErrSendfileNotSupported |
||||
|
} |
||||
|
|
||||
|
func isSendfileSupported() bool { |
||||
|
return false |
||||
|
} |
||||
@ -0,0 +1,126 @@ |
|||||
|
//go:build unix
|
||||
|
|
||||
|
package fuse |
||||
|
|
||||
|
import ( |
||||
|
"syscall" |
||||
|
"unsafe" |
||||
|
) |
||||
|
|
||||
|
// Vectored I/O support for Unix-like systems
|
||||
|
|
||||
|
// IOVec represents an I/O vector for readv/writev operations
|
||||
|
type IOVec struct { |
||||
|
Base *byte |
||||
|
Len uint64 |
||||
|
} |
||||
|
|
||||
|
func readvFile(fd int, iovs []IOVec) (int, error) { |
||||
|
if len(iovs) == 0 { |
||||
|
return 0, nil |
||||
|
} |
||||
|
|
||||
|
n, _, errno := syscall.Syscall(syscall.SYS_READV, |
||||
|
uintptr(fd), |
||||
|
uintptr(unsafe.Pointer(&iovs[0])), |
||||
|
uintptr(len(iovs))) |
||||
|
|
||||
|
if errno != 0 { |
||||
|
return 0, errno |
||||
|
} |
||||
|
return int(n), nil |
||||
|
} |
||||
|
|
||||
|
func writevFile(fd int, iovs []IOVec) (int, error) { |
||||
|
if len(iovs) == 0 { |
||||
|
return 0, nil |
||||
|
} |
||||
|
|
||||
|
n, _, errno := syscall.Syscall(syscall.SYS_WRITEV, |
||||
|
uintptr(fd), |
||||
|
uintptr(unsafe.Pointer(&iovs[0])), |
||||
|
uintptr(len(iovs))) |
||||
|
|
||||
|
if errno != 0 { |
||||
|
return 0, errno |
||||
|
} |
||||
|
return int(n), nil |
||||
|
} |
||||
|
|
||||
|
func preadvFile(fd int, iovs []IOVec, offset int64) (int, error) { |
||||
|
if len(iovs) == 0 { |
||||
|
return 0, nil |
||||
|
} |
||||
|
|
||||
|
// preadv/pwritev may not be available on all Unix systems
|
||||
|
// Fall back to individual pread calls
|
||||
|
totalRead := 0 |
||||
|
currentOffset := offset |
||||
|
|
||||
|
for _, iov := range iovs { |
||||
|
if iov.Len == 0 { |
||||
|
continue |
||||
|
} |
||||
|
|
||||
|
buf := (*[1 << 30]byte)(unsafe.Pointer(iov.Base))[:iov.Len:iov.Len] |
||||
|
n, err := syscall.Pread(fd, buf, currentOffset) |
||||
|
if err != nil { |
||||
|
return totalRead, err |
||||
|
} |
||||
|
totalRead += n |
||||
|
currentOffset += int64(n) |
||||
|
|
||||
|
if n < int(iov.Len) { |
||||
|
break // EOF or partial read
|
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return totalRead, nil |
||||
|
} |
||||
|
|
||||
|
func pwritevFile(fd int, iovs []IOVec, offset int64) (int, error) { |
||||
|
if len(iovs) == 0 { |
||||
|
return 0, nil |
||||
|
} |
||||
|
|
||||
|
// preadv/pwritev may not be available on all Unix systems
|
||||
|
// Fall back to individual pwrite calls
|
||||
|
totalWritten := 0 |
||||
|
currentOffset := offset |
||||
|
|
||||
|
for _, iov := range iovs { |
||||
|
if iov.Len == 0 { |
||||
|
continue |
||||
|
} |
||||
|
|
||||
|
buf := (*[1 << 30]byte)(unsafe.Pointer(iov.Base))[:iov.Len:iov.Len] |
||||
|
n, err := syscall.Pwrite(fd, buf, currentOffset) |
||||
|
if err != nil { |
||||
|
return totalWritten, err |
||||
|
} |
||||
|
totalWritten += n |
||||
|
currentOffset += int64(n) |
||||
|
|
||||
|
if n < int(iov.Len) { |
||||
|
break // Partial write
|
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return totalWritten, nil |
||||
|
} |
||||
|
|
||||
|
// Helper function to create IOVec from byte slices
|
||||
|
func makeIOVecs(buffers [][]byte) []IOVec { |
||||
|
iovs := make([]IOVec, len(buffers)) |
||||
|
for i, buf := range buffers { |
||||
|
if len(buf) > 0 { |
||||
|
iovs[i].Base = &buf[0] |
||||
|
iovs[i].Len = uint64(len(buf)) |
||||
|
} |
||||
|
} |
||||
|
return iovs |
||||
|
} |
||||
|
|
||||
|
func isVectoredIOSupported() bool { |
||||
|
return true |
||||
|
} |
||||
@ -0,0 +1,41 @@ |
|||||
|
//go:build !unix
|
||||
|
|
||||
|
package fuse |
||||
|
|
||||
|
import ( |
||||
|
"errors" |
||||
|
) |
||||
|
|
||||
|
// Vectored I/O support for unsupported platforms
|
||||
|
|
||||
|
var ErrVectoredIONotSupported = errors.New("vectored I/O not supported on this platform") |
||||
|
|
||||
|
// IOVec represents an I/O vector for readv/writev operations
|
||||
|
type IOVec struct { |
||||
|
Base *byte |
||||
|
Len uint64 |
||||
|
} |
||||
|
|
||||
|
func readvFile(fd int, iovs []IOVec) (int, error) { |
||||
|
return 0, ErrVectoredIONotSupported |
||||
|
} |
||||
|
|
||||
|
func writevFile(fd int, iovs []IOVec) (int, error) { |
||||
|
return 0, ErrVectoredIONotSupported |
||||
|
} |
||||
|
|
||||
|
func preadvFile(fd int, iovs []IOVec, offset int64) (int, error) { |
||||
|
return 0, ErrVectoredIONotSupported |
||||
|
} |
||||
|
|
||||
|
func pwritevFile(fd int, iovs []IOVec, offset int64) (int, error) { |
||||
|
return 0, ErrVectoredIONotSupported |
||||
|
} |
||||
|
|
||||
|
func makeIOVecs(buffers [][]byte) []IOVec { |
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
func isVectoredIOSupported() bool { |
||||
|
return false |
||||
|
} |
||||
@ -0,0 +1,125 @@ |
|||||
|
//go:build darwin
|
||||
|
|
||||
|
package fuse |
||||
|
|
||||
|
import ( |
||||
|
"syscall" |
||||
|
"unsafe" |
||||
|
) |
||||
|
|
||||
|
// Extended attributes support for macOS
|
||||
|
|
||||
|
const ( |
||||
|
// macOS-specific flags
|
||||
|
XATTR_NOFOLLOW = 0x0001 |
||||
|
XATTR_CREATE = 0x0002 |
||||
|
XATTR_REPLACE = 0x0004 |
||||
|
) |
||||
|
|
||||
|
func setXattr(path, name string, value []byte, flags int) error { |
||||
|
pathBytes, err := syscall.BytePtrFromString(path) |
||||
|
if err != nil { |
||||
|
return err |
||||
|
} |
||||
|
nameBytes, err := syscall.BytePtrFromString(name) |
||||
|
if err != nil { |
||||
|
return err |
||||
|
} |
||||
|
|
||||
|
var valuePtr unsafe.Pointer |
||||
|
if len(value) > 0 { |
||||
|
valuePtr = unsafe.Pointer(&value[0]) |
||||
|
} |
||||
|
|
||||
|
_, _, errno := syscall.Syscall6(syscall.SYS_SETXATTR, |
||||
|
uintptr(unsafe.Pointer(pathBytes)), |
||||
|
uintptr(unsafe.Pointer(nameBytes)), |
||||
|
uintptr(valuePtr), |
||||
|
uintptr(len(value)), |
||||
|
uintptr(0), // position (not used for regular files)
|
||||
|
uintptr(flags)) |
||||
|
|
||||
|
if errno != 0 { |
||||
|
return errno |
||||
|
} |
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
func getXattr(path, name string, value []byte) (int, error) { |
||||
|
pathBytes, err := syscall.BytePtrFromString(path) |
||||
|
if err != nil { |
||||
|
return 0, err |
||||
|
} |
||||
|
nameBytes, err := syscall.BytePtrFromString(name) |
||||
|
if err != nil { |
||||
|
return 0, err |
||||
|
} |
||||
|
|
||||
|
var valuePtr unsafe.Pointer |
||||
|
if len(value) > 0 { |
||||
|
valuePtr = unsafe.Pointer(&value[0]) |
||||
|
} |
||||
|
|
||||
|
size, _, errno := syscall.Syscall6(syscall.SYS_GETXATTR, |
||||
|
uintptr(unsafe.Pointer(pathBytes)), |
||||
|
uintptr(unsafe.Pointer(nameBytes)), |
||||
|
uintptr(valuePtr), |
||||
|
uintptr(len(value)), |
||||
|
uintptr(0), // position
|
||||
|
uintptr(0)) // options
|
||||
|
|
||||
|
if errno != 0 { |
||||
|
return 0, errno |
||||
|
} |
||||
|
return int(size), nil |
||||
|
} |
||||
|
|
||||
|
func listXattr(path string, list []byte) (int, error) { |
||||
|
pathBytes, err := syscall.BytePtrFromString(path) |
||||
|
if err != nil { |
||||
|
return 0, err |
||||
|
} |
||||
|
|
||||
|
var listPtr unsafe.Pointer |
||||
|
if len(list) > 0 { |
||||
|
listPtr = unsafe.Pointer(&list[0]) |
||||
|
} |
||||
|
|
||||
|
size, _, errno := syscall.Syscall6(syscall.SYS_LISTXATTR, |
||||
|
uintptr(unsafe.Pointer(pathBytes)), |
||||
|
uintptr(listPtr), |
||||
|
uintptr(len(list)), |
||||
|
uintptr(0), // options
|
||||
|
0, 0) |
||||
|
|
||||
|
if errno != 0 { |
||||
|
return 0, errno |
||||
|
} |
||||
|
return int(size), nil |
||||
|
} |
||||
|
|
||||
|
func removeXattr(path, name string) error { |
||||
|
pathBytes, err := syscall.BytePtrFromString(path) |
||||
|
if err != nil { |
||||
|
return err |
||||
|
} |
||||
|
nameBytes, err := syscall.BytePtrFromString(name) |
||||
|
if err != nil { |
||||
|
return err |
||||
|
} |
||||
|
|
||||
|
_, _, errno := syscall.Syscall6(syscall.SYS_REMOVEXATTR, |
||||
|
uintptr(unsafe.Pointer(pathBytes)), |
||||
|
uintptr(unsafe.Pointer(nameBytes)), |
||||
|
uintptr(0), // options
|
||||
|
0, 0, 0) |
||||
|
|
||||
|
if errno != 0 { |
||||
|
return errno |
||||
|
} |
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
func isXattrSupported() bool { |
||||
|
return true |
||||
|
} |
||||
@ -0,0 +1,115 @@ |
|||||
|
//go:build linux
|
||||
|
|
||||
|
package fuse |
||||
|
|
||||
|
import ( |
||||
|
"syscall" |
||||
|
"unsafe" |
||||
|
) |
||||
|
|
||||
|
// Extended attributes support for Linux
|
||||
|
|
||||
|
func setXattr(path, name string, value []byte, flags int) error { |
||||
|
pathBytes, err := syscall.BytePtrFromString(path) |
||||
|
if err != nil { |
||||
|
return err |
||||
|
} |
||||
|
nameBytes, err := syscall.BytePtrFromString(name) |
||||
|
if err != nil { |
||||
|
return err |
||||
|
} |
||||
|
|
||||
|
var valuePtr unsafe.Pointer |
||||
|
if len(value) > 0 { |
||||
|
valuePtr = unsafe.Pointer(&value[0]) |
||||
|
} |
||||
|
|
||||
|
_, _, errno := syscall.Syscall6(syscall.SYS_SETXATTR, |
||||
|
uintptr(unsafe.Pointer(pathBytes)), |
||||
|
uintptr(unsafe.Pointer(nameBytes)), |
||||
|
uintptr(valuePtr), |
||||
|
uintptr(len(value)), |
||||
|
uintptr(flags), |
||||
|
0) |
||||
|
|
||||
|
if errno != 0 { |
||||
|
return errno |
||||
|
} |
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
func getXattr(path, name string, value []byte) (int, error) { |
||||
|
pathBytes, err := syscall.BytePtrFromString(path) |
||||
|
if err != nil { |
||||
|
return 0, err |
||||
|
} |
||||
|
nameBytes, err := syscall.BytePtrFromString(name) |
||||
|
if err != nil { |
||||
|
return 0, err |
||||
|
} |
||||
|
|
||||
|
var valuePtr unsafe.Pointer |
||||
|
if len(value) > 0 { |
||||
|
valuePtr = unsafe.Pointer(&value[0]) |
||||
|
} |
||||
|
|
||||
|
size, _, errno := syscall.Syscall6(syscall.SYS_GETXATTR, |
||||
|
uintptr(unsafe.Pointer(pathBytes)), |
||||
|
uintptr(unsafe.Pointer(nameBytes)), |
||||
|
uintptr(valuePtr), |
||||
|
uintptr(len(value)), |
||||
|
0, |
||||
|
0) |
||||
|
|
||||
|
if errno != 0 { |
||||
|
return 0, errno |
||||
|
} |
||||
|
return int(size), nil |
||||
|
} |
||||
|
|
||||
|
func listXattr(path string, list []byte) (int, error) { |
||||
|
pathBytes, err := syscall.BytePtrFromString(path) |
||||
|
if err != nil { |
||||
|
return 0, err |
||||
|
} |
||||
|
|
||||
|
var listPtr unsafe.Pointer |
||||
|
if len(list) > 0 { |
||||
|
listPtr = unsafe.Pointer(&list[0]) |
||||
|
} |
||||
|
|
||||
|
size, _, errno := syscall.Syscall(syscall.SYS_LISTXATTR, |
||||
|
uintptr(unsafe.Pointer(pathBytes)), |
||||
|
uintptr(listPtr), |
||||
|
uintptr(len(list))) |
||||
|
|
||||
|
if errno != 0 { |
||||
|
return 0, errno |
||||
|
} |
||||
|
return int(size), nil |
||||
|
} |
||||
|
|
||||
|
func removeXattr(path, name string) error { |
||||
|
pathBytes, err := syscall.BytePtrFromString(path) |
||||
|
if err != nil { |
||||
|
return err |
||||
|
} |
||||
|
nameBytes, err := syscall.BytePtrFromString(name) |
||||
|
if err != nil { |
||||
|
return err |
||||
|
} |
||||
|
|
||||
|
_, _, errno := syscall.Syscall(syscall.SYS_REMOVEXATTR, |
||||
|
uintptr(unsafe.Pointer(pathBytes)), |
||||
|
uintptr(unsafe.Pointer(nameBytes)), |
||||
|
0) |
||||
|
|
||||
|
if errno != 0 { |
||||
|
return errno |
||||
|
} |
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
func isXattrSupported() bool { |
||||
|
return true |
||||
|
} |
||||
@ -0,0 +1,31 @@ |
|||||
|
//go:build !linux && !darwin
|
||||
|
|
||||
|
package fuse |
||||
|
|
||||
|
import ( |
||||
|
"errors" |
||||
|
) |
||||
|
|
||||
|
// Extended attributes support for unsupported platforms
|
||||
|
|
||||
|
var ErrXattrNotSupported = errors.New("extended attributes not supported on this platform") |
||||
|
|
||||
|
func setXattr(path, name string, value []byte, flags int) error { |
||||
|
return ErrXattrNotSupported |
||||
|
} |
||||
|
|
||||
|
func getXattr(path, name string, value []byte) (int, error) { |
||||
|
return 0, ErrXattrNotSupported |
||||
|
} |
||||
|
|
||||
|
func listXattr(path string, list []byte) (int, error) { |
||||
|
return 0, ErrXattrNotSupported |
||||
|
} |
||||
|
|
||||
|
func removeXattr(path, name string) error { |
||||
|
return ErrXattrNotSupported |
||||
|
} |
||||
|
|
||||
|
func isXattrSupported() bool { |
||||
|
return false |
||||
|
} |
||||
Write
Preview
Loading…
Cancel
Save
Reference in new issue