diff --git a/CMakeLists.txt b/CMakeLists.txt index f3ace51..89c53a4 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,7 +2,7 @@ cmake_minimum_required(VERSION 3.13 FATAL_ERROR) # Set up the project project(filex - LANGUAGES C ASM + LANGUAGES C ) diff --git a/common/src/fx_utility_logical_sector_write.c b/common/src/fx_utility_logical_sector_write.c index 9da0ffc..825c64c 100644 --- a/common/src/fx_utility_logical_sector_write.c +++ b/common/src/fx_utility_logical_sector_write.c @@ -306,8 +306,6 @@ UCHAR cache_found = FX_FALSE; return(FX_SECTOR_INVALID); } - /* Just write the buffer to the media. */ - #ifndef FX_MEDIA_STATISTICS_DISABLE /* Increment the number of driver write sector(s) requests. */ diff --git a/ports/win64/vs_2022/CMakeLists.txt b/ports/win64/vs_2022/CMakeLists.txt new file mode 100644 index 0000000..6bc1b86 --- /dev/null +++ b/ports/win64/vs_2022/CMakeLists.txt @@ -0,0 +1,10 @@ +target_sources(${PROJECT_NAME} PRIVATE + # {{BEGIN_TARGET_SOURCES}} + + # {{END_TARGET_SOURCES}} +) + +target_include_directories(${PROJECT_NAME} + PUBLIC + ${CMAKE_CURRENT_LIST_DIR}/inc +) diff --git a/ports/win64/vs_2022/inc/fx_port.h b/ports/win64/vs_2022/inc/fx_port.h new file mode 100644 index 0000000..68e9d8f --- /dev/null +++ b/ports/win64/vs_2022/inc/fx_port.h @@ -0,0 +1,368 @@ +/*************************************************************************** + * Copyright (C) 2026 Eclipse ThreadX contributors + * + * This program and the accompanying materials are made available under the + * terms of the MIT License which is available at + * https://opensource.org/licenses/MIT. + * + * AI Disclosure: This file was largely AI-generated by Copilot (Claude Sonnet 4.6). + * The AI-generated portions may be considered public domain (CC0-1.0) + * and not subject to the project's licence. The human contributor has + * reviewed and verified that the code is correct. + * + * SPDX-License-Identifier: MIT and CC0-1.0 + **************************************************************************/ + + +/**************************************************************************/ +/**************************************************************************/ +/** */ +/** FileX Component */ +/** */ +/** Port Specific */ +/** */ +/**************************************************************************/ +/**************************************************************************/ + + +/**************************************************************************/ +/* */ +/* PORT SPECIFIC C INFORMATION RELEASE */ +/* */ +/* fx_port.h Win64/Visual */ +/* 6.5.1.202602 */ +/* */ +/* AUTHOR */ +/* */ +/* Eclipse ThreadX contributors */ +/* */ +/* DESCRIPTION */ +/* */ +/* This file contains data type definitions that make the FileX FAT */ +/* compatible file system function identically on a variety of */ +/* different processor architectures. For example, the byte offset of */ +/* various entries in the boot record, and directory entries are */ +/* defined in this file. */ +/* */ +/**************************************************************************/ + +#ifndef FX_PORT_H +#define FX_PORT_H + + +/* Determine if the optional FileX user define file should be used. */ + +#ifdef FX_INCLUDE_USER_DEFINE_FILE + + +/* Yes, include the user defines in fx_user.h. The defines in this file may + alternately be defined on the command line. */ + +#include "fx_user.h" +#endif + +#include + + +/* Include the ThreadX api file. */ + +#ifndef FX_STANDALONE_ENABLE +#include "tx_api.h" + + +/* Define ULONG64 typedef, if not already defined. */ + +#ifndef ULONG64_DEFINED +#define ULONG64_DEFINED +typedef unsigned long long ULONG64; +#endif + +#else + +/* Define compiler library include files. */ + +#include +#include + +#ifndef VOID +#define VOID void +typedef char CHAR; +typedef char BOOL; +typedef unsigned char UCHAR; +typedef int INT; +typedef unsigned int UINT; +typedef long LONG; +typedef unsigned long ULONG; +typedef short SHORT; +typedef unsigned short USHORT; +#endif + +#ifndef ULONG64_DEFINED +#define ULONG64_DEFINED +typedef unsigned long long ULONG64; +#endif + + +/* Define basic alignment type used in block and byte pool operations. This data type must + be at least 32-bits in size and also be large enough to hold a pointer type. + On Win64, pointers are 64 bits so ULONG (32-bit on Windows LLP64) is insufficient. */ + +#ifndef ALIGN_TYPE_DEFINED +#define ALIGN_TYPE_DEFINED +#define ALIGN_TYPE ULONG64 +#endif + + +#endif /* FX_STANDALONE_ENABLE */ + + +#ifdef FX_REGRESSION_TEST + +/* Define parameters for regression test suite. */ + +#define FX_MAX_SECTOR_CACHE 256 +#define FX_MAX_FAT_CACHE 64 +#define FX_FAT_MAP_SIZE 1 + + +/* Define variables and macros used to introduce errors for the regression test suite. */ + +extern ULONG _fx_ram_driver_io_error_request; +extern ULONG _fx_ram_driver_io_request_count; +extern ULONG _fx_file_open_max_file_size_request; +extern ULONG _fx_directory_entry_read_count; +extern ULONG _fx_directory_entry_read_error_request; +extern ULONG _fx_directory_entry_write_count; +extern ULONG _fx_directory_entry_write_error_request; +extern ULONG _fx_utility_fat_entry_write_count; +extern ULONG _fx_utility_fat_entry_write_error_request; +extern ULONG _fx_utility_fat_entry_read_count; +extern ULONG _fx_utility_fat_entry_read_error_request; +extern ULONG _fx_utility_logical_sector_flush_count; +extern ULONG _fx_utility_logical_sector_flush_error_request; +extern ULONG _fx_utility_logical_sector_write_count; +extern ULONG _fx_utility_logical_sector_write_error_request; +extern ULONG _fx_utility_logical_sector_read_count; +extern ULONG _fx_utility_logical_sector_read_error_request; +extern ULONG _fx_utility_logical_sector_read_1_count; +extern ULONG _fx_utility_logical_sector_read_1_error_request; + +#ifdef FX_ENABLE_FAULT_TOLERANT +struct FX_MEDIA_STRUCT; +extern VOID fault_tolerant_enable_callback(struct FX_MEDIA_STRUCT *media_ptr, + UCHAR *fault_tolerant_memory_buffer, + ULONG log_size); +extern VOID fault_tolerant_apply_log_callback(struct FX_MEDIA_STRUCT *media_ptr, + UCHAR *fault_tolerant_memory_buffer, + ULONG log_size); +#endif /* FX_ENABLE_FAULT_TOLERANT */ + + +#define FX_DIRECTORY_ENTRY_READ_EXTENSION _fx_directory_entry_read_count++; \ + if (_fx_directory_entry_read_error_request) \ + { \ + _fx_directory_entry_read_error_request--; \ + if (_fx_directory_entry_read_error_request == 0) \ + { \ + return(FX_IO_ERROR); \ + } \ + } + +#define FX_DIRECTORY_ENTRY_WRITE_EXTENSION _fx_directory_entry_write_count++; \ + if (_fx_directory_entry_write_error_request) \ + { \ + _fx_directory_entry_write_error_request--; \ + if (_fx_directory_entry_write_error_request == 0) \ + { \ + return(FX_IO_ERROR); \ + } \ + } + +#define FX_UTILITY_FAT_ENTRY_READ_EXTENSION _fx_utility_fat_entry_read_count++; \ + if (_fx_utility_fat_entry_read_error_request) \ + { \ + _fx_utility_fat_entry_read_error_request--; \ + if (_fx_utility_fat_entry_read_error_request == 0) \ + { \ + return(FX_IO_ERROR); \ + } \ + if (_fx_utility_fat_entry_read_error_request == 10000) \ + { \ + *entry_ptr = 1; \ + _fx_utility_fat_entry_read_error_request = 0; \ + return(FX_SUCCESS); \ + } \ + if (_fx_utility_fat_entry_read_error_request == 20000) \ + { \ + *entry_ptr = media_ptr -> fx_media_fat_reserved; \ + _fx_utility_fat_entry_read_error_request = 0; \ + return(FX_SUCCESS); \ + } \ + if (_fx_utility_fat_entry_read_error_request == 30000) \ + { \ + *entry_ptr = cluster; \ + _fx_utility_fat_entry_read_error_request = 0; \ + return(FX_SUCCESS); \ + } \ + if (_fx_utility_fat_entry_read_error_request == 40000) \ + { \ + media_ptr -> fx_media_total_clusters = 0; \ + _fx_utility_fat_entry_read_error_request = 0; \ + return(FX_SUCCESS); \ + } \ + } + +#define FX_UTILITY_FAT_ENTRY_WRITE_EXTENSION _fx_utility_fat_entry_write_count++; \ + if (_fx_utility_fat_entry_write_error_request) \ + { \ + _fx_utility_fat_entry_write_error_request--; \ + if (_fx_utility_fat_entry_write_error_request == 0) \ + { \ + return(FX_IO_ERROR); \ + } \ + } + +#define FX_UTILITY_LOGICAL_SECTOR_FLUSH_EXTENSION _fx_utility_logical_sector_flush_count++; \ + if (_fx_utility_logical_sector_flush_error_request) \ + { \ + _fx_utility_logical_sector_flush_error_request--; \ + if (_fx_utility_logical_sector_flush_error_request == 0) \ + { \ + return(FX_IO_ERROR); \ + } \ + } + +#define FX_UTILITY_LOGICAL_SECTOR_READ_EXTENSION _fx_utility_logical_sector_read_count++; \ + if (_fx_utility_logical_sector_read_error_request) \ + { \ + _fx_utility_logical_sector_read_error_request--; \ + if (_fx_utility_logical_sector_read_error_request == 0) \ + { \ + return(FX_IO_ERROR); \ + } \ + } + +#define FX_UTILITY_LOGICAL_SECTOR_READ_EXTENSION_1 _fx_utility_logical_sector_read_1_count++; \ + if (_fx_utility_logical_sector_read_1_error_request) \ + { \ + _fx_utility_logical_sector_read_1_error_request--; \ + if (_fx_utility_logical_sector_read_1_error_request == 0) \ + { \ + cache_entry = FX_NULL; \ + } \ + } + +#define FX_UTILITY_LOGICAL_SECTOR_WRITE_EXTENSION _fx_utility_logical_sector_write_count++; \ + if (_fx_utility_logical_sector_write_error_request) \ + { \ + _fx_utility_logical_sector_write_error_request--; \ + if (_fx_utility_logical_sector_write_error_request == 0) \ + { \ + return(FX_IO_ERROR); \ + } \ + } + +#define FX_FAULT_TOLERANT_ENABLE_EXTENSION fault_tolerant_enable_callback(media_ptr, media_ptr -> fx_media_fault_tolerant_memory_buffer, total_size); +#define FX_FAULT_TOLERANT_APPLY_LOGS_EXTENSION fault_tolerant_apply_log_callback(media_ptr, media_ptr -> fx_media_fault_tolerant_memory_buffer, size); + +#endif /* FX_REGRESSION_TEST */ + + +/* Define FileX internal protection macros. If FX_SINGLE_THREAD is defined, + these protection macros are effectively disabled. However, for multi-thread + uses, the macros are setup to utilize a ThreadX mutex for multiple thread + access control into an open media. */ + +/* Reduce the mutex error checking for testing purpose. */ + +#if defined(FX_SINGLE_THREAD) || defined(FX_STANDALONE_ENABLE) +#define FX_PROTECT +#define FX_UNPROTECT +#else +#define FX_PROTECT tx_mutex_get(&(media_ptr -> fx_media_protect), TX_WAIT_FOREVER); +#define FX_UNPROTECT tx_mutex_put(&(media_ptr -> fx_media_protect)); +#endif + + +/* Define interrupt lockout constructs to protect the system date/time from being updated + while they are being read. */ + +#ifndef FX_STANDALONE_ENABLE +#define FX_INT_SAVE_AREA unsigned int old_interrupt_posture; +#define FX_DISABLE_INTS old_interrupt_posture = tx_interrupt_control(TX_INT_DISABLE); +#define FX_RESTORE_INTS tx_interrupt_control(old_interrupt_posture); +#else +#ifndef FX_LEGACY_INTERRUPT_PROTECTION +#define FX_LEGACY_INTERRUPT_PROTECTION +#endif +#define FX_INT_SAVE_AREA +#define FX_DISABLE_INTS +#define FX_RESTORE_INTS +#endif + +/* Define the error checking logic to determine if there is a caller error in the FileX API. + The default definitions assume ThreadX is being used. This code can be completely turned + off by just defining these macros to white space. */ + +#ifndef FX_STANDALONE_ENABLE +#ifndef TX_TIMER_PROCESS_IN_ISR + +#define FX_CALLER_CHECKING_EXTERNS extern TX_THREAD *_tx_thread_current_ptr; \ + extern TX_THREAD _tx_timer_thread; \ + extern volatile ULONG _tx_thread_system_state; + +#define FX_CALLER_CHECKING_CODE if ((_tx_thread_system_state) || \ + (_tx_thread_current_ptr == TX_NULL) || \ + (_tx_thread_current_ptr == &_tx_timer_thread)) \ + return(FX_CALLER_ERROR); + +#else +#define FX_CALLER_CHECKING_EXTERNS extern TX_THREAD *_tx_thread_current_ptr; \ + extern volatile ULONG _tx_thread_system_state; + +#define FX_CALLER_CHECKING_CODE if ((_tx_thread_system_state) || \ + (_tx_thread_current_ptr == TX_NULL)) \ + return(FX_CALLER_ERROR); +#endif +#else +#define FX_CALLER_CHECKING_EXTERNS +#define FX_CALLER_CHECKING_CODE +#endif + +/* Define the update rate of the system timer. These values may also be defined at the command + line when compiling the fx_system_initialize.c module in the FileX library build. Alternatively, + they can be modified in this file or fx_user.h. Note: the update rate must be an even number of + seconds greater than or equal to 2, which is the minimal update rate for FAT time. */ + +/* Define the number of seconds the timer parameters are updated in FileX. The default + value is 10 seconds. This value can be overwritten externally. */ + +#ifndef FX_UPDATE_RATE_IN_SECONDS +#define FX_UPDATE_RATE_IN_SECONDS 10 +#endif + + +/* Defines the number of ThreadX timer ticks required to achieve the update rate specified by + FX_UPDATE_RATE_IN_SECONDS defined previously. By default, the ThreadX timer tick is 10ms, + so the default value for this constant is 1000. If TX_TIMER_TICKS_PER_SECOND is defined, + this value is derived from TX_TIMER_TICKS_PER_SECOND. */ + +#ifndef FX_UPDATE_RATE_IN_TICKS +#if (defined(TX_TIMER_TICKS_PER_SECOND) && (!defined(FX_STANDALONE_ENABLE))) +#define FX_UPDATE_RATE_IN_TICKS (TX_TIMER_TICKS_PER_SECOND * FX_UPDATE_RATE_IN_SECONDS) +#else +#define FX_UPDATE_RATE_IN_TICKS 1000 +#endif +#endif + + +/* Define the version ID of FileX. This may be utilized by the application. */ + +#ifdef FX_SYSTEM_INIT +CHAR _fx_version_id[] = + "Copyright (c) 2026 Eclipse ThreadX contributors. * FileX Win64/Visual 6.5.1.202602 *"; +#else +extern CHAR _fx_version_id[]; +#endif + +#endif /* FX_PORT_H */ diff --git a/scripts/build_fx.ps1 b/scripts/build_fx.ps1 new file mode 100644 index 0000000..5407157 --- /dev/null +++ b/scripts/build_fx.ps1 @@ -0,0 +1,69 @@ +[CmdletBinding()] +param( + [ValidateSet('win64', 'win32')] + [string]$Arch = 'win64', + + [AllowNull()] + [object]$Configuration = 'all', + + [int]$Parallel = [Math]::Max(1, [Environment]::ProcessorCount), + + [int]$BuildTimeoutSeconds = 120, + + [string]$BuildDir, + + [string]$ThreadXDir, + + [switch]$Clean +) + +$ErrorActionPreference = 'Stop' +. (Join-Path $PSScriptRoot 'fx_windows_common.ps1') + +$repoRoot = Split-Path -Parent $PSScriptRoot +$settings = Get-PortSettings -SelectedArch $Arch + +if (-not $BuildDir) { + $BuildDir = Join-Path $repoRoot "build\tests\$Arch" +} + +if (-not $ThreadXDir) { + $ThreadXDir = Join-Path (Split-Path -Parent $repoRoot) 'threadx-fd-codex' +} + +$selectedConfigurations = Resolve-RegressionConfigurations -RequestedConfigurations $Configuration +Write-Host "Selected configurations: $($selectedConfigurations -join ', ')" + +Enter-VisualStudioDevShell -VsArch $settings.VsArch + +foreach ($currentConfiguration in $selectedConfigurations) { + $currentBuildDirName = Get-RegressionBuildDirectoryName -ConfigurationName $currentConfiguration + $currentBuildDir = Join-Path $BuildDir $currentBuildDirName + + if ($Clean) { + Remove-BuildDirectory -Path $currentBuildDir -RepoRoot $repoRoot + } + + Remove-NinjaLock -Path $currentBuildDir + + Write-Host "Configuring $Arch / $currentConfiguration" + Invoke-NativeCommand -FilePath 'cmake' -Arguments @( + '-S', (Join-Path $repoRoot 'test\cmake'), + '-B', $currentBuildDir, + '-G', 'Ninja', + '-DCMAKE_C_COMPILER_FORCED=TRUE', + '-DCMAKE_C_COMPILER_WORKS=TRUE', + '-DCMAKE_C_ABI_COMPILED=TRUE', + # FileX's root CMakeLists.txt declares LANGUAGES C ASM; suppress the + # ASM compiler probe because the win64 port has no assembly sources. + '-DCMAKE_ASM_COMPILER_FORCED=TRUE', + '-DCMAKE_ASM_COMPILER_WORKS=TRUE', + "-DCMAKE_BUILD_TYPE=$currentConfiguration", + "-DTHREADX_ARCH=$($settings.FileXArch)", + "-DTHREADX_TOOLCHAIN=$($settings.FileXToolchain)", + "-DTHREADX_SOURCE_DIR=$ThreadXDir" + ) + + Write-Host "Building $Arch / $currentConfiguration" + Invoke-CMakeBuild -BuildDir $currentBuildDir -Parallel $Parallel -TimeoutSeconds $BuildTimeoutSeconds +} diff --git a/scripts/fx_windows_common.ps1 b/scripts/fx_windows_common.ps1 new file mode 100644 index 0000000..b063438 --- /dev/null +++ b/scripts/fx_windows_common.ps1 @@ -0,0 +1,939 @@ +[CmdletBinding()] +param() + +$ErrorActionPreference = 'Stop' + +function Invoke-NativeCommand { + param( + [Parameter(Mandatory = $true)] + [string]$FilePath, + + [Parameter()] + [string[]]$Arguments = @() + ) + + & $FilePath @Arguments + if ($LASTEXITCODE -ne 0) { + throw "Command failed with exit code ${LASTEXITCODE}: $FilePath $($Arguments -join ' ')" + } +} + +function Get-CommandPathIfAvailable { + param( + [Parameter(Mandatory = $true)] + [string]$CommandName + ) + + $command = Get-Command $CommandName -ErrorAction SilentlyContinue + if ($null -eq $command) { + return $null + } + + return $command.Source +} + +function Get-PortSettings { + param( + [Parameter(Mandatory = $true)] + [string]$SelectedArch + ) + + switch ($SelectedArch) { + 'win32' { + return @{ + FileXArch = 'win32' + FileXToolchain = 'vs_2019' + VsArch = 'x86' + } + } + 'win64' { + return @{ + FileXArch = 'win64' + FileXToolchain = 'vs_2022' + VsArch = 'amd64' + } + } + default { + throw "Unsupported architecture: $SelectedArch" + } + } +} + +function Get-RegressionConfigurations { + return @( + 'default_build_coverage', + 'no_cache_build', + 'no_cache_standalone_build', + 'fault_tolerant_build_coverage', + 'no_check_build', + 'no_cache_fault_tolerant_build', + 'standalone_build_coverage', + 'standalone_fault_tolerant_build_coverage', + 'standalone_no_cache_fault_tolerant_build' + ) +} + +function Resolve-RegressionConfigurations { + param( + [Parameter(Mandatory = $false)] + [AllowNull()] + [AllowEmptyCollection()] + [object]$RequestedConfigurations = 'all' + ) + + $allConfigurations = Get-RegressionConfigurations + $resolvedConfigurations = @() + + if ($null -eq $RequestedConfigurations) { + $resolvedConfigurations = @('all') + } + elseif ($RequestedConfigurations -is [System.Array]) { + foreach ($requestedConfiguration in $RequestedConfigurations) { + if ($null -ne $requestedConfiguration) { + $resolvedConfigurations += [string]$requestedConfiguration + } + } + } + else { + $resolvedConfigurations = @([string]$RequestedConfigurations) + } + + $normalizedConfigurations = @() + foreach ($requestedConfiguration in $resolvedConfigurations) { + foreach ($configurationPart in ($requestedConfiguration -split ',')) { + $trimmedConfiguration = $configurationPart.Trim() + if ($trimmedConfiguration.Length -gt 0) { + $normalizedConfigurations += $trimmedConfiguration + } + } + } + + if (($normalizedConfigurations.Count -eq 0) -or ($normalizedConfigurations -contains 'all')) { + return $allConfigurations + } + + foreach ($normalizedConfiguration in $normalizedConfigurations) { + if ($allConfigurations -notcontains $normalizedConfiguration) { + throw "Unsupported configuration: $normalizedConfiguration" + } + } + + return $normalizedConfigurations +} + +function Get-RegressionBuildDirectoryName { + param( + [Parameter(Mandatory = $true)] + [string]$ConfigurationName + ) + + switch ($ConfigurationName) { + 'default_build_coverage' { return 'dbc' } + 'no_cache_build' { return 'nc' } + 'no_cache_standalone_build' { return 'ncs' } + 'fault_tolerant_build_coverage' { return 'ft' } + 'no_check_build' { return 'nck' } + 'no_cache_fault_tolerant_build' { return 'ncft' } + 'standalone_build_coverage' { return 'sa' } + 'standalone_fault_tolerant_build_coverage' { return 'saft' } + 'standalone_no_cache_fault_tolerant_build' { return 'sancft' } + default { + throw "Unsupported configuration: $ConfigurationName" + } + } +} + +function Enter-VisualStudioDevShell { + param( + [Parameter(Mandatory = $true)] + [string]$VsArch + ) + + $targetArch = switch ($VsArch) { + 'amd64' { 'x64' } + 'x86' { 'x86' } + default { $VsArch } + } + + if ((Get-Command cl -ErrorAction SilentlyContinue) -and ($env:VSCMD_ARG_TGT_ARCH -eq $targetArch)) { + return + } + + $vsWherePath = Join-Path ${env:ProgramFiles(x86)} 'Microsoft Visual Studio\Installer\vswhere.exe' + if (-not (Test-Path -LiteralPath $vsWherePath)) { + throw "Unable to locate vswhere.exe at $vsWherePath" + } + + $installationPath = & $vsWherePath -latest -products * -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath + if (-not $installationPath) { + throw 'Unable to locate a Visual Studio 2022 installation with MSVC build tools.' + } + + $launchScript = Join-Path $installationPath 'Common7\Tools\Launch-VsDevShell.ps1' + if (-not (Test-Path -LiteralPath $launchScript)) { + throw "Unable to locate Launch-VsDevShell.ps1 at $launchScript" + } + + $env:VSCMD_SKIP_SENDTELEMETRY = '1' + & $launchScript -VsInstallationPath $installationPath -Arch $VsArch -HostArch amd64 -SkipAutomaticLocation | Out-Null + + if (-not (Get-Command cl -ErrorAction SilentlyContinue)) { + throw 'MSVC compiler environment was not activated successfully.' + } +} + +function Remove-BuildDirectory { + param( + [Parameter(Mandatory = $true)] + [string]$Path, + + [Parameter(Mandatory = $true)] + [string]$RepoRoot + ) + + $fullRepoRoot = [System.IO.Path]::GetFullPath($RepoRoot) + $fullPath = [System.IO.Path]::GetFullPath($Path) + + if (-not $fullPath.StartsWith($fullRepoRoot, [System.StringComparison]::OrdinalIgnoreCase)) { + throw "Refusing to remove a directory outside the repository: $fullPath" + } + + if (Test-Path -LiteralPath $fullPath) { + try { + Remove-Item -LiteralPath $fullPath -Recurse -Force -ErrorAction Stop + return + } catch { + Write-Warning "Failed to remove build directory '$fullPath': $($_.Exception.Message)" + } + + Get-ChildItem -LiteralPath $fullPath -Force -Recurse -ErrorAction SilentlyContinue | ForEach-Object { + try { + if (($_.Attributes -band [System.IO.FileAttributes]::ReadOnly) -ne 0) { + $_.Attributes = ($_.Attributes -band (-bnot [System.IO.FileAttributes]::ReadOnly)) + } + } catch { + } + } + + try { + Remove-Item -LiteralPath $fullPath -Recurse -Force -ErrorAction Stop + } catch { + Write-Warning "Proceeding with partially cleaned build directory '$fullPath': $($_.Exception.Message)" + } + } +} + +function Remove-CtestTestingDirectory { + param( + [Parameter(Mandatory = $true)] + [string]$Path + ) + + if (-not (Test-Path -LiteralPath $Path)) { + return + } + + try { + Remove-Item -LiteralPath $Path -Recurse -Force -ErrorAction Stop + return + } catch { + Write-Warning "Failed to remove CTest directory '$Path': $($_.Exception.Message)" + } + + Get-ChildItem -LiteralPath $Path -Force -Recurse -ErrorAction SilentlyContinue | ForEach-Object { + try { + if (($_.Attributes -band [System.IO.FileAttributes]::ReadOnly) -ne 0) { + $_.Attributes = ($_.Attributes -band (-bnot [System.IO.FileAttributes]::ReadOnly)) + } + } catch { + } + } + + try { + Remove-Item -LiteralPath $Path -Recurse -Force -ErrorAction Stop + } catch { + Write-Warning "Proceeding with partially cleaned CTest directory '$Path': $($_.Exception.Message)" + } +} + +function Remove-NinjaLock { + param( + [Parameter(Mandatory = $true)] + [string]$Path + ) + + $ninjaLockPath = Join-Path $Path '.ninja_lock' + if (Test-Path -LiteralPath $ninjaLockPath) { + Remove-Item -LiteralPath $ninjaLockPath -Force + } +} + +function Get-WindowsDebuggerPath { + $debuggerPath = Get-CommandPathIfAvailable -CommandName 'cdb.exe' + if ($debuggerPath) { + return $debuggerPath + } + + $candidatePaths = @( + (Join-Path ${env:ProgramFiles(x86)} 'Windows Kits\10\Debuggers\x64\cdb.exe'), + (Join-Path ${env:ProgramFiles(x86)} 'Windows Kits\10\Debuggers\x86\cdb.exe') + ) + + foreach ($candidatePath in $candidatePaths) { + if (Test-Path -LiteralPath $candidatePath) { + return $candidatePath + } + } + + return $null +} + +function Get-SanitizedFileName { + param( + [Parameter(Mandatory = $true)] + [string]$Name + ) + + $safeName = [regex]::Replace($Name, '[<>:"/\\|?*]', '_') + $safeName = $safeName -replace '\s+', '_' + return $safeName +} + +function Initialize-MinidumpSupport { + if ($null -ne ('FileX.WindowsMiniDump' -as [type])) { + return + } + + Add-Type -TypeDefinition @' +using System; +using System.IO; +using System.Runtime.InteropServices; + +namespace FileX +{ + public static class WindowsMiniDump + { + [DllImport("Dbghelp.dll", SetLastError = true)] + private static extern bool MiniDumpWriteDump( + IntPtr hProcess, + uint processId, + IntPtr hFile, + uint dumpType, + IntPtr exceptionParam, + IntPtr userStreamParam, + IntPtr callbackParam); + + [DllImport("kernel32.dll", SetLastError = true)] + private static extern IntPtr OpenProcess(uint desiredAccess, bool inheritHandle, int processId); + + [DllImport("kernel32.dll", SetLastError = true)] + private static extern bool CloseHandle(IntPtr handle); + + private const uint ProcessQueryInformation = 0x0400U; + private const uint ProcessVmRead = 0x0010U; + private const uint ProcessDupHandle = 0x0040U; + + public static bool WriteDump(int processId, string dumpPath, uint dumpType, out int errorCode) + { + IntPtr processHandle = OpenProcess(ProcessQueryInformation | ProcessVmRead | ProcessDupHandle, false, processId); + if (processHandle == IntPtr.Zero) + { + errorCode = Marshal.GetLastWin32Error(); + return false; + } + + try + { + using (FileStream dumpStream = new FileStream(dumpPath, FileMode.Create, FileAccess.ReadWrite, FileShare.Read)) + { + bool success = MiniDumpWriteDump( + processHandle, + unchecked((uint)processId), + dumpStream.SafeFileHandle.DangerousGetHandle(), + dumpType, + IntPtr.Zero, + IntPtr.Zero, + IntPtr.Zero); + + errorCode = success ? 0 : Marshal.GetLastWin32Error(); + return success; + } + } + finally + { + CloseHandle(processHandle); + } + } + } +} +'@ +} + +function Wait-FileReadable { + param( + [Parameter(Mandatory = $true)] + [string]$Path, + + [Parameter()] + [int]$TimeoutSeconds = 10 + ) + + $deadline = (Get-Date).AddSeconds($TimeoutSeconds) + while ((Get-Date) -lt $deadline) { + if (-not (Test-Path -LiteralPath $Path)) { + Start-Sleep -Milliseconds 200 + continue + } + + try { + $fileStream = [System.IO.File]::Open($Path, [System.IO.FileMode]::Open, [System.IO.FileAccess]::Read, [System.IO.FileShare]::ReadWrite) + $fileStream.Dispose() + return $true + } + catch { + Start-Sleep -Milliseconds 200 + } + } + + return $false +} + +function Get-CtestTestMetadata { + param( + [Parameter(Mandatory = $true)] + [string]$BuildDir + ) + + $ctestOutput = & ctest --test-dir $BuildDir --show-only=json-v1 + if ($LASTEXITCODE -ne 0) { + throw "Unable to enumerate ctest metadata in $BuildDir" + } + + return (($ctestOutput -join [Environment]::NewLine) | ConvertFrom-Json).tests +} + +function Get-CtestFailedTestNames { + param( + [Parameter(Mandatory = $true)] + [string]$TestingTemporaryDir + ) + + $lastFailedPath = Join-Path $TestingTemporaryDir 'LastTestsFailed.log' + if (-not (Test-Path -LiteralPath $lastFailedPath)) { + return @() + } + + $failedTestNames = @() + foreach ($logLine in (Get-Content -LiteralPath $lastFailedPath)) { + if ([string]::IsNullOrWhiteSpace($logLine)) { + continue + } + + if ($logLine -match '^\s*\d+:(?.+)\s*$') { + $failedTestNames += $Matches['name'].Trim() + } + else { + $failedTestNames += $logLine.Trim() + } + } + + return $failedTestNames +} + +function Invoke-ProcessDumpCapture { + param( + [Parameter(Mandatory = $true)] + [int]$ProcessId, + + [Parameter(Mandatory = $true)] + [string]$DumpPath, + + [Parameter()] + [int]$TimeoutSeconds = 15 + ) + + $outputDirectory = Split-Path -Parent $DumpPath + if (-not (Test-Path -LiteralPath $outputDirectory)) { + New-Item -ItemType Directory -Path $outputDirectory | Out-Null + } + + Remove-Item -LiteralPath $DumpPath -Force -ErrorAction SilentlyContinue + + Initialize-MinidumpSupport + $dumpType = [uint32]0x00001006 + $errorCode = 0 + $dumpCaptured = [FileX.WindowsMiniDump]::WriteDump($ProcessId, $DumpPath, $dumpType, [ref]$errorCode) + + if (-not $dumpCaptured) { + Write-Warning "MiniDumpWriteDump failed for PID ${ProcessId} with Win32 error $errorCode" + return $false + } + + return (Test-Path -LiteralPath $DumpPath) +} + +function Invoke-DumpStackAnalysis { + param( + [Parameter(Mandatory = $true)] + [string]$DumpPath, + + [Parameter(Mandatory = $true)] + [string]$OutputBasePath, + + [Parameter()] + [string]$SymbolPath, + + [Parameter()] + [int]$TimeoutSeconds = 15 + ) + + if (-not (Test-Path -LiteralPath $DumpPath)) { + Write-Warning "Skipping dump analysis because the dump file was not created: $DumpPath" + return $false + } + + if (-not (Wait-FileReadable -Path $DumpPath)) { + Write-Warning "Skipping dump analysis because the dump file is not readable yet: $DumpPath" + return $false + } + + $debuggerPath = Get-WindowsDebuggerPath + if (-not $debuggerPath) { + Write-Warning 'Skipping dump analysis because cdb.exe is not available.' + return $false + } + + $outputDirectory = Split-Path -Parent $OutputBasePath + if (-not (Test-Path -LiteralPath $outputDirectory)) { + New-Item -ItemType Directory -Path $outputDirectory | Out-Null + } + + $stdoutPath = "${OutputBasePath}.stdout.txt" + $stderrPath = "${OutputBasePath}.stderr.txt" + $commandFilePath = "${OutputBasePath}.commands.txt" + Set-Content -LiteralPath $commandFilePath -Value @( + '!runaway 7' + '~* kb 200' + 'q' + ) -Encoding Ascii + $cdbArguments = @( + '-lines', + '-z', $DumpPath + ) + + if ($SymbolPath) { + $cdbArguments += @('-y', $SymbolPath) + } + + $cdbArguments += @('-cf', $commandFilePath) + $cdbProcess = Start-Process -FilePath $debuggerPath -ArgumentList $cdbArguments -PassThru -NoNewWindow ` + -RedirectStandardOutput $stdoutPath -RedirectStandardError $stderrPath + + try { + $cdbProcess | Wait-Process -Timeout $TimeoutSeconds -ErrorAction Stop + } + catch { + if (-not $cdbProcess.HasExited) { + $null = Start-Process -FilePath 'taskkill.exe' -ArgumentList @('/PID', $cdbProcess.Id.ToString(), '/T', '/F') ` + -WindowStyle Hidden -Wait -PassThru + } + } + + return $true +} + +function Invoke-ProcessWithTimeout { + param( + [Parameter(Mandatory = $true)] + [string]$FilePath, + + [Parameter()] + [string[]]$Arguments = @(), + + [Parameter()] + [int]$TimeoutSeconds = 0, + + [Parameter()] + [string]$WorkingDirectory, + + [Parameter()] + [string]$RedirectStandardOutputPath, + + [Parameter()] + [string]$RedirectStandardErrorPath, + + [Parameter()] + [scriptblock]$OnTimeout, + + [Parameter()] + [scriptblock]$PostTimeout + ) + + $argumentList = @() + foreach ($argument in $Arguments) { + if ($argument -match '\s|"') { + $argumentList += '"' + ($argument -replace '"', '\"') + '"' + } + else { + $argumentList += $argument + } + } + + $startProcessParameters = @{ + FilePath = $FilePath + NoNewWindow = $true + PassThru = $true + } + + if ($argumentList.Count -gt 0) { + $startProcessParameters['ArgumentList'] = $argumentList + } + + if ($WorkingDirectory) { + $startProcessParameters['WorkingDirectory'] = $WorkingDirectory + } + + if ($RedirectStandardOutputPath) { + $startProcessParameters['RedirectStandardOutput'] = $RedirectStandardOutputPath + } + + if ($RedirectStandardErrorPath) { + $startProcessParameters['RedirectStandardError'] = $RedirectStandardErrorPath + } + + $process = Start-Process @startProcessParameters + if ($TimeoutSeconds -le 0) { + $process | Wait-Process + $completed = $true + } + else { + try { + $process | Wait-Process -Timeout $TimeoutSeconds -ErrorAction Stop + $completed = $true + } + catch { + $completed = $false + } + } + + if (-not $completed) { + if ($null -ne $OnTimeout) { + & $OnTimeout $process + } + + $null = Start-Process -FilePath 'taskkill.exe' -ArgumentList @('/PID', $process.Id.ToString(), '/T', '/F') -WindowStyle Hidden -Wait -PassThru + + if ($null -ne $PostTimeout) { + & $PostTimeout $process + } + + return @{ + Completed = $false + ExitCode = $null + ProcessId = $process.Id + } + } + + $process.Refresh() + return @{ + Completed = $true + ExitCode = $process.ExitCode + ProcessId = $process.Id + } +} + +function Invoke-CtestFailureDiagnostics { + param( + [Parameter(Mandatory = $true)] + [string]$BuildDir, + + [Parameter(Mandatory = $true)] + [string]$TestingTemporaryDir, + + [Parameter(Mandatory = $true)] + [int]$TimeoutSeconds + ) + + $failedTestNames = Get-CtestFailedTestNames -TestingTemporaryDir $TestingTemporaryDir + if ($failedTestNames.Count -eq 0) { + Write-Warning "No failed tests were recorded in $TestingTemporaryDir" + return + } + + $testMetadataList = Get-CtestTestMetadata -BuildDir $BuildDir + $testMetadataMap = @{} + foreach ($testMetadata in $testMetadataList) { + $testMetadataMap[$testMetadata.name] = $testMetadata + } + + $diagnosticsRoot = Join-Path $TestingTemporaryDir 'FailureDiagnostics' + if (-not (Test-Path -LiteralPath $diagnosticsRoot)) { + New-Item -ItemType Directory -Path $diagnosticsRoot | Out-Null + } + + foreach ($failedTestName in $failedTestNames) { + if (-not $testMetadataMap.ContainsKey($failedTestName)) { + Write-Warning "Unable to locate ctest metadata for failed test: $failedTestName" + continue + } + + $testMetadata = $testMetadataMap[$failedTestName] + if (($null -eq $testMetadata.command) -or ($testMetadata.command.Count -eq 0)) { + Write-Warning "No executable command was recorded for failed test: $failedTestName" + continue + } + + $testArguments = @() + if ($testMetadata.command.Count -gt 1) { + $testArguments = @($testMetadata.command[1..($testMetadata.command.Count - 1)]) + } + + $safeTestName = Get-SanitizedFileName -Name $failedTestName + $stdoutPath = Join-Path $diagnosticsRoot "${safeTestName}.stdout.txt" + $stderrPath = Join-Path $diagnosticsRoot "${safeTestName}.stderr.txt" + $debugOutputBasePath = Join-Path $diagnosticsRoot "${safeTestName}.cdb" + $workingDirectory = $null + $symbolDirectory = Split-Path -Parent $testMetadata.command[0] + + if ($null -ne $testMetadata.properties) { + foreach ($testProperty in $testMetadata.properties) { + if ($testProperty.name -eq 'WORKING_DIRECTORY') { + $workingDirectory = $testProperty.value + break + } + } + } + + Write-Warning "Collecting failure diagnostics for $failedTestName" + $dumpPath = '{0}.{1}.dmp' -f $debugOutputBasePath, ([DateTime]::UtcNow.ToString('yyyyMMddHHmmssfff')) + $testResult = Invoke-ProcessWithTimeout -FilePath $testMetadata.command[0] -Arguments $testArguments ` + -TimeoutSeconds $TimeoutSeconds -WorkingDirectory $workingDirectory -RedirectStandardOutputPath $stdoutPath ` + -RedirectStandardErrorPath $stderrPath -OnTimeout { + param($timedOutProcess) + Invoke-ProcessDumpCapture -ProcessId $timedOutProcess.Id -DumpPath $dumpPath | Out-Null + } -PostTimeout { + param($timedOutProcess) + if (Test-Path -LiteralPath $dumpPath) { + Invoke-DumpStackAnalysis -DumpPath $dumpPath -OutputBasePath $debugOutputBasePath -SymbolPath $symbolDirectory | Out-Null + } + } + + if (-not $testResult.Completed) { + Write-Warning "Timeout diagnostics were captured for $failedTestName under $diagnosticsRoot" + continue + } + + Write-Warning "Replay finished for $failedTestName with exit code $($testResult.ExitCode). Output was saved under $diagnosticsRoot" + } +} + +function Test-IsNinjaBuildDirectory { + param( + [Parameter(Mandatory = $true)] + [string]$BuildDir + ) + + return (Test-Path -LiteralPath (Join-Path $BuildDir 'build.ninja')) +} + +function Get-NinjaBuildStatements { + param( + [Parameter(Mandatory = $true)] + [string]$BuildDir + ) + + $buildNinjaPath = Join-Path $BuildDir 'build.ninja' + if (-not (Test-Path -LiteralPath $buildNinjaPath)) { + throw "Unable to locate build.ninja in $BuildDir" + } + + return Get-Content -LiteralPath $buildNinjaPath +} + +function New-NinjaRspFile { + param( + [Parameter(Mandatory = $true)] + [string]$BuildDir, + + [Parameter(Mandatory = $true)] + [string]$RspRelativePath + ) + + $buildStatements = Get-NinjaBuildStatements -BuildDir $BuildDir + $rspLine = ' RSP_FILE = ' + $RspRelativePath + $rspIndex = -1 + + for ($index = 0; $index -lt $buildStatements.Count; $index++) { + if ($buildStatements[$index] -eq $rspLine) { + $rspIndex = $index + break + } + } + + if ($rspIndex -lt 0) { + throw "Unable to locate RSP_FILE entry for $RspRelativePath in build.ninja." + } + + $buildIndex = -1 + for ($index = $rspIndex; $index -ge 0; $index--) { + if ($buildStatements[$index].StartsWith('build ')) { + $buildIndex = $index + break + } + } + + if ($buildIndex -lt 0) { + throw "Unable to locate the build statement that owns $RspRelativePath." + } + + $buildLine = $buildStatements[$buildIndex] + if ($buildLine -notmatch '^build\s+\S+:\s+\S+\s+(.+)$') { + throw "Unable to parse build statement for $RspRelativePath." + } + + $rspContents = ($Matches[1] -split '\s+') -join [Environment]::NewLine + $rspPath = Join-Path $BuildDir $RspRelativePath + $rspParent = Split-Path -Parent $rspPath + + if (-not (Test-Path -LiteralPath $rspParent)) { + New-Item -ItemType Directory -Path $rspParent | Out-Null + } + + Set-Content -LiteralPath $rspPath -Value $rspContents +} + +function Ensure-NinjaRspFiles { + param( + [Parameter(Mandatory = $true)] + [string]$BuildDir, + + [Parameter(Mandatory = $true)] + [string]$CommandLine + ) + + $rspMatches = [regex]::Matches($CommandLine, '@(?[^\s"]+\.rsp)') + foreach ($rspMatch in $rspMatches) { + $rspRelativePath = $rspMatch.Groups['path'].Value + $rspPath = Join-Path $BuildDir $rspRelativePath + if (-not (Test-Path -LiteralPath $rspPath)) { + New-NinjaRspFile -BuildDir $BuildDir -RspRelativePath $rspRelativePath + } + } +} + +function Get-PendingNinjaCommands { + param( + [Parameter(Mandatory = $true)] + [string]$BuildDir + ) + + $commandLines = & ninja -C $BuildDir -t commands + if ($LASTEXITCODE -ne 0) { + throw "Unable to enumerate pending Ninja commands in $BuildDir" + } + + return $commandLines | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } +} + +function Invoke-NinjaFallbackBuild { + param( + [Parameter(Mandatory = $true)] + [string]$BuildDir + ) + + $pendingCommands = Get-PendingNinjaCommands -BuildDir $BuildDir + if ($pendingCommands.Count -eq 0) { + return + } + + Push-Location $BuildDir + try { + foreach ($pendingCommand in $pendingCommands) { + Ensure-NinjaRspFiles -BuildDir $BuildDir -CommandLine $pendingCommand + + $commandToRun = $pendingCommand -replace '\s/showIncludes(?=\s|$)', '' + + if ($commandToRun -match '^[^ ]*cmd(?:\.exe)?\s+/C\s+"(?.*)"\s*$') { + & cmd.exe /C $Matches['inner'] + } + else { + & cmd.exe /C $commandToRun + } + + if ($LASTEXITCODE -ne 0) { + throw "Fallback Ninja command failed with exit code ${LASTEXITCODE}: $pendingCommand" + } + } + } + finally { + Pop-Location + } +} + +function Invoke-CMakeBuild { + param( + [Parameter(Mandatory = $true)] + [string]$BuildDir, + + [Parameter(Mandatory = $true)] + [int]$Parallel, + + [Parameter()] + [int]$TimeoutSeconds = 0 + ) + + Remove-NinjaLock -Path $BuildDir + $isNinjaBuild = Test-IsNinjaBuildDirectory -BuildDir $BuildDir + + if ($TimeoutSeconds -le 0) { + if ($isNinjaBuild) { + Invoke-NativeCommand -FilePath 'ninja' -Arguments @( + '-C', $BuildDir, + '-j', $Parallel.ToString() + ) + } + else { + Invoke-NativeCommand -FilePath 'cmake' -Arguments @( + '--build', $BuildDir, + '--parallel', $Parallel.ToString() + ) + } + return + } + + if ($isNinjaBuild) { + $buildToolName = 'Ninja' + $buildResult = Invoke-ProcessWithTimeout -FilePath 'ninja' -Arguments @( + '-C', $BuildDir, + '-j', $Parallel.ToString() + ) -TimeoutSeconds $TimeoutSeconds + } + else { + $buildToolName = 'CMake' + $buildResult = Invoke-ProcessWithTimeout -FilePath 'cmake' -Arguments @( + '--build', $BuildDir, + '--parallel', $Parallel.ToString() + ) -TimeoutSeconds $TimeoutSeconds + } + + if ($buildResult.Completed -and ($buildResult.ExitCode -eq 0)) { + return + } + + if (-not $isNinjaBuild) { + if (-not $buildResult.Completed) { + throw "$buildToolName build timed out after $TimeoutSeconds seconds in $BuildDir" + } + + throw "$buildToolName build failed with exit code $($buildResult.ExitCode) in $BuildDir" + } + + if ($buildResult.Completed) { + throw "$buildToolName build failed with exit code $($buildResult.ExitCode) in $BuildDir" + } + + Write-Warning "$buildToolName build timed out after $TimeoutSeconds seconds in $BuildDir. Replaying pending Ninja commands from PowerShell." + + Remove-NinjaLock -Path $BuildDir + Invoke-NinjaFallbackBuild -BuildDir $BuildDir +} diff --git a/scripts/test_fx.ps1 b/scripts/test_fx.ps1 new file mode 100644 index 0000000..f60fdae --- /dev/null +++ b/scripts/test_fx.ps1 @@ -0,0 +1,121 @@ +[CmdletBinding()] +param( + [ValidateSet('win64', 'win32')] + [string]$Arch = 'win64', + + [AllowNull()] + [object]$Configuration = 'all', + + [int]$Parallel = [Math]::Max(1, [Environment]::ProcessorCount), + + [int]$RepeatFailCount = 1, + + [int]$TestTimeoutSeconds = 60, + + [switch]$CollectFailureDiagnostics = $true, + + [string]$TestRegex, + + [switch]$RerunFailedOnly, + + [string]$BuildDir, + + [switch]$Clean +) + +$ErrorActionPreference = 'Stop' +. (Join-Path $PSScriptRoot 'fx_windows_common.ps1') + +$repoRoot = Split-Path -Parent $PSScriptRoot +$settings = Get-PortSettings -SelectedArch $Arch + +if (-not $BuildDir) { + $BuildDir = Join-Path $repoRoot "build\tests\$Arch" +} + +$selectedConfigurations = Resolve-RegressionConfigurations -RequestedConfigurations $Configuration +Write-Host "Selected configurations: $($selectedConfigurations -join ', ')" + +Enter-VisualStudioDevShell -VsArch $settings.VsArch + +if (($settings.FileXArch -eq 'win32') -or ($settings.FileXArch -eq 'win64')) { + if ($Parallel -ne 1) { + Write-Warning "Windows simulator regression tests are timing-sensitive under concurrent ctest execution. Forcing -Parallel 1." + $Parallel = 1 + } +} + +$failedConfigurations = @() + +foreach ($currentConfiguration in $selectedConfigurations) { + $currentBuildDirName = Get-RegressionBuildDirectoryName -ConfigurationName $currentConfiguration + $currentBuildDir = Join-Path $BuildDir $currentBuildDirName + $currentTestingTemporaryDir = Join-Path $currentBuildDir 'Testing\Temporary' + + try { + if ($Clean) { + $currentTestingDir = Join-Path $currentBuildDir 'Testing' + Remove-CtestTestingDirectory -Path $currentTestingDir + } + + if (-not (Test-Path -LiteralPath $currentBuildDir)) { + throw "Build directory does not exist for $Arch / ${currentConfiguration}: $currentBuildDir" + } + + Remove-NinjaLock -Path $currentBuildDir + if (Test-Path -LiteralPath $currentTestingTemporaryDir) { + Remove-Item -LiteralPath (Join-Path $currentTestingTemporaryDir 'LastTest.log') -Force -ErrorAction SilentlyContinue + Remove-Item -LiteralPath (Join-Path $currentTestingTemporaryDir 'LastTestsFailed.log') -Force -ErrorAction SilentlyContinue + } + + Write-Host "Testing $Arch / $currentConfiguration" + $ctestArguments = @( + '--test-dir', $currentBuildDir, + '--output-on-failure', + '--timeout', $TestTimeoutSeconds.ToString(), + '-j', $Parallel.ToString() + ) + + if ($RepeatFailCount -gt 1) { + $ctestArguments += @('--repeat', "until-pass:$RepeatFailCount") + } + + if ($TestRegex) { + $ctestArguments += @('-R', $TestRegex) + } + + if ($RerunFailedOnly) { + $ctestArguments += '--rerun-failed' + } + + Invoke-NativeCommand -FilePath 'ctest' -Arguments $ctestArguments + } + catch { + if ($CollectFailureDiagnostics -and (Test-Path -LiteralPath $currentBuildDir)) { + try { + Invoke-CtestFailureDiagnostics -BuildDir $currentBuildDir -TestingTemporaryDir $currentTestingTemporaryDir ` + -TimeoutSeconds $TestTimeoutSeconds + } + catch { + Write-Warning "Failure diagnostics collection failed for ${currentConfiguration}: $($_.Exception.Message)" + } + } + + $failedConfigurations += @{ + Configuration = $currentConfiguration + Message = $_.Exception.Message + } + + Write-Warning "Configuration failed: $currentConfiguration" + } +} + +if ($failedConfigurations.Count -gt 0) { + Write-Host '' + Write-Host 'Configuration failure summary:' + foreach ($failedConfiguration in $failedConfigurations) { + Write-Host "- $($failedConfiguration.Configuration): $($failedConfiguration.Message)" + } + + throw "One or more configurations failed: $($failedConfigurations.Configuration -join ', ')" +} diff --git a/test/cmake/CMakeLists.txt b/test/cmake/CMakeLists.txt index a81cc17..d6ccd66 100644 --- a/test/cmake/CMakeLists.txt +++ b/test/cmake/CMakeLists.txt @@ -2,13 +2,23 @@ cmake_minimum_required(VERSION 3.13 FATAL_ERROR) cmake_policy(SET CMP0054 NEW) cmake_policy(SET CMP0057 NEW) -project(threadx_test LANGUAGES C) +# Suppress try_compile link step when MSVC cannot produce a runnable executable +# (e.g. when the VS environment is not fully initialised for a given target arch). +if((DEFINED THREADX_ARCH) AND ((THREADX_ARCH STREQUAL "win32") OR (THREADX_ARCH STREQUAL "win64"))) + set(CMAKE_TRY_COMPILE_TARGET_TYPE STATIC_LIBRARY) +endif() + +project(filex_test LANGUAGES C) + +set(CMAKE_C_STANDARD 99) +set(CMAKE_C_STANDARD_REQUIRED ON) +set(CMAKE_C_EXTENSIONS OFF) # Set build configurations set(BUILD_CONFIGURATIONS default_build_coverage no_cache_build no_cache_standalone_build fault_tolerant_build_coverage no_check_build no_cache_fault_tolerant_build - standalone_build_coverage standalone_fault_tolerant_build_coverage + standalone_build_coverage standalone_fault_tolerant_build_coverage standalone_no_cache_fault_tolerant_build) set(CMAKE_CONFIGURATION_TYPES ${BUILD_CONFIGURATIONS} @@ -31,25 +41,32 @@ set(FX_FAULT_TOLERANT_DEFINITIONS set(default_build_coverage "") set(no_cache_build -DFX_DISABLE_CACHE -DFX_DISABLE_FAT_ENTRY_REFRESH) set(fault_tolerant_build_coverage ${FX_FAULT_TOLERANT_DEFINITIONS}) -set(standalone_build_coverage -DFX_STANDALONE_ENABLE) +set(standalone_build_coverage -DFX_STANDALONE_ENABLE) set(standalone_fault_tolerant_build_coverage ${FX_FAULT_TOLERANT_DEFINITIONS} - -DFX_STANDALONE_ENABLE) + -DFX_STANDALONE_ENABLE) set(no_cache_standalone_build -DFX_DISABLE_CACHE -DFX_STANDALONE_ENABLE) -set(no_check_build ${FX_COMPILE_DEFINITIONS} -DFX_DISABLE_ERROR_CHECKING) +set(no_check_build -DFX_DISABLE_ERROR_CHECKING) set(no_cache_fault_tolerant_build ${no_cache_build} ${FX_FAULT_TOLERANT_DEFINITIONS}) set(standalone_no_cache_fault_tolerant_build ${no_cache_build} ${FX_FAULT_TOLERANT_DEFINITIONS} -DFX_STANDALONE_ENABLE) -add_compile_options( - -m32 - -std=c99 - -ggdb - -g3 - -gdwarf-2 - -fdiagnostics-color - -Werror - -DFX_REGRESSION_TEST - ${${CMAKE_BUILD_TYPE}}) -add_link_options(-m32) +if(MSVC) + add_compile_options(/W3 /Zi) + add_link_options(/DEBUG /INCREMENTAL:NO) + # Both cl.exe and gcc accept -DFOO; use it to keep the per-configuration + # definitions consistent with the Linux path. + add_compile_options(-DFX_REGRESSION_TEST ${${CMAKE_BUILD_TYPE}}) +else() + add_compile_options( + -m32 + -ggdb + -g3 + -gdwarf-2 + -fdiagnostics-color + -Werror + -DFX_REGRESSION_TEST + ${${CMAKE_BUILD_TYPE}}) + add_link_options(-m32) +endif() if(CMAKE_BUILD_TYPE MATCHES ".*standalone.*") set(FX_STANDALONE_ENABLE @@ -59,18 +76,30 @@ endif() enable_testing() +# ThreadX dependency: on Windows build from source; on Linux use run.sh. +if(MSVC) + if(NOT FX_STANDALONE_ENABLE) + if(NOT DEFINED THREADX_SOURCE_DIR) + message(FATAL_ERROR + "THREADX_SOURCE_DIR must point to a ThreadX source tree for non-standalone Windows builds. " + "Pass -DTHREADX_SOURCE_DIR= to cmake, or use build_fx.ps1 which sets it automatically.") + endif() + add_subdirectory(${THREADX_SOURCE_DIR} threadx_build) + endif() +endif() + add_subdirectory(${CMAKE_CURRENT_LIST_DIR}/../.. filex) add_subdirectory(regression) add_subdirectory(samples) -# Coverage -if(CMAKE_BUILD_TYPE MATCHES ".*_coverage") +# Coverage instrumentation (GCC/Clang only — not supported by MSVC) +if(NOT MSVC AND CMAKE_BUILD_TYPE MATCHES ".*_coverage") target_compile_options(filex PRIVATE -fprofile-arcs -ftest-coverage) target_link_options(filex PRIVATE -fprofile-arcs -ftest-coverage) endif() -# Build ThreadX library once -if(NOT FX_STANDALONE_ENABLE) +# Build ThreadX shared library for Linux +if(NOT MSVC AND NOT FX_STANDALONE_ENABLE) execute_process(COMMAND ${CMAKE_CURRENT_LIST_DIR}/run.sh build_libs) add_custom_target(build_libs ALL COMMAND ${CMAKE_CURRENT_LIST_DIR}/run.sh build_libs) @@ -83,23 +112,27 @@ if(NOT FX_STANDALONE_ENABLE) ${CMAKE_BINARY_DIR}/../libs/threadx/libthreadx.so) endif() -target_compile_options( - filex - PRIVATE -Werror - -Wall - -Wextra - -pedantic - -fmessage-length=0 - -fsigned-char - -ffunction-sections - -fdata-sections - -Wunused - -Wuninitialized - -Wmissing-declarations - -Wconversion - -Wpointer-arith - -Wshadow - -Wlogical-op - -Waggregate-return - -Wfloat-equal) +if(MSVC) + target_compile_options(filex PRIVATE /W3) +else() + target_compile_options( + filex + PRIVATE -Werror + -Wall + -Wextra + -pedantic + -fmessage-length=0 + -fsigned-char + -ffunction-sections + -fdata-sections + -Wunused + -Wuninitialized + -Wmissing-declarations + -Wconversion + -Wpointer-arith + -Wshadow + -Wlogical-op + -Waggregate-return + -Wfloat-equal) +endif() diff --git a/test/cmake/regression/CMakeLists.txt b/test/cmake/regression/CMakeLists.txt index 2674ef6..613f601 100644 --- a/test/cmake/regression/CMakeLists.txt +++ b/test/cmake/regression/CMakeLists.txt @@ -147,6 +147,9 @@ add_library(test_utility ${SOURCE_DIR}/fx_ram_driver_test.c ${SOURCE_DIR}/filextestcontrol.c) target_link_libraries(test_utility PUBLIC azrtos::filex) target_compile_definitions(test_utility PUBLIC BATCH_TEST CTEST) +if(MSVC AND FX_STANDALONE_ENABLE) + target_include_directories(test_utility PUBLIC ${SOURCE_DIR}/win64_compat) +endif() foreach(test_case ${regression_test_cases}) get_filename_component(test_name ${test_case} NAME_WE) @@ -154,3 +157,14 @@ foreach(test_case ${regression_test_cases}) target_link_libraries(${test_name} PRIVATE test_utility) add_test(${CMAKE_BUILD_TYPE}::${test_name} ${test_name}) endforeach() + +# These two tests write or corrupt large amounts of data on a 900 MB RAM disk and are +# inherently slow (≈95 s and ≈330 s on Windows). Override the default 60-second ctest +# timeout so that they are not mis-reported as failures. +if(MSVC) + set_tests_properties( + ${CMAKE_BUILD_TYPE}::filex_fault_tolerant_file_corruption_test + ${CMAKE_BUILD_TYPE}::filex_fault_tolerant_media_full_test + PROPERTIES TIMEOUT 600 + ) +endif() diff --git a/test/cmake/samples/CMakeLists.txt b/test/cmake/samples/CMakeLists.txt index 17530dc..b9824f0 100644 --- a/test/cmake/samples/CMakeLists.txt +++ b/test/cmake/samples/CMakeLists.txt @@ -12,6 +12,6 @@ foreach(sample_file ${sample_files}) get_filename_component(sample_file_name ${sample_file} NAME_WE) add_executable(${sample_file_name} ${sample_file} ${CMAKE_CURRENT_LIST_DIR}/../../regression_test/fx_ram_driver_test.c) - target_compile_options(${sample_file_name} PRIVATE -DSAMPLE_BUILD) + target_compile_definitions(${sample_file_name} PRIVATE SAMPLE_BUILD) target_link_libraries(${sample_file_name} PRIVATE azrtos::filex) endforeach() diff --git a/test/regression_test/fx_ram_driver_test.c b/test/regression_test/fx_ram_driver_test.c index 63eb4a5..938624f 100644 --- a/test/regression_test/fx_ram_driver_test.c +++ b/test/regression_test/fx_ram_driver_test.c @@ -1,4 +1,4 @@ -/**************************************************************************/ +/**************************************************************************/ /* */ /* Copyright (c) 1996-2017 by Express Logic Inc. */ /* */ @@ -81,6 +81,36 @@ ULONG64 media_size; UCHAR large_file_name_format[]; /* Define memory for tests to be run in standalone mode (without Azure RTOS: ThreadX) */ +/* On MSVC, BSS arrays cause demand-zero page faults on first access, which serialises + with test execution and makes large-disk tests significantly slower than on Linux. + Pre-allocating with calloc commits and zero-fills all pages before any test runs, + eliminating page-fault overhead at runtime. This also avoids LNK1248 (PE image > 2 GB) + for configurations that would otherwise exceed the hard linker limit. + On other compilers keep the BSS arrays as before. */ +#ifdef _MSC_VER +#include +UCHAR *ram_disk_memory_large = NULL; +UCHAR *large_data_buffer = NULL; + +#if defined(FX_STANDALONE_ENABLE) && !defined(SAMPLE_BUILD) +UCHAR *ram_disk_memory = NULL; +UCHAR *ram_disk_memory1 = NULL; +#endif + +static void _fx_alloc_test_buffers(void) +{ + ram_disk_memory_large = (UCHAR *)calloc(900000000UL, 1U); + large_data_buffer = (UCHAR *)calloc(900000000UL, 1U); +#if defined(FX_STANDALONE_ENABLE) && !defined(SAMPLE_BUILD) + ram_disk_memory = (UCHAR *)calloc(300000000UL, 1U); + ram_disk_memory1 = (UCHAR *)calloc( 30000000UL, 1U); +#endif +} +#pragma section(".CRT$XCU", read) +__declspec(allocate(".CRT$XCU")) static void (*_p_fx_alloc)(void) = _fx_alloc_test_buffers; + +#else /* !_MSC_VER */ + #if defined(FX_STANDALONE_ENABLE) && !defined(SAMPLE_BUILD) UCHAR ram_disk_memory[300000000]; UCHAR ram_disk_memory1[30000000]; @@ -88,6 +118,7 @@ UCHAR ram_disk_memory1[30000000]; UCHAR ram_disk_memory_large[900000000]; UCHAR large_data_buffer[900000000]; +#endif /* _MSC_VER */ /* Define the callback function. */ @@ -298,8 +329,16 @@ UINT offset; /* Calculate the RAM disk sector offset. Note the RAM disk memory is pointed to by the fx_media_driver_info pointer, which is supplied by the application in the call to fx_media_open. */ - source_buffer = ((UCHAR *) media_ptr -> fx_media_driver_info) + - ((media_ptr -> fx_media_driver_logical_sector + media_ptr -> fx_media_hidden_sectors) * media_ptr -> fx_media_bytes_per_sector); + { + /* Compute effective sector and guard against pointer arithmetic overflow on 64-bit + platforms when sector is near ULONG64_MAX. Out-of-range accesses are redirected + to large_data_buffer so the test can verify return codes without crashing. */ + ULONG64 _sector = media_ptr -> fx_media_driver_logical_sector + media_ptr -> fx_media_hidden_sectors; + if (media_ptr -> fx_media_total_sectors > 0 && _sector >= media_ptr -> fx_media_total_sectors) + source_buffer = large_data_buffer; /* harmless scratch — test does not verify data */ + else + source_buffer = ((UCHAR *) media_ptr -> fx_media_driver_info) + (_sector * media_ptr -> fx_media_bytes_per_sector); + } /* Copy the RAM sector into the destination. */ _fx_utility_memory_copy(source_buffer, media_ptr -> fx_media_driver_buffer, @@ -318,8 +357,17 @@ UINT offset; /* Calculate the RAM disk sector offset. Note the RAM disk memory is pointed to by the fx_media_driver_info pointer, which is supplied by the application in the call to fx_media_open. */ - data_start = ((UCHAR *) media_ptr -> fx_media_driver_info) + - ((media_ptr -> fx_media_driver_logical_sector + media_ptr -> fx_media_hidden_sectors) * media_ptr -> fx_media_bytes_per_sector); + { + /* Compute effective sector and guard against pointer arithmetic overflow on 64-bit + platforms when sector is near ULONG64_MAX. Out-of-range writes are discarded. */ + ULONG64 _sector = media_ptr -> fx_media_driver_logical_sector + media_ptr -> fx_media_hidden_sectors; + if (media_ptr -> fx_media_total_sectors > 0 && _sector >= media_ptr -> fx_media_total_sectors) + { + media_ptr -> fx_media_driver_status = FX_SUCCESS; + return; + } + data_start = ((UCHAR *) media_ptr -> fx_media_driver_info) + (_sector * media_ptr -> fx_media_bytes_per_sector); + } /* Driver write callback function entry for calling. */ if (driver_write_callback != FX_NULL) diff --git a/test/regression_test/win64_compat/pthread.h b/test/regression_test/win64_compat/pthread.h new file mode 100644 index 0000000..2ae24b3 --- /dev/null +++ b/test/regression_test/win64_compat/pthread.h @@ -0,0 +1,68 @@ +/* SPDX-License-Identifier: MIT */ +/* Copyright (c) 2025 Contributors to the Eclipse Foundation */ +/* AI-generated with GitHub Copilot */ + +/* Minimal pthreads shim for FileX regression tests on Windows/x64. */ + +#ifndef WIN64_COMPAT_PTHREAD_H +#define WIN64_COMPAT_PTHREAD_H + +#include + +typedef HANDLE pthread_t; +typedef void pthread_attr_t; + +#define PTHREAD_CANCEL_ENABLE 0 +#define PTHREAD_CANCEL_DISABLE 1 +#define PTHREAD_CANCEL_DEFERRED 0 +#define PTHREAD_CANCEL_ASYNCHRONOUS 1 + +static inline int pthread_create(pthread_t *tid, const pthread_attr_t *attr, + void *(*start_routine)(void *), void *arg) +{ + (void)attr; + *tid = CreateThread(NULL, 0U, (LPTHREAD_START_ROUTINE)(void *)start_routine, arg, 0U, NULL); + return (*tid == NULL) ? -1 : 0; +} + +static inline int pthread_join(pthread_t tid, void **retval) +{ + (void)retval; + WaitForSingleObject(tid, INFINITE); + CloseHandle(tid); + return 0; +} + +static inline int pthread_cancel(pthread_t tid) +{ + TerminateThread(tid, 0U); + return 0; +} + +/* No-op: allows test_control_thread_entry to reach exit(test_control_failed_tests). */ +static inline void pthread_exit(void *retval) +{ + (void)retval; +} + +static inline int pthread_setcancelstate(int state, int *oldstate) +{ + (void)state; + if (oldstate != NULL) + { + *oldstate = PTHREAD_CANCEL_ENABLE; + } + return 0; +} + +static inline int pthread_setcanceltype(int type, int *oldtype) +{ + (void)type; + if (oldtype != NULL) + { + *oldtype = PTHREAD_CANCEL_DEFERRED; + } + return 0; +} + +#endif /* WIN64_COMPAT_PTHREAD_H */ diff --git a/test/regression_test/win64_compat/unistd.h b/test/regression_test/win64_compat/unistd.h new file mode 100644 index 0000000..89a99ee --- /dev/null +++ b/test/regression_test/win64_compat/unistd.h @@ -0,0 +1,22 @@ +/* SPDX-License-Identifier: MIT */ +/* Copyright (c) 2025 Contributors to the Eclipse Foundation */ +/* AI-generated with GitHub Copilot */ + +/* Minimal POSIX unistd shim for FileX regression tests on Windows/x64. */ + +#ifndef WIN64_COMPAT_UNISTD_H +#define WIN64_COMPAT_UNISTD_H + +#include + +typedef unsigned int useconds_t; + +/* Sleep for `usec` microseconds. Windows resolution is ~1 ms; round up. */ +static inline int usleep(useconds_t usec) +{ + DWORD ms = (DWORD)((usec + 999U) / 1000U); + Sleep((ms > 0U) ? ms : 1U); + return 0; +} + +#endif /* WIN64_COMPAT_UNISTD_H */