browser4-cli 0.1.24 → 0.1.25
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/browser4-cli-darwin-arm64 +0 -0
- package/bin/browser4-cli-darwin-x64 +0 -0
- package/bin/browser4-cli-linux-arm64 +0 -0
- package/bin/browser4-cli-linux-musl-arm64 +0 -0
- package/bin/browser4-cli-linux-musl-x64 +0 -0
- package/bin/browser4-cli-linux-x64 +0 -0
- package/bin/browser4-cli-win32-x64.exe +0 -0
- package/package.json +1 -1
- package/scripts/install-browser4-cli.ps1 +103 -55
- package/scripts/install-browser4-cli.sh +47 -42
- package/scripts/tests/install-browser4-cli.tests.ps1 +366 -0
- package/scripts/tests/install-browser4-cli.tests.sh +322 -0
|
@@ -16,19 +16,20 @@
|
|
|
16
16
|
# --version, -v TAG Release tag (e.g. "v4.11.0"). Default: latest.
|
|
17
17
|
# --install-dir, -d DIR Install directory (default: ~/.local/bin).
|
|
18
18
|
# --source SRC Force download source: "github" or "oss".
|
|
19
|
-
# Default (auto): locale-aware
|
|
19
|
+
# Default (auto): locale-aware -- OSS first for China mainland.
|
|
20
20
|
# --no-path Skip adding install dir to PATH.
|
|
21
21
|
# --skip-local Skip checking for a locally-bundled binary.
|
|
22
22
|
# --locate Print detection results and exit (no install).
|
|
23
23
|
# --silent, -s Suppress non-error output.
|
|
24
24
|
# --dry-run Print what would be done without doing it.
|
|
25
|
+
# --skip-if-installed Skip download if binary already exists at install path.
|
|
25
26
|
# --help, -h Show this message.
|
|
26
27
|
|
|
27
28
|
set -euo pipefail
|
|
28
29
|
|
|
29
|
-
#
|
|
30
|
+
# ----------------------------------------------
|
|
30
31
|
# Globals
|
|
31
|
-
#
|
|
32
|
+
# ----------------------------------------------
|
|
32
33
|
|
|
33
34
|
GITHUB_REPO="platonai/Browser4"
|
|
34
35
|
OSS_BASE="https://browser4.oss-cn-beijing.aliyuncs.com"
|
|
@@ -39,19 +40,20 @@ SOURCE=""
|
|
|
39
40
|
ADD_TO_PATH=true
|
|
40
41
|
SILENT=false
|
|
41
42
|
DRY_RUN=false
|
|
43
|
+
SKIP_IF_INSTALLED=false
|
|
42
44
|
SKIP_LOCAL=false
|
|
43
45
|
LOCATE_MODE=false
|
|
44
46
|
CHINA_DETECTED=false
|
|
45
47
|
SCRIPT_DIR=""
|
|
46
48
|
|
|
47
|
-
#
|
|
49
|
+
# ----------------------------------------------
|
|
48
50
|
# Helpers
|
|
49
|
-
#
|
|
51
|
+
# ----------------------------------------------
|
|
50
52
|
|
|
51
53
|
say() { if [[ "$SILENT" != true ]]; then echo -e "$*"; fi; }
|
|
52
|
-
step() { say "
|
|
53
|
-
ok() { say "
|
|
54
|
-
warn() { say "
|
|
54
|
+
step() { say " -> $*"; }
|
|
55
|
+
ok() { say " [v] $*"; }
|
|
56
|
+
warn() { say " [!] $*" >&2; }
|
|
55
57
|
die() { echo "ERROR: $*" >&2; exit 1; }
|
|
56
58
|
|
|
57
59
|
color_cyan='\033[0;36m'
|
|
@@ -61,16 +63,16 @@ color_reset='\033[0m'
|
|
|
61
63
|
|
|
62
64
|
header() {
|
|
63
65
|
if [[ "$SILENT" != true ]]; then
|
|
64
|
-
echo -e "${color_cyan}
|
|
65
|
-
echo -e "${color_cyan}
|
|
66
|
-
echo -e "${color_cyan}
|
|
66
|
+
echo -e "${color_cyan}==========================================${color_reset}"
|
|
67
|
+
echo -e "${color_cyan} browser4-cli Installer${color_reset}"
|
|
68
|
+
echo -e "${color_cyan}==========================================${color_reset}"
|
|
67
69
|
echo ""
|
|
68
70
|
fi
|
|
69
71
|
}
|
|
70
72
|
|
|
71
|
-
#
|
|
72
|
-
# Script location
|
|
73
|
-
#
|
|
73
|
+
# ----------------------------------------------
|
|
74
|
+
# Script location -- find ourselves on disk
|
|
75
|
+
# ----------------------------------------------
|
|
74
76
|
|
|
75
77
|
detect_script_dir() {
|
|
76
78
|
# BASH_SOURCE works even when sourced; prefer it over $0.
|
|
@@ -79,7 +81,7 @@ detect_script_dir() {
|
|
|
79
81
|
elif [[ -n "${0:-}" ]] && [[ "$0" != "bash" ]] && [[ "$0" != "-bash" ]] && [[ -f "$0" ]]; then
|
|
80
82
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
81
83
|
fi
|
|
82
|
-
# If piped via curl | bash, SCRIPT_DIR stays empty
|
|
84
|
+
# If piped via curl | bash, SCRIPT_DIR stays empty -- no local binaries available.
|
|
83
85
|
}
|
|
84
86
|
|
|
85
87
|
# Search for a pre-downloaded binary near the script (bundled/sideload install).
|
|
@@ -127,6 +129,7 @@ Options:
|
|
|
127
129
|
--locate Print detection results and exit without installing.
|
|
128
130
|
--silent, -s Suppress non-error output.
|
|
129
131
|
--dry-run Print what would be done without doing it.
|
|
132
|
+
--skip-if-installed Skip download if binary already exists at install path.
|
|
130
133
|
--help, -h Show this message.
|
|
131
134
|
|
|
132
135
|
Examples:
|
|
@@ -137,12 +140,13 @@ Examples:
|
|
|
137
140
|
$(basename "$0") --locate # Run diagnostics (no install)
|
|
138
141
|
$(basename "$0") --skip-local # Force download, ignore bundled binary
|
|
139
142
|
$(basename "$0") --source oss # Force Aliyun OSS (China mainland)
|
|
143
|
+
$(basename "$0") --skip-if-installed # Skip download if already installed
|
|
140
144
|
EOF
|
|
141
145
|
}
|
|
142
146
|
|
|
143
|
-
#
|
|
147
|
+
# ----------------------------------------------
|
|
144
148
|
# Argument parsing
|
|
145
|
-
#
|
|
149
|
+
# ----------------------------------------------
|
|
146
150
|
|
|
147
151
|
while [[ $# -gt 0 ]]; do
|
|
148
152
|
case "$1" in
|
|
@@ -163,29 +167,30 @@ while [[ $# -gt 0 ]]; do
|
|
|
163
167
|
--locate) LOCATE_MODE=true; shift ;;
|
|
164
168
|
--silent|-s) SILENT=true; shift ;;
|
|
165
169
|
--dry-run) DRY_RUN=true; shift ;;
|
|
170
|
+
--skip-if-installed) SKIP_IF_INSTALLED=true; shift ;;
|
|
166
171
|
--help|-h) usage; exit 0 ;;
|
|
167
172
|
*) die "Unknown argument: $1 (use --help)";;
|
|
168
173
|
esac
|
|
169
174
|
done
|
|
170
175
|
|
|
171
|
-
#
|
|
176
|
+
# ----------------------------------------------
|
|
172
177
|
# China mainland locale detection (zero-network)
|
|
173
|
-
#
|
|
178
|
+
# ----------------------------------------------
|
|
174
179
|
|
|
175
180
|
detect_china_locale() {
|
|
176
|
-
# 1
|
|
181
|
+
# 1 -- Locale env vars
|
|
177
182
|
local lang
|
|
178
183
|
lang="${LC_ALL:-${LANG:-${LC_CTYPE:-${LC_MESSAGES:-}}}}"
|
|
179
184
|
case "$lang" in
|
|
180
185
|
zh_CN*|zh-CN*|"Chinese (Simplified)_China"*) return 0 ;;
|
|
181
186
|
esac
|
|
182
187
|
|
|
183
|
-
# 2
|
|
188
|
+
# 2 -- TZ env var
|
|
184
189
|
case "${TZ:-}" in
|
|
185
190
|
Asia/Shanghai|Asia/Chongqing|Asia/Urumqi|Asia/Harbin) return 0 ;;
|
|
186
191
|
esac
|
|
187
192
|
|
|
188
|
-
# 3
|
|
193
|
+
# 3 -- /etc/timezone
|
|
189
194
|
if [[ -f /etc/timezone ]]; then
|
|
190
195
|
local tz
|
|
191
196
|
tz=$(cat /etc/timezone 2>/dev/null || true)
|
|
@@ -197,9 +202,9 @@ detect_china_locale() {
|
|
|
197
202
|
return 1
|
|
198
203
|
}
|
|
199
204
|
|
|
200
|
-
#
|
|
205
|
+
# ----------------------------------------------
|
|
201
206
|
# Platform detection
|
|
202
|
-
#
|
|
207
|
+
# ----------------------------------------------
|
|
203
208
|
|
|
204
209
|
detect_os() {
|
|
205
210
|
case "$(uname -s)" in
|
|
@@ -235,7 +240,7 @@ detect_libc() {
|
|
|
235
240
|
fi
|
|
236
241
|
fi
|
|
237
242
|
|
|
238
|
-
# Check for musl loader
|
|
243
|
+
# Check for musl loader -- covers common architectures
|
|
239
244
|
# x86_64, aarch64, armhf (32-bit ARM), i386, riscv64, s390x, ppc64le, mips64
|
|
240
245
|
local musl_loader
|
|
241
246
|
for musl_loader in \
|
|
@@ -298,9 +303,9 @@ get_default_install_dir() {
|
|
|
298
303
|
echo "$dir"
|
|
299
304
|
}
|
|
300
305
|
|
|
301
|
-
#
|
|
306
|
+
# ----------------------------------------------
|
|
302
307
|
# Download
|
|
303
|
-
#
|
|
308
|
+
# ----------------------------------------------
|
|
304
309
|
|
|
305
310
|
check_commands() {
|
|
306
311
|
local missing=()
|
|
@@ -392,7 +397,7 @@ download_file() {
|
|
|
392
397
|
return 0
|
|
393
398
|
fi
|
|
394
399
|
|
|
395
|
-
warn "Downloaded file too small (${size} bytes)
|
|
400
|
+
warn "Downloaded file too small (${size} bytes) -- may be an error page"
|
|
396
401
|
rm -f "$dest"
|
|
397
402
|
return 1
|
|
398
403
|
fi
|
|
@@ -402,9 +407,9 @@ download_file() {
|
|
|
402
407
|
return 1
|
|
403
408
|
}
|
|
404
409
|
|
|
405
|
-
#
|
|
410
|
+
# ----------------------------------------------
|
|
406
411
|
# PATH management
|
|
407
|
-
#
|
|
412
|
+
# ----------------------------------------------
|
|
408
413
|
|
|
409
414
|
add_to_shell_rc() {
|
|
410
415
|
local dir="$1"
|
|
@@ -468,9 +473,9 @@ add_to_shell_rc() {
|
|
|
468
473
|
say " Reload with: source $rc_file"
|
|
469
474
|
}
|
|
470
475
|
|
|
471
|
-
#
|
|
476
|
+
# ----------------------------------------------
|
|
472
477
|
# Symlinks
|
|
473
|
-
#
|
|
478
|
+
# ----------------------------------------------
|
|
474
479
|
|
|
475
480
|
create_symlinks() {
|
|
476
481
|
local binary_name="$1"
|
|
@@ -514,9 +519,9 @@ create_symlinks() {
|
|
|
514
519
|
fi
|
|
515
520
|
}
|
|
516
521
|
|
|
517
|
-
#
|
|
522
|
+
# ----------------------------------------------
|
|
518
523
|
# Main
|
|
519
|
-
#
|
|
524
|
+
# ----------------------------------------------
|
|
520
525
|
|
|
521
526
|
main() {
|
|
522
527
|
check_commands
|
|
@@ -538,11 +543,11 @@ main() {
|
|
|
538
543
|
platform_key=$(get_platform_key)
|
|
539
544
|
binary_name=$(get_binary_name "$platform_key")
|
|
540
545
|
|
|
541
|
-
#
|
|
546
|
+
# -- Locate mode: print diagnostics and exit --
|
|
542
547
|
if [[ "$LOCATE_MODE" == true ]]; then
|
|
543
|
-
echo -e "${color_cyan}
|
|
548
|
+
echo -e "${color_cyan}--- Locate / diagnostics ---${color_reset}"
|
|
544
549
|
echo ""
|
|
545
|
-
step "Script dir: ${SCRIPT_DIR:-'(not available
|
|
550
|
+
step "Script dir: ${SCRIPT_DIR:-'(not available -- piped via curl?)'}"
|
|
546
551
|
step "Platform key: $platform_key"
|
|
547
552
|
step "Binary name: $binary_name"
|
|
548
553
|
step "Default install: $(get_default_install_dir)"
|
|
@@ -606,7 +611,7 @@ main() {
|
|
|
606
611
|
|
|
607
612
|
local binary_path="${INSTALL_DIR}/${binary_name}"
|
|
608
613
|
|
|
609
|
-
#
|
|
614
|
+
# -- Local binary discovery (bundled/sideload) --
|
|
610
615
|
local use_local_binary=false
|
|
611
616
|
local local_binary_path=""
|
|
612
617
|
if [[ "$SKIP_LOCAL" != true ]]; then
|
|
@@ -616,7 +621,7 @@ main() {
|
|
|
616
621
|
ok "Local binary verified (--version OK)"
|
|
617
622
|
use_local_binary=true
|
|
618
623
|
else
|
|
619
|
-
warn "Local binary found but --version check failed
|
|
624
|
+
warn "Local binary found but --version check failed -- will download instead"
|
|
620
625
|
fi
|
|
621
626
|
fi
|
|
622
627
|
else
|
|
@@ -624,7 +629,7 @@ main() {
|
|
|
624
629
|
fi
|
|
625
630
|
|
|
626
631
|
# Install binary: local copy > already installed > download
|
|
627
|
-
if [[ -f "$binary_path" ]] && [[ -z "$VERSION" ]] && [[ "$use_local_binary" != true ]]; then
|
|
632
|
+
if [[ -f "$binary_path" ]] && [[ -z "$VERSION" ]] && [[ "$SKIP_IF_INSTALLED" == true ]] && [[ "$use_local_binary" != true ]]; then
|
|
628
633
|
ok "Binary already installed: $binary_path"
|
|
629
634
|
elif [[ "$use_local_binary" == true ]]; then
|
|
630
635
|
# Copy local binary to install dir
|
|
@@ -705,10 +710,10 @@ main() {
|
|
|
705
710
|
say ""
|
|
706
711
|
if [[ "$DRY_RUN" != true ]]; then
|
|
707
712
|
if version_output=$("$binary_path" --version 2>&1); then
|
|
708
|
-
echo -e "${color_green}
|
|
713
|
+
echo -e "${color_green}[v] browser4-cli installed successfully${color_reset}"
|
|
709
714
|
say " Version: $version_output"
|
|
710
715
|
else
|
|
711
|
-
echo -e "${color_green}
|
|
716
|
+
echo -e "${color_green}[v] Binary installed at: $binary_path${color_reset}"
|
|
712
717
|
warn "Could not verify --version (this is normal on first install)"
|
|
713
718
|
fi
|
|
714
719
|
else
|
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
<#
|
|
2
|
+
.SYNOPSIS
|
|
3
|
+
Tests for install-browser4-cli.ps1
|
|
4
|
+
PowerShell 5.1+ only — zero external dependencies.
|
|
5
|
+
Run: powershell -NoProfile -ExecutionPolicy Bypass -File install-browser4-cli.tests.ps1
|
|
6
|
+
#>
|
|
7
|
+
|
|
8
|
+
$ErrorActionPreference = "Stop"
|
|
9
|
+
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
|
10
|
+
$installScript = Join-Path (Split-Path $scriptDir -Parent) "install-browser4-cli.ps1"
|
|
11
|
+
|
|
12
|
+
$pass = 0
|
|
13
|
+
$fail = 0
|
|
14
|
+
|
|
15
|
+
function Test($name, [ScriptBlock]$block) {
|
|
16
|
+
try {
|
|
17
|
+
$null = & $block
|
|
18
|
+
$script:pass++
|
|
19
|
+
Write-Host " PASS $name" -ForegroundColor Green
|
|
20
|
+
} catch {
|
|
21
|
+
$script:fail++
|
|
22
|
+
Write-Host " FAIL $name" -ForegroundColor Red
|
|
23
|
+
Write-Host " $($_.Exception.Message)" -ForegroundColor Red
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function RunScript([string]$scriptArgs, [ref]$exitCode) {
|
|
28
|
+
# Use -Command to control stderr redirect (2> path must come before script args,
|
|
29
|
+
# which -File would pass as literal arguments to the script).
|
|
30
|
+
$tmpErr = [System.IO.Path]::GetTempFileName()
|
|
31
|
+
$cmd = "& '$installScript' $scriptArgs 2>'$tmpErr'"
|
|
32
|
+
$psi = New-Object System.Diagnostics.ProcessStartInfo
|
|
33
|
+
$psi.FileName = "powershell.exe"
|
|
34
|
+
$psi.Arguments = "-NoProfile -ExecutionPolicy Bypass -Command `"$cmd`""
|
|
35
|
+
$psi.RedirectStandardOutput = $true
|
|
36
|
+
$psi.UseShellExecute = $false
|
|
37
|
+
$psi.CreateNoWindow = $true
|
|
38
|
+
$proc = [System.Diagnostics.Process]::Start($psi)
|
|
39
|
+
$out = $proc.StandardOutput.ReadToEnd()
|
|
40
|
+
$proc.WaitForExit()
|
|
41
|
+
$exitCode.Value = $proc.ExitCode
|
|
42
|
+
$err = if (Test-Path $tmpErr) { Get-Content $tmpErr -Raw -ErrorAction SilentlyContinue; Remove-Item $tmpErr -Force -ErrorAction SilentlyContinue } else { "" }
|
|
43
|
+
return [PSCustomObject]@{ Output = $out; Error = $err; ExitCode = $proc.ExitCode }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
Write-Host "============================================" -ForegroundColor Cyan
|
|
47
|
+
Write-Host " install-browser4-cli.ps1 Test Suite" -ForegroundColor Cyan
|
|
48
|
+
Write-Host "============================================" -ForegroundColor Cyan
|
|
49
|
+
Write-Host ""
|
|
50
|
+
|
|
51
|
+
# ── Pre-flight ──
|
|
52
|
+
Write-Host "--- Pre-flight ---" -ForegroundColor Cyan
|
|
53
|
+
|
|
54
|
+
Test "file exists" {
|
|
55
|
+
if (-not (Test-Path $installScript)) { throw "Not found: $installScript" }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
Test "file is readable" {
|
|
59
|
+
$null = Get-Content $installScript -Raw -ErrorAction Stop
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
Test "no non-ASCII bytes" {
|
|
63
|
+
$bytes = [System.IO.File]::ReadAllBytes($installScript)
|
|
64
|
+
$nonAscii = @($bytes | Where-Object { $_ -gt 127 })
|
|
65
|
+
if ($nonAscii.Count -gt 0) {
|
|
66
|
+
throw "Found $($nonAscii.Count) non-ASCII bytes"
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
Test "AST parses without errors" {
|
|
71
|
+
$parseErrors = $null
|
|
72
|
+
$ast = [System.Management.Automation.Language.Parser]::ParseFile(
|
|
73
|
+
$installScript, [ref]$null, [ref]$parseErrors
|
|
74
|
+
)
|
|
75
|
+
if ($parseErrors.Count -gt 0) {
|
|
76
|
+
$msg = ($parseErrors | ForEach-Object { "L$($_.Extent.StartLineNumber): $($_.Message)" }) -join "; "
|
|
77
|
+
throw $msg
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
Write-Host ""
|
|
82
|
+
|
|
83
|
+
# ── Param block (via AST) ──
|
|
84
|
+
Write-Host "--- Param block ---" -ForegroundColor Cyan
|
|
85
|
+
|
|
86
|
+
$content = Get-Content $installScript -Raw
|
|
87
|
+
$parseErrors = $null
|
|
88
|
+
$ast = [System.Management.Automation.Language.Parser]::ParseInput($content, [ref]$null, [ref]$parseErrors)
|
|
89
|
+
|
|
90
|
+
Test "param block defines all expected parameters" {
|
|
91
|
+
$expected = @('Version', 'InstallDir', 'Source', 'AddToPath',
|
|
92
|
+
'Silent', 'DryRun', 'SkipIfInstalled', 'SkipLocal', 'Locate')
|
|
93
|
+
# Extract param block text via AST extents
|
|
94
|
+
$paramAst = $ast.ParamBlock
|
|
95
|
+
if (-not $paramAst) { throw "Could not find param block AST" }
|
|
96
|
+
$txt = $content.Substring($paramAst.Extent.StartOffset, $paramAst.Extent.EndOffset - $paramAst.Extent.StartOffset)
|
|
97
|
+
foreach ($p in $expected) {
|
|
98
|
+
if ($txt -notmatch ('\$' + $p + '\b')) {
|
|
99
|
+
throw "Missing parameter: $p"
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
Test "Source has ValidateSet with empty string" {
|
|
105
|
+
if ($content -notmatch 'ValidateSet\("",\s*"github",\s*"oss"\)') {
|
|
106
|
+
throw "ValidateSet must include empty string for iex compatibility"
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
Write-Host ""
|
|
111
|
+
|
|
112
|
+
# ── Locate mode ──
|
|
113
|
+
Write-Host "--- Locate mode ---" -ForegroundColor Cyan
|
|
114
|
+
|
|
115
|
+
$ec = 0
|
|
116
|
+
$r = RunScript -scriptArgs "-Locate" -exitCode ([ref]$ec)
|
|
117
|
+
|
|
118
|
+
Test "-Locate exits with code 0" {
|
|
119
|
+
if ($ec -ne 0) { throw "Exit code: $ec" }
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
Test "-Locate shows platform key" {
|
|
123
|
+
if ($r.Output -notmatch 'Platform key') {
|
|
124
|
+
throw "Missing 'Platform key' in: $($r.Output.Substring(0, [Math]::Min(500, $r.Output.Length)))"
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
Test "-Locate shows binary name" {
|
|
129
|
+
if ($r.Output -notmatch 'Binary name') {
|
|
130
|
+
throw "Missing 'Binary name' in output (len=$($r.Output.Length))"
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
Test "-Locate shows download order" {
|
|
135
|
+
if ($r.Output -notmatch 'Download order') {
|
|
136
|
+
throw "Missing 'Download order' in output"
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
Write-Host ""
|
|
141
|
+
|
|
142
|
+
# ── Download URLs (via -Locate output) ──
|
|
143
|
+
Write-Host "--- Download URLs ---" -ForegroundColor Cyan
|
|
144
|
+
|
|
145
|
+
Test "locate shows correct GitHub latest/download URL" {
|
|
146
|
+
if ($r.Output -notmatch 'github\.com/platonai/Browser4/releases/latest/download/') {
|
|
147
|
+
$lines = ($r.Output -split '\n' | Where-Object { $_ -match 'ownload' }) -join '; '
|
|
148
|
+
throw "GitHub latest URL not found. Download lines: $lines"
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
Test "locate shows correct OSS download/latest URL" {
|
|
153
|
+
if ($r.Output -notmatch 'oss-cn-beijing.*?releases/download/latest/') {
|
|
154
|
+
$lines = ($r.Output -split '\n' | Where-Object { $_ -match 'ownload' }) -join '; '
|
|
155
|
+
throw "OSS latest URL not found. Download lines: $lines"
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
# Test versioned URLs
|
|
160
|
+
$ec2 = 0
|
|
161
|
+
$r2 = RunScript -scriptArgs "-Version v4.11.0 -Locate" -exitCode ([ref]$ec2)
|
|
162
|
+
|
|
163
|
+
Test "-Version shows tag-based URLs" {
|
|
164
|
+
if ($r2.Output -notmatch 'releases/download/v4\.11\.0/') {
|
|
165
|
+
throw "Versioned URL not in output"
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
Write-Host ""
|
|
170
|
+
|
|
171
|
+
# ── Parameter acceptance ──
|
|
172
|
+
Write-Host "--- Parameter acceptance ---" -ForegroundColor Cyan
|
|
173
|
+
|
|
174
|
+
Test "-SkipIfInstalled flag accepted" {
|
|
175
|
+
$ec3 = 0; $r3 = RunScript -scriptArgs "-SkipIfInstalled -DryRun" -exitCode ([ref]$ec3)
|
|
176
|
+
if ($ec3 -ne 0) { throw "Exit code: $ec3, output: $($r3.Output)" }
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
Test "-SkipLocal flag accepted" {
|
|
180
|
+
$ec4 = 0; $r4 = RunScript -scriptArgs "-SkipLocal -DryRun" -exitCode ([ref]$ec4)
|
|
181
|
+
if ($ec4 -ne 0) { throw "Exit code: $ec4, output: $($r4.Output)" }
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
Test "-Force rejected (replaced by -SkipIfInstalled)" {
|
|
185
|
+
$ec5 = 0; $r5 = RunScript -scriptArgs "-Force -DryRun" -exitCode ([ref]$ec5)
|
|
186
|
+
if ($r5.Error -notmatch 'Force') {
|
|
187
|
+
throw "-Force should be rejected, got: $($r5.Error)"
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
Test "-Source oss accepted" {
|
|
192
|
+
$ec6 = 0; $r6 = RunScript -scriptArgs "-Source oss -DryRun" -exitCode ([ref]$ec6)
|
|
193
|
+
if ($ec6 -ne 0) { throw "Exit code: $ec6, output: $($r6.Output)" }
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
Test "-Source github accepted" {
|
|
197
|
+
$ec7 = 0; $r7 = RunScript -scriptArgs "-Source github -DryRun" -exitCode ([ref]$ec7)
|
|
198
|
+
if ($ec7 -ne 0) { throw "Exit code: $ec7, output: $($r7.Output)" }
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
Test "-Source invalid rejected" {
|
|
202
|
+
$ec8 = 0; $r8 = RunScript -scriptArgs "-Source invalid -DryRun" -exitCode ([ref]$ec8)
|
|
203
|
+
if ($r8.Error -notmatch 'Source|invalid|parameter') {
|
|
204
|
+
throw "-Source invalid should be rejected, got: $($r8.Error)"
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
Test "-Silent flag accepted" {
|
|
209
|
+
$ec9 = 0; $r9 = RunScript -scriptArgs "-Silent -DryRun" -exitCode ([ref]$ec9)
|
|
210
|
+
if ($ec9 -ne 0) { throw "Exit code: $ec9, output: $($r9.Output)" }
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
Test "-Version flag accepted" {
|
|
214
|
+
$ec10 = 0; $r10 = RunScript -scriptArgs "-Version v4.11.0 -DryRun" -exitCode ([ref]$ec10)
|
|
215
|
+
if ($ec10 -ne 0) { throw "Exit code: $ec10, output: $($r10.Output)" }
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
Write-Host ""
|
|
219
|
+
|
|
220
|
+
# ── Functions via dot-source ──
|
|
221
|
+
Write-Host "--- Functions ---" -ForegroundColor Cyan
|
|
222
|
+
|
|
223
|
+
# Strip trailing Main call and dot-source for function-level tests
|
|
224
|
+
$scriptContent = Get-Content $installScript -Raw
|
|
225
|
+
$scriptContent = $scriptContent -replace '\r?\nMain\s*$', ''
|
|
226
|
+
$scriptContent = $scriptContent -replace '\$ErrorActionPreference\s*=\s*"Stop"', ''
|
|
227
|
+
$sb = [ScriptBlock]::Create($scriptContent)
|
|
228
|
+
|
|
229
|
+
& {
|
|
230
|
+
# Suppress output
|
|
231
|
+
$Silent = $true
|
|
232
|
+
$DryRun = $false
|
|
233
|
+
$SkipLocal = $false
|
|
234
|
+
$Locate = $false
|
|
235
|
+
$Source = ""
|
|
236
|
+
$Version = ""
|
|
237
|
+
$InstallDir = ""
|
|
238
|
+
$AddToPath = $true
|
|
239
|
+
|
|
240
|
+
. $sb
|
|
241
|
+
|
|
242
|
+
Test "Get-PlatformKey returns valid format" {
|
|
243
|
+
$key = Get-PlatformKey
|
|
244
|
+
if ($key -notmatch '^(win32|linux|darwin)-(x64|arm64)$' -and
|
|
245
|
+
$key -notmatch '^linux-musl-(x64|arm64)$') {
|
|
246
|
+
throw "Unexpected platform key: $key"
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
Test "Get-BinaryName includes .exe on win32" {
|
|
251
|
+
$name = Get-BinaryName -PlatformKey "win32-x64"
|
|
252
|
+
if ($name -ne "browser4-cli-win32-x64.exe") { throw "Got: $name" }
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
Test "Get-BinaryName excludes .exe on linux" {
|
|
256
|
+
$name = Get-BinaryName -PlatformKey "linux-x64"
|
|
257
|
+
if ($name -ne "browser4-cli-linux-x64") { throw "Got: $name" }
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
Test "Get-BinaryName excludes .exe on darwin" {
|
|
261
|
+
$name = Get-BinaryName -PlatformKey "darwin-arm64"
|
|
262
|
+
if ($name -ne "browser4-cli-darwin-arm64") { throw "Got: $name" }
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
Test "Get-DefaultInstallDir returns non-empty" {
|
|
266
|
+
$dir = Get-DefaultInstallDir
|
|
267
|
+
if ([string]::IsNullOrEmpty($dir)) { throw "Empty install dir" }
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
Test "Test-ChinaLocale returns [bool]" {
|
|
271
|
+
$result = Test-ChinaLocale
|
|
272
|
+
if ($result -isnot [bool]) { throw "Expected [bool], got $($result.GetType())" }
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
Test "Find-LocalBinary returns null for non-existent" {
|
|
276
|
+
$result = Find-LocalBinary -BinaryName "nonexistent-file-xyz.exe"
|
|
277
|
+
if ($result -ne $null) { throw "Expected null, got: $result" }
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
Test "Test-LocalBinary returns false for empty string" {
|
|
281
|
+
if (Test-LocalBinary -Path "") { throw "Should be false" }
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
Test "Test-LocalBinary returns false for null" {
|
|
285
|
+
if (Test-LocalBinary -Path $null) { throw "Should be false" }
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
# ── Set-BinaryFile (locked-binary-safe replacement) ──
|
|
289
|
+
Test "Set-BinaryFile creates new file (Copy)" {
|
|
290
|
+
$tmpDir = Join-Path $env:TEMP "b4-test-$(New-Guid)"
|
|
291
|
+
New-Item -ItemType Directory $tmpDir -Force | Out-Null
|
|
292
|
+
try {
|
|
293
|
+
$src = Join-Path $tmpDir "source.exe"
|
|
294
|
+
$dst = Join-Path $tmpDir "target.exe"
|
|
295
|
+
"new-content" | Out-File $src -Encoding ASCII
|
|
296
|
+
Set-BinaryFile -TargetPath $dst -SourcePath $src
|
|
297
|
+
if (-not (Test-Path $dst)) { throw "Target was not created" }
|
|
298
|
+
$actual = (Get-Content $dst -Raw).Trim()
|
|
299
|
+
if ($actual -ne "new-content") { throw "Wrong content: '$actual'" }
|
|
300
|
+
# Source still exists (Copy mode)
|
|
301
|
+
if (-not (Test-Path $src)) { throw "Source should still exist after copy" }
|
|
302
|
+
} finally {
|
|
303
|
+
Remove-Item $tmpDir -Recurse -Force -ErrorAction SilentlyContinue
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
Test "Set-BinaryFile creates new file (Move)" {
|
|
308
|
+
$tmpDir = Join-Path $env:TEMP "b4-test-$(New-Guid)"
|
|
309
|
+
New-Item -ItemType Directory $tmpDir -Force | Out-Null
|
|
310
|
+
try {
|
|
311
|
+
$src = Join-Path $tmpDir "source.exe"
|
|
312
|
+
$dst = Join-Path $tmpDir "target.exe"
|
|
313
|
+
"moved-content" | Out-File $src -Encoding ASCII
|
|
314
|
+
Set-BinaryFile -TargetPath $dst -SourcePath $src -Move
|
|
315
|
+
if (-not (Test-Path $dst)) { throw "Target was not created" }
|
|
316
|
+
$actual = (Get-Content $dst -Raw).Trim()
|
|
317
|
+
if ($actual -ne "moved-content") { throw "Wrong content: '$actual'" }
|
|
318
|
+
# Source should be gone (Move mode)
|
|
319
|
+
if (Test-Path $src) { throw "Source should not exist after move" }
|
|
320
|
+
} finally {
|
|
321
|
+
Remove-Item $tmpDir -Recurse -Force -ErrorAction SilentlyContinue
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
Test "Set-BinaryFile replaces existing file (Copy)" {
|
|
326
|
+
$tmpDir = Join-Path $env:TEMP "b4-test-$(New-Guid)"
|
|
327
|
+
New-Item -ItemType Directory $tmpDir -Force | Out-Null
|
|
328
|
+
try {
|
|
329
|
+
$src = Join-Path $tmpDir "source.exe"
|
|
330
|
+
$dst = Join-Path $tmpDir "target.exe"
|
|
331
|
+
"old" | Out-File $dst -Encoding ASCII
|
|
332
|
+
"new" | Out-File $src -Encoding ASCII
|
|
333
|
+
Set-BinaryFile -TargetPath $dst -SourcePath $src
|
|
334
|
+
if ((Get-Content $dst -Raw).Trim() -ne "new") { throw "Content not replaced" }
|
|
335
|
+
} finally {
|
|
336
|
+
Remove-Item $tmpDir -Recurse -Force -ErrorAction SilentlyContinue
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
Test "Set-BinaryFile replaces existing file (Move)" {
|
|
341
|
+
$tmpDir = Join-Path $env:TEMP "b4-test-$(New-Guid)"
|
|
342
|
+
New-Item -ItemType Directory $tmpDir -Force | Out-Null
|
|
343
|
+
try {
|
|
344
|
+
$src = Join-Path $tmpDir "source.exe"
|
|
345
|
+
$dst = Join-Path $tmpDir "target.exe"
|
|
346
|
+
"old" | Out-File $dst -Encoding ASCII
|
|
347
|
+
"moved" | Out-File $src -Encoding ASCII
|
|
348
|
+
Set-BinaryFile -TargetPath $dst -SourcePath $src -Move
|
|
349
|
+
if ((Get-Content $dst -Raw).Trim() -ne "moved") { throw "Content not replaced" }
|
|
350
|
+
if (Test-Path $src) { throw "Source should not exist after move" }
|
|
351
|
+
} finally {
|
|
352
|
+
Remove-Item $tmpDir -Recurse -Force -ErrorAction SilentlyContinue
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
Write-Host ""
|
|
358
|
+
|
|
359
|
+
# ── Summary ──
|
|
360
|
+
Write-Host "============================================" -ForegroundColor Cyan
|
|
361
|
+
$total = $pass + $fail
|
|
362
|
+
Write-Host " Results: $pass / $total passed" -ForegroundColor $(if ($fail -eq 0) { "Green" } else { "Red" })
|
|
363
|
+
Write-Host "============================================" -ForegroundColor Cyan
|
|
364
|
+
|
|
365
|
+
if ($fail -gt 0) { exit 1 }
|
|
366
|
+
exit 0
|