Archive reviewed candidate; not production acceptance (2026-09-24)
Android client acceptance / test-and-build (push) Has been cancelled
Android client acceptance / test-and-build (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
# Local candidate only. Uses cached dependencies; never starts adb or an emulator.
|
||||
param([string]$JobDirectory = '',[switch]$ResumeAfterBuild)
|
||||
$ErrorActionPreference = 'Stop'
|
||||
Set-StrictMode -Version Latest
|
||||
$repo = [IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..'))
|
||||
$tempRoot = [IO.Path]::TrimEndingDirectorySeparator([IO.Path]::GetTempPath())
|
||||
if ($ResumeAfterBuild -and -not $JobDirectory) { throw 'Resume requires the exact completed build directory' }
|
||||
if (-not $JobDirectory) { $JobDirectory = Join-Path $tempRoot ('ucvl-android-104-' + [guid]::NewGuid().ToString('N')) }
|
||||
$job = [IO.Path]::GetFullPath($JobDirectory)
|
||||
if ([IO.Path]::GetDirectoryName($job) -ine $tempRoot -or [IO.Path]::GetFileName($job) -cnotmatch '^ucvl-android-104-[a-f0-9]{32}$' -or $job -match '[^\x00-\x7F]') {
|
||||
throw 'A fresh scoped ASCII temporary directory is required'
|
||||
}
|
||||
if ((Test-Path -LiteralPath $job) -and -not $ResumeAfterBuild) { throw 'Candidate directory already exists' }
|
||||
foreach ($name in @('universal','arm64-v8a','armeabi-v7a','x86','x86_64')) {
|
||||
if (Test-Path -LiteralPath (Join-Path $repo "releases/UCVL-Zonghengjia-1.0.4-android-$name.apk")) { throw 'Signed 1.0.4 candidate already exists; do not replace it' }
|
||||
}
|
||||
$buildText = Get-Content -LiteralPath (Join-Path $repo 'V2rayNG/app/build.gradle.kts') -Raw
|
||||
if ($buildText -notmatch 'versionCode = 10004' -or $buildText -notmatch 'versionName = "1.0.4"') { throw 'Source version mismatch' }
|
||||
$project = Join-Path $job 'project'
|
||||
$source = Join-Path $repo 'V2rayNG'
|
||||
function Save-Json([string]$Name,$Value) {
|
||||
[IO.File]::WriteAllText((Join-Path $job "evidence/$Name"),($Value | ConvertTo-Json -Depth 12),[Text.UTF8Encoding]::new($false))
|
||||
}
|
||||
$env:JAVA_HOME = Join-Path $repo '.tools/jdk-staging/jdk-21.0.12.1+1'
|
||||
$env:ANDROID_HOME = Join-Path $repo '.tools/android-sdk'
|
||||
if (-not $ResumeAfterBuild) {
|
||||
$null = New-Item -ItemType Directory -Path $job,$project,(Join-Path $job 'evidence'),(Join-Path $job 'artifacts')
|
||||
foreach ($name in @('gradlew','gradlew.bat','gradle.properties','settings.gradle.kts','build.gradle.kts','gradle')) {
|
||||
Copy-Item -LiteralPath (Join-Path $source $name) -Destination $project -Recurse
|
||||
}
|
||||
$null = New-Item -ItemType Directory -Path (Join-Path $project 'app')
|
||||
foreach ($name in @('src','libs','build.gradle.kts','proguard-rules.pro')) {
|
||||
Copy-Item -LiteralPath (Join-Path $source "app/$name") -Destination (Join-Path $project 'app') -Recurse
|
||||
}
|
||||
$inputs = @(Get-ChildItem -LiteralPath $project -File -Recurse | Sort-Object FullName | ForEach-Object {
|
||||
[ordered]@{Path=[IO.Path]::GetRelativePath($project,$_.FullName).Replace('\','/');Bytes=$_.Length;Sha256=(Get-FileHash -LiteralPath $_.FullName -Algorithm SHA256).Hash}
|
||||
})
|
||||
Save-Json 'build-inputs.json' $inputs
|
||||
$inputsHash = (Get-FileHash -LiteralPath (Join-Path $job 'evidence/build-inputs.json')).Hash
|
||||
$started = [DateTimeOffset]::Now
|
||||
Push-Location $project
|
||||
try {
|
||||
& ./gradlew.bat --offline --no-daemon --max-workers=2 :app:testPlaystoreReleaseUnitTest :app:assemblePlaystoreRelease *> (Join-Path $job 'evidence/gradle.log')
|
||||
$buildExit = $LASTEXITCODE
|
||||
} finally { Pop-Location }
|
||||
$suites = @(Get-ChildItem -LiteralPath (Join-Path $project 'app/build/test-results/testPlaystoreReleaseUnitTest') -Filter 'TEST-*.xml' -File -ErrorAction SilentlyContinue | ForEach-Object {
|
||||
[xml]$xml = Get-Content -LiteralPath $_.FullName -Raw
|
||||
[pscustomobject]@{Name=$xml.testsuite.name;Tests=[int]$xml.testsuite.tests;Failures=[int]$xml.testsuite.failures;Errors=[int]$xml.testsuite.errors;Skipped=[int]$xml.testsuite.skipped}
|
||||
})
|
||||
$tests = [ordered]@{StartedAt=$started.ToString('O');ExitCode=$buildExit;Offline=$true;Suites=$suites;BuildInputsSha256=$inputsHash;PhysicalDeviceTested=$false;EmulatorStarted=$false;Published=$false}
|
||||
Save-Json 'tests.json' $tests
|
||||
} else {
|
||||
$tests = Get-Content -LiteralPath (Join-Path $job 'evidence/tests.json') -Raw | ConvertFrom-Json
|
||||
$suites = $tests.Suites
|
||||
$buildExit = $tests.ExitCode
|
||||
$inputs = Get-Content -LiteralPath (Join-Path $job 'evidence/build-inputs.json') -Raw | ConvertFrom-Json
|
||||
$inputsHash = (Get-FileHash -LiteralPath (Join-Path $job 'evidence/build-inputs.json')).Hash
|
||||
if ($inputsHash -ne $tests.BuildInputsSha256) { throw 'Completed build input manifest changed' }
|
||||
}
|
||||
if ($buildExit -ne 0) { throw "Gradle failed ($buildExit); evidence retained at $job" }
|
||||
if (($suites | Measure-Object Tests -Sum).Sum -ne 41 -or ($suites | Measure-Object Failures -Sum).Sum -ne 0 -or ($suites | Measure-Object Errors -Sum).Sum -ne 0 -or ($suites | Measure-Object Skipped -Sum).Sum -ne 0) {
|
||||
throw 'Unexpected unit test coverage or failures'
|
||||
}
|
||||
foreach ($row in $inputs) {
|
||||
if ((Get-FileHash -LiteralPath (Join-Path $project $row.Path)).Hash -ne $row.Sha256 -or (Get-FileHash -LiteralPath (Join-Path $source $row.Path)).Hash -ne $row.Sha256) {
|
||||
throw "Input changed during build: $($row.Path)"
|
||||
}
|
||||
}
|
||||
& (Join-Path $repo 'Sign-Candidate.ps1') -Version '1.0.4' -InputDirectory (Join-Path $project 'app/build/outputs/apk/playstore/release') *> (Join-Path $job 'evidence/signing.log')
|
||||
$java = Join-Path $env:JAVA_HOME 'bin/java.exe'
|
||||
$signer = Join-Path $env:ANDROID_HOME 'build-tools/37.0.0/lib/apksigner.jar'
|
||||
$aapt = Join-Path $env:ANDROID_HOME 'build-tools/37.0.0/aapt.exe'
|
||||
$expectedCertificate = '0f1a09870d6ee1e73b62138f99463e7066f4228ce28b7f16ff0aeccca59ea14f'
|
||||
foreach ($version in @('1.0.2','1.0.3')) {
|
||||
$verification = (& $java -jar $signer verify --verbose --print-certs (Join-Path $repo "releases/UCVL-Zonghengjia-$version-android-universal.apk") 2>&1 | Out-String)
|
||||
if ($LASTEXITCODE -ne 0 -or $verification -notmatch $expectedCertificate) { throw "Old $version signing identity differs" }
|
||||
}
|
||||
$packages = @(foreach ($file in Get-ChildItem -LiteralPath (Join-Path $repo 'releases') -Filter 'UCVL-Zonghengjia-1.0.4-android-*.apk' -File | Sort-Object Name) {
|
||||
$verification = (& $java -jar $signer verify --verbose --print-certs $file.FullName 2>&1 | Out-String)
|
||||
if ($LASTEXITCODE -ne 0 -or $verification -notmatch $expectedCertificate -or $verification -notmatch 'v2\): true' -or $verification -notmatch 'v3\): true') { throw "Candidate signature mismatch: $($file.Name)" }
|
||||
$copy = Join-Path $job "artifacts/$($file.Name)"
|
||||
Copy-Item -LiteralPath $file.FullName -Destination $copy
|
||||
$badging = (& $aapt dump badging $copy 2>&1 | Out-String)
|
||||
if ($LASTEXITCODE -ne 0 -or $badging -notmatch "package: name='cn.toplc.zonghengjia' versionCode='4010004' versionName='1.0.4'") { throw "Candidate manifest mismatch: $($file.Name)" }
|
||||
[ordered]@{Name=$file.Name;Bytes=$file.Length;Sha256=(Get-FileHash -LiteralPath $copy).Hash;Version='1.0.4';VersionCode=4010004;SignerSha256=$expectedCertificate;V2=$true;V3=$true}
|
||||
})
|
||||
if ($packages.Count -ne 5) { throw 'Expected all five ABI/universal packages' }
|
||||
Save-Json 'candidate.json' ([ordered]@{Version='1.0.4';VersionCode=4010004;BaseVersionCode=10004;JobDirectory=$job;BuiltAt=[DateTimeOffset]::Now.ToString('O');Packages=$packages;BuildInputsSha256=$inputsHash;UnitTests=41;AuthorizationLifecycleJvmTests=10;SameSignerAs=@('1.0.2','1.0.3');PhysicalDeviceTested=$false;AndroidServiceBroadcastTested=$false;StableReleaseAccepted=$false;Published=$false})
|
||||
Write-Output "CANDIDATE_JOB=$job"
|
||||
Write-Output 'CANDIDATE_BUILD_SIGN_VERIFY_PASSED'
|
||||
@@ -0,0 +1,136 @@
|
||||
"""Bind isolated APKs to their exact build inputs and reviewed GPL source. No network or devices."""
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import re
|
||||
import shutil
|
||||
import zipfile
|
||||
|
||||
REPO = Path(__file__).resolve().parents[1]
|
||||
VERSION = '1.0.4'
|
||||
LOCKED_BINARY_INPUTS = {
|
||||
'app/libs/libv2ray.aar': '3d43b9344723e9c0625527de4f2bea0ee02c21180224e5ecab3384af143ca6d0',
|
||||
'app/libs/quickie-foss-1.14.0.aar': '4d90e9cb37e07b57eaa3a839be2abdc422c0ce9ac8e969720a2a71c968cd771f',
|
||||
'app/libs/Toasty-1.5.2.aar': '57866e731ebe3ef82328942cd4d96cf940b2406c9440f33857f69777027f36b5',
|
||||
'app/libs/arm64-v8a/libhev-socks5-tunnel.so': '4eb0b2353f4fb6d45f43cca39fe72197ce9df8c8f5ba5a82feac87abceb9ea7a',
|
||||
'app/libs/armeabi-v7a/libhev-socks5-tunnel.so': '816c48bab1785fcdaaf7fc2f49dc6b0733cf11e68b9379640d22d6a539f306ff',
|
||||
'app/libs/x86/libhev-socks5-tunnel.so': '6e42da45387d76630a8ffd52ea45cd5738395da39df31dba1a48806fa79d3572',
|
||||
'app/libs/x86_64/libhev-socks5-tunnel.so': 'c61a54f38f61feb73b465d2245fe6e4746c2efba7e9024de8de025ce3fc9206e',
|
||||
}
|
||||
|
||||
|
||||
def digest(path):
|
||||
with Path(path).open('rb') as stream:
|
||||
return hashlib.file_digest(stream, 'sha256').hexdigest()
|
||||
|
||||
|
||||
def save(path, value):
|
||||
with Path(path).open('x', encoding='utf-8', newline='\n') as stream:
|
||||
json.dump(value, stream, indent=2, ensure_ascii=False)
|
||||
stream.write('\n')
|
||||
|
||||
|
||||
def main(job):
|
||||
job = Path(job).resolve()
|
||||
if not re.fullmatch(r'ucvl-android-104-[a-f0-9]{32}', job.name):
|
||||
raise RuntimeError('Unexpected candidate job')
|
||||
candidate = json.loads((job / 'evidence/candidate.json').read_text(encoding='utf-8'))
|
||||
if candidate['Version'] != VERSION or candidate['VersionCode'] != 4010004 or candidate['Published']:
|
||||
raise RuntimeError('Unexpected candidate contract')
|
||||
inputs_path = job / 'evidence/build-inputs.json'
|
||||
if digest(inputs_path) != candidate['BuildInputsSha256'].lower():
|
||||
raise RuntimeError('Build input manifest changed')
|
||||
inputs = json.loads(inputs_path.read_text(encoding='utf-8'))
|
||||
public = job / 'public-source-final'
|
||||
matched = 0
|
||||
binary_inputs = []
|
||||
for row in inputs:
|
||||
name, expected = row['Path'], row['Sha256'].lower()
|
||||
if digest(job / 'project' / name) != expected or digest(REPO / 'V2rayNG' / name) != expected:
|
||||
raise RuntimeError('Build/source drift: ' + name)
|
||||
if name in LOCKED_BINARY_INPUTS:
|
||||
if expected != LOCKED_BINARY_INPUTS[name]:
|
||||
raise RuntimeError('Dependency differs from public restoration lock: ' + name)
|
||||
binary_inputs.append({'path': name, 'sha256': expected})
|
||||
else:
|
||||
if digest(public / 'V2rayNG' / name) != expected:
|
||||
raise RuntimeError('GPL snapshot does not match compiled input: ' + name)
|
||||
matched += 1
|
||||
if len(binary_inputs) != len(LOCKED_BINARY_INPUTS):
|
||||
raise RuntimeError('Missing locked binary dependencies')
|
||||
|
||||
inventory = []
|
||||
forbidden = {'.jks', '.keystore', '.p12', '.pfx', '.pem', '.apk', '.aar', '.so', '.idsig', '.log'}
|
||||
for path in sorted(public.rglob('*')):
|
||||
if not path.is_file():
|
||||
continue
|
||||
name = path.relative_to(public).as_posix()
|
||||
if path.is_symlink() or name.casefold().startswith('zero3-helper/') or any(part.casefold() in {'.git', '.tools', 'build', 'releases', '.gradle'} for part in path.relative_to(public).parts):
|
||||
raise RuntimeError('Private/generated directory exported: ' + name)
|
||||
if path.suffix.casefold() in forbidden or path.name in {'signing.properties', 'local.properties'}:
|
||||
raise RuntimeError('Private/binary build input exported: ' + name)
|
||||
if re.search(rb'-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----', path.read_bytes()):
|
||||
raise RuntimeError('Private key marker exported: ' + name)
|
||||
inventory.append({'path': name, 'bytes': path.stat().st_size, 'sha256': digest(path)})
|
||||
required = ['LICENSE', 'README.md', 'README-UPSTREAM.md', 'SOURCE-PROVENANCE.json', 'Restore-Dependencies.py',
|
||||
'V2rayNG/app/src/main/java/com/v2ray/ang/ucvl/UcvlAuthorizationGuard.kt',
|
||||
'V2rayNG/app/src/test/java/com/v2ray/ang/ucvl/UcvlAuthorizationGuardTest.kt']
|
||||
if any(not (public / name).is_file() for name in required):
|
||||
raise RuntimeError('GPL source snapshot incomplete')
|
||||
output = REPO / '.tools/candidates' / ('android-1.0.4-20260924-' + job.name.rsplit('-', 1)[1][:8] + '-final')
|
||||
output.mkdir(parents=True, exist_ok=False)
|
||||
packages = []
|
||||
for package in candidate['Packages']:
|
||||
signed = job / 'artifacts' / package['Name']
|
||||
if digest(signed) != package['Sha256'].lower():
|
||||
raise RuntimeError('Signed APK changed')
|
||||
unsigned = job / 'project/app/build/outputs/apk/playstore/release' / package['Name']
|
||||
with zipfile.ZipFile(unsigned) as before, zipfile.ZipFile(signed) as after:
|
||||
added = set(after.namelist()) - set(before.namelist())
|
||||
if before.testzip() is not None or after.testzip() is not None or set(before.namelist()) - set(after.namelist()):
|
||||
raise RuntimeError('Signed APK removed or corrupted compiled entries')
|
||||
if added != {'META-INF/MANIFEST.MF', 'META-INF/UCVL-ZON.RSA', 'META-INF/UCVL-ZON.SF'}:
|
||||
raise RuntimeError('Unexpected entries added during signing')
|
||||
for name in before.namelist():
|
||||
if before.read(name) != after.read(name):
|
||||
raise RuntimeError('APK entry modified after build: ' + name)
|
||||
dex = {name: hashlib.sha256(after.read(name)).hexdigest() for name in after.namelist() if re.fullmatch(r'classes\d*\.dex', name)}
|
||||
shutil.copy2(signed, output / signed.name)
|
||||
packages.append({**package, 'DexSha256': dex, 'AllUnsignedZipEntriesPreserved': True, 'AddedSigningMetadata': sorted(added)})
|
||||
|
||||
save(output / 'android-1.0.4-source-inventory.json', inventory)
|
||||
source_archive = output / 'UCVL-Zonghengjia-1.0.4-source.zip'
|
||||
with zipfile.ZipFile(source_archive, 'x', compression=zipfile.ZIP_DEFLATED, compresslevel=6) as archive:
|
||||
for row in inventory:
|
||||
archive.write(public / row['path'], 'UCVL-Zonghengjia-1.0.4-source/' + row['path'])
|
||||
with zipfile.ZipFile(source_archive) as archive:
|
||||
if archive.testzip() is not None or len(archive.namelist()) != len(inventory):
|
||||
raise RuntimeError('Source archive verification failed')
|
||||
report = {
|
||||
'Version': VERSION, 'BaseVersionCode': 10004, 'ApkVersionCode': 4010004,
|
||||
'Packages': packages, 'SourceArchive': source_archive.name, 'SourceArchiveSha256': digest(source_archive),
|
||||
'SourceInventorySha256': digest(output / 'android-1.0.4-source-inventory.json'), 'SourceFiles': len(inventory),
|
||||
'CompiledPublicSourceInputsMatched': matched, 'LockedRestorableBinaryInputs': binary_inputs,
|
||||
'BuildInputManifestSha256': digest(inputs_path), 'UnitTests': 41, 'AuthorizationLifecycleJvmTests': 10,
|
||||
'OriginalCompanySignerMatches102And103': True, 'PrivateZero3Excluded': True,
|
||||
'PhysicalDeviceTested': False, 'AndroidServiceBroadcastTested': False, 'EmulatorStarted': False,
|
||||
'StableReleaseAccepted': False, 'Published': False,
|
||||
'RemainingGates': ['Physical Android VPN permission and data path', 'Real Service/broadcast lifecycle for replaced login',
|
||||
'Wi-Fi/mobile transitions, sleep/wake and recovery', 'Authenticated sustained business session'],
|
||||
}
|
||||
save(output / 'android-1.0.4-candidate.json', report)
|
||||
shutil.copytree(job / 'evidence', output / 'local-evidence')
|
||||
shutil.copytree(job / 'source-evidence-final', output / 'source-export-evidence')
|
||||
with (output / 'SHA256SUMS.txt').open('x', encoding='ascii', newline='\n') as stream:
|
||||
for path in sorted(output.iterdir()):
|
||||
if path.is_file() and path.name != 'SHA256SUMS.txt':
|
||||
stream.write(digest(path) + ' ' + path.name + '\n')
|
||||
print(json.dumps({'output': str(output), 'source_files': len(inventory), 'source_sha256': digest(source_archive),
|
||||
'public_compiled_inputs_matched': matched, 'packages': packages}, ensure_ascii=False, indent=2))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('job')
|
||||
main(parser.parse_args().job)
|
||||
@@ -0,0 +1,48 @@
|
||||
param(
|
||||
[string]$Runtime = '',
|
||||
[ValidateSet('before','after','full')][string]$Phase = 'before'
|
||||
)
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$repo = (Resolve-Path (Join-Path $PSScriptRoot '..')).Path
|
||||
$source = Join-Path $repo 'V2rayNG'
|
||||
$tempRoot = [IO.Path]::GetFullPath([IO.Path]::GetTempPath())
|
||||
if (-not $Runtime) { $Runtime = Join-Path $tempRoot ('ucvl-auth-20260917-' + [guid]::NewGuid().ToString('N')) }
|
||||
$Runtime = [IO.Path]::GetFullPath($Runtime)
|
||||
if (-not $Runtime.StartsWith($tempRoot, [StringComparison]::OrdinalIgnoreCase) -or (Split-Path $Runtime -Leaf) -notmatch '^ucvl-auth-20260917-[a-f0-9]{32}$') {
|
||||
throw 'Only a dedicated temporary ASCII build directory is allowed'
|
||||
}
|
||||
New-Item -ItemType Directory -Path $Runtime -Force | Out-Null
|
||||
foreach ($name in @('gradlew','gradlew.bat','gradle.properties','settings.gradle.kts','build.gradle.kts')) {
|
||||
Copy-Item -LiteralPath (Join-Path $source $name) -Destination $Runtime -Force
|
||||
}
|
||||
Copy-Item -LiteralPath (Join-Path $source 'gradle') -Destination $Runtime -Recurse -Force
|
||||
$appTarget = Join-Path $Runtime 'app'
|
||||
New-Item -ItemType Directory -Path $appTarget -Force | Out-Null
|
||||
foreach ($name in @('src','libs','build.gradle.kts','proguard-rules.pro')) {
|
||||
Copy-Item -LiteralPath (Join-Path $source ('app/' + $name)) -Destination $appTarget -Recurse -Force
|
||||
}
|
||||
$env:JAVA_HOME = Join-Path $repo '.tools/jdk-staging/jdk-21.0.12.1+1'
|
||||
$env:ANDROID_HOME = Join-Path $repo '.tools/android-sdk'
|
||||
if (-not (Test-Path (Join-Path $env:JAVA_HOME 'bin/java.exe'))) { throw 'Expected JDK 21 is missing' }
|
||||
$log = Join-Path $repo ('acceptance/android-auth-' + $Phase + '-20260917.log')
|
||||
$started = Get-Date
|
||||
Push-Location $Runtime
|
||||
try {
|
||||
$gradleArgs = @('--offline','--no-daemon','--max-workers=2',':app:testPlaystoreReleaseUnitTest')
|
||||
if ($Phase -ne 'full') { $gradleArgs += @('--tests','com.v2ray.ang.ucvl.UcvlAuthorizationGuardTest') }
|
||||
& .\gradlew.bat @gradleArgs *> $log
|
||||
$testExit = $LASTEXITCODE
|
||||
} finally { Pop-Location }
|
||||
$suites = @()
|
||||
$resultDir = Join-Path $Runtime 'app/build/test-results/testPlaystoreReleaseUnitTest'
|
||||
if (Test-Path $resultDir) {
|
||||
foreach ($file in Get-ChildItem $resultDir -Filter 'TEST-*.xml' -File) {
|
||||
if ($file.LastWriteTime -lt $started) { continue }
|
||||
[xml]$xml = Get-Content -LiteralPath $file.FullName -Raw
|
||||
$suites += [ordered]@{name=$xml.testsuite.name; tests=[int]$xml.testsuite.tests; failures=[int]$xml.testsuite.failures; errors=[int]$xml.testsuite.errors; skipped=[int]$xml.testsuite.skipped}
|
||||
}
|
||||
}
|
||||
$report = [ordered]@{phase=$Phase; started_at=$started.ToString('o'); runtime=$Runtime; offline=$true; exit_code=$testExit; suites=$suites; source_copied=$true; emulator_started=$false; device_installed=$false; apk_published=$false; network_modified=$false}
|
||||
$report | ConvertTo-Json -Depth 5 | Set-Content -Encoding utf8 (Join-Path $repo ('acceptance/android-auth-' + $Phase + '-20260917.json'))
|
||||
$report | ConvertTo-Json -Depth 5
|
||||
exit $testExit
|
||||
@@ -0,0 +1,66 @@
|
||||
"""Same-certificate upgrade test on our dedicated emulator; never a phone.
|
||||
|
||||
Seeds only a synthetic non-authenticating session. Does not alter host networking
|
||||
or request a production account. Run after building/signing the test APK.
|
||||
"""
|
||||
import datetime
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
ADB = ROOT / ".tools/android-sdk/platform-tools/adb.exe"
|
||||
PACKAGE = "cn.toplc.zonghengjia"
|
||||
SERIAL = "emulator-5554"
|
||||
|
||||
|
||||
def adb(*args, timeout=60):
|
||||
return subprocess.run([str(ADB), "-s", SERIAL, *map(str, args)], capture_output=True,
|
||||
text=True, encoding="utf-8", errors="replace", timeout=timeout,
|
||||
check=True, creationflags=subprocess.CREATE_NO_WINDOW).stdout
|
||||
|
||||
|
||||
def instrument(method):
|
||||
result = adb("shell", "am", "instrument", "-w", "-r", "-e", "oldVersion", "1.0.2",
|
||||
"-e", "newVersion", "1.0.3", "-e", "class",
|
||||
"com.v2ray.ang.ucvl.UpgradeSmokeTest#" + method,
|
||||
PACKAGE + ".test/androidx.test.runner.AndroidJUnitRunner", timeout=90)
|
||||
if "OK (1 test)" not in result:
|
||||
raise RuntimeError("Instrumentation failed: " + result[-3000:])
|
||||
|
||||
|
||||
def main():
|
||||
assert adb("shell", "getprop", "ro.kernel.qemu").strip() == "1", "Dedicated emulator only"
|
||||
assert "ucvl_acceptance" in adb("emu", "avd", "name"), "Unexpected emulator image"
|
||||
assert adb("shell", "getprop", "sys.boot_completed").strip() == "1", "Emulator has not booted"
|
||||
old = ROOT / "releases/UCVL-Zonghengjia-1.0.2-android-universal.apk"
|
||||
new = ROOT / "releases/UCVL-Zonghengjia-1.0.3-android-universal.apk"
|
||||
test = ROOT / "releases/app-playstore-release-androidTest-1.0.3.apk"
|
||||
assert all(path.is_file() for path in [old, new, test])
|
||||
evidence = ROOT / "acceptance/upgrade-1.0.3.json"
|
||||
report = {"timestamp": datetime.datetime.now().astimezone().isoformat(), "serial": SERIAL,
|
||||
"old_version": "1.0.2", "new_version": "1.0.3", "passed": False,
|
||||
"physical_device_tested": False, "host_network_modified": False,
|
||||
"authenticated_vpn_connection_tested": False, "steps": []}
|
||||
try:
|
||||
adb("shell", "am", "force-stop", PACKAGE)
|
||||
assert "Success" in adb("install", "-r", old)
|
||||
assert "Success" in adb("install", "-r", test)
|
||||
instrument("seedUpgrade")
|
||||
report["steps"].append("seed_synthetic_session_on_1.0.2")
|
||||
assert "Success" in adb("install", "-r", new)
|
||||
instrument("verifyUpgrade")
|
||||
report["steps"].append("cover_upgrade_preserves_encrypted_session_login_and_device_id")
|
||||
instrument("loginVisibleAndEmptyFormIsSafe")
|
||||
report["steps"].append("login_visible_empty_submission_safe_fixture_cleared")
|
||||
report["apk_sha256"] = hashlib.sha256(new.read_bytes()).hexdigest()
|
||||
report["passed"] = True
|
||||
finally:
|
||||
adb("shell", "am", "force-stop", PACKAGE)
|
||||
evidence.write_text(json.dumps(report, indent=2), encoding="utf-8")
|
||||
print(json.dumps(report))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user