commit frequently please

This commit is contained in:
lda
2026-04-06 16:20:48 +07:00 Unverified
parent 9fcb191a8e
commit 0048d5938e
4 changed files with 226 additions and 17 deletions
+34
View File
@@ -31,6 +31,40 @@ function Quote-ShArg {
return "'" + ($Value -replace "'", ("'" + '"' + "'" + '"' + "'")) + "'" return "'" + ($Value -replace "'", ("'" + '"' + "'" + '"' + "'")) + "'"
} }
function Normalize-PosixPath {
param([string]$Path)
if ([string]::IsNullOrWhiteSpace($Path)) {
return $Path
}
$normalized = $Path -replace '\\', '/'
if ($normalized.Length -gt 1) {
$normalized = $normalized -replace '/+', '/'
}
return $normalized
}
function Join-PosixPath {
param(
[string]$Left,
[string]$Right
)
$leftNorm = Normalize-PosixPath $Left
$rightNorm = Normalize-PosixPath $Right
if ([string]::IsNullOrWhiteSpace($leftNorm)) {
return $rightNorm
}
if ([string]::IsNullOrWhiteSpace($rightNorm)) {
return $leftNorm
}
$leftTrim = $leftNorm.TrimEnd('/')
$rightTrim = $rightNorm.TrimStart('/')
return "$leftTrim/$rightTrim"
}
function Invoke-Ext { function Invoke-Ext {
param($Exe, $Arguments, $Label) param($Exe, $Arguments, $Label)
$displayArgs = $Arguments.Clone() $displayArgs = $Arguments.Clone()
+81 -6
View File
@@ -4,14 +4,15 @@
param( param(
[string]$Package = "lda-ipjs", [string]$Package = "lda-ipjs",
[switch]$AllPackages, [switch]$AllPackages,
[string]$BinaryFilter = "",
[string]$Filter = "", [string]$Filter = "",
[switch]$Exact, [switch]$Exact,
[switch]$List, [switch]$List,
[ValidateSet("debug", "release")] [ValidateSet("debug", "release")]
[string]$BuildProfile = "debug", [string]$BuildProfile = "release",
[string]$password, [string]$password,
[switch]$Verbose, [switch]$Verbose,
[string]$RemoteTestPath = "/root/.bin/test", [string]$RemoteTestPath = "/tmp/tmp/wakey-test",
[string]$RemoteHost = "[email protected]", [string]$RemoteHost = "[email protected]",
[switch]$Ignored, [switch]$Ignored,
[switch]$IncludeIgnored, [switch]$IncludeIgnored,
@@ -62,6 +63,51 @@ function Build-RemoteExecCommand {
"chmod +x $quotedPath && $exec" "chmod +x $quotedPath && $exec"
} }
function New-RemoteRunDir {
param(
[string]$BasePath,
[string]$PackageName
)
$baseNorm = Normalize-PosixPath $BasePath
$leaf = [IO.Path]::GetFileName($baseNorm)
$parent = Normalize-PosixPath ([IO.Path]::GetDirectoryName($baseNorm))
if ([string]::IsNullOrWhiteSpace($parent)) {
$parent = "/tmp"
}
if ([string]::IsNullOrWhiteSpace($leaf)) {
$leaf = "wakey-test"
}
$random = [System.Guid]::NewGuid().ToString("N").Substring(0, 10)
$safePackage = ($PackageName -replace '[^A-Za-z0-9._-]', '_')
return (Join-PosixPath $parent "$leaf-$safePackage-$random")
}
function New-RemoteBinaryPath {
param(
[string]$RemoteRunDir,
[System.IO.FileInfo]$TestBinary
)
$safeName = ($TestBinary.Name -replace '[^A-Za-z0-9._-]', '_')
Join-PosixPath $RemoteRunDir $safeName
}
function Ensure-RemoteParentDir {
param(
[string]$RemoteDirPath,
[string]$RemoteHost,
[string]$Password
)
$dir = Normalize-PosixPath $RemoteDirPath
if ([string]::IsNullOrWhiteSpace($dir)) {
return
}
Invoke-Ssh -Cmd ("mkdir -p " + (Quote-ShArg $dir)) -Remote $RemoteHost -Pass $Password -Quiet
}
$packages = if ($AllPackages) { Get-WorkspacePackages } else { @($Package) } $packages = if ($AllPackages) { Get-WorkspacePackages } else { @($Package) }
$failures = New-Object System.Collections.Generic.List[string] $failures = New-Object System.Collections.Generic.List[string]
@@ -87,19 +133,38 @@ foreach ($packageName in $packages) {
$testBinaryPaths = Get-TestBinaryPaths $cargoOutput $testBinaryPaths = Get-TestBinaryPaths $cargoOutput
$testBinaries = @($testBinaryPaths | Get-Item) $testBinaries = @($testBinaryPaths | Get-Item)
if ($BinaryFilter) {
$testBinaries = @(
$testBinaries |
Where-Object {
$_.Name -like "*$BinaryFilter*" -or $_.BaseName -like "*$BinaryFilter*"
}
)
}
if ($testBinaries.Count -eq 0) { if ($testBinaries.Count -eq 0) {
Write-Host "Skipping ${packageName}: no test executables" -ForegroundColor DarkYellow $reason = if ($BinaryFilter) {
"no test executables matched binary filter '$BinaryFilter'"
} else {
"no test executables"
}
Write-Host "Skipping ${packageName}: $reason" -ForegroundColor DarkYellow
continue continue
} }
Write-Host "Found $($testBinaries.Count) test $($testBinaries.Count -eq 1 ? "binary" : "binaries") for $packageName" -ForegroundColor Green Write-Host "Found $($testBinaries.Count) test $($testBinaries.Count -eq 1 ? "binary" : "binaries") for $packageName" -ForegroundColor Green
$remoteRunDir = New-RemoteRunDir -BasePath $RemoteTestPath -PackageName $packageName
Ensure-RemoteParentDir -RemoteDirPath $remoteRunDir -RemoteHost $RemoteHost -Password $password
try {
foreach ($testBinary in $testBinaries) { foreach ($testBinary in $testBinaries) {
Write-Host "`nTesting: $packageName / $($testBinary.Name)" -ForegroundColor Cyan Write-Host "`nTesting: $packageName / $($testBinary.Name)" -ForegroundColor Cyan
try { $remoteBinaryPath = New-RemoteBinaryPath -RemoteRunDir $remoteRunDir -TestBinary $testBinary
# Copy and make executable # Copy and make executable
Invoke-Scp -Local $testBinary.FullName -Dest "${RemoteHost}:$RemoteTestPath" -Pass $password -Quiet Invoke-Scp -Local $testBinary.FullName -Dest "${RemoteHost}:$remoteBinaryPath" -Pass $password -Quiet
# Build test args # Build test args
$parts = @() $parts = @()
@@ -111,9 +176,10 @@ foreach ($packageName in $packages) {
if ($ShowOutput -or $Verbose) { $parts += "--show-output" } if ($ShowOutput -or $Verbose) { $parts += "--show-output" }
if ($NoCapture -or $Verbose) { $parts += "--nocapture" } if ($NoCapture -or $Verbose) { $parts += "--nocapture" }
if ($Threads -gt 0) { $parts += "--test-threads"; $parts += $Threads } if ($Threads -gt 0) { $parts += "--test-threads"; $parts += $Threads }
$remoteCmd = Build-RemoteExecCommand -RemotePath $RemoteTestPath -Arguments $parts $remoteCmd = Build-RemoteExecCommand -RemotePath $remoteBinaryPath -Arguments $parts
# Run test binary (with chmod to ensure executable) # Run test binary (with chmod to ensure executable)
try {
Invoke-Ssh -Cmd $remoteCmd -Remote $RemoteHost -Pass $password Invoke-Ssh -Cmd $remoteCmd -Remote $RemoteHost -Pass $password
} }
catch { catch {
@@ -124,6 +190,15 @@ foreach ($packageName in $packages) {
} }
} }
} }
finally {
try {
Invoke-Ssh -Cmd ("rm -r " + (Quote-ShArg $remoteRunDir)) -Remote $RemoteHost -Pass $password -Quiet
}
catch {
Write-Warning "Failed to remove remote test dir: $remoteRunDir"
}
}
}
if ($failures.Count -gt 0) { if ($failures.Count -gt 0) {
Write-Error ("One or more test binaries failed: {0}" -f ($failures -join ", ")) Write-Error ("One or more test binaries failed: {0}" -f ($failures -join ", "))
+23 -4
View File
@@ -8,7 +8,9 @@ use wakey_core::{DeviceFilters, DeviceQuery, InterfaceSummary, WakeResult};
#[derive(Parser)] #[derive(Parser)]
#[command(name = "wakey")] #[command(name = "wakey")]
#[command(version, about = "CLI and temporary HTTP adapter for Wakey")] #[command(version, about = "CLI and temporary HTTP adapter for Wakey")]
#[command(long_about = "Wakey can run as a local/operator CLI or serve the legacy HTTP/static interface during the migration to a service-first architecture.")] #[command(
long_about = "Wakey can run as a local/operator CLI or serve the legacy HTTP/static interface during the migration to a service-first architecture."
)]
struct Cli { struct Cli {
#[command(subcommand)] #[command(subcommand)]
command: Command, command: Command,
@@ -49,7 +51,15 @@ struct LeasesArgs {
} }
#[derive(Args)] #[derive(Args)]
#[command(after_long_help = "Examples:\n wakey wake bedroom-pc\n wakey wake --mac aa:bb:cc:dd:ee:ff\n wakey wake --mac aa:bb:cc:dd:ee:ff --ip 192.168.1.255\n\nRules:\n - query mode and explicit --mac/--ip mode are mutually exclusive\n - --ip requires --mac\n - --mac without --ip fans out to interface broadcast targets")] #[command(after_long_help = "Examples:
wakey wake bedroom-pc
wakey wake --mac aa:bb:cc:dd:ee:ff
wakey wake --mac aa:bb:cc:dd:ee:ff --ip 192.168.1.255
Rules:
- query mode and explicit --mac/--ip mode are mutually exclusive
- --ip requires --mac
- --mac without --ip fans out to interface broadcast targets")]
struct WakeArgs { struct WakeArgs {
/// Free-form device query, for example a hostname, IP, MAC, interface, or NUD state. /// Free-form device query, for example a hostname, IP, MAC, interface, or NUD state.
query: Option<String>, query: Option<String>,
@@ -65,7 +75,12 @@ struct WakeArgs {
} }
#[derive(Args)] #[derive(Args)]
#[command(after_long_help = "Examples:\n wakey status bedroom-pc\n wakey status --mac aa:bb:cc:dd:ee:ff\n wakey status --dev br-lan --nud reachable\n\nIf only the positional query is provided, it is treated as free-form input and resolved through the smart selector path.")] #[command(after_long_help = "Examples:
wakey status bedroom-pc
wakey status --mac aa:bb:cc:dd:ee:ff
wakey status --dev br-lan --nud reachable
If only the positional query is provided, it is treated as free-form input and resolved through the smart selector path.")]
struct StatusArgs { struct StatusArgs {
/// Free-form device query. /// Free-form device query.
query: Option<String>, query: Option<String>,
@@ -90,7 +105,11 @@ struct StatusArgs {
} }
#[derive(Args)] #[derive(Args)]
#[command(after_long_help = "Examples:\n wakey devs\n wakey devs br-lan\n wakey devs --up\n wakey devs --json")] #[command(after_long_help = "Examples:
wakey devs
wakey devs br-lan
wakey devs --up
wakey devs --json")]
struct DevsArgs { struct DevsArgs {
/// Optional interface name to show. /// Optional interface name to show.
dev: Option<String>, dev: Option<String>,
+81
View File
@@ -0,0 +1,81 @@
use std::net::IpAddr;
use wakey::{broadcast_wake_targets, get_interface_summaries, get_status, get_status_for_input};
use wakey_core::{DeviceFilters, DeviceQuery};
#[tokio::test]
#[ignore = "runs against live router data; use on-device or via scripts/test_remote.ps1"]
async fn interfaces_real_router_prints_interface_summaries() -> anyhow::Result<()> {
let interfaces = get_interface_summaries().await?;
assert!(
!interfaces.is_empty(),
"expected at least one non-loopback interface summary"
);
println!("{}", serde_json::to_string_pretty(&interfaces)?);
Ok(())
}
#[tokio::test]
#[ignore = "runs against live router data; use on-device or via scripts/test_remote.ps1"]
async fn status_real_router_default_query_returns_rows_or_empty_cleanly() -> anyhow::Result<()> {
let status = get_status(DeviceQuery::default()).await?;
println!("{}", serde_json::to_string_pretty(&status)?);
Ok(())
}
#[tokio::test]
#[ignore = "runs against live router data; use on-device or via scripts/test_remote.ps1"]
async fn status_real_router_for_interface_filter_succeeds() -> anyhow::Result<()> {
let interfaces = get_interface_summaries().await?;
let first = interfaces
.first()
.map(|iface| iface.ifname.clone())
.expect("expected at least one interface");
let status = get_status(DeviceQuery {
name: None,
filter: DeviceFilters {
devs: vec![first.clone()],
..Default::default()
},
})
.await?;
println!("filtered dev: {first}");
println!("{}", serde_json::to_string_pretty(&status)?);
Ok(())
}
#[tokio::test]
#[ignore = "runs against live router data; use on-device or via scripts/test_remote.ps1"]
async fn status_real_router_string_input_for_interface_succeeds() -> anyhow::Result<()> {
let interfaces = get_interface_summaries().await?;
let first = interfaces
.first()
.map(|iface| iface.ifname.clone())
.expect("expected at least one interface");
let status = get_status_for_input(first.clone()).await?;
println!("selector: {first}");
println!("{}", serde_json::to_string_pretty(&status)?);
Ok(())
}
#[tokio::test]
#[ignore = "runs against live router data; use on-device or via scripts/test_remote.ps1"]
async fn broadcast_wake_targets_real_router_resolve_from_interfaces() -> anyhow::Result<()> {
let mac: macaddr::MacAddr = "aa:bb:cc:dd:ee:ff".parse()?;
let targets = broadcast_wake_targets(mac).await?;
assert!(
targets.iter().all(|target| target.mac == Some(mac)),
"all broadcast targets should preserve the requested MAC"
);
assert!(
targets.iter().all(|target| matches!(target.ip, Some(IpAddr::V4(_)))),
"broadcast targets should be IPv4 broadcast destinations"
);
println!("{}", serde_json::to_string_pretty(&targets)?);
Ok(())
}