diff --git a/CHANGELOG.md b/CHANGELOG.md
index 36d65cd..1d4ff75 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,9 @@ will be documented in this file.
## [0.5.0] Unreleased
+- Add new `Locate` command
+ [#25](https://github.com/continuous-delphi/delphi-inspect/issues/25)
+
- Ensure `PowerShell 5.1` compatibility for the delphi-inspect.ps1 script
(Tests remain the newer `pwsh`)
[#23](https://github.com/continuous-delphi/delphi-inspect/issues/23)
@@ -47,7 +50,7 @@ Default platform parameter to `Win32`, default build parameter to `MSBuild`
-## `Delphi-Inspect` - a developer tool from Continuous Delphi
+## `delphi-inspect` - a developer tool from Continuous Delphi

diff --git a/README.md b/README.md
index feb3bdb..764d190 100644
--- a/README.md
+++ b/README.md
@@ -21,6 +21,13 @@ $all = pwsh delphi-inspect.ps1 -ListKnown
$inst = pwsh delphi-inspect.ps1 -ListInstalled -Platform Win32 -BuildSystem DCC -Readiness all
$best = pwsh delphi-inspect.ps1 -DetectLatest
+# Example using MSBuild with Delphi 13 Florence
+# Use "Locate" to pin a Delphi version within your script so it can be run
+# on developer machines that may have custom RAD Studio installation paths
+# Locate with: VER370, Florence, Delphi 13, or 13 Florence...whichever is your preference
+./delphi-inspect.ps1 -Locate -Name Florence | ./delphi-msbuild.ps1 -ProjectFile 'MyProj.dproj' -Platform Win64
+
+
# Text format -- human-readable output
pwsh delphi-inspect.ps1
pwsh delphi-inspect.ps1 -Version -Format text
@@ -65,6 +72,7 @@ Note: the test suite requires `pwsh`.
| `ListKnown` | List all known Delphi versions from the dataset |
| `ListInstalled` | List all Delphi versions with readiness state |
| `DetectLatest` | Return the single highest-versioned ready install |
+| `Locate` | Return the installation root directory for a specific installed version |
| `Resolve` | Resolve an alias or VER### to a canonical entry |
See [docs/commands.md](docs/commands.md) for full command reference including switches,
diff --git a/docs/commands.md b/docs/commands.md
index 5c0a431..9920068 100644
--- a/docs/commands.md
+++ b/docs/commands.md
@@ -7,11 +7,13 @@ This document describes the command-line interface for
# Overview
-`delphi-inspect.ps1` provides five primary actions:
+`delphi-inspect.ps1` provides six primary actions:
- `-Version` --- Display tool and dataset metadata
- `-Resolve` --- Resolve a Delphi alias or VER### constant to
canonical version data
+- `-Locate` --- Return the installation root directory for a specific
+ installed version
- `-ListKnown` --- List all known Delphi versions from the dataset
- `-ListInstalled` --- List all Delphi versions with readiness state
- `-DetectLatest` --- Return the single highest-versioned ready install
@@ -152,6 +154,89 @@ being omitted.
------------------------------------------------------------------------
+## -Locate
+
+Look up a specific Delphi version by alias or VER### constant and return
+its installation root directory (`RootDir`) from the Windows registry.
+
+This is the complement to `-Resolve`: where `-Resolve` returns dataset
+metadata, `-Locate` returns the actual on-disk installation path for a
+named version. It is intended for build scripts that need to pin to a
+specific Delphi version and pass its root path to another tool (e.g.
+`delphi-msbuild`).
+
+### Syntax
+
+ -Locate
+ -Locate -Name
+
+`-Name` is mandatory. It may be supplied positionally (first argument
+after `-Locate`) or explicitly via `-Name`. Omitting it is a
+parameter binding error (exit code 1).
+
+Matching is case-insensitive. The lookup checks each entry in order:
+`verDefine` (e.g. `VER150`), then `productName` (e.g. `Delphi 7`),
+then the `aliases` array (e.g. `D7`, `Delphi 11`). The first match
+wins.
+
+### Examples
+
+ pwsh delphi-inspect.ps1 -Locate "Delphi 13"
+ pwsh delphi-inspect.ps1 -Locate VER370
+ pwsh delphi-inspect.ps1 -Locate -Name VER370
+ pwsh delphi-inspect.ps1 -Locate VER370 -Format json
+
+ # Pass the root path to another tool
+ $root = (.\delphi-inspect.ps1 -Locate VER370).rootDir
+ & "$root\bin\rsvars.bat"
+
+### Output (object format, default)
+
+Returns one `pscustomobject` with properties:
+
+ verDefine -- canonical VER### constant from the dataset
+ productName -- human-readable product name
+ rootDir -- installation root directory from the registry
+
+### Output (text format)
+
+Labels are left-padded to a 20-character column width.
+
+ verDefine VER370
+ productName Delphi 13 Florence
+ rootDir C:\Program Files (x86)\Embarcadero\Studio\24.0\
+
+### Output (json format)
+
+ {
+ "ok": true,
+ "command": "locate",
+ "tool": {
+ "name": "delphi-inspect",
+ "version": "0.1.0"
+ },
+ "result": {
+ "verDefine": "VER370",
+ "productName": "Delphi 13 Florence",
+ "rootDir": "C:\\Program Files (x86)\\Embarcadero\\Studio\\24.0\\"
+ }
+ }
+
+### Exit codes
+
+| Code | Condition |
+|------|-----------|
+| `0` | Version found and installed; `rootDir` returned |
+| `4` | Name not found in the dataset |
+| `5` | Registry access error |
+| `6` | Version known but not installed (registry entry absent or `RootDir` empty) |
+
+When exit code 6 is returned in json mode, a json error envelope is
+emitted (`ok: false`) rather than a success envelope, because no partial
+result is meaningful for a point lookup.
+
+------------------------------------------------------------------------
+
## -ListKnown
List all known Delphi versions from the dataset.
@@ -691,11 +776,12 @@ the versions present in that file.
# Parameter Rules
-- `-Version`, `-Resolve`, `-ListKnown`, `-ListInstalled`, and
- `-DetectLatest` are mutually exclusive (enforced by PowerShell
+- `-Version`, `-Resolve`, `-Locate`, `-ListKnown`, `-ListInstalled`,
+ and `-DetectLatest` are mutually exclusive (enforced by PowerShell
parameter sets; exit code 1 if more than one is supplied).
- With no action switch, the default action is `-Version`.
-- `-Resolve` requires `-Name`; it may be supplied positionally.
+- `-Resolve` and `-Locate` both require `-Name`; it may be supplied
+ positionally as the first argument after the action switch.
- `-ListInstalled` requires both `-Platform` and `-BuildSystem`;
neither may be supplied positionally.
- `-DetectLatest` accepts `-Platform` and `-BuildSystem` as optional
@@ -716,9 +802,9 @@ the versions present in that file.
| `1` | PowerShell parameter binding error or unexpected internal error |
| `2` | Reserved (script-body argument validation; not currently used) |
| `3` | Dataset missing or unreadable |
-| `4` | Alias not found (`-Resolve` only) |
-| `5` | Registry access error (`-ListInstalled` and `-DetectLatest` only) |
-| `6` | No installations found (`-ListInstalled` and `-DetectLatest` only) |
+| `4` | Alias not found (`-Resolve` and `-Locate` only) |
+| `5` | Registry access error (`-ListInstalled`, `-DetectLatest`, and `-Locate` only) |
+| `6` | No installations found (`-ListInstalled`, `-DetectLatest`, and `-Locate` only) |
**PowerShell implementation note:** the PowerShell binder runs before the
@@ -748,14 +834,16 @@ written to stderr, nothing is written to stdout on error.
- On dataset errors (exit 3): stderr contains the error message,
stdout is empty.
- On unknown alias (exit 4): stderr contains "Alias not found",
- stdout is empty.
+ stdout is empty. Applies to `-Resolve` and `-Locate`.
- On registry access error (exit 5): stderr contains the error
message, stdout is empty.
-- On no installations found (exit 6): in text mode, stdout contains
- "No installations found" (for `-ListInstalled`) or
- "No ready installation found" (for `-DetectLatest`), stderr is empty.
- In object mode, nothing is emitted to the pipeline; exit code 6 is
- the signal.
+- On no installations found (exit 6):
+ - `-ListInstalled` (text): stdout contains "No installations found",
+ stderr is empty. Object mode: nothing emitted; exit code is signal.
+ - `-DetectLatest` (text): stdout contains "No ready installation found",
+ stderr is empty. Object mode: nothing emitted; exit code is signal.
+ - `-Locate` (text and object): stderr contains "Not installed: ",
+ stdout is empty. Exit code 6 is the signal.
## JSON format (-Format json)
@@ -768,11 +856,13 @@ written to stderr, nothing is written to stdout on error.
- On dataset errors (exit 3), unknown alias (exit 4), or registry
access error (exit 5): stdout contains a JSON error envelope,
stderr is empty.
-- On no installations found (exit 6): stdout contains the normal
- JSON success envelope (ok: true); stderr is empty. Exit code 6 is
- the signal -- the envelope is still well-formed and machine-readable.
- For `-ListInstalled`, all installations are listed as notFound.
- For `-DetectLatest`, `installation` is null.
+- On no installations found (exit 6):
+ - `-ListInstalled` and `-DetectLatest`: stdout contains a JSON success
+ envelope (ok: true); exit code 6 is the signal. For
+ `-ListInstalled`, all installations are listed as notFound. For
+ `-DetectLatest`, `installation` is null.
+ - `-Locate`: stdout contains a JSON error envelope (ok: false),
+ stderr is empty. Exit code 6 is the signal.
JSON error envelope:
diff --git a/source/delphi-inspect.ps1 b/source/delphi-inspect.ps1
index d0adf16..a68a8ce 100644
--- a/source/delphi-inspect.ps1
+++ b/source/delphi-inspect.ps1
@@ -17,6 +17,9 @@ USAGE
pwsh ./source/delphi-inspect.ps1 -DataFile
pwsh ./source/delphi-inspect.ps1 -DetectLatest -Platform Win32 -BuildSystem DCC
pwsh ./source/delphi-inspect.ps1 -DetectLatest -Platform Win32 -BuildSystem DCC -Format json
+ pwsh ./source/delphi-inspect.ps1 -Locate -Name
+ pwsh ./source/delphi-inspect.ps1 -Locate
+ pwsh ./source/delphi-inspect.ps1 -Locate -Name -Format json
pwsh ./source/delphi-inspect.ps1 -ListInstalled -Platform Win32 -BuildSystem DCC
pwsh ./source/delphi-inspect.ps1 -ListInstalled -Platform Win32 -BuildSystem DCC -Readiness all
pwsh ./source/delphi-inspect.ps1 -ListInstalled -Platform Win32 -BuildSystem DCC -Readiness partialInstall
@@ -28,10 +31,21 @@ NOTES
-Resolve looks up an alias or VER### string in the dataset (case-insensitive)
and prints the canonical entry fields. Exit 4 when the alias is not found.
+ -Locate looks up an alias or VER### string (same matching as -Resolve) and
+ returns the RootDir of the installed version from the registry.
+ Exit 4 when the alias is not found in the dataset.
+ Exit 6 when the version is not installed (registry entry absent or RootDir empty).
+
-DetectLatest scans all dataset entries and returns the single highest-versioned
entry whose readiness is 'ready' for the specified platform and build system.
Exit 0 on success; exit 6 when no ready installation exists.
+ Dataset resolution order when -DataFile is not supplied:
+ 1. /delphi-compiler-versions.json (sibling / standalone deployment)
+ 2. /submodules/delphi-compiler-versions/data/delphi-compiler-versions.json
+ The first path that exists on disk is used. If neither exists, path 2 is
+ returned so Import-JsonData produces a meaningful "Data file not found" error.
+
-Format selects output format. Valid values: object (default), text, json.
object -- emit PowerShell objects to the pipeline (default; best for scripting)
text -- human-readable formatted output
@@ -53,7 +67,11 @@ param(
[Parameter(ParameterSetName='Resolve', Mandatory=$true)]
[switch]$Resolve,
+ [Parameter(ParameterSetName='Locate', Mandatory=$true)]
+ [switch]$Locate,
+
[Parameter(ParameterSetName='Resolve', Mandatory=$true, Position=0)]
+ [Parameter(ParameterSetName='Locate', Mandatory=$true, Position=0)]
[string]$Name,
[Parameter(ParameterSetName='ListKnown')]
@@ -127,15 +145,29 @@ function Resolve-DefaultDataFilePath {
$scriptDir = Split-Path -Parent $ScriptPath
- # Prefer the submodule layout:
- # ../delphi-compiler-versions/data/delphi-compiler-versions.json
+ # Candidate 1 (preferred): sibling file next to the script
+ # /delphi-compiler-versions.json
+ # Checked first so a locally deployed dataset takes precedence over the submodule.
+ $siblingPath = Join-Path $scriptDir 'delphi-compiler-versions.json'
+
+ if (Test-Path -LiteralPath $siblingPath) {
+ return $siblingPath
+ }
+
+ # Candidate 2: submodule layout relative to repo root
+ # /submodules/delphi-compiler-versions/data/delphi-compiler-versions.json
# Use Join-Path to remain path-separator-safe if invoked on non-Windows runners.
- $repoRoot = Split-Path $scriptDir -Parent
- $specRoot = Join-Path (Join-Path $repoRoot 'submodules') 'delphi-compiler-versions'
- $dataDir = Join-Path $specRoot 'data'
- $defaultPath = Join-Path $dataDir 'delphi-compiler-versions.json'
+ $repoRoot = Split-Path $scriptDir -Parent
+ $specRoot = Join-Path (Join-Path $repoRoot 'submodules') 'delphi-compiler-versions'
+ $submodulePath = Join-Path (Join-Path $specRoot 'data') 'delphi-compiler-versions.json'
+
+ if (Test-Path -LiteralPath $submodulePath) {
+ return $submodulePath
+ }
- return $defaultPath
+ # Neither found: return the submodule path so Import-JsonData produces a
+ # meaningful "Data file not found: ..." error pointing at the expected location.
+ return $submodulePath
}
function Import-JsonData {
@@ -306,6 +338,43 @@ function Write-ResolveOutput {
}
}
+function Write-LocateOutput {
+ param(
+ [psobject]$Entry,
+ [string]$RootDir,
+ [string]$ToolVersion = '',
+ [string]$Format = 'object'
+ )
+
+ if ($Format -eq 'object') {
+ Write-Output ([pscustomobject]@{
+ verDefine = $Entry.verDefine
+ productName = $Entry.productName
+ rootDir = $RootDir
+ })
+ return
+ }
+
+ if ($Format -eq 'json') {
+ Write-JsonOutput ([pscustomobject]@{
+ ok = $true
+ command = 'locate'
+ tool = [pscustomobject]@{ name = 'delphi-inspect'; version = $ToolVersion }
+ result = [pscustomobject]@{
+ verDefine = $Entry.verDefine
+ productName = $Entry.productName
+ rootDir = $RootDir
+ }
+ })
+ return
+ }
+
+ # text format -- label column matches -Resolve at 20 chars
+ Write-Output ("verDefine {0}" -f $Entry.verDefine)
+ Write-Output ("productName {0}" -f $Entry.productName)
+ Write-Output ("rootDir {0}" -f $RootDir)
+}
+
function Write-ListKnownOutput {
param(
[psobject]$Data,
@@ -734,8 +803,8 @@ try {
# Default behavior: if no action switches specified, treat as -Version.
# Mutual exclusion and mandatory -Name are enforced by parameter sets.
$doVersion = $Version
- if (-not $doVersion -and -not $Resolve -and -not $ListKnown -and -not $ListInstalled -and -not $DetectLatest) { $doVersion = $true }
- $commandName = if ($Resolve) { 'resolve' } elseif ($ListKnown) { 'listKnown' } elseif ($ListInstalled) { 'listInstalled' } elseif ($DetectLatest) { 'detectLatest' } else { 'version' }
+ if (-not $doVersion -and -not $Resolve -and -not $Locate -and -not $ListKnown -and -not $ListInstalled -and -not $DetectLatest) { $doVersion = $true }
+ $commandName = if ($Resolve) { 'resolve' } elseif ($Locate) { 'locate' } elseif ($ListKnown) { 'listKnown' } elseif ($ListInstalled) { 'listInstalled' } elseif ($DetectLatest) { 'detectLatest' } else { 'version' }
if ([string]::IsNullOrWhiteSpace($DataFile)) {
$DataFile = Resolve-DefaultDataFilePath -ScriptPath $scriptPath
@@ -776,6 +845,47 @@ try {
exit $ExitSuccess
}
+ if ($Locate) {
+ $entry = Resolve-VersionEntry -Name $Name -Data $data
+ if ($null -eq $entry) {
+ if ($Format -eq 'json') {
+ Write-JsonError -ToolVersion $ToolVersion -Command 'locate' -Code $ExitAliasNotFound -Message "Alias not found: $Name"
+ } else {
+ Write-Error "Alias not found: $Name" -ErrorAction Continue
+ }
+ exit $ExitAliasNotFound
+ }
+ if ([string]::IsNullOrWhiteSpace($entry.regKeyRelativePath)) {
+ if ($Format -eq 'json') {
+ Write-JsonError -ToolVersion $ToolVersion -Command 'locate' -Code $ExitNoInstallationsFound -Message "No registry path known for: $Name"
+ } else {
+ Write-Error "No registry path known for: $Name" -ErrorAction Continue
+ }
+ exit $ExitNoInstallationsFound
+ }
+ $rootDir = $null
+ try {
+ $rootDir = Get-RegistryRootDir -RelativePath $entry.regKeyRelativePath
+ } catch {
+ if ($Format -eq 'json') {
+ Write-JsonError -ToolVersion $ToolVersion -Command 'locate' -Code $ExitRegistryError -Message "Registry access failed: $($_.Exception.Message)"
+ } else {
+ Write-Error "Registry access failed: $($_.Exception.Message)" -ErrorAction Continue
+ }
+ exit $ExitRegistryError
+ }
+ if ($null -eq $rootDir) {
+ if ($Format -eq 'json') {
+ Write-JsonError -ToolVersion $ToolVersion -Command 'locate' -Code $ExitNoInstallationsFound -Message "Not installed: $Name"
+ } else {
+ Write-Error "Not installed: $Name" -ErrorAction Continue
+ }
+ exit $ExitNoInstallationsFound
+ }
+ Write-LocateOutput -Entry $entry -RootDir $rootDir -ToolVersion $ToolVersion -Format $Format
+ exit $ExitSuccess
+ }
+
if ($ListKnown) {
Write-ListKnownOutput -Data $data -ToolVersion $ToolVersion -Format $Format
exit $ExitSuccess
diff --git a/tests/pwsh/Resolve-DefaultDataFilePath.Tests.ps1 b/tests/pwsh/Resolve-DefaultDataFilePath.Tests.ps1
index 12566ec..fc59f5f 100644
--- a/tests/pwsh/Resolve-DefaultDataFilePath.Tests.ps1
+++ b/tests/pwsh/Resolve-DefaultDataFilePath.Tests.ps1
@@ -4,13 +4,21 @@
Tests for Resolve-DefaultDataFilePath in delphi-inspect.ps1
.DESCRIPTION
- Covers: path construction from a given script location.
+ Covers: path resolution priority and fallback behaviour.
- Context 1 - Pure construction (no filesystem access):
- Verifies the returned path ends with the canonical data file name.
- Verifies the returned path contains the spec submodule directory name.
+ Context 1 - Neither submodule nor sibling file present:
+ Verifies the returned path ends with the canonical data file name and
+ contains the spec submodule directory name (i.e. falls back to the
+ canonical submodule path for a meaningful error downstream).
- Context 2 - Real repository layout:
+ Context 2 - Sibling data file present, no submodule:
+ Verifies that a delphi-compiler-versions.json placed next to the script
+ is returned when the submodule path does not exist.
+
+ Context 3 - Both submodule and sibling data file present:
+ Verifies that the sibling file takes priority over the submodule path.
+
+ Context 4 - Real repository layout:
Verifies the resolved path exists on disk.
Requires delphi-compiler-versions to be present as a submodule.
#>
@@ -32,13 +40,13 @@ Describe 'Resolve-DefaultDataFilePath' {
. $script:scriptUnderTest
}
- Context 'Given a script path in a standard source layout' {
+ Context 'Given neither the submodule path nor a sibling data file exists' {
BeforeAll {
$fakeRepo = Join-Path ([System.IO.Path]::GetTempPath()) 'delphi-inspect-test-repo'
$fakeScriptDir = Join-Path $fakeRepo 'source'
$script:fakeScriptPath = Join-Path $fakeScriptDir 'delphi-inspect.ps1'
- # Create a placeholder file so the guard's Test-Path check passes.
+ # Create a placeholder script file only -- no data files anywhere.
$null = New-Item -ItemType Directory -Path $fakeScriptDir -Force
$null = New-Item -ItemType File -Path $script:fakeScriptPath -Force
}
@@ -54,9 +62,63 @@ Describe 'Resolve-DefaultDataFilePath' {
$result | Should -Match ([regex]::Escape('delphi-compiler-versions.json'))
}
- It 'returns a path containing the spec submodule directory name' {
+ It 'returns the submodule path (canonical fallback for error reporting)' {
+ $result = Resolve-DefaultDataFilePath -ScriptPath $script:fakeScriptPath
+ $result | Should -Match ([regex]::Escape('submodules'))
+ }
+
+ }
+
+ Context 'Given only a sibling data file exists next to the script' {
+
+ BeforeAll {
+ $fakeRepo = Join-Path ([System.IO.Path]::GetTempPath()) 'delphi-inspect-test-sibling'
+ $fakeScriptDir = Join-Path $fakeRepo 'source'
+ $script:fakeScriptPath = Join-Path $fakeScriptDir 'delphi-inspect.ps1'
+ $script:siblingPath = Join-Path $fakeScriptDir 'delphi-compiler-versions.json'
+ $null = New-Item -ItemType Directory -Path $fakeScriptDir -Force
+ $null = New-Item -ItemType File -Path $script:fakeScriptPath -Force
+ $null = New-Item -ItemType File -Path $script:siblingPath -Force
+ }
+
+ AfterAll {
+ foreach ($p in @($script:fakeScriptPath, $script:siblingPath)) {
+ if (Test-Path -LiteralPath $p) { Remove-Item -LiteralPath $p -Force }
+ }
+ }
+
+ It 'returns the sibling file path' {
+ $result = Resolve-DefaultDataFilePath -ScriptPath $script:fakeScriptPath
+ $result | Should -Be $script:siblingPath
+ }
+
+ }
+
+ Context 'Given both a submodule data file and a sibling data file exist' {
+
+ BeforeAll {
+ $fakeRepo = Join-Path ([System.IO.Path]::GetTempPath()) 'delphi-inspect-test-both'
+ $fakeScriptDir = Join-Path $fakeRepo 'source'
+ $script:fakeScriptPath = Join-Path $fakeScriptDir 'delphi-inspect.ps1'
+ $script:siblingPath = Join-Path $fakeScriptDir 'delphi-compiler-versions.json'
+ $submoduleDataDir = Join-Path (Join-Path (Join-Path $fakeRepo 'submodules') 'delphi-compiler-versions') 'data'
+ $script:submodulePath = Join-Path $submoduleDataDir 'delphi-compiler-versions.json'
+ $null = New-Item -ItemType Directory -Path $fakeScriptDir -Force
+ $null = New-Item -ItemType Directory -Path $submoduleDataDir -Force
+ $null = New-Item -ItemType File -Path $script:fakeScriptPath -Force
+ $null = New-Item -ItemType File -Path $script:siblingPath -Force
+ $null = New-Item -ItemType File -Path $script:submodulePath -Force
+ }
+
+ AfterAll {
+ foreach ($p in @($script:fakeScriptPath, $script:siblingPath, $script:submodulePath)) {
+ if (Test-Path -LiteralPath $p) { Remove-Item -LiteralPath $p -Force }
+ }
+ }
+
+ It 'returns the sibling path (sibling takes priority)' {
$result = Resolve-DefaultDataFilePath -ScriptPath $script:fakeScriptPath
- $result | Should -Match ([regex]::Escape('delphi-compiler-versions'))
+ $result | Should -Be $script:siblingPath
}
}
diff --git a/tests/pwsh/Write-LocateOutput.Tests.ps1 b/tests/pwsh/Write-LocateOutput.Tests.ps1
new file mode 100644
index 0000000..e17c61d
--- /dev/null
+++ b/tests/pwsh/Write-LocateOutput.Tests.ps1
@@ -0,0 +1,131 @@
+#Requires -Modules @{ ModuleName='Pester'; ModuleVersion='5.7.0' }
+<#
+.SYNOPSIS
+ Tests for Write-LocateOutput in delphi-inspect.ps1
+
+.DESCRIPTION
+ Covers: output produced for all three format modes.
+
+ Context 1 - -Format text:
+ Verifies verDefine, productName, and rootDir lines are present, and
+ that the total line count is exactly 3.
+
+ Context 2 - -Format json:
+ Verifies the output is a single item that parses as valid JSON,
+ ok is $true, command is 'locate', and result contains verDefine,
+ productName, and rootDir with the expected values.
+
+ Context 3 - -Format object (default):
+ Verifies that one pscustomobject is emitted with the correct
+ verDefine, productName, and rootDir properties.
+#>
+
+# PESTER 5 SCOPING RULES apply here -- see Resolve-DefaultDataFilePath.Tests.ps1
+# for the canonical explanation. Dot-source TestHelpers.ps1 and the script
+# under test inside BeforeAll, not at the top level of the file.
+
+Describe 'Write-LocateOutput' {
+
+ BeforeAll {
+ . "$PSScriptRoot/TestHelpers.ps1"
+ $script:scriptUnderTest = Get-ScriptUnderTestPath
+ . $script:scriptUnderTest
+
+ $script:entry = [pscustomobject]@{
+ verDefine = 'VER370'
+ productName = 'Delphi 13 Florence'
+ }
+ $script:rootDir = 'C:\Program Files (x86)\Embarcadero\Studio\24.0\'
+ }
+
+ Context 'Given -Format text' {
+
+ BeforeAll {
+ $script:output = Write-LocateOutput -Entry $script:entry -RootDir $script:rootDir -Format 'text'
+ }
+
+ It 'output includes a line with the verDefine value' {
+ ($script:output -match 'verDefine\s+VER370') | Should -Not -BeNullOrEmpty
+ }
+
+ It 'output includes a line with the productName value' {
+ ($script:output -match 'productName\s+Delphi 13 Florence') | Should -Not -BeNullOrEmpty
+ }
+
+ It 'output includes a line with the rootDir value' {
+ ($script:output -match 'rootDir\s+') | Should -Not -BeNullOrEmpty
+ }
+
+ It 'output has exactly three lines' {
+ $script:output | Should -HaveCount 3
+ }
+
+ }
+
+ Context 'Given -Format json' {
+
+ BeforeAll {
+ $script:output = Write-LocateOutput -Entry $script:entry -RootDir $script:rootDir -ToolVersion '0.1.0' -Format 'json'
+ $script:json = $script:output | ConvertFrom-Json
+ }
+
+ It 'output is a single item' {
+ $script:output | Should -HaveCount 1
+ }
+
+ It 'output parses as valid JSON' {
+ { $script:output | ConvertFrom-Json } | Should -Not -Throw
+ }
+
+ It 'ok is true' {
+ $script:json.ok | Should -Be $true
+ }
+
+ It 'command is locate' {
+ $script:json.command | Should -Be 'locate'
+ }
+
+ It 'result.verDefine matches the entry value' {
+ $script:json.result.verDefine | Should -Be 'VER370'
+ }
+
+ It 'result.productName matches the entry value' {
+ $script:json.result.productName | Should -Be 'Delphi 13 Florence'
+ }
+
+ It 'result.rootDir matches the supplied rootDir' {
+ $script:json.result.rootDir | Should -Be $script:rootDir
+ }
+
+ It 'result does not contain unexpected properties' {
+ $props = $script:json.result.PSObject.Properties.Name | Sort-Object
+ $props | Should -Be @('productName', 'rootDir', 'verDefine')
+ }
+
+ }
+
+ Context 'Given -Format object (default)' {
+
+ BeforeAll {
+ $script:output = Write-LocateOutput -Entry $script:entry -RootDir $script:rootDir
+ }
+
+ It 'emits one pscustomobject' {
+ $script:output | Should -HaveCount 1
+ }
+
+ It 'has verDefine property with correct value' {
+ $script:output.verDefine | Should -Be 'VER370'
+ }
+
+ It 'has productName property with correct value' {
+ $script:output.productName | Should -Be 'Delphi 13 Florence'
+ }
+
+ It 'has rootDir property with correct value' {
+ $script:output.rootDir | Should -Be $script:rootDir
+ }
+
+ }
+
+}
diff --git a/tests/pwsh/delphi-inspect.Integration.Tests.ps1 b/tests/pwsh/delphi-inspect.Integration.Tests.ps1
index c765ff9..d855be7 100644
--- a/tests/pwsh/delphi-inspect.Integration.Tests.ps1
+++ b/tests/pwsh/delphi-inspect.Integration.Tests.ps1
@@ -146,6 +146,36 @@
Context 30 - -DetectLatest omitting -BuildSystem (uses MSBuild default):
Exit 6, JSON result.buildSystem=MSBuild, result.platform=Win32 (explicit), clean stderr.
+
+ Contexts 31-38 cover the -Locate dispatch branch. All supply -DataFile explicitly
+ using the resolve fixture (delphi-compiler-versions.resolve.json).
+ The test machine has no Delphi installed so all registry checks return null (not installed).
+
+ Context 31 - -Locate -Name VER150 (text mode, not installed):
+ Exit 6, no stdout, stderr contains "Not installed".
+
+ Context 32 - -Locate VER150 (positional -Name, text mode):
+ Exit 6. Verifies Position=0 on -Name.
+
+ Context 33 - -Locate -Name ver150 (case-insensitive, text mode):
+ Exit 6, verifies the lookup is case-insensitive.
+
+ Context 34 - -Locate -Name for an alias not in the dataset:
+ Exit 4, no stdout, stderr contains "Alias not found".
+
+ Context 35 - -Locate without -Name:
+ Exit 1 (PowerShell parameter binding failure), no stdout, stderr present.
+
+ Context 36 - -Locate -Name VER150 -Format json (not installed):
+ Exit 6, stdout is a JSON error envelope (ok=false, error.code=6,
+ error.message contains "Not installed"). Clean stderr.
+
+ Context 37 - -Locate -Name for unknown alias -Format json:
+ Exit 4, stdout is a JSON error envelope (ok=false, error.code=4,
+ error.message contains "Alias not found"). Clean stderr.
+
+ Context 38 - -Locate default format (object), not installed:
+ Exit 6, no stdout, stderr present.
#>
Describe 'delphi-inspect.ps1 (subprocess)' {
@@ -1227,4 +1257,196 @@ Describe 'delphi-inspect.ps1 (subprocess)' {
}
+ # ---- -Locate integration tests ----
+
+ Context 'Given -Locate -Name VER150 (text mode, not installed on test machine)' {
+
+ BeforeAll {
+ $script:run = Invoke-ToolProcess -ScriptPath $script:scriptPath `
+ -Arguments @('-Locate', '-Name', 'VER150', '-Format', 'text', '-DataFile', $script:resolveFixturePath)
+ }
+
+ It 'exits with code 6 (version known but not installed)' {
+ $script:run.ExitCode | Should -Be 6
+ }
+
+ It 'produces no stdout' {
+ $script:run.StdOut | Should -BeNullOrEmpty
+ }
+
+ It 'emits at least one stderr line containing "Not installed"' {
+ $script:run.StdErr | Should -Not -BeNullOrEmpty
+ ($script:run.StdErr -join "`n") | Should -Match 'Not installed'
+ }
+
+ }
+
+ Context 'Given -Locate VER150 (positional -Name, text mode)' {
+
+ BeforeAll {
+ $script:run = Invoke-ToolProcess -ScriptPath $script:scriptPath `
+ -Arguments @('-Locate', 'VER150', '-Format', 'text', '-DataFile', $script:resolveFixturePath)
+ }
+
+ It 'exits with code 6' {
+ $script:run.ExitCode | Should -Be 6
+ }
+
+ It 'produces no stdout' {
+ $script:run.StdOut | Should -BeNullOrEmpty
+ }
+
+ }
+
+ Context 'Given -Locate -Name ver150 (case-insensitive, text mode)' {
+
+ BeforeAll {
+ $script:run = Invoke-ToolProcess -ScriptPath $script:scriptPath `
+ -Arguments @('-Locate', '-Name', 'ver150', '-Format', 'text', '-DataFile', $script:resolveFixturePath)
+ }
+
+ It 'exits with code 6 (match found, not installed)' {
+ # exit 4 would mean the dataset lookup failed; exit 6 proves it matched VER150
+ $script:run.ExitCode | Should -Be 6
+ }
+
+ }
+
+ Context 'Given -Locate -Name for an alias not in the dataset' {
+
+ BeforeAll {
+ $script:run = Invoke-ToolProcess -ScriptPath $script:scriptPath `
+ -Arguments @('-Locate', '-Name', 'DelphiX', '-DataFile', $script:resolveFixturePath)
+ }
+
+ It 'exits with code 4' {
+ $script:run.ExitCode | Should -Be 4
+ }
+
+ It 'produces no stdout' {
+ $script:run.StdOut | Should -BeNullOrEmpty
+ }
+
+ It 'emits at least one stderr line containing "Alias not found"' {
+ $script:run.StdErr | Should -Not -BeNullOrEmpty
+ ($script:run.StdErr -join "`n") | Should -Match 'Alias not found'
+ }
+
+ }
+
+ Context 'Given -Locate without -Name' {
+
+ BeforeAll {
+ $script:run = Invoke-ToolProcess -ScriptPath $script:scriptPath `
+ -Arguments @('-Locate', '-DataFile', $script:resolveFixturePath)
+ }
+
+ It 'exits with code 1 (PowerShell parameter binding failure)' {
+ $script:run.ExitCode | Should -Be 1
+ }
+
+ It 'produces no stdout' {
+ $script:run.StdOut | Should -BeNullOrEmpty
+ }
+
+ It 'emits stderr referencing the mandatory Name parameter' {
+ $script:run.StdErr | Should -Not -BeNullOrEmpty
+ ($script:run.StdErr -join "`n") | Should -Match 'Name'
+ }
+
+ }
+
+ Context 'Given -Locate -Name VER150 -Format json (not installed)' {
+
+ BeforeAll {
+ $script:run = Invoke-ToolProcess -ScriptPath $script:scriptPath `
+ -Arguments @('-Locate', '-Name', 'VER150', '-Format', 'json', '-DataFile', $script:resolveFixturePath)
+ $script:json = ($script:run.StdOut -join "`n") | ConvertFrom-Json
+ }
+
+ It 'exits with code 6' {
+ $script:run.ExitCode | Should -Be 6
+ }
+
+ It 'stdout parses as valid JSON' {
+ { ($script:run.StdOut -join "`n") | ConvertFrom-Json } | Should -Not -Throw
+ }
+
+ It 'JSON ok is false' {
+ $script:json.ok | Should -Be $false
+ }
+
+ It 'JSON command is locate' {
+ $script:json.command | Should -Be 'locate'
+ }
+
+ It 'JSON error.code is 6' {
+ $script:json.error.code | Should -Be 6
+ }
+
+ It 'JSON error.message contains "Not installed"' {
+ $script:json.error.message | Should -Match 'Not installed'
+ }
+
+ It 'produces no stderr' {
+ $script:run.StdErr | Should -BeNullOrEmpty
+ }
+
+ }
+
+ Context 'Given -Locate -Name for unknown alias -Format json' {
+
+ BeforeAll {
+ $script:run = Invoke-ToolProcess -ScriptPath $script:scriptPath `
+ -Arguments @('-Locate', '-Name', 'DelphiX', '-Format', 'json', '-DataFile', $script:resolveFixturePath)
+ $script:json = ($script:run.StdOut -join "`n") | ConvertFrom-Json
+ }
+
+ It 'exits with code 4' {
+ $script:run.ExitCode | Should -Be 4
+ }
+
+ It 'stdout parses as valid JSON' {
+ { ($script:run.StdOut -join "`n") | ConvertFrom-Json } | Should -Not -Throw
+ }
+
+ It 'JSON ok is false' {
+ $script:json.ok | Should -Be $false
+ }
+
+ It 'JSON error.code is 4' {
+ $script:json.error.code | Should -Be 4
+ }
+
+ It 'JSON error.message contains "Alias not found"' {
+ $script:json.error.message | Should -Match 'Alias not found'
+ }
+
+ It 'produces no stderr' {
+ $script:run.StdErr | Should -BeNullOrEmpty
+ }
+
+ }
+
+ Context 'Given -Locate default format (object), not installed' {
+
+ BeforeAll {
+ $script:run = Invoke-ToolProcess -ScriptPath $script:scriptPath `
+ -Arguments @('-Locate', 'VER150', '-DataFile', $script:resolveFixturePath)
+ }
+
+ It 'exits with code 6' {
+ $script:run.ExitCode | Should -Be 6
+ }
+
+ It 'produces no stdout' {
+ $script:run.StdOut | Should -BeNullOrEmpty
+ }
+
+ It 'emits stderr' {
+ $script:run.StdErr | Should -Not -BeNullOrEmpty
+ }
+
+ }
+
}