mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-08-18 21:26:56 +00:00
ci: run WinFsp's conformance suite against the windows mount (#10555)
* ci: run WinFsp's conformance suite against the windows mount
The FUSE mount is held to pjdfstest with an empty known-failures list;
the Windows mount had 24 hand-written tests. winfsp-tests is what WinFsp
uses to check a filesystem behaves like NTFS, and --fuse-external points
it at ours instead of the bundled memfs, so it is the same bar in the
same shape: anything failing that is not listed is a regression.
It reaches oplocks, security descriptors, POSIX unlink-and-rename and
directory-buffer resumption — the places a Windows filesystem actually
breaks, and none of which the current suite touches.
known_failures.txt starts with the four groups that cannot pass by
construction. The first run will show what else needs listing.
* ci: make the conformance runner fail loudly instead of running empty
The first run reported "0 excluded entries" and then died with
STATUS_DLL_NOT_FOUND, so it never tested anything while looking like a
normal failing run.
winfsp-tests links against winfsp-x64.dll, which the installer puts
somewhere the loader does not search, so the WinFsp bin directory goes on
PATH. A missing or empty known-failures list is now an error rather than
a silent run with nothing excluded, which would read as a clean sweep
with no known failures. ${env:ProgramFiles(x86)} needs the braces, and a
mount point without a trailing separator makes Join-Path build a path
relative to the drive's current directory rather than its root.
* ci: read winfsp-tests failures from its report, and list the real ones
The first run exited zero with 30 of 50 tests reporting KO, and the job
went green: --no-abort keeps the suite going past a failure and the exit
code stops reflecting them, so trusting it meant the check could not fail.
The report is now parsed for KO lines and each one named in the error.
known_failures.txt is populated from that run rather than guessed. The
groups are real gaps, not suite quirks: cached and overlapped IO fails as
a block, delete-while-open has no pending state, Windows file attributes
and creation time are not round-tripped, and directory enumeration does
not resume from a marker.
* ci: stop excluding the extended attribute tests
Forwarding landed, so the group runs instead of being taken on trust —
which is the only coverage it has had.
This commit is contained in:
@@ -0,0 +1,118 @@
|
||||
name: "mount: windows conformance"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
paths:
|
||||
- 'weed/mount/**'
|
||||
- 'weed/command/mount*.go'
|
||||
- 'test/winfsp-conformance/**'
|
||||
- '.github/workflows/mount-windows-conformance.yml'
|
||||
# No base branch filter: this is the only thing that runs the Windows mount,
|
||||
# so it should cover a pull request stacked on another one too.
|
||||
pull_request:
|
||||
paths:
|
||||
- 'weed/mount/**'
|
||||
- 'weed/command/mount*.go'
|
||||
- 'test/winfsp-conformance/**'
|
||||
- '.github/workflows/mount-windows-conformance.yml'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
conformance:
|
||||
name: WinFsp conformance
|
||||
runs-on: windows-latest
|
||||
timeout-minutes: 60
|
||||
env:
|
||||
# The runner ships MinGW, so cgo is on by default and cgofuse picks its
|
||||
# cgo variant, which wants WinFsp's headers. The nocgo variant loads
|
||||
# winfsp-x64.dll at run time instead, which is how weed.exe is released.
|
||||
CGO_ENABLED: 0
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: actions/setup-go@v7
|
||||
with:
|
||||
go-version-file: 'go.mod'
|
||||
|
||||
# cgofuse loads winfsp-x64.dll at run time, so WinFsp is needed here but
|
||||
# not to build.
|
||||
- name: Install WinFsp
|
||||
run: choco install winfsp -y --no-progress
|
||||
|
||||
- name: Build weed.exe
|
||||
run: go build -o weed.exe ./weed
|
||||
|
||||
# The runner tears down a step's process tree when its shell exits, so a
|
||||
# cluster started in one step is gone by the next. Everything that needs
|
||||
# the cluster and the mount alive has to share a step.
|
||||
- name: Mount and run winfsp-tests
|
||||
shell: pwsh
|
||||
run: |
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
function Test-Port($port) {
|
||||
# A plain connect, because Test-NetConnection has reported success
|
||||
# here for a port nothing was listening on.
|
||||
$client = New-Object System.Net.Sockets.TcpClient
|
||||
try { $client.Connect('127.0.0.1', $port); return $client.Connected }
|
||||
catch { return $false }
|
||||
finally { $client.Dispose() }
|
||||
}
|
||||
|
||||
function Start-Mount($log) {
|
||||
Start-Process -FilePath .\weed.exe `
|
||||
-ArgumentList '-logtostderr','mount','-filer=127.0.0.1:8888','-dir=S:' `
|
||||
-RedirectStandardOutput "C:\$log.log" -RedirectStandardError "C:\$log.err.log"
|
||||
$deadline = (Get-Date).AddMinutes(2)
|
||||
while ((Get-Date) -lt $deadline) {
|
||||
if (Test-Path S:\) { Write-Host "S: is mounted"; return }
|
||||
Start-Sleep -Seconds 2
|
||||
}
|
||||
Get-Content "C:\$log.log", "C:\$log.err.log" -ErrorAction SilentlyContinue
|
||||
throw "S: never appeared"
|
||||
}
|
||||
|
||||
New-Item -ItemType Directory -Force -Path C:\seaweed-data | Out-Null
|
||||
# -ip pins the cluster to loopback; it otherwise advertises and binds
|
||||
# the runner's LAN address, which 127.0.0.1 cannot reach.
|
||||
Start-Process -FilePath .\weed.exe `
|
||||
-ArgumentList '-logtostderr','mini','-dir=C:\seaweed-data','-ip=127.0.0.1' `
|
||||
-RedirectStandardOutput C:\seaweed-mini.log -RedirectStandardError C:\seaweed-mini.err.log
|
||||
|
||||
$deadline = (Get-Date).AddMinutes(3)
|
||||
while ((Get-Date) -lt $deadline) {
|
||||
# The mount dials grpc, not http, so both ports have to answer.
|
||||
if ((Test-Port 8888) -and (Test-Port 18888)) { break }
|
||||
Start-Sleep -Seconds 3
|
||||
}
|
||||
if (-not ((Test-Port 8888) -and (Test-Port 18888))) {
|
||||
Get-Content C:\seaweed-mini.log, C:\seaweed-mini.err.log -ErrorAction SilentlyContinue
|
||||
throw "filer never came up"
|
||||
}
|
||||
Write-Host "filer is up on http 8888 and grpc 18888"
|
||||
|
||||
Start-Mount 'seaweed-mount'
|
||||
|
||||
Write-Host "::group::winfsp-tests"
|
||||
& pwsh -File test/winfsp-conformance/run.ps1 -MountPoint S:\
|
||||
$code = $LASTEXITCODE
|
||||
Write-Host "::endgroup::"
|
||||
if ($code -ne 0) { throw "winfsp-tests failed with exit $code" }
|
||||
|
||||
- name: Logs
|
||||
if: always()
|
||||
shell: pwsh
|
||||
run: |
|
||||
foreach ($f in 'C:\seaweed-mount.log','C:\seaweed-mount.err.log','C:\seaweed-mini.log','C:\seaweed-mini.err.log') {
|
||||
if (Test-Path $f) { Write-Host "===== $f"; Get-Content $f -Tail 200 }
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
# Known winfsp-tests failures for the SeaweedFS Windows mount.
|
||||
#
|
||||
# One test name per line, as winfsp-tests reports it. A trailing * matches a
|
||||
# prefix, so "stream_*" covers a whole group. Listed tests are excluded from
|
||||
# the run; a failure in anything NOT listed fails CI, which is what catches a
|
||||
# regression.
|
||||
#
|
||||
# Populated from the first real run: 20 of 50 passed. Extended attributes are
|
||||
# forwarded now, so that group runs rather than being excluded. Every entry below is a
|
||||
# gap in the mount rather than a quirk of the suite, and the list is meant to
|
||||
# shrink. Keep a reason on each group — an entry with no reason cannot be told
|
||||
# apart from one nobody has looked at.
|
||||
|
||||
# Cannot pass by construction
|
||||
# ---------------------------
|
||||
# Alternate data streams: the filer stores one byte range per entry, and the
|
||||
# mount exposes no second stream.
|
||||
stream_*
|
||||
# Reparse points: Symlink is refused, so there is nothing to resolve.
|
||||
reparse_*
|
||||
# Byte-range locks live in the WinFsp driver, so they never reach the mount.
|
||||
lock_*
|
||||
|
||||
# Security descriptors
|
||||
# --------------------
|
||||
# The mount carries unix mode bits and no ACL, and Chown accepts and discards,
|
||||
# so a descriptor written here does not read back.
|
||||
create_sd_test
|
||||
getsecurity_test
|
||||
|
||||
# Cached and overlapped IO
|
||||
# ------------------------
|
||||
# The whole rdwr group fails together, so this is one defect rather than nine:
|
||||
# the mount does not yet satisfy what Windows expects of cached, write-through
|
||||
# and overlapped IO. The first thing worth fixing.
|
||||
rdwr_cached_test
|
||||
rdwr_cached_append_test
|
||||
rdwr_cached_overlapped_test
|
||||
rdwr_noncached_test
|
||||
rdwr_noncached_overlapped_test
|
||||
rdwr_writethru_test
|
||||
rdwr_writethru_append_test
|
||||
rdwr_writethru_overlapped_test
|
||||
rdwr_mixed_test
|
||||
flush_test
|
||||
|
||||
# Delete semantics
|
||||
# ----------------
|
||||
# WinFsp advertises POSIX unlink, so a file can be deleted while handles are
|
||||
# still open; the mount does not implement the delete-pending state.
|
||||
delete_access_test
|
||||
delete_ex_test
|
||||
delete_mmap_test
|
||||
delete_pending_test
|
||||
|
||||
# Rename semantics
|
||||
# ----------------
|
||||
# Rename over an open file, and rename of a directory holding open handles,
|
||||
# which WinFsp is stricter about than POSIX.
|
||||
rename_ex_test
|
||||
rename_mmap_test
|
||||
rename_open_test
|
||||
exec_rename_dir_test
|
||||
|
||||
# File information
|
||||
# ----------------
|
||||
# Windows attributes (hidden, system, readonly) and creation time are not
|
||||
# round-tripped: attrToStat leaves Flags unset and reports ctime as birthtime.
|
||||
create_fileattr_test
|
||||
create_readonlydir_test
|
||||
getfileinfo_test
|
||||
setfileinfo_test
|
||||
|
||||
# Directory enumeration
|
||||
# ---------------------
|
||||
# Marker-based resumption and the buffer-overflow path, neither of which the
|
||||
# hand-written suite reaches.
|
||||
querydir_test
|
||||
querydir_buffer_overflow_test
|
||||
querydir_namelen_test
|
||||
|
||||
# Name length
|
||||
# -----------
|
||||
# checkName caps at 255 bytes while WinFsp allows 255 characters, so a CJK
|
||||
# name that is legal to Windows is rejected here.
|
||||
create_namelen_test
|
||||
|
||||
# Sharing modes
|
||||
# -------------
|
||||
# Windows share-access checking on create, which the mount does not enforce.
|
||||
create_share_test
|
||||
create_test
|
||||
@@ -0,0 +1,91 @@
|
||||
# Runs WinFsp's own conformance suite against a mounted SeaweedFS drive.
|
||||
#
|
||||
# winfsp-tests is what WinFsp uses to check a filesystem behaves like NTFS,
|
||||
# and --fuse-external points it at somebody else's filesystem instead of the
|
||||
# bundled memfs. It is the Windows counterpart of the pjdfstest run the FUSE
|
||||
# mount already goes through, and it is scored the same way: anything failing
|
||||
# that is not in known_failures.txt is a regression.
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][string]$MountPoint,
|
||||
[string]$KnownFailures = "$PSScriptRoot\known_failures.txt",
|
||||
[string]$WinFspTestsVersion = '2.1.25156'
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
# "S:" and "S:\" mean different things to Join-Path: without the separator the
|
||||
# result is relative to the drive's current directory, not its root.
|
||||
if ($MountPoint -notmatch '[\\/]$') { $MountPoint = $MountPoint + '\' }
|
||||
|
||||
# winfsp-tests links against winfsp-x64.dll, which the MSI puts somewhere the
|
||||
# loader does not look by default.
|
||||
foreach ($candidate in @("${env:ProgramFiles(x86)}\WinFsp\bin", "$env:ProgramFiles\WinFsp\bin")) {
|
||||
if (Test-Path $candidate) {
|
||||
$env:PATH = "$candidate;$env:PATH"
|
||||
Write-Host "using WinFsp binaries from $candidate"
|
||||
}
|
||||
}
|
||||
|
||||
$toolDir = Join-Path $env:TEMP 'winfsp-tests'
|
||||
$exe = Join-Path $toolDir 'winfsp-tests-x64.exe'
|
||||
if (-not (Test-Path $exe)) {
|
||||
# Shipped as its own archive rather than in the MSI.
|
||||
$url = "https://github.com/winfsp/winfsp/releases/download/v2.1/winfsp-tests-$WinFspTestsVersion.zip"
|
||||
$zip = Join-Path $env:TEMP 'winfsp-tests.zip'
|
||||
Write-Host "downloading $url"
|
||||
Invoke-WebRequest -Uri $url -OutFile $zip -UseBasicParsing
|
||||
Expand-Archive -LiteralPath $zip -DestinationPath $toolDir -Force
|
||||
}
|
||||
if (-not (Test-Path $exe)) {
|
||||
throw "winfsp-tests-x64.exe not found under $toolDir"
|
||||
}
|
||||
|
||||
# A missing or unreadable list would otherwise run with nothing excluded and
|
||||
# report that as normal, which reads like a pass with no known failures.
|
||||
if (-not (Test-Path $KnownFailures)) {
|
||||
throw "known failures list not found at $KnownFailures"
|
||||
}
|
||||
$excluded = @(Get-Content $KnownFailures |
|
||||
ForEach-Object { $_.Trim() } |
|
||||
Where-Object { $_ -and -not $_.StartsWith('#') })
|
||||
Write-Host "read $($excluded.Count) exclusions from $KnownFailures"
|
||||
if ($excluded.Count -eq 0) {
|
||||
throw "known failures list at $KnownFailures parsed to nothing"
|
||||
}
|
||||
|
||||
# The suite refuses to run anywhere but a drive, and works in the current
|
||||
# directory, so it has to be driven from inside the mount.
|
||||
$workDir = Join-Path $MountPoint 'winfsp-conformance'
|
||||
New-Item -ItemType Directory -Force -Path $workDir | Out-Null
|
||||
Push-Location $workDir
|
||||
try {
|
||||
# --fuse-external: a third-party FUSE filesystem, not the bundled memfs.
|
||||
# --resilient: tolerate operations this filesystem does not implement.
|
||||
# --no-abort: report every failure instead of stopping at the first.
|
||||
$arguments = @('--fuse-external', '--resilient', '--no-abort')
|
||||
foreach ($name in $excluded) {
|
||||
$arguments += "-$name"
|
||||
}
|
||||
Write-Host "running winfsp-tests with $($excluded.Count) excluded entries"
|
||||
# --no-abort keeps going past a failure, and the exit code stops reflecting
|
||||
# them, so the report itself is what has to be read.
|
||||
$output = & $exe @arguments 2>&1
|
||||
$output | ForEach-Object { Write-Host $_ }
|
||||
$failed = @($output |
|
||||
ForEach-Object { if ($_ -match '^([a-z_0-9]+)\.+\s+KO') { $Matches[1] } })
|
||||
$code = if ($failed.Count -gt 0) { 1 } else { $LASTEXITCODE }
|
||||
} finally {
|
||||
Pop-Location
|
||||
Remove-Item $workDir -Recurse -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
if ($failed.Count -gt 0) {
|
||||
Write-Host "::error::winfsp-tests failures outside known_failures.txt: $($failed -join ', ')"
|
||||
exit 1
|
||||
}
|
||||
if ($code -ne 0) {
|
||||
Write-Host "::error::winfsp-tests exited $code with no failure reported"
|
||||
exit $code
|
||||
}
|
||||
Write-Host 'winfsp-tests passed'
|
||||
Reference in New Issue
Block a user