Compare commits
2
Commits
9fcb191a8e
...
6f59c0c011
No files matched your search
+38
-2
@@ -31,6 +31,40 @@ function Quote-ShArg {
|
||||
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 {
|
||||
param($Exe, $Arguments, $Label)
|
||||
$displayArgs = $Arguments.Clone()
|
||||
@@ -57,14 +91,16 @@ function Invoke-Scp {
|
||||
if ($Recurse) { $arguments += '-r' }
|
||||
if ($HostKey) { $arguments += @('-batch', '-hostkey', $HostKey) }
|
||||
if ($Pass) { $arguments += @('-pw', $Pass) }
|
||||
$arguments += @($Local, $Dest)
|
||||
$arguments += @($Local)
|
||||
$arguments += $Dest
|
||||
Invoke-Ext -Exe $pscp.Path -Arguments $arguments -Label 'scp'
|
||||
}
|
||||
else {
|
||||
$arguments = @('-O')
|
||||
if ($Quiet) { $arguments += '-q' }
|
||||
if ($Recurse) { $arguments += '-r' }
|
||||
$arguments += @($Local, $Dest)
|
||||
$arguments += @($Local)
|
||||
$arguments += $Dest
|
||||
Invoke-Ext -Exe 'scp' -Arguments $arguments -Label 'scp'
|
||||
}
|
||||
}
|
||||
|
||||
+120
-16
@@ -4,14 +4,15 @@
|
||||
param(
|
||||
[string]$Package = "lda-ipjs",
|
||||
[switch]$AllPackages,
|
||||
[string]$BinaryFilter = "",
|
||||
[string]$Filter = "",
|
||||
[switch]$Exact,
|
||||
[switch]$List,
|
||||
[ValidateSet("debug", "release")]
|
||||
[string]$BuildProfile = "debug",
|
||||
[string]$BuildProfile = "release",
|
||||
[string]$password,
|
||||
[switch]$Verbose,
|
||||
[string]$RemoteTestPath = "/root/.bin/test",
|
||||
[string]$RemoteTestPath = "/tmp/tmp/wakey-test",
|
||||
[string]$RemoteHost = "[email protected]",
|
||||
[switch]$Ignored,
|
||||
[switch]$IncludeIgnored,
|
||||
@@ -43,11 +44,40 @@ function Get-WorkspacePackages {
|
||||
function Get-TestBinaryPaths {
|
||||
param([string[]]$CargoOutput)
|
||||
|
||||
@(
|
||||
$CargoOutput |
|
||||
Select-String -Pattern "Executable.*\((.+)\)" |
|
||||
ForEach-Object { $_.Matches.Groups[1].Value }
|
||||
)
|
||||
$paths = New-Object System.Collections.Generic.List[string]
|
||||
|
||||
foreach ($line in $CargoOutput) {
|
||||
if ([string]::IsNullOrWhiteSpace($line)) {
|
||||
continue
|
||||
}
|
||||
|
||||
try {
|
||||
$msg = $line | ConvertFrom-Json -ErrorAction Stop
|
||||
}
|
||||
catch {
|
||||
continue
|
||||
}
|
||||
|
||||
if ($msg.reason -ne "compiler-artifact") {
|
||||
continue
|
||||
}
|
||||
|
||||
if (-not $msg.executable) {
|
||||
continue
|
||||
}
|
||||
|
||||
$isTestProfile = $false
|
||||
if ($null -ne $msg.profile -and $null -ne $msg.profile.test) {
|
||||
$isTestProfile = [bool]$msg.profile.test
|
||||
}
|
||||
if (-not $isTestProfile) {
|
||||
continue
|
||||
}
|
||||
|
||||
$paths.Add([string]$msg.executable)
|
||||
}
|
||||
|
||||
@($paths)
|
||||
}
|
||||
|
||||
function Build-RemoteExecCommand {
|
||||
@@ -62,6 +92,51 @@ function Build-RemoteExecCommand {
|
||||
"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) }
|
||||
$failures = New-Object System.Collections.Generic.List[string]
|
||||
|
||||
@@ -69,12 +144,12 @@ foreach ($packageName in $packages) {
|
||||
Write-Host "Building tests for $packageName..." -ForegroundColor Cyan
|
||||
|
||||
# Stream cargo output and convert to text
|
||||
$cargoOutput = cargo test --no-run -p $packageName --target armv7-unknown-linux-musleabihf $(if ($BuildProfile -eq "release") { "-r" }) 2>&1 |
|
||||
$cargoOutput = cargo test --no-run -p $packageName --target armv7-unknown-linux-musleabihf --message-format json $(if ($BuildProfile -eq "release") { "-r" }) 2>&1 |
|
||||
ForEach-Object {
|
||||
$line = if ($_ -is [System.Management.Automation.ErrorRecord]) { $_.ToString() } else { $_ }
|
||||
if ($Verbose) {
|
||||
Write-Host $line
|
||||
}
|
||||
# if ($Verbose) {
|
||||
# Write-Host $line # this thing fills QUICK im not printing any
|
||||
# }
|
||||
$line
|
||||
}
|
||||
|
||||
@@ -87,19 +162,38 @@ foreach ($packageName in $packages) {
|
||||
$testBinaryPaths = Get-TestBinaryPaths $cargoOutput
|
||||
$testBinaries = @($testBinaryPaths | Get-Item)
|
||||
|
||||
if ($BinaryFilter) {
|
||||
$testBinaries = @(
|
||||
$testBinaries |
|
||||
Where-Object {
|
||||
$_.Name -like "*$BinaryFilter*" -or $_.BaseName -like "*$BinaryFilter*"
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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 {
|
||||
$remoteUploadDir = (Normalize-PosixPath $remoteRunDir).TrimEnd('/') + '/'
|
||||
Invoke-Scp -Local ($testBinaries | ForEach-Object { $_.FullName }) -Dest "${RemoteHost}:$remoteUploadDir" -Pass $password -Quiet
|
||||
|
||||
foreach ($testBinary in $testBinaries) {
|
||||
Write-Host "`nTesting: $packageName / $($testBinary.Name)" -ForegroundColor Cyan
|
||||
|
||||
try {
|
||||
# Copy and make executable
|
||||
Invoke-Scp -Local $testBinary.FullName -Dest "${RemoteHost}:$RemoteTestPath" -Pass $password -Quiet
|
||||
$remoteBinaryPath = New-RemoteBinaryPath -RemoteRunDir $remoteRunDir -TestBinary $testBinary
|
||||
|
||||
# Build test args
|
||||
$parts = @()
|
||||
@@ -111,9 +205,10 @@ foreach ($packageName in $packages) {
|
||||
if ($ShowOutput -or $Verbose) { $parts += "--show-output" }
|
||||
if ($NoCapture -or $Verbose) { $parts += "--nocapture" }
|
||||
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)
|
||||
try {
|
||||
Invoke-Ssh -Cmd $remoteCmd -Remote $RemoteHost -Pass $password
|
||||
}
|
||||
catch {
|
||||
@@ -123,6 +218,15 @@ foreach ($packageName in $packages) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
finally {
|
||||
try {
|
||||
Invoke-Ssh -Cmd ("rm -rf " + (Quote-ShArg $remoteRunDir)) -Remote $RemoteHost -Pass $password -Quiet
|
||||
}
|
||||
catch {
|
||||
Write-Warning "Failed to remove remote test dir: $remoteRunDir"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($failures.Count -gt 0) {
|
||||
|
||||
+23
-4
@@ -8,7 +8,9 @@ use wakey_core::{DeviceFilters, DeviceQuery, InterfaceSummary, WakeResult};
|
||||
#[derive(Parser)]
|
||||
#[command(name = "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 {
|
||||
#[command(subcommand)]
|
||||
command: Command,
|
||||
@@ -49,7 +51,15 @@ struct LeasesArgs {
|
||||
}
|
||||
|
||||
#[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 {
|
||||
/// Free-form device query, for example a hostname, IP, MAC, interface, or NUD state.
|
||||
query: Option<String>,
|
||||
@@ -65,7 +75,12 @@ struct WakeArgs {
|
||||
}
|
||||
|
||||
#[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 {
|
||||
/// Free-form device query.
|
||||
query: Option<String>,
|
||||
@@ -90,7 +105,11 @@ struct StatusArgs {
|
||||
}
|
||||
|
||||
#[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 {
|
||||
/// Optional interface name to show.
|
||||
dev: Option<String>,
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
Reference in new issue
Block a user