From a6868524f9c5a927ceba7aeef02697d662df5839 Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Mon, 20 Jul 2026 13:05:08 +0700 Subject: [PATCH 1/7] setup test using latest nuget packages --- .gitignore | 5 ++ README.rst | 48 ++++++++++ flexlibs/code/FLExGlobals.py | 135 ++++++++++++++++++++++------- make.bat | 71 ++++++++++++--- tests/nuget-overlay/Marker.cs | 7 ++ tests/nuget-overlay/Overlay.csproj | 19 ++++ tests/nuget-overlay/restore.ps1 | 55 ++++++++++++ 7 files changed, 295 insertions(+), 45 deletions(-) create mode 100644 tests/nuget-overlay/Marker.cs create mode 100644 tests/nuget-overlay/Overlay.csproj create mode 100644 tests/nuget-overlay/restore.ps1 diff --git a/.gitignore b/.gitignore index fffe044..9cc170b 100644 --- a/.gitignore +++ b/.gitignore @@ -54,6 +54,11 @@ coverage.xml .hypothesis/ .pytest_cache/ +# NuGet overlay restore output (make test-nuget) +.fw-nuget-overlay/ +tests/nuget-overlay/bin/ +tests/nuget-overlay/obj/ + # Translations *.mo *.pot diff --git a/README.rst b/README.rst index 2dd1e52..3980323 100644 --- a/README.rst +++ b/README.rst @@ -33,6 +33,54 @@ Installation Run: ``pip install flexlibs`` +Testing against latest NuGet packages +------------------------------------- + +By default flexlibs loads FieldWorks assemblies from an installed FieldWorks +9.x tree (discovered via the Windows registry). + +For integration testing against newer SIL libraries from nuget.org, restore +an overlay and run the existing tests:: + + make test-nuget + +This publishes the latest prerelease ``SIL.LCModel``, ``SIL.Core``, +``SIL.WritingSystems`` (and related) packages into ``.fw-nuget-overlay`` and +sets ``FLEXLIBS_ASSEMBLY_DIR`` so those DLLs shadow the copies from the +FieldWorks install. App assemblies such as ``FwUtils`` still come from +FieldWorks. + +Environment overrides (useful for CI without a Windows installer) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +- ``FLEXLIBS_FW_CODE_DIR`` — directory containing ``FieldWorks.exe`` and app DLLs +- ``FLEXLIBS_FW_PROJECTS_DIR`` — FieldWorks projects directory (required when + ``FLEXLIBS_FW_CODE_DIR`` is set) +- ``FLEXLIBS_ASSEMBLY_DIR`` — optional folder of DLLs prepended on the assembly + search path (set automatically by ``make test-nuget``) + +When the code/projects env vars are set, registry discovery is skipped. You +still need a complete FieldWorks binary tree (install or unzipped build) plus +matching Python bitness; NuGet alone does not supply ``FwUtils`` / +``FieldWorks.exe``. + +Requires the .NET SDK (for ``dotnet publish``) in addition to the normal +test prerequisites. + +The custom-field write test needs a FieldWorks project with an entry-level +custom text field. Defaults are project ``__flexlibs_testing`` and field +``EntryFlags``; override with:: + + set FLEXLIBS_TEST_PROJECT=MyProject + set FLEXLIBS_TEST_CUSTOM_FIELD=MyCustomField + +The other tests use any project already present in the projects directory. + +Skip the custom-field test (or pass any pytest args):: + + .\make.bat test-nuget -k "not CustomFields" + + Usage ----- diff --git a/flexlibs/code/FLExGlobals.py b/flexlibs/code/FLExGlobals.py index 9a51ddf..f2dec02 100644 --- a/flexlibs/code/FLExGlobals.py +++ b/flexlibs/code/FLExGlobals.py @@ -55,6 +55,11 @@ "9" : r"SOFTWARE\SIL\FieldWorks\9", } +# Env overrides (CI / no-installer layouts). Checked before registry. +ENV_FW_CODE_DIR = "FLEXLIBS_FW_CODE_DIR" +ENV_FW_PROJECTS_DIR = "FLEXLIBS_FW_PROJECTS_DIR" +ENV_ASSEMBLY_DIR = "FLEXLIBS_ASSEMBLY_DIR" + # ---------------------------------------------------------------- def GetFWRegKey(): @@ -87,19 +92,7 @@ def GetFWRegKey(): # ------------------------------------------------------------------- -def InitialiseFWGlobals(): - global FWCodeDir - global FWProjectsDir - global FWExecutable - global FWShortVersion - global FWLongVersion - - try: - rKey = GetFWRegKey() - except Exception as e: - logging.exception("Couldn't find FieldWorks registry entry") - raise - +def _resolveCodeDirFromRegistry(rKey): if platform.system() == "Linux": # ********************************************************** # TODO: First attempt at Linux support. @@ -116,15 +109,12 @@ def InitialiseFWGlobals(): # which in turn calls /usr/lib/fieldworks/run-app FieldWorks.exe etc. # # The following is based on the logic in /usr/lib/fieldworks/environ - FWCodeDir = os.path.join(rKey.GetValue(FWREG_CODEDIR), "../../lib/fieldworks") + codeDir = os.path.join(rKey.GetValue(FWREG_CODEDIR), "../../lib/fieldworks") else: # On windows, FWREG_CODEDIR is correct. - FWCodeDir = rKey.GetValue(FWREG_CODEDIR) + codeDir = rKey.GetValue(FWREG_CODEDIR) - # FWREG_PROJECTSDIR is correct on Windows and Linux. - FWProjectsDir = rKey.GetValue(FWREG_PROJECTSDIR) - - if not os.access(os.path.join(FWCodeDir, "FieldWorks.exe"), os.F_OK): + if not os.access(os.path.join(codeDir, "FieldWorks.exe"), os.F_OK): # On developer's machines we also check the build directories # for FieldWorks.exe if platform.system() == "Linux": @@ -134,25 +124,98 @@ def InitialiseFWGlobals(): else: # Windows devPaths = [ - os.path.join(FWCodeDir, r"..\Output\Release\\"), - os.path.join(FWCodeDir, r"..\Output\Debug\\"), + os.path.join(codeDir, r"..\Output\Release\\"), + os.path.join(codeDir, r"..\Output\Debug\\"), ] for p in devPaths: if os.access(os.path.join(p, "FieldWorks.exe"), os.F_OK): - FWCodeDir = p - break - else: - # This can happen if there is a ghost registry entry for - # an uninstalled FLEx - msg = "FieldWorks.exe not found in %s" \ - % FWCodeDir - logger.error(msg) - raise Exception(msg) + return p + + msg = "FieldWorks.exe not found in %s" % codeDir + logger.error(msg) + raise Exception(msg) + + return codeDir + + +def _resolvePathsFromEnv(): + """ + Resolve FWCodeDir / FWProjectsDir from FLEXLIBS_* env vars. + Returns (codeDir, projectsDir) or None if FLEXLIBS_FW_CODE_DIR is unset. + """ + codeDir = os.environ.get(ENV_FW_CODE_DIR) + if not codeDir: + return None + + codeDir = os.path.abspath(codeDir) + if not os.path.isdir(codeDir): + raise Exception("%s is not a directory: %s" % (ENV_FW_CODE_DIR, codeDir)) + if not os.access(os.path.join(codeDir, "FieldWorks.exe"), os.F_OK): + raise Exception("FieldWorks.exe not found in %s=%s" % (ENV_FW_CODE_DIR, codeDir)) + + projectsDir = os.environ.get(ENV_FW_PROJECTS_DIR) + if not projectsDir: + raise Exception( + "%s is set but %s is missing (required for env-based discovery)" + % (ENV_FW_CODE_DIR, ENV_FW_PROJECTS_DIR)) + + projectsDir = os.path.abspath(projectsDir) + if not os.path.isdir(projectsDir): + raise Exception("%s is not a directory: %s" % (ENV_FW_PROJECTS_DIR, projectsDir)) + + logger.info("Using FieldWorks paths from environment") + return codeDir, projectsDir + + +def _applyAssemblyOverlay(): + """ + If FLEXLIBS_ASSEMBLY_DIR is set, prepend it to sys.path so NuGet + (or other) DLLs shadow copies from the FieldWorks code directory. + """ + overlayDir = os.environ.get(ENV_ASSEMBLY_DIR) + if not overlayDir: + return None + + overlayDir = os.path.abspath(overlayDir) + if not os.path.isdir(overlayDir): + raise Exception("%s is not a directory: %s" % (ENV_ASSEMBLY_DIR, overlayDir)) + + dlls = [f for f in os.listdir(overlayDir) if f.lower().endswith(".dll")] + if not dlls: + raise Exception("%s contains no DLLs: %s" % (ENV_ASSEMBLY_DIR, overlayDir)) + + sys.path.insert(0, overlayDir) + logger.info("Assembly overlay active: %s (%d DLLs)" % (overlayDir, len(dlls))) + return overlayDir + + +def InitialiseFWGlobals(): + global FWCodeDir + global FWProjectsDir + global FWExecutable + global FWShortVersion + global FWLongVersion + + envPaths = _resolvePathsFromEnv() + if envPaths: + FWCodeDir, FWProjectsDir = envPaths + else: + try: + rKey = GetFWRegKey() + except Exception as e: + logging.exception("Couldn't find FieldWorks registry entry") + raise + + FWCodeDir = _resolveCodeDirFromRegistry(rKey) + # FWREG_PROJECTSDIR is correct on Windows and Linux. + FWProjectsDir = rKey.GetValue(FWREG_PROJECTSDIR) + logger.info("Using FieldWorks paths from registry") FWExecutable = os.path.join(FWCodeDir, "FieldWorks.exe") - - # Add the FW code directory to the search path for importing FW libs. + + # Optional NuGet/other overlay first, then the FieldWorks code dir. + _applyAssemblyOverlay() sys.path.append(FWCodeDir) logger.info("sys.path = \n\t%s" % "\n\t".join(sys.path)) @@ -172,3 +235,11 @@ def InitialiseFWGlobals(): logger.info("FWShortVersion = %s" % FWShortVersion) logger.info("FWLongVersion = %s" % FWLongVersion) + # When overlaying NuGet packages, log where a key SIL assembly loaded from. + try: + clr.AddReference("SIL.LCModel") + from SIL.LCModel import LcmCache + lcmAsm = Assembly.GetAssembly(LcmCache) + logger.info("SIL.LCModel loaded from %s" % lcmAsm.Location) + except Exception: + logger.debug("Could not report SIL.LCModel load location", exc_info=True) diff --git a/make.bat b/make.bat index 03549ae..972dc63 100644 --- a/make.bat +++ b/make.bat @@ -1,12 +1,22 @@ @ECHO OFF REM Simple build commands for flexlibs +SETLOCAL EnableExtensions EnableDelayedExpansion -REM Build with the default Python version -set PYTHON=py +REM Prefer a supported Python (flexlibs requires >=3.8,<3.14). +REM Override with: set PYTHON=py -3.11 +if not defined PYTHON ( + call :FindPython + if not defined PYTHON ( + echo No supported Python found ^(need 3.8-3.13^). Install one or set PYTHON=. + exit /b 1 + ) +) +echo Using PYTHON=%PYTHON% REM Check that the argument is a valid command, and do it. /I ignores case. FOR %%C IN ("Init" "Test" + "Test-nuget" "Clean" "Build" "Publish") DO ( @@ -17,40 +27,75 @@ FOR %%C IN ("Init" echo Usage: echo make init - Install the libraries for building echo make test - Run the unit tests + echo make test-nuget - Restore latest SIL NuGet packages and run tests with overlay echo make clean - Clean out build files echo make build - Build the project echo make publish - Publish the project to PyPI - goto :End + echo. + echo Extra args after test / test-nuget are passed to pytest, e.g.: + echo .\make.bat test-nuget -k "not CustomFields" -q + exit /b 1 :DoInit %PYTHON% -m pip install -r requirements.txt - goto :End + exit /b !ERRORLEVEL! :DoTest - %PYTHON% -m pytest - goto :End + call :CollectPytestArgs %* + %PYTHON% -m pytest !PYTEST_ARGS! + exit /b !ERRORLEVEL! + +:DoTest-nuget + call :CollectPytestArgs %* + powershell -NoProfile -ExecutionPolicy Bypass -File ".\tests\nuget-overlay\restore.ps1" + if errorlevel 1 exit /b 1 + set "FLEXLIBS_ASSEMBLY_DIR=%CD%\.fw-nuget-overlay" + %PYTHON% -m pytest !PYTEST_ARGS! + exit /b !ERRORLEVEL! :DoClean - rmdir /s /q ".\build" - rmdir /s /q ".\dist" - rmdir /s /q ".\flexlibs\docs" - goto :End + if exist ".\build" rmdir /s /q ".\build" + if exist ".\dist" rmdir /s /q ".\dist" + if exist ".\flexlibs\docs" rmdir /s /q ".\flexlibs\docs" + if exist ".\.fw-nuget-overlay" rmdir /s /q ".\.fw-nuget-overlay" + if exist ".\tests\nuget-overlay\bin" rmdir /s /q ".\tests\nuget-overlay\bin" + if exist ".\tests\nuget-overlay\obj" rmdir /s /q ".\tests\nuget-overlay\obj" + exit /b 0 :DoBuild @REM Build the Sphinx docs sphinx-build docs/sphinx flexlibs/docs/flexlibsAPI + if errorlevel 1 exit /b 1 @REM Build the wheel with setuptools %PYTHON% -m build -w -nx + if errorlevel 1 exit /b 1 @REM Check for package errors %PYTHON% -m twine check .\dist\* - goto :End + exit /b !ERRORLEVEL! :DoPublish echo Publishing wheel to PyPI %PYTHON% -m twine upload .\dist\flexlibs* - goto :End + exit /b !ERRORLEVEL! +:FindPython + for %%V in (3.13 3.12 3.11 3.10 3.9 3.8) do ( + py -%%V -c "import sys" >nul 2>&1 + if !ERRORLEVEL! EQU 0 ( + set "PYTHON=py -%%V" + goto :eof + ) + ) + goto :eof -:End +:CollectPytestArgs + REM Drop the make command (%1), keep the rest for pytest. + set "PYTEST_ARGS=" + shift +:CollectPytestArgsLoop + if "%~1"=="" goto :eof + set "PYTEST_ARGS=!PYTEST_ARGS! %1" + shift + goto CollectPytestArgsLoop diff --git a/tests/nuget-overlay/Marker.cs b/tests/nuget-overlay/Marker.cs new file mode 100644 index 0000000..6181577 --- /dev/null +++ b/tests/nuget-overlay/Marker.cs @@ -0,0 +1,7 @@ +// Placeholder so the SDK builds a library; only the package DLLs matter. +namespace FlexlibsNugetOverlay +{ + public static class Marker + { + } +} diff --git a/tests/nuget-overlay/Overlay.csproj b/tests/nuget-overlay/Overlay.csproj new file mode 100644 index 0000000..0e8d7e2 --- /dev/null +++ b/tests/nuget-overlay/Overlay.csproj @@ -0,0 +1,19 @@ + + + net462 + Library + FlexlibsNugetOverlay + disable + disable + + true + + + + + + + + + + diff --git a/tests/nuget-overlay/restore.ps1 b/tests/nuget-overlay/restore.ps1 new file mode 100644 index 0000000..91257d3 --- /dev/null +++ b/tests/nuget-overlay/restore.ps1 @@ -0,0 +1,55 @@ +#Requires -Version 5.1 +<# +.SYNOPSIS + Restore latest SIL NuGet packages into a flat folder for flexlibs overlay tests. + +.DESCRIPTION + Publishes tests/nuget-overlay to .fw-nuget-overlay at the repo root and writes + versions.txt listing resolved package versions. +#> +$ErrorActionPreference = "Stop" + +$RepoRoot = Resolve-Path (Join-Path $PSScriptRoot "..\..") +$Project = Join-Path $PSScriptRoot "Overlay.csproj" +$OutDir = Join-Path $RepoRoot ".fw-nuget-overlay" + +Write-Host "Publishing NuGet overlay to $OutDir" + +if (Test-Path $OutDir) { + Remove-Item -Recurse -Force $OutDir +} + +dotnet publish $Project -c Release -o $OutDir --nologo +if ($LASTEXITCODE -ne 0) { + throw "dotnet publish failed with exit code $LASTEXITCODE" +} + +$DllCount = @(Get-ChildItem -Path $OutDir -Filter "*.dll" -File).Count +if ($DllCount -eq 0) { + throw "No DLLs published to $OutDir" +} + +# Record resolved package versions from the assets file for debugging. +$Assets = Join-Path $PSScriptRoot "obj\project.assets.json" +$VersionsPath = Join-Path $OutDir "versions.txt" +$Lines = New-Object System.Collections.Generic.List[string] +$Lines.Add("Published: $(Get-Date -Format o)") +$Lines.Add("DLL count: $DllCount") +$Lines.Add("") + +if (Test-Path $Assets) { + $Lines.Add("Resolved packages (from project.assets.json):") + $json = Get-Content -Raw $Assets | ConvertFrom-Json + $libs = $json.libraries.PSObject.Properties + foreach ($lib in ($libs | Sort-Object Name)) { + if ($lib.Name -match '^(SIL\.(LCModel|Core|WritingSystems))') { + $Lines.Add(" $($lib.Name)") + } + } +} else { + $Lines.Add("(project.assets.json not found; versions unknown)") +} + +$Lines | Set-Content -Path $VersionsPath -Encoding UTF8 +Write-Host "Wrote $VersionsPath ($DllCount DLLs)" +Write-Host "Set FLEXLIBS_ASSEMBLY_DIR=$OutDir to use this overlay." From 27013771540b0a44718a2c1e903b614f282e52a1 Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Fri, 24 Jul 2026 13:41:25 +0700 Subject: [PATCH 2/7] Add Windows CI that installs FieldWorks and runs NuGet overlay tests. Downloads the offline FW 9.3.9 installer (cached), silently installs, seeds a sample project, and runs pytest with the NuGet assembly overlay. Co-authored-by: Cursor --- .github/workflows/test-windows.yml | 84 +++++++++++++++++++++++ flexlibs/tests/test_CustomFields.py | 14 ++-- scripts/ci/Install-FieldWorks.ps1 | 101 ++++++++++++++++++++++++++++ scripts/ci/Setup-SampleProject.ps1 | 63 +++++++++++++++++ 4 files changed, 257 insertions(+), 5 deletions(-) create mode 100644 .github/workflows/test-windows.yml create mode 100644 scripts/ci/Install-FieldWorks.ps1 create mode 100644 scripts/ci/Setup-SampleProject.ps1 diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml new file mode 100644 index 0000000..23d6706 --- /dev/null +++ b/.github/workflows/test-windows.yml @@ -0,0 +1,84 @@ +name: Windows tests + +on: + push: + branches: [main, "ci/**"] + pull_request: + workflow_dispatch: + +concurrency: + group: windows-tests-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + FW_VERSION: "9.3.9.1" + FW_BUILD: "1439" + FW_CODE_DIR: "C:\\Program Files\\SIL\\FieldWorks 9" + FW_PROJECTS_DIR: "C:\\ProgramData\\SIL\\FieldWorks\\Projects" + FW_INSTALLER_DIR: "C:\\fw-ci\\installer" + FW_SAMPLE_DIR: "C:\\fw-ci\\samples" + +jobs: + test-nuget-overlay: + name: FieldWorks + NuGet overlay + runs-on: windows-latest + timeout-minutes: 120 + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + architecture: x64 + + - name: Set up .NET SDK + uses: actions/setup-dotnet@v4 + with: + dotnet-version: "8.0.x" + + - name: Cache FieldWorks install + installer + sample + id: cache-fw + uses: actions/cache@v4 + with: + path: | + ${{ env.FW_CODE_DIR }} + ${{ env.FW_PROJECTS_DIR }} + ${{ env.FW_INSTALLER_DIR }} + ${{ env.FW_SAMPLE_DIR }} + C:\ProgramData\SIL\FieldWorks + key: fw-${{ env.FW_VERSION }}-${{ env.FW_BUILD }}-win-x64-v1 + + - name: Install FieldWorks (or refresh registry after cache restore) + shell: pwsh + run: | + ./scripts/ci/Install-FieldWorks.ps1 ` + -Version $env:FW_VERSION ` + -Build $env:FW_BUILD ` + -CodeDir $env:FW_CODE_DIR ` + -ProjectsDir $env:FW_PROJECTS_DIR ` + -DownloadDir $env:FW_INSTALLER_DIR + + - name: Set up sample FieldWorks project + shell: pwsh + run: | + ./scripts/ci/Setup-SampleProject.ps1 ` + -ProjectsDir $env:FW_PROJECTS_DIR ` + -DownloadDir $env:FW_SAMPLE_DIR + + - name: Install Python test dependencies + run: | + python -m pip install --upgrade pip + python -m pip install -r requirements.txt + python -m pip install -e . + + - name: Restore NuGet overlay and run tests + shell: cmd + env: + PYTHON: python + FLEXLIBS_FW_CODE_DIR: ${{ env.FW_CODE_DIR }} + FLEXLIBS_FW_PROJECTS_DIR: ${{ env.FW_PROJECTS_DIR }} + run: | + call make.bat test-nuget -k "not CustomFields" -q diff --git a/flexlibs/tests/test_CustomFields.py b/flexlibs/tests/test_CustomFields.py index 223e47f..184b40b 100644 --- a/flexlibs/tests/test_CustomFields.py +++ b/flexlibs/tests/test_CustomFields.py @@ -1,14 +1,17 @@ from builtins import str +import os import unittest from flexlibs import FLExInitialize, FLExCleanup from flexlibs import FLExProject, AllProjectNames, FP_FileLockedError # --- Constants --- +# Override with FLEXLIBS_TEST_PROJECT / FLEXLIBS_TEST_CUSTOM_FIELD. +# The project must have an entry-level custom text field with that name. -TEST_PROJECT = r"__flexlibs_testing" -CUSTOM_FIELD = r"EntryFlags" +TEST_PROJECT = os.environ.get("FLEXLIBS_TEST_PROJECT", r"__flexlibs_testing") +CUSTOM_FIELD = os.environ.get("FLEXLIBS_TEST_CUSTOM_FIELD", r"EntryFlags") CUSTOM_VALUE = r"Test.Value" #----------------------------------------------------------- @@ -32,7 +35,7 @@ def _openProject(self): except Exception as e: self.fail("Exception opening project %s:\n%s" % - (TEST_PROJECT, e.Message)) + (TEST_PROJECT, e)) return fp def _closeProject(self, fp): @@ -42,7 +45,8 @@ def test_WriteFields(self): fp = self._openProject() flags_field = fp.LexiconGetEntryCustomFieldNamed(CUSTOM_FIELD) if not flags_field: - self.fail("Entry-level custom field named '%s' not found." % CUSTOM_FIELD) + self.fail("Entry-level custom field named '%s' not found in project '%s'." + % (CUSTOM_FIELD, TEST_PROJECT)) # Traverse the whole lexicon for lexEntry in fp.LexiconAllEntries(): @@ -51,7 +55,7 @@ def test_WriteFields(self): fp.LexiconSetFieldText(lexEntry, flags_field, CUSTOM_VALUE) except Exception as e: self.fail("Exception writing custom field %s:\n%s" % - (CUSTOM_FIELD, e.Message)) + (CUSTOM_FIELD, e)) # Read back and check that the values were written. for lexEntry in fp.LexiconAllEntries(): diff --git a/scripts/ci/Install-FieldWorks.ps1 b/scripts/ci/Install-FieldWorks.ps1 new file mode 100644 index 0000000..6f90086 --- /dev/null +++ b/scripts/ci/Install-FieldWorks.ps1 @@ -0,0 +1,101 @@ +#Requires -Version 5.1 +<# +.SYNOPSIS + Download and silently install FieldWorks for CI, or restore registry after a cache hit. + +.DESCRIPTION + Uses the offline x64 installer from downloads.languagetechnology.org. + On cache restore of the install directories, skip download/install and only + ensure HKLM registry keys exist so discovery (and FwRegistryHelper) work. +#> +[CmdletBinding()] +param( + [string]$Version = "9.3.9.1", + [string]$Build = "1439", + [string]$InstallerUrl = "", + [string]$CodeDir = "C:\Program Files\SIL\FieldWorks 9", + [string]$ProjectsDir = "C:\ProgramData\SIL\FieldWorks\Projects", + [string]$DownloadDir = "C:\fw-ci\installer", + [string]$InstallLog = "C:\fw-ci\fw-install.log" +) + +$ErrorActionPreference = "Stop" + +function Ensure-Registry { + param([string]$CodeDir, [string]$ProjectsDir) + + $regPath = "HKLM:\SOFTWARE\SIL\FieldWorks\9" + if (-not (Test-Path $regPath)) { + New-Item -Path $regPath -Force | Out-Null + } + New-ItemProperty -Path $regPath -Name "RootCodeDir" -Value $CodeDir -PropertyType String -Force | Out-Null + New-ItemProperty -Path $regPath -Name "ProjectsDir" -Value $ProjectsDir -PropertyType String -Force | Out-Null + New-ItemProperty -Path $regPath -Name "Data_Directory" -Value (Split-Path $ProjectsDir -Parent) -PropertyType String -Force | Out-Null + Write-Host "Registry ready: RootCodeDir=$CodeDir ProjectsDir=$ProjectsDir" +} + +function Test-FieldWorksInstalled { + param([string]$CodeDir) + return (Test-Path (Join-Path $CodeDir "FieldWorks.exe")) -and (Test-Path (Join-Path $CodeDir "FwUtils.dll")) +} + +if (-not $InstallerUrl) { + # Path pattern from https://software.sil.org/fieldworks/download/fw-93/fw-939/ + $InstallerUrl = "https://downloads.languagetechnology.org/fieldworks/9.3.9/$Build/FieldWorks_${Version}_Offline_x64.exe" +} + +New-Item -ItemType Directory -Force -Path $DownloadDir | Out-Null +New-Item -ItemType Directory -Force -Path (Split-Path $InstallLog) | Out-Null +New-Item -ItemType Directory -Force -Path $ProjectsDir | Out-Null + +if (Test-FieldWorksInstalled -CodeDir $CodeDir) { + Write-Host "FieldWorks already present at $CodeDir (cache hit or prior install)" + Ensure-Registry -CodeDir $CodeDir -ProjectsDir $ProjectsDir + exit 0 +} + +$installerName = Split-Path $InstallerUrl -Leaf +$installerPath = Join-Path $DownloadDir $installerName + +if (-not (Test-Path $installerPath)) { + Write-Host "Downloading FieldWorks installer..." + Write-Host " URL: $InstallerUrl" + Write-Host " To: $installerPath" + # Use BITS-friendly curl for large files with resume + & curl.exe -L --retry 3 --retry-delay 5 -o $installerPath $InstallerUrl + if ($LASTEXITCODE -ne 0) { + throw "Download failed with exit code $LASTEXITCODE" + } +} else { + Write-Host "Using cached installer: $installerPath" +} + +$sizeMB = [math]::Round((Get-Item $installerPath).Length / 1MB, 1) +Write-Host "Installer size: ${sizeMB} MB" +Write-Host "Silent installing FieldWorks (/quiet /norestart)..." +Write-Host "Log: $InstallLog" + +$proc = Start-Process -FilePath $installerPath ` + -ArgumentList @("/quiet", "/norestart", "/log", $InstallLog) ` + -Wait -PassThru -NoNewWindow + +Write-Host "Installer exit code: $($proc.ExitCode)" +# Burn/WiX often returns 0 (success), 3010 (reboot required), or 1641 (restart initiated) +if ($proc.ExitCode -notin @(0, 3010, 1641)) { + if (Test-Path $InstallLog) { + Write-Host "---- last 80 lines of install log ----" + Get-Content $InstallLog -Tail 80 + } + throw "FieldWorks installer failed with exit code $($proc.ExitCode)" +} + +if (-not (Test-FieldWorksInstalled -CodeDir $CodeDir)) { + if (Test-Path $InstallLog) { + Write-Host "---- last 80 lines of install log ----" + Get-Content $InstallLog -Tail 80 + } + throw "FieldWorks.exe not found after install at $CodeDir" +} + +Ensure-Registry -CodeDir $CodeDir -ProjectsDir $ProjectsDir +Write-Host "FieldWorks install complete." diff --git a/scripts/ci/Setup-SampleProject.ps1 b/scripts/ci/Setup-SampleProject.ps1 new file mode 100644 index 0000000..b1c7674 --- /dev/null +++ b/scripts/ci/Setup-SampleProject.ps1 @@ -0,0 +1,63 @@ +#Requires -Version 5.1 +<# +.SYNOPSIS + Download a sample FieldWorks backup and extract it into the projects directory. +#> +[CmdletBinding()] +param( + [string]$ProjectsDir = "C:\ProgramData\SIL\FieldWorks\Projects", + [string]$BackupUrl = "https://downloads.languagetechnology.org/fieldworks/9.0.4/Sena%202%202026-06-09%201659.fwbackup", + [string]$ProjectName = "Sena 2", + [string]$DownloadDir = "C:\fw-ci\samples" +) + +$ErrorActionPreference = "Stop" +Add-Type -AssemblyName System.IO.Compression.FileSystem + +New-Item -ItemType Directory -Force -Path $DownloadDir | Out-Null +New-Item -ItemType Directory -Force -Path $ProjectsDir | Out-Null + +$projectDir = Join-Path $ProjectsDir $ProjectName +$fwdata = Join-Path $projectDir ($ProjectName + ".fwdata") +if (Test-Path $fwdata) { + Write-Host "Sample project already present: $fwdata" + exit 0 +} + +$backupPath = Join-Path $DownloadDir ($ProjectName + ".fwbackup") +if (-not (Test-Path $backupPath)) { + Write-Host "Downloading sample project backup..." + Write-Host " URL: $BackupUrl" + & curl.exe -L --retry 3 --retry-delay 5 -o $backupPath $BackupUrl + if ($LASTEXITCODE -ne 0) { + throw "Sample project download failed with exit code $LASTEXITCODE" + } +} + +if (Test-Path $projectDir) { + Remove-Item -Recurse -Force $projectDir +} +New-Item -ItemType Directory -Force -Path $projectDir | Out-Null + +Write-Host "Extracting $backupPath -> $projectDir" +[System.IO.Compression.ZipFile]::ExtractToDirectory($backupPath, $projectDir) + +# fwbackup often has ProjectName.fwdata at the archive root +if (-not (Test-Path $fwdata)) { + $found = Get-ChildItem -Path $projectDir -Filter "*.fwdata" -Recurse | Select-Object -First 1 + if (-not $found) { + throw "No .fwdata found after extracting sample project" + } + if ($found.DirectoryName -ne $projectDir) { + Move-Item -Force $found.FullName (Join-Path $projectDir $found.Name) + } + if ($found.Name -ne ($ProjectName + ".fwdata")) { + Rename-Item -Force (Join-Path $projectDir $found.Name) ($ProjectName + ".fwdata") + } +} + +if (-not (Test-Path $fwdata)) { + throw "Expected project file missing: $fwdata" +} + +Write-Host "Sample project ready: $fwdata" From 035e0c2872f90a7f9c63c73e21543ca727b915fb Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Fri, 24 Jul 2026 13:53:05 +0700 Subject: [PATCH 3/7] Fix CI FieldWorks discovery and fall back to BuildDir.zip. Silent installer reports success but may not leave FieldWorks.exe at the expected path; search harder, upload logs, and extract the matching GitHub BuildDir.zip if needed. Co-authored-by: Cursor --- .github/workflows/test-windows.yml | 19 +++- scripts/ci/Install-FieldWorks.ps1 | 166 ++++++++++++++++++++++++----- 2 files changed, 159 insertions(+), 26 deletions(-) diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 23d6706..b2f1344 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -49,7 +49,10 @@ jobs: ${{ env.FW_INSTALLER_DIR }} ${{ env.FW_SAMPLE_DIR }} C:\ProgramData\SIL\FieldWorks - key: fw-${{ env.FW_VERSION }}-${{ env.FW_BUILD }}-win-x64-v1 + C:\fw-ci\FieldWorks + C:\fw-ci\builddir + C:\fw-ci\fw-code-dir.txt + key: fw-${{ env.FW_VERSION }}-${{ env.FW_BUILD }}-win-x64-v2 - name: Install FieldWorks (or refresh registry after cache restore) shell: pwsh @@ -61,6 +64,16 @@ jobs: -ProjectsDir $env:FW_PROJECTS_DIR ` -DownloadDir $env:FW_INSTALLER_DIR + - name: Upload FieldWorks install logs + if: failure() + uses: actions/upload-artifact@v4 + with: + name: fw-install-logs + path: | + C:\fw-ci\fw-install*.log + C:\fw-ci\fw-code-dir.txt + if-no-files-found: ignore + - name: Set up sample FieldWorks project shell: pwsh run: | @@ -81,4 +94,8 @@ jobs: FLEXLIBS_FW_CODE_DIR: ${{ env.FW_CODE_DIR }} FLEXLIBS_FW_PROJECTS_DIR: ${{ env.FW_PROJECTS_DIR }} run: | + if exist C:\fw-ci\fw-code-dir.txt ( + set /p FLEXLIBS_FW_CODE_DIR= [CmdletBinding()] param( @@ -16,7 +11,8 @@ param( [string]$CodeDir = "C:\Program Files\SIL\FieldWorks 9", [string]$ProjectsDir = "C:\ProgramData\SIL\FieldWorks\Projects", [string]$DownloadDir = "C:\fw-ci\installer", - [string]$InstallLog = "C:\fw-ci\fw-install.log" + [string]$InstallLog = "C:\fw-ci\fw-install.log", + [string]$CodeDirOutFile = "C:\fw-ci\fw-code-dir.txt" ) $ErrorActionPreference = "Stop" @@ -36,11 +32,114 @@ function Ensure-Registry { function Test-FieldWorksInstalled { param([string]$CodeDir) + if (-not $CodeDir) { return $false } return (Test-Path (Join-Path $CodeDir "FieldWorks.exe")) -and (Test-Path (Join-Path $CodeDir "FwUtils.dll")) } +function Find-FieldWorksCodeDir { + param([string]$Preferred) + + if (Test-FieldWorksInstalled -CodeDir $Preferred) { + return $Preferred + } + + foreach ($hive in @("HKLM:\SOFTWARE\SIL\FieldWorks\9", "HKLM:\SOFTWARE\WOW6432Node\SIL\FieldWorks\9")) { + if (Test-Path $hive) { + $fromReg = (Get-ItemProperty -Path $hive -ErrorAction SilentlyContinue).RootCodeDir + if (Test-FieldWorksInstalled -CodeDir $fromReg) { + Write-Host "Found FieldWorks via registry $hive => $fromReg" + return $fromReg + } + } + } + + $searchRoots = @( + "C:\Program Files\SIL", + "C:\Program Files (x86)\SIL", + "C:\Program Files", + "C:\Program Files (x86)" + ) + foreach ($root in $searchRoots) { + if (-not (Test-Path $root)) { continue } + $exe = Get-ChildItem -Path $root -Filter "FieldWorks.exe" -Recurse -ErrorAction SilentlyContinue | + Select-Object -First 1 + if ($exe) { + Write-Host "Found FieldWorks.exe at $($exe.FullName)" + return $exe.DirectoryName + } + } + return $null +} + +function Write-Diagnostics { + Write-Host "---- Diagnostics ----" + foreach ($p in @( + "C:\Program Files\SIL", + "C:\Program Files (x86)\SIL", + "C:\ProgramData\SIL" + )) { + if (Test-Path $p) { + Write-Host "Listing $p" + Get-ChildItem $p -ErrorAction SilentlyContinue | ForEach-Object { Write-Host " $($_.FullName)" } + } else { + Write-Host "Missing: $p" + } + } + foreach ($hive in @("HKLM:\SOFTWARE\SIL", "HKLM:\SOFTWARE\WOW6432Node\SIL")) { + if (Test-Path $hive) { + Write-Host "Registry under $hive :" + Get-ChildItem $hive -Recurse -ErrorAction SilentlyContinue | ForEach-Object { Write-Host " $($_.Name)" } + } + } + Get-ChildItem "C:\fw-ci\fw-install*.log" -ErrorAction SilentlyContinue | ForEach-Object { + Write-Host "---- $($_.Name) (last 40 lines) ----" + Get-Content $_.FullName -Tail 40 + } +} + +function Install-FromBuildDir { + param( + [string]$Build, + [string]$TargetDir = "C:\fw-ci\FieldWorks", + [string]$ZipDir = "C:\fw-ci\builddir" + ) + + New-Item -ItemType Directory -Force -Path $ZipDir | Out-Null + $zipPath = Join-Path $ZipDir "BuildDir-$Build.zip" + $url = "https://github.com/sillsdev/FieldWorks/releases/download/build-$Build/BuildDir.zip" + + if (-not (Test-Path $zipPath)) { + Write-Host "Downloading FieldWorks BuildDir.zip ($url)..." + & curl.exe -L --retry 3 --retry-delay 5 -o $zipPath $url + if ($LASTEXITCODE -ne 0) { + throw "BuildDir.zip download failed with exit code $LASTEXITCODE" + } + } else { + Write-Host "Using cached BuildDir.zip: $zipPath" + } + + if (Test-Path $TargetDir) { + Remove-Item -Recurse -Force $TargetDir + } + New-Item -ItemType Directory -Force -Path $TargetDir | Out-Null + + Write-Host "Extracting BuildDir.zip to $TargetDir (this may take a few minutes)..." + # Prefer tar on Windows (faster); fall back to Expand-Archive + & tar.exe -xf $zipPath -C $TargetDir 2>$null + if ($LASTEXITCODE -ne 0) { + Expand-Archive -Path $zipPath -DestinationPath $TargetDir -Force + } + + $exe = Get-ChildItem -Path $TargetDir -Filter "FieldWorks.exe" -Recurse -ErrorAction SilentlyContinue | + Select-Object -First 1 + if (-not $exe) { + throw "BuildDir.zip extracted but FieldWorks.exe was not found under $TargetDir" + } + Write-Host "BuildDir FieldWorks.exe at $($exe.FullName)" + return $exe.DirectoryName +} + if (-not $InstallerUrl) { - # Path pattern from https://software.sil.org/fieldworks/download/fw-93/fw-939/ $InstallerUrl = "https://downloads.languagetechnology.org/fieldworks/9.3.9/$Build/FieldWorks_${Version}_Offline_x64.exe" } @@ -48,9 +147,23 @@ New-Item -ItemType Directory -Force -Path $DownloadDir | Out-Null New-Item -ItemType Directory -Force -Path (Split-Path $InstallLog) | Out-Null New-Item -ItemType Directory -Force -Path $ProjectsDir | Out-Null -if (Test-FieldWorksInstalled -CodeDir $CodeDir) { - Write-Host "FieldWorks already present at $CodeDir (cache hit or prior install)" - Ensure-Registry -CodeDir $CodeDir -ProjectsDir $ProjectsDir +$existing = Find-FieldWorksCodeDir -Preferred $CodeDir +if (-not $existing) { + $existing = Find-FieldWorksCodeDir -Preferred "C:\fw-ci\FieldWorks" +} +if (-not $existing) { + # BuildDir.zip may extract with a nested folder + $nested = Get-ChildItem "C:\fw-ci\FieldWorks" -Filter "FieldWorks.exe" -Recurse -ErrorAction SilentlyContinue | + Select-Object -First 1 + if ($nested) { $existing = $nested.DirectoryName } +} +if ($existing) { + Write-Host "FieldWorks already present at $existing (cache hit or prior install)" + Ensure-Registry -CodeDir $existing -ProjectsDir $ProjectsDir + Set-Content -Path $CodeDirOutFile -Value $existing -Encoding UTF8 + if ($env:GITHUB_ENV) { + Add-Content -Path $env:GITHUB_ENV -Value "FW_CODE_DIR=$existing" + } exit 0 } @@ -61,7 +174,6 @@ if (-not (Test-Path $installerPath)) { Write-Host "Downloading FieldWorks installer..." Write-Host " URL: $InstallerUrl" Write-Host " To: $installerPath" - # Use BITS-friendly curl for large files with resume & curl.exe -L --retry 3 --retry-delay 5 -o $installerPath $InstallerUrl if ($LASTEXITCODE -ne 0) { throw "Download failed with exit code $LASTEXITCODE" @@ -72,30 +184,34 @@ if (-not (Test-Path $installerPath)) { $sizeMB = [math]::Round((Get-Item $installerPath).Length / 1MB, 1) Write-Host "Installer size: ${sizeMB} MB" -Write-Host "Silent installing FieldWorks (/quiet /norestart)..." +Write-Host "Silent installing FieldWorks (/quiet /norestart ADDLOCAL=ALL)..." Write-Host "Log: $InstallLog" $proc = Start-Process -FilePath $installerPath ` - -ArgumentList @("/quiet", "/norestart", "/log", $InstallLog) ` + -ArgumentList @("/quiet", "/norestart", "/log", $InstallLog, "ADDLOCAL=ALL") ` -Wait -PassThru -NoNewWindow Write-Host "Installer exit code: $($proc.ExitCode)" -# Burn/WiX often returns 0 (success), 3010 (reboot required), or 1641 (restart initiated) if ($proc.ExitCode -notin @(0, 3010, 1641)) { - if (Test-Path $InstallLog) { - Write-Host "---- last 80 lines of install log ----" - Get-Content $InstallLog -Tail 80 - } + Write-Diagnostics throw "FieldWorks installer failed with exit code $($proc.ExitCode)" } -if (-not (Test-FieldWorksInstalled -CodeDir $CodeDir)) { - if (Test-Path $InstallLog) { - Write-Host "---- last 80 lines of install log ----" - Get-Content $InstallLog -Tail 80 - } - throw "FieldWorks.exe not found after install at $CodeDir" +$resolved = Find-FieldWorksCodeDir -Preferred $CodeDir +if (-not $resolved) { + Write-Host "Installer finished but FieldWorks.exe not found; falling back to GitHub BuildDir.zip" + $resolved = Install-FromBuildDir -Build $Build -TargetDir "C:\fw-ci\FieldWorks" +} + +if (-not $resolved) { + Write-Diagnostics + throw "FieldWorks.exe not found after install (checked Program Files, registry, and BuildDir.zip)" } -Ensure-Registry -CodeDir $CodeDir -ProjectsDir $ProjectsDir +Write-Host "Resolved FieldWorks code dir: $resolved" +Ensure-Registry -CodeDir $resolved -ProjectsDir $ProjectsDir +Set-Content -Path $CodeDirOutFile -Value $resolved -Encoding UTF8 +if ($env:GITHUB_ENV) { + Add-Content -Path $env:GITHUB_ENV -Value "FW_CODE_DIR=$resolved" +} Write-Host "FieldWorks install complete." From 7a6f55579d6a671c51cc88e1dcb4ea81885eed90 Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Fri, 24 Jul 2026 14:02:41 +0700 Subject: [PATCH 4/7] Point CI at the actual FieldWorks9 install path. The silent offline installer places files under C:\Program Files\FieldWorks9 rather than SIL\FieldWorks 9. Co-authored-by: Cursor --- .github/workflows/test-windows.yml | 2 +- scripts/ci/Install-FieldWorks.ps1 | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index b2f1344..9e5c18c 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -13,7 +13,7 @@ concurrency: env: FW_VERSION: "9.3.9.1" FW_BUILD: "1439" - FW_CODE_DIR: "C:\\Program Files\\SIL\\FieldWorks 9" + FW_CODE_DIR: "C:\\Program Files\\FieldWorks9" FW_PROJECTS_DIR: "C:\\ProgramData\\SIL\\FieldWorks\\Projects" FW_INSTALLER_DIR: "C:\\fw-ci\\installer" FW_SAMPLE_DIR: "C:\\fw-ci\\samples" diff --git a/scripts/ci/Install-FieldWorks.ps1 b/scripts/ci/Install-FieldWorks.ps1 index 4b1e763..83a7837 100644 --- a/scripts/ci/Install-FieldWorks.ps1 +++ b/scripts/ci/Install-FieldWorks.ps1 @@ -8,7 +8,7 @@ param( [string]$Version = "9.3.9.1", [string]$Build = "1439", [string]$InstallerUrl = "", - [string]$CodeDir = "C:\Program Files\SIL\FieldWorks 9", + [string]$CodeDir = "C:\Program Files\FieldWorks9", [string]$ProjectsDir = "C:\ProgramData\SIL\FieldWorks\Projects", [string]$DownloadDir = "C:\fw-ci\installer", [string]$InstallLog = "C:\fw-ci\fw-install.log", @@ -54,6 +54,7 @@ function Find-FieldWorksCodeDir { } $searchRoots = @( + "C:\Program Files\FieldWorks9", "C:\Program Files\SIL", "C:\Program Files (x86)\SIL", "C:\Program Files", From 4fc7bbe381e02210ec24504e0be9ef5528513586 Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Fri, 24 Jul 2026 14:12:37 +0700 Subject: [PATCH 5/7] Bump GitHub Actions to current major versions. checkout/setup-python/upload-artifact v7, setup-dotnet/cache v6. Co-authored-by: Cursor --- .github/workflows/test-windows.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 9e5c18c..653b8e7 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -26,22 +26,22 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v7 - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v7 with: python-version: "3.12" architecture: x64 - name: Set up .NET SDK - uses: actions/setup-dotnet@v4 + uses: actions/setup-dotnet@v6 with: dotnet-version: "8.0.x" - name: Cache FieldWorks install + installer + sample id: cache-fw - uses: actions/cache@v4 + uses: actions/cache@v6 with: path: | ${{ env.FW_CODE_DIR }} @@ -66,7 +66,7 @@ jobs: - name: Upload FieldWorks install logs if: failure() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: fw-install-logs path: | From a25f97a0ed4699868821ff0686128fd616966486 Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Fri, 24 Jul 2026 14:30:13 +0700 Subject: [PATCH 6/7] Remove untested BuildDir.zip fallback from CI install. The silent offline installer path is what green CI used; drop the unused GitHub BuildDir recovery path and its cache entries. Co-authored-by: Cursor --- .github/workflows/test-windows.yml | 4 +- scripts/ci/Install-FieldWorks.ps1 | 70 ++++-------------------------- 2 files changed, 10 insertions(+), 64 deletions(-) diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 653b8e7..38bb662 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -49,10 +49,8 @@ jobs: ${{ env.FW_INSTALLER_DIR }} ${{ env.FW_SAMPLE_DIR }} C:\ProgramData\SIL\FieldWorks - C:\fw-ci\FieldWorks - C:\fw-ci\builddir C:\fw-ci\fw-code-dir.txt - key: fw-${{ env.FW_VERSION }}-${{ env.FW_BUILD }}-win-x64-v2 + key: fw-${{ env.FW_VERSION }}-${{ env.FW_BUILD }}-win-x64-v3 - name: Install FieldWorks (or refresh registry after cache restore) shell: pwsh diff --git a/scripts/ci/Install-FieldWorks.ps1 b/scripts/ci/Install-FieldWorks.ps1 index 83a7837..773fde0 100644 --- a/scripts/ci/Install-FieldWorks.ps1 +++ b/scripts/ci/Install-FieldWorks.ps1 @@ -75,6 +75,7 @@ function Find-FieldWorksCodeDir { function Write-Diagnostics { Write-Host "---- Diagnostics ----" foreach ($p in @( + "C:\Program Files\FieldWorks9", "C:\Program Files\SIL", "C:\Program Files (x86)\SIL", "C:\ProgramData\SIL" @@ -98,46 +99,13 @@ function Write-Diagnostics { } } -function Install-FromBuildDir { - param( - [string]$Build, - [string]$TargetDir = "C:\fw-ci\FieldWorks", - [string]$ZipDir = "C:\fw-ci\builddir" - ) - - New-Item -ItemType Directory -Force -Path $ZipDir | Out-Null - $zipPath = Join-Path $ZipDir "BuildDir-$Build.zip" - $url = "https://github.com/sillsdev/FieldWorks/releases/download/build-$Build/BuildDir.zip" - - if (-not (Test-Path $zipPath)) { - Write-Host "Downloading FieldWorks BuildDir.zip ($url)..." - & curl.exe -L --retry 3 --retry-delay 5 -o $zipPath $url - if ($LASTEXITCODE -ne 0) { - throw "BuildDir.zip download failed with exit code $LASTEXITCODE" - } - } else { - Write-Host "Using cached BuildDir.zip: $zipPath" - } - - if (Test-Path $TargetDir) { - Remove-Item -Recurse -Force $TargetDir - } - New-Item -ItemType Directory -Force -Path $TargetDir | Out-Null - - Write-Host "Extracting BuildDir.zip to $TargetDir (this may take a few minutes)..." - # Prefer tar on Windows (faster); fall back to Expand-Archive - & tar.exe -xf $zipPath -C $TargetDir 2>$null - if ($LASTEXITCODE -ne 0) { - Expand-Archive -Path $zipPath -DestinationPath $TargetDir -Force - } +function Write-CodeDirOut { + param([string]$Resolved) - $exe = Get-ChildItem -Path $TargetDir -Filter "FieldWorks.exe" -Recurse -ErrorAction SilentlyContinue | - Select-Object -First 1 - if (-not $exe) { - throw "BuildDir.zip extracted but FieldWorks.exe was not found under $TargetDir" + Set-Content -Path $CodeDirOutFile -Value $Resolved -Encoding UTF8 + if ($env:GITHUB_ENV) { + Add-Content -Path $env:GITHUB_ENV -Value "FW_CODE_DIR=$Resolved" } - Write-Host "BuildDir FieldWorks.exe at $($exe.FullName)" - return $exe.DirectoryName } if (-not $InstallerUrl) { @@ -149,22 +117,10 @@ New-Item -ItemType Directory -Force -Path (Split-Path $InstallLog) | Out-Null New-Item -ItemType Directory -Force -Path $ProjectsDir | Out-Null $existing = Find-FieldWorksCodeDir -Preferred $CodeDir -if (-not $existing) { - $existing = Find-FieldWorksCodeDir -Preferred "C:\fw-ci\FieldWorks" -} -if (-not $existing) { - # BuildDir.zip may extract with a nested folder - $nested = Get-ChildItem "C:\fw-ci\FieldWorks" -Filter "FieldWorks.exe" -Recurse -ErrorAction SilentlyContinue | - Select-Object -First 1 - if ($nested) { $existing = $nested.DirectoryName } -} if ($existing) { Write-Host "FieldWorks already present at $existing (cache hit or prior install)" Ensure-Registry -CodeDir $existing -ProjectsDir $ProjectsDir - Set-Content -Path $CodeDirOutFile -Value $existing -Encoding UTF8 - if ($env:GITHUB_ENV) { - Add-Content -Path $env:GITHUB_ENV -Value "FW_CODE_DIR=$existing" - } + Write-CodeDirOut -Resolved $existing exit 0 } @@ -199,20 +155,12 @@ if ($proc.ExitCode -notin @(0, 3010, 1641)) { } $resolved = Find-FieldWorksCodeDir -Preferred $CodeDir -if (-not $resolved) { - Write-Host "Installer finished but FieldWorks.exe not found; falling back to GitHub BuildDir.zip" - $resolved = Install-FromBuildDir -Build $Build -TargetDir "C:\fw-ci\FieldWorks" -} - if (-not $resolved) { Write-Diagnostics - throw "FieldWorks.exe not found after install (checked Program Files, registry, and BuildDir.zip)" + throw "FieldWorks.exe not found after install (checked Program Files and registry)" } Write-Host "Resolved FieldWorks code dir: $resolved" Ensure-Registry -CodeDir $resolved -ProjectsDir $ProjectsDir -Set-Content -Path $CodeDirOutFile -Value $resolved -Encoding UTF8 -if ($env:GITHUB_ENV) { - Add-Content -Path $env:GITHUB_ENV -Value "FW_CODE_DIR=$resolved" -} +Write-CodeDirOut -Resolved $resolved Write-Host "FieldWorks install complete." From af621b7c88d28241c271e6152c9f39c148723983 Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Fri, 24 Jul 2026 14:44:30 +0700 Subject: [PATCH 7/7] Document follow-up ideas for deeper flexlibs integration tests. Capture a prioritized roadmap (project-shape, lexicon fields, write round-trip, overlay asserts, matrices) now that Windows CI can run against FieldWorks. Co-authored-by: Cursor --- docs/future-testing.md | 76 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 docs/future-testing.md diff --git a/docs/future-testing.md b/docs/future-testing.md new file mode 100644 index 0000000..39e22ae --- /dev/null +++ b/docs/future-testing.md @@ -0,0 +1,76 @@ +# Future testing ideas + +Notes for expanding beyond the current smoke tests now that Windows CI can +install FieldWorks, seed a sample project (Sena 2), and run the NuGet overlay. + +## Current coverage + +- `test_FLExInit` — initialize / cleanup +- `test_FLExProject` — list projects, open first project, walk headwords +- `test_CustomFields` — write/read a named entry custom field (skipped in CI; + needs a fixture project with that field) + +CI runs `make test-nuget -k "not CustomFields"` against the Sena 2 sample. + +## Suggested next steps (priority order) + +### 1. Project-shape read-only test + +Promote the checks in `flexlibs/examples/demo_openproject.py` into pytest: + +- parts of speech (non-empty list of strings) +- writing systems and default vernacular / analysis WS +- custom field discovery (entry / sense lists are iterable) +- lexicon entry count and text count are non-negative integers + +Use the sample project (or `FLEXLIBS_TEST_PROJECT`). Fully CI-friendly; no writes. + +### 2. Deeper lexicon field access + +Extend beyond “headword is a string”. For the first N entries (or a few known +ones), call and type-check: + +- lexeme / citation form +- sense gloss and POS +- semantic domains (may be empty) +- example text when present + +Still read-only. Catches writing-system and LCM regressions that the headword +smoke test misses. + +### 3. Write round-trip on a project copy + +Do not mutate the cached Sena 2 tree in place. In CI: + +1. Copy the project folder to a temp projects path +2. Open write-enabled +3. Set a gloss (or lexeme), read it back, clear it +4. Close and discard the copy + +Exercises the real write path without requiring `__flexlibs_testing` / +`EntryFlags`. Keep `test_CustomFields` as an optional local fixture. + +### 4. NuGet overlay load assertion + +When `FLEXLIBS_ASSEMBLY_DIR` is set, assert that `SIL.LCModel` (and optionally +`SIL.Core`) loaded from a path under that directory. Turns “overlay actually +applied” into a failing test instead of a log-only check. + +### 5. Optional matrices (later) + +- Python 3.9 and 3.12 +- Job without overlay (install DLLs only) vs with NuGet overlay +- Pinned LCM version vs floating `*-*` latest + +Useful for separating “new SIL beta broke us” from “our wrapper is wrong.” + +## Lower priority / skip for now + +- Deep custom-field and list-field coverage in CI (fixture-heavy) +- Text corpus, reversal indexes, publications (interesting later, lower ROI) +- Mocking LCM — wrong layer; this library needs real interop tests + +## Practical first PR + +Implement (1), (2), and (4) together: still fully read-only on Sena 2, then +add (3) once that suite is stable.