Add Vagrant, fixes for CI/CD pipeline, and Windows dev bootstrap script

This commit is contained in:
Jason Ross
2026-03-21 16:56:11 -05:00
parent ee6d5e0960
commit 18ede809e2
12 changed files with 1277 additions and 20 deletions
+8
View File
@@ -0,0 +1,8 @@
{
"permissions": {
"allow": [
"WebFetch(domain:community.chocolatey.org)",
"WebFetch(domain:github.com)"
]
}
}
Generated
+310 -1
View File
@@ -308,6 +308,46 @@ d = ["aiohttp (>=3.10)"]
jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"]
uvloop = ["uvloop (>=0.15.2) ; sys_platform != \"win32\"", "winloop (>=0.5.0) ; sys_platform == \"win32\""]
[[package]]
name = "boto3"
version = "1.42.73"
description = "The AWS SDK for Python"
optional = false
python-versions = ">=3.9"
groups = ["ci"]
files = [
{file = "boto3-1.42.73-py3-none-any.whl", hash = "sha256:1f81b79b873f130eeab14bb556417a7c66d38f3396b7f2fe3b958b3f9094f455"},
{file = "boto3-1.42.73.tar.gz", hash = "sha256:d37b58d6cd452ca808dd6823ae19ca65b6244096c5125ef9052988b337298bae"},
]
[package.dependencies]
botocore = ">=1.42.73,<1.43.0"
jmespath = ">=0.7.1,<2.0.0"
s3transfer = ">=0.16.0,<0.17.0"
[package.extras]
crt = ["botocore[crt] (>=1.21.0,<2.0a0)"]
[[package]]
name = "botocore"
version = "1.42.73"
description = "Low-level, data-driven core of boto 3."
optional = false
python-versions = ">=3.9"
groups = ["ci"]
files = [
{file = "botocore-1.42.73-py3-none-any.whl", hash = "sha256:7b62e2a12f7a1b08eb7360eecd23bb16fe3b7ab7f5617cf91b25476c6f86a0fe"},
{file = "botocore-1.42.73.tar.gz", hash = "sha256:575858641e4949aaf2af1ced145b8524529edf006d075877af6b82ff96ad854c"},
]
[package.dependencies]
jmespath = ">=0.7.1,<2.0.0"
python-dateutil = ">=2.1,<3.0.0"
urllib3 = {version = ">=1.25.4,<2.2.0 || >2.2.0,<3", markers = "python_version >= \"3.10\""}
[package.extras]
crt = ["awscrt (==0.31.2)"]
[[package]]
name = "cachetools"
version = "7.0.5"
@@ -1135,6 +1175,88 @@ test-downstream = ["aiobotocore (>=2.5.4,<3.0.0)", "dask[dataframe,test]", "moto
test-full = ["adlfs", "aiohttp (!=4.0.0a0,!=4.0.0a1)", "backports-zstd ; python_version < \"3.14\"", "cloudpickle", "dask", "distributed", "dropbox", "dropboxdrivefs", "fastparquet", "fusepy", "gcsfs", "jinja2", "kerchunk", "libarchive-c", "lz4", "notebook", "numpy", "ocifs", "pandas (<3.0.0)", "panel", "paramiko", "pyarrow", "pyarrow (>=1)", "pyftpdlib", "pygit2", "pytest", "pytest-asyncio (!=0.22.0)", "pytest-benchmark", "pytest-cov", "pytest-mock", "pytest-recording", "pytest-rerunfailures", "python-snappy", "requests", "smbprotocol", "tqdm", "urllib3", "zarr", "zstandard ; python_version < \"3.14\""]
tqdm = ["tqdm"]
[[package]]
name = "google-api-core"
version = "2.30.0"
description = "Google API client core library"
optional = false
python-versions = ">=3.9"
groups = ["ci"]
files = [
{file = "google_api_core-2.30.0-py3-none-any.whl", hash = "sha256:80be49ee937ff9aba0fd79a6eddfde35fe658b9953ab9b79c57dd7061afa8df5"},
{file = "google_api_core-2.30.0.tar.gz", hash = "sha256:02edfa9fab31e17fc0befb5f161b3bf93c9096d99aed584625f38065c511ad9b"},
]
[package.dependencies]
google-auth = ">=2.14.1,<3.0.0"
googleapis-common-protos = ">=1.56.3,<2.0.0"
grpcio = [
{version = ">=1.75.1,<2.0.0", optional = true, markers = "python_version >= \"3.14\" and extra == \"grpc\""},
{version = ">=1.49.1,<2.0.0", optional = true, markers = "python_version >= \"3.11\" and extra == \"grpc\""},
]
grpcio-status = [
{version = ">=1.75.1,<2.0.0", optional = true, markers = "python_version >= \"3.14\" and extra == \"grpc\""},
{version = ">=1.49.1,<2.0.0", optional = true, markers = "python_version >= \"3.11\" and extra == \"grpc\""},
]
proto-plus = {version = ">=1.25.0,<2.0.0", markers = "python_version >= \"3.13\""}
protobuf = ">=4.25.8,<7.0.0"
requests = ">=2.20.0,<3.0.0"
[package.extras]
async-rest = ["google-auth[aiohttp] (>=2.35.0,<3.0.0)"]
grpc = ["grpcio (>=1.33.2,<2.0.0)", "grpcio (>=1.49.1,<2.0.0) ; python_version >= \"3.11\"", "grpcio (>=1.75.1,<2.0.0) ; python_version >= \"3.14\"", "grpcio-status (>=1.33.2,<2.0.0)", "grpcio-status (>=1.49.1,<2.0.0) ; python_version >= \"3.11\"", "grpcio-status (>=1.75.1,<2.0.0) ; python_version >= \"3.14\""]
[[package]]
name = "google-auth"
version = "2.49.1"
description = "Google Authentication Library"
optional = false
python-versions = ">=3.8"
groups = ["ci"]
files = [
{file = "google_auth-2.49.1-py3-none-any.whl", hash = "sha256:195ebe3dca18eddd1b3db5edc5189b76c13e96f29e73043b923ebcf3f1a860f7"},
{file = "google_auth-2.49.1.tar.gz", hash = "sha256:16d40da1c3c5a0533f57d268fe72e0ebb0ae1cc3b567024122651c045d879b64"},
]
[package.dependencies]
cryptography = ">=38.0.3"
pyasn1-modules = ">=0.2.1"
[package.extras]
aiohttp = ["aiohttp (>=3.6.2,<4.0.0)", "requests (>=2.20.0,<3.0.0)"]
cryptography = ["cryptography (>=38.0.3)"]
enterprise-cert = ["pyopenssl"]
pyjwt = ["pyjwt (>=2.0)"]
pyopenssl = ["pyopenssl (>=20.0.0)"]
reauth = ["pyu2f (>=0.1.5)"]
requests = ["requests (>=2.20.0,<3.0.0)"]
rsa = ["rsa (>=3.1.4,<5)"]
testing = ["aiohttp (<3.10.0)", "aiohttp (>=3.6.2,<4.0.0)", "aioresponses", "flask", "freezegun", "grpcio", "packaging", "pyjwt (>=2.0)", "pyopenssl (<24.3.0)", "pyopenssl (>=20.0.0)", "pytest", "pytest-asyncio", "pytest-cov", "pytest-localserver", "pyu2f (>=0.1.5)", "requests (>=2.20.0,<3.0.0)", "responses", "urllib3"]
urllib3 = ["packaging", "urllib3"]
[[package]]
name = "google-cloud-secret-manager"
version = "2.26.0"
description = "Google Cloud Secret Manager API client library"
optional = false
python-versions = ">=3.7"
groups = ["ci"]
files = [
{file = "google_cloud_secret_manager-2.26.0-py3-none-any.whl", hash = "sha256:940a5447a6ec9951446fd1a0f22c81a4303fde164cd747aae152c5f5c8e6723e"},
{file = "google_cloud_secret_manager-2.26.0.tar.gz", hash = "sha256:0d1d6f76327685a0ed78a4cf50f289e1bfbbe56026ed0affa98663b86d6d50d6"},
]
[package.dependencies]
google-api-core = {version = ">=1.34.1,<2.0.dev0 || >=2.11.dev0,<3.0.0", extras = ["grpc"]}
google-auth = ">=2.14.1,<2.24.0 || >2.24.0,<2.25.0 || >2.25.0,<3.0.0"
grpc-google-iam-v1 = ">=0.14.0,<1.0.0"
grpcio = [
{version = ">=1.75.1,<2.0.0", markers = "python_version >= \"3.14\""},
{version = ">=1.33.2,<2.0.0", markers = "python_version < \"3.14\""},
]
proto-plus = {version = ">=1.25.0,<2.0.0", markers = "python_version >= \"3.13\""}
protobuf = ">=3.20.2,<4.21.0 || >4.21.0,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<7.0.0"
[[package]]
name = "googleapis-common-protos"
version = "1.73.0"
@@ -1148,6 +1270,7 @@ files = [
]
[package.dependencies]
grpcio = {version = ">=1.44.0,<2.0.0", optional = true, markers = "extra == \"grpc\""}
protobuf = ">=3.20.2,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<7.0.0"
[package.extras]
@@ -1328,6 +1451,117 @@ files = [
[package.extras]
pypi = ["pip (>=24.0)", "platformdirs (>=4.2)", "wheel (>=0.42)"]
[[package]]
name = "grpc-google-iam-v1"
version = "0.14.3"
description = "IAM API client library"
optional = false
python-versions = ">=3.7"
groups = ["ci"]
files = [
{file = "grpc_google_iam_v1-0.14.3-py3-none-any.whl", hash = "sha256:7a7f697e017a067206a3dfef44e4c634a34d3dee135fe7d7a4613fe3e59217e6"},
{file = "grpc_google_iam_v1-0.14.3.tar.gz", hash = "sha256:879ac4ef33136c5491a6300e27575a9ec760f6cdf9a2518798c1b8977a5dc389"},
]
[package.dependencies]
googleapis-common-protos = {version = ">=1.56.0,<2.0.0", extras = ["grpc"]}
grpcio = ">=1.44.0,<2.0.0"
protobuf = ">=3.20.2,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<7.0.0"
[[package]]
name = "grpcio"
version = "1.78.0"
description = "HTTP/2-based RPC framework"
optional = false
python-versions = ">=3.9"
groups = ["ci"]
files = [
{file = "grpcio-1.78.0-cp310-cp310-linux_armv7l.whl", hash = "sha256:7cc47943d524ee0096f973e1081cb8f4f17a4615f2116882a5f1416e4cfe92b5"},
{file = "grpcio-1.78.0-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:c3f293fdc675ccba4db5a561048cca627b5e7bd1c8a6973ffedabe7d116e22e2"},
{file = "grpcio-1.78.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:10a9a644b5dd5aec3b82b5b0b90d41c0fa94c85ef42cb42cf78a23291ddb5e7d"},
{file = "grpcio-1.78.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:4c5533d03a6cbd7f56acfc9cfb44ea64f63d29091e40e44010d34178d392d7eb"},
{file = "grpcio-1.78.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ff870aebe9a93a85283837801d35cd5f8814fe2ad01e606861a7fb47c762a2b7"},
{file = "grpcio-1.78.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:391e93548644e6b2726f1bb84ed60048d4bcc424ce5e4af0843d28ca0b754fec"},
{file = "grpcio-1.78.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:df2c8f3141f7cbd112a6ebbd760290b5849cda01884554f7c67acc14e7b1758a"},
{file = "grpcio-1.78.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:bd8cb8026e5f5b50498a3c4f196f57f9db344dad829ffae16b82e4fdbaea2813"},
{file = "grpcio-1.78.0-cp310-cp310-win32.whl", hash = "sha256:f8dff3d9777e5d2703a962ee5c286c239bf0ba173877cc68dc02c17d042e29de"},
{file = "grpcio-1.78.0-cp310-cp310-win_amd64.whl", hash = "sha256:94f95cf5d532d0e717eed4fc1810e8e6eded04621342ec54c89a7c2f14b581bf"},
{file = "grpcio-1.78.0-cp311-cp311-linux_armv7l.whl", hash = "sha256:2777b783f6c13b92bd7b716667452c329eefd646bfb3f2e9dabea2e05dbd34f6"},
{file = "grpcio-1.78.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:9dca934f24c732750389ce49d638069c3892ad065df86cb465b3fa3012b70c9e"},
{file = "grpcio-1.78.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:459ab414b35f4496138d0ecd735fed26f1318af5e52cb1efbc82a09f0d5aa911"},
{file = "grpcio-1.78.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:082653eecbdf290e6e3e2c276ab2c54b9e7c299e07f4221872380312d8cf395e"},
{file = "grpcio-1.78.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:85f93781028ec63f383f6bc90db785a016319c561cc11151fbb7b34e0d012303"},
{file = "grpcio-1.78.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f12857d24d98441af6a1d5c87442d624411db486f7ba12550b07788f74b67b04"},
{file = "grpcio-1.78.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5397fff416b79e4b284959642a4e95ac4b0f1ece82c9993658e0e477d40551ec"},
{file = "grpcio-1.78.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:fbe6e89c7ffb48518384068321621b2a69cab509f58e40e4399fdd378fa6d074"},
{file = "grpcio-1.78.0-cp311-cp311-win32.whl", hash = "sha256:6092beabe1966a3229f599d7088b38dfc8ffa1608b5b5cdda31e591e6500f856"},
{file = "grpcio-1.78.0-cp311-cp311-win_amd64.whl", hash = "sha256:1afa62af6e23f88629f2b29ec9e52ec7c65a7176c1e0a83292b93c76ca882558"},
{file = "grpcio-1.78.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:f9ab915a267fc47c7e88c387a3a28325b58c898e23d4995f765728f4e3dedb97"},
{file = "grpcio-1.78.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:3f8904a8165ab21e07e58bf3e30a73f4dffc7a1e0dbc32d51c61b5360d26f43e"},
{file = "grpcio-1.78.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:859b13906ce098c0b493af92142ad051bf64c7870fa58a123911c88606714996"},
{file = "grpcio-1.78.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:b2342d87af32790f934a79c3112641e7b27d63c261b8b4395350dad43eff1dc7"},
{file = "grpcio-1.78.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:12a771591ae40bc65ba67048fa52ef4f0e6db8279e595fd349f9dfddeef571f9"},
{file = "grpcio-1.78.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:185dea0d5260cbb2d224c507bf2a5444d5abbb1fa3594c1ed7e4c709d5eb8383"},
{file = "grpcio-1.78.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:51b13f9aed9d59ee389ad666b8c2214cc87b5de258fa712f9ab05f922e3896c6"},
{file = "grpcio-1.78.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fd5f135b1bd58ab088930b3c613455796dfa0393626a6972663ccdda5b4ac6ce"},
{file = "grpcio-1.78.0-cp312-cp312-win32.whl", hash = "sha256:94309f498bcc07e5a7d16089ab984d42ad96af1d94b5a4eb966a266d9fcabf68"},
{file = "grpcio-1.78.0-cp312-cp312-win_amd64.whl", hash = "sha256:9566fe4ababbb2610c39190791e5b829869351d14369603702e890ef3ad2d06e"},
{file = "grpcio-1.78.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:ce3a90455492bf8bfa38e56fbbe1dbd4f872a3d8eeaf7337dc3b1c8aa28c271b"},
{file = "grpcio-1.78.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:2bf5e2e163b356978b23652c4818ce4759d40f4712ee9ec5a83c4be6f8c23a3a"},
{file = "grpcio-1.78.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8f2ac84905d12918e4e55a16da17939eb63e433dc11b677267c35568aa63fc84"},
{file = "grpcio-1.78.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:b58f37edab4a3881bc6c9bca52670610e0c9ca14e2ea3cf9debf185b870457fb"},
{file = "grpcio-1.78.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:735e38e176a88ce41840c21bb49098ab66177c64c82426e24e0082500cc68af5"},
{file = "grpcio-1.78.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2045397e63a7a0ee7957c25f7dbb36ddc110e0cfb418403d110c0a7a68a844e9"},
{file = "grpcio-1.78.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:a9f136fbafe7ccf4ac7e8e0c28b31066e810be52d6e344ef954a3a70234e1702"},
{file = "grpcio-1.78.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:748b6138585379c737adc08aeffd21222abbda1a86a0dca2a39682feb9196c20"},
{file = "grpcio-1.78.0-cp313-cp313-win32.whl", hash = "sha256:271c73e6e5676afe4fc52907686670c7cea22ab2310b76a59b678403ed40d670"},
{file = "grpcio-1.78.0-cp313-cp313-win_amd64.whl", hash = "sha256:f2d4e43ee362adfc05994ed479334d5a451ab7bc3f3fee1b796b8ca66895acb4"},
{file = "grpcio-1.78.0-cp314-cp314-linux_armv7l.whl", hash = "sha256:e87cbc002b6f440482b3519e36e1313eb5443e9e9e73d6a52d43bd2004fcfd8e"},
{file = "grpcio-1.78.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:c41bc64626db62e72afec66b0c8a0da76491510015417c127bfc53b2fe6d7f7f"},
{file = "grpcio-1.78.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8dfffba826efcf366b1e3ccc37e67afe676f290e13a3b48d31a46739f80a8724"},
{file = "grpcio-1.78.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:74be1268d1439eaaf552c698cdb11cd594f0c49295ae6bb72c34ee31abbe611b"},
{file = "grpcio-1.78.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:be63c88b32e6c0f1429f1398ca5c09bc64b0d80950c8bb7807d7d7fb36fb84c7"},
{file = "grpcio-1.78.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:3c586ac70e855c721bda8f548d38c3ca66ac791dc49b66a8281a1f99db85e452"},
{file = "grpcio-1.78.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:35eb275bf1751d2ffbd8f57cdbc46058e857cf3971041521b78b7db94bdaf127"},
{file = "grpcio-1.78.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:207db540302c884b8848036b80db352a832b99dfdf41db1eb554c2c2c7800f65"},
{file = "grpcio-1.78.0-cp314-cp314-win32.whl", hash = "sha256:57bab6deef2f4f1ca76cc04565df38dc5713ae6c17de690721bdf30cb1e0545c"},
{file = "grpcio-1.78.0-cp314-cp314-win_amd64.whl", hash = "sha256:dce09d6116df20a96acfdbf85e4866258c3758180e8c49845d6ba8248b6d0bbb"},
{file = "grpcio-1.78.0-cp39-cp39-linux_armv7l.whl", hash = "sha256:86f85dd7c947baa707078a236288a289044836d4b640962018ceb9cd1f899af5"},
{file = "grpcio-1.78.0-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:de8cb00d1483a412a06394b8303feec5dcb3b55f81d83aa216dbb6a0b86a94f5"},
{file = "grpcio-1.78.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e888474dee2f59ff68130f8a397792d8cb8e17e6b3434339657ba4ee90845a8c"},
{file = "grpcio-1.78.0-cp39-cp39-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:86ce2371bfd7f212cf60d8517e5e854475c2c43ce14aa910e136ace72c6db6c1"},
{file = "grpcio-1.78.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b0c689c02947d636bc7fab3e30cc3a3445cca99c834dfb77cd4a6cabfc1c5597"},
{file = "grpcio-1.78.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:ce7599575eeb25c0f4dc1be59cada6219f3b56176f799627f44088b21381a28a"},
{file = "grpcio-1.78.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:684083fd383e9dc04c794adb838d4faea08b291ce81f64ecd08e4577c7398adf"},
{file = "grpcio-1.78.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:ab399ef5e3cd2a721b1038a0f3021001f19c5ab279f145e1146bb0b9f1b2b12c"},
{file = "grpcio-1.78.0-cp39-cp39-win32.whl", hash = "sha256:f3d6379493e18ad4d39537a82371c5281e153e963cecb13f953ebac155756525"},
{file = "grpcio-1.78.0-cp39-cp39-win_amd64.whl", hash = "sha256:5361a0630a7fdb58a6a97638ab70e1dae2893c4d08d7aba64ded28bb9e7a29df"},
{file = "grpcio-1.78.0.tar.gz", hash = "sha256:7382b95189546f375c174f53a5fa873cef91c4b8005faa05cc5b3beea9c4f1c5"},
]
[package.dependencies]
typing-extensions = ">=4.12,<5.0"
[package.extras]
protobuf = ["grpcio-tools (>=1.78.0)"]
[[package]]
name = "grpcio-status"
version = "1.78.0"
description = "Status proto mapping for gRPC"
optional = false
python-versions = ">=3.9"
groups = ["ci"]
files = [
{file = "grpcio_status-1.78.0-py3-none-any.whl", hash = "sha256:b492b693d4bf27b47a6c32590701724f1d3b9444b36491878fb71f6208857f34"},
{file = "grpcio_status-1.78.0.tar.gz", hash = "sha256:a34cfd28101bfea84b5aa0f936b4b423019e9213882907166af6b3bddc59e189"},
]
[package.dependencies]
googleapis-common-protos = ">=1.5.5"
grpcio = ">=1.78.0"
protobuf = ">=6.31.1,<7.0.0"
[[package]]
name = "h11"
version = "0.16.0"
@@ -1543,6 +1777,18 @@ files = [
humanize = ">=3.14.0"
jinja2 = "*"
[[package]]
name = "jmespath"
version = "1.1.0"
description = "JSON Matching Expressions"
optional = false
python-versions = ">=3.9"
groups = ["ci"]
files = [
{file = "jmespath-1.1.0-py3-none-any.whl", hash = "sha256:a5663118de4908c91729bea0acadca56526eb2698e83de10cd116ae0f4e97c64"},
{file = "jmespath-1.1.0.tar.gz", hash = "sha256:472c87d80f36026ae83c6ddd0f1d05d4e510134ed462851fd5f754c8c3cbb88d"},
]
[[package]]
name = "jsonpatch"
version = "1.33"
@@ -2814,6 +3060,24 @@ files = [
{file = "propcache-0.4.1.tar.gz", hash = "sha256:f48107a8c637e80362555f37ecf49abe20370e557cc4ab374f04ec4423c97c3d"},
]
[[package]]
name = "proto-plus"
version = "1.27.1"
description = "Beautiful, Pythonic protocol buffers"
optional = false
python-versions = ">=3.7"
groups = ["ci"]
files = [
{file = "proto_plus-1.27.1-py3-none-any.whl", hash = "sha256:e4643061f3a4d0de092d62aa4ad09fa4756b2cbb89d4627f3985018216f9fefc"},
{file = "proto_plus-1.27.1.tar.gz", hash = "sha256:912a7460446625b792f6448bade9e55cd4e41e6ac10e27009ef71a7f317fa147"},
]
[package.dependencies]
protobuf = ">=3.19.0,<7.0.0"
[package.extras]
testing = ["google-api-core (>=1.31.5)"]
[[package]]
name = "protobuf"
version = "6.33.6"
@@ -2876,6 +3140,33 @@ valkey = ["valkey-glide (>=2.1.0)"]
vault = ["hvac (>=2.3.0)", "types-hvac (>=2.3.0)"]
wrappers-encryption = ["cryptography (>=45.0.0)"]
[[package]]
name = "pyasn1"
version = "0.6.3"
description = "Pure-Python implementation of ASN.1 types and DER/BER/CER codecs (X.208)"
optional = false
python-versions = ">=3.8"
groups = ["ci"]
files = [
{file = "pyasn1-0.6.3-py3-none-any.whl", hash = "sha256:a80184d120f0864a52a073acc6fc642847d0be408e7c7252f31390c0f4eadcde"},
{file = "pyasn1-0.6.3.tar.gz", hash = "sha256:697a8ecd6d98891189184ca1fa05d1bb00e2f84b5977c481452050549c8a72cf"},
]
[[package]]
name = "pyasn1-modules"
version = "0.4.2"
description = "A collection of ASN.1-based protocols modules"
optional = false
python-versions = ">=3.8"
groups = ["ci"]
files = [
{file = "pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a"},
{file = "pyasn1_modules-0.4.2.tar.gz", hash = "sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6"},
]
[package.dependencies]
pyasn1 = ">=0.6.1,<0.7.0"
[[package]]
name = "pycodestyle"
version = "2.14.0"
@@ -4033,6 +4324,24 @@ files = [
{file = "ruamel_yaml_clib-0.2.15.tar.gz", hash = "sha256:46e4cc8c43ef6a94885f72512094e482114a8a706d3c555a34ed4b0d20200600"},
]
[[package]]
name = "s3transfer"
version = "0.16.0"
description = "An Amazon S3 Transfer Manager"
optional = false
python-versions = ">=3.9"
groups = ["ci"]
files = [
{file = "s3transfer-0.16.0-py3-none-any.whl", hash = "sha256:18e25d66fed509e3868dc1572b3f427ff947dd2c56f844a5bf09481ad3f3b2fe"},
{file = "s3transfer-0.16.0.tar.gz", hash = "sha256:8e990f13268025792229cd52fa10cb7163744bf56e719e0b9cb925ab79abf920"},
]
[package.dependencies]
botocore = ">=1.37.4,<2.0a0"
[package.extras]
crt = ["botocore[crt] (>=1.37.4,<2.0a0)"]
[[package]]
name = "semver"
version = "3.0.4"
@@ -4857,4 +5166,4 @@ type = ["pytest-mypy"]
[metadata]
lock-version = "2.1"
python-versions = ">=3.13, <3.15"
content-hash = "7b655afe35515359a21bee45caa6d7f5fb221506565198101b70392eaecb63e4"
content-hash = "1dc0bb464d30417e0477b11e10ba84fca942bb3b59bdb3affa1e9068eefd1153"
+252
View File
@@ -0,0 +1,252 @@
# bootstrap.ps1 — Windows developer environment bootstrapper for Android File Handler
# - Installs Chocolatey (if missing)
# - Installs Vagrant (Chocolatey or direct from HashiCorp)
# - Installs pyenv-win (if missing)
# - Installs Python build dependencies (Chocolatey)
# - Installs zlib from source (https://zlib.net/current/zlib.tar.gz)
# - Installs libffi from GitHub releases (https://github.com/libffi/libffi)
# - Installs latest Python (from pyproject.toml)
# - Sets that Python as global default
Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"
Write-Host "=== Android File Handler Windows Dev Bootstrap ===" -ForegroundColor Cyan
# --- Install Chocolatey ---
if (-not (Get-Command choco -ErrorAction SilentlyContinue)) {
Write-Host "Installing Chocolatey..." -ForegroundColor Yellow
[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12
Invoke-Expression ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1'))
$env:Path = [System.Environment]::GetEnvironmentVariable("Path", "Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path", "User")
}
# --- Install Vagrant ---
$vagrantInstalled = Get-Command vagrant -ErrorAction SilentlyContinue
if (-not $vagrantInstalled) {
Write-Host "Installing Vagrant..." -ForegroundColor Yellow
$chocoAvailable = Get-Command choco -ErrorAction SilentlyContinue
if ($chocoAvailable) {
Write-Host " Installing via Chocolatey..." -ForegroundColor Yellow
choco install vagrant -y --no-progress
}
else {
Write-Host " Chocolatey not available — downloading from HashiCorp..." -ForegroundColor Yellow
$vagrantVersion = "2.4.9"
$msiUrl = "https://releases.hashicorp.com/vagrant/$vagrantVersion/vagrant_${vagrantVersion}_windows_amd64.msi"
$msiPath = "$env:TEMP\vagrant_${vagrantVersion}_windows_amd64.msi"
[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12
Write-Host " Downloading $msiUrl ..." -ForegroundColor Yellow
(New-Object System.Net.WebClient).DownloadFile($msiUrl, $msiPath)
Write-Host " Installing MSI..." -ForegroundColor Yellow
$msiProc = Start-Process -FilePath "msiexec.exe" `
-ArgumentList "/i", $msiPath, "/qn", "/norestart" `
-Wait -PassThru
if ($msiProc.ExitCode -ne 0) {
Write-Host "ERROR: Vagrant MSI install failed with exit code $($msiProc.ExitCode)" -ForegroundColor Red
exit 1
}
Remove-Item $msiPath -Force -ErrorAction SilentlyContinue
}
$env:Path = [System.Environment]::GetEnvironmentVariable("Path", "Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path", "User")
Write-Host " Vagrant installed: $(vagrant --version)" -ForegroundColor Green
}
else {
Write-Host "Vagrant already installed: $(vagrant --version)" -ForegroundColor Green
}
# --- Install pyenv-win ---
$pyenvRoot = "$env:USERPROFILE\.pyenv"
$pyenvBin = "$pyenvRoot\pyenv-win\bin\pyenv.bat"
if (-not (Test-Path $pyenvBin)) {
Write-Host "Installing pyenv-win..." -ForegroundColor Yellow
choco install pyenv-win -y --no-progress
$env:Path = [System.Environment]::GetEnvironmentVariable("Path", "Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path", "User")
}
else {
Write-Host "pyenv-win already installed." -ForegroundColor Green
}
# --- Install Python build dependencies ---
# Package IDs verified against https://community.chocolatey.org/packages
# zlib has no Chocolatey package; libffi has no Chocolatey package — both omitted
$buildDeps = @('visualcpp-build-tools', 'make', 'openssl.light', 'bzip2', 'SQLite')
$installedPkgs = choco list --no-progress --limit-output 2>&1 |
Where-Object { $_ -match '\|' } |
ForEach-Object { ($_ -split '\|')[0].ToLower() }
$toInstall = @($buildDeps | Where-Object { $_.ToLower() -notin $installedPkgs })
if ($toInstall.Count -gt 0) {
Write-Host "Installing Python build dependencies: $($toInstall -join ', ')..." -ForegroundColor Yellow
choco install -y --no-progress @toInstall
} else {
Write-Host "Python build dependencies already installed." -ForegroundColor Green
}
# Shared install root for libraries not on Chocolatey
$devLibsRoot = "$env:USERPROFILE\.devlibs"
New-Item -ItemType Directory -Path $devLibsRoot -Force | Out-Null
# --- Install zlib from source (https://zlib.net/current/zlib.tar.gz) ---
$zlibInstallDir = "$devLibsRoot\zlib"
if (-not (Test-Path "$zlibInstallDir\include\zlib.h")) {
Write-Host "Installing zlib from source..." -ForegroundColor Yellow
# Locate MSVC build environment via vswhere
$vswhere = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe"
if (-not (Test-Path $vswhere)) {
Write-Host "ERROR: vswhere.exe not found — Visual Studio Build Tools required to build zlib." -ForegroundColor Red
exit 1
}
$vsPath = (& $vswhere -latest -products * -requires Microsoft.VisualCpp.Tools.HostX86.TargetX64 -property installationPath 2>&1).Trim()
$vcvarsall = "$vsPath\VC\Auxiliary\Build\vcvarsall.bat"
if (-not (Test-Path $vcvarsall)) {
Write-Host "ERROR: vcvarsall.bat not found at $vcvarsall" -ForegroundColor Red
exit 1
}
# Download source tarball
$zlibTar = "$env:TEMP\zlib.tar.gz"
Write-Host " Downloading zlib source..." -ForegroundColor Yellow
[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12
(New-Object System.Net.WebClient).DownloadFile('https://zlib.net/current/zlib.tar.gz', $zlibTar)
# Extract (tar.exe ships with Windows 10 1803+)
$zlibBuildDir = "$env:TEMP\zlib-build"
if (Test-Path $zlibBuildDir) { Remove-Item $zlibBuildDir -Recurse -Force }
New-Item -ItemType Directory -Path $zlibBuildDir | Out-Null
tar -xzf $zlibTar -C $zlibBuildDir --strip-components=1
# Build with MSVC nmake via a wrapper cmd script
Write-Host " Building zlib with MSVC nmake..." -ForegroundColor Yellow
$buildCmd = "$env:TEMP\zlib-build.cmd"
"@echo off`r`ncall `"$vcvarsall`" x64`r`ncd /d `"$zlibBuildDir`"`r`nnmake -f win32\Makefile.msc`r`n" |
Set-Content $buildCmd -Encoding ASCII
$proc = Start-Process cmd.exe -ArgumentList "/c `"$buildCmd`"" -Wait -PassThru -NoNewWindow
Remove-Item $buildCmd -Force -ErrorAction SilentlyContinue
if ($proc.ExitCode -ne 0) {
Write-Host "ERROR: zlib build failed (exit $($proc.ExitCode))" -ForegroundColor Red
exit 1
}
# Install headers and libs
New-Item -ItemType Directory -Path "$zlibInstallDir\include" -Force | Out-Null
New-Item -ItemType Directory -Path "$zlibInstallDir\lib" -Force | Out-Null
New-Item -ItemType Directory -Path "$zlibInstallDir\bin" -Force | Out-Null
Copy-Item "$zlibBuildDir\zlib.h" "$zlibInstallDir\include\"
Copy-Item "$zlibBuildDir\zconf.h" "$zlibInstallDir\include\"
Copy-Item "$zlibBuildDir\zlib.lib" "$zlibInstallDir\lib\"
Copy-Item "$zlibBuildDir\zdll.lib" "$zlibInstallDir\lib\" -ErrorAction SilentlyContinue
Copy-Item "$zlibBuildDir\zlib1.dll" "$zlibInstallDir\bin\" -ErrorAction SilentlyContinue
# Cleanup
Remove-Item $zlibTar -Force -ErrorAction SilentlyContinue
Remove-Item $zlibBuildDir -Recurse -Force -ErrorAction SilentlyContinue
Write-Host "zlib installed to $zlibInstallDir" -ForegroundColor Green
} else {
Write-Host "zlib already installed at $zlibInstallDir" -ForegroundColor Green
}
# Add zlib bin dir to user PATH (persists across sessions; also updates current session)
$zlibBinDir = "$zlibInstallDir\bin"
$userPath = [System.Environment]::GetEnvironmentVariable('Path', 'User')
if ($userPath -notmatch [regex]::Escape($zlibBinDir)) {
[System.Environment]::SetEnvironmentVariable('Path', "$userPath;$zlibBinDir", 'User')
Write-Host " Added $zlibBinDir to user PATH" -ForegroundColor Green
}
$env:Path = [System.Environment]::GetEnvironmentVariable('Path', 'Machine') + ';' +
[System.Environment]::GetEnvironmentVariable('Path', 'User')
# --- Install libffi from GitHub releases (https://github.com/libffi/libffi) ---
$libffiInstallDir = "$devLibsRoot\libffi"
if (-not (Test-Path "$libffiInstallDir\include\ffi.h")) {
Write-Host "Fetching latest libffi release from GitHub..." -ForegroundColor Yellow
$apiResponse = Invoke-RestMethod `
-Uri 'https://api.github.com/repos/libffi/libffi/releases/latest' `
-Headers @{ 'User-Agent' = 'android-file-handler-bootstrap' }
$libffiTag = $apiResponse.tag_name # e.g. "v3.5.2"
$libffiVersion = $libffiTag -replace '^v', '' # e.g. "3.5.2"
Write-Host " Latest libffi: $libffiTag" -ForegroundColor Yellow
$zipName = "libffi-$libffiVersion-x86-64bit-msvc-binaries.zip"
$downloadUrl = "https://github.com/libffi/libffi/releases/download/$libffiTag/$zipName"
$zipPath = "$env:TEMP\$zipName"
Write-Host " Downloading $zipName..." -ForegroundColor Yellow
[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12
(New-Object System.Net.WebClient).DownloadFile($downloadUrl, $zipPath)
# Extract — zip contains a single top-level subdirectory
$extractTemp = "$env:TEMP\libffi-extract"
if (Test-Path $extractTemp) { Remove-Item $extractTemp -Recurse -Force }
Expand-Archive -Path $zipPath -DestinationPath $extractTemp -Force
if (Test-Path $libffiInstallDir) { Remove-Item $libffiInstallDir -Recurse -Force }
$innerDir = Get-ChildItem $extractTemp -Directory | Select-Object -First 1
if ($innerDir) {
Move-Item $innerDir.FullName $libffiInstallDir
} else {
# Flat zip — use the extract root directly
Move-Item $extractTemp $libffiInstallDir
}
Remove-Item $zipPath -Force -ErrorAction SilentlyContinue
if (Test-Path $extractTemp) { Remove-Item $extractTemp -Recurse -Force -ErrorAction SilentlyContinue }
Write-Host "libffi $libffiVersion installed to $libffiInstallDir" -ForegroundColor Green
} else {
Write-Host "libffi already installed at $libffiInstallDir" -ForegroundColor Green
}
# --- Parse Python version from pyproject.toml ---
$scriptDir = if ($PSScriptRoot) { $PSScriptRoot } else { Split-Path -Parent $MyInvocation.MyCommand.Path }
$repoRoot = Resolve-Path (Join-Path $scriptDir "..\..\..")
$pyprojectPath = Join-Path $repoRoot "pyproject.toml"
if (-not (Test-Path $pyprojectPath)) {
Write-Host "ERROR: pyproject.toml not found at $pyprojectPath" -ForegroundColor Red
exit 1
}
$pyproject = Get-Content $pyprojectPath -Raw
$pyverMatch = $pyproject | Select-String -Pattern 'requires-python\s*=\s*"([^"]+)"' | ForEach-Object { $_.Matches[0].Groups[1].Value }
if (-not $pyverMatch) {
Write-Host "ERROR: Could not find requires-python in pyproject.toml" -ForegroundColor Red
exit 1
}
# Use the highest version in the range (e.g. ">=3.13, <3.15" → 3.14.x)
$pyver = $pyverMatch -replace '[^0-9.,<>= ]', ''
$maxVer = ($pyver -split ',') | Where-Object { $_ -match '<' } | ForEach-Object { $_ -replace '[^0-9.]', '' } | Sort-Object -Descending | Select-Object -First 1
if ($maxVer) {
$major, $minor = $maxVer -split '\.'
$targetMinor = [int]$minor - 1
$targetVer = "$major.$targetMinor.x"
} else {
$targetVer = $pyver -replace '[^0-9.]', ''
}
Write-Host "Target Python version: $targetVer" -ForegroundColor Cyan
# --- Install and set global Python version ---
$pyenv = "pyenv"
if (-not (Get-Command $pyenv -ErrorAction SilentlyContinue)) {
$pyenv = "$pyenvBin"
}
$versionPrefix = $targetVer -replace '\.x$', ''
$installedVersions = (& $pyenv versions 2>&1) | Out-String
if ($installedVersions -notmatch [regex]::Escape($versionPrefix)) {
Write-Host "Installing Python $targetVer via pyenv-win..." -ForegroundColor Yellow
& $pyenv install $targetVer -q
} else {
Write-Host "Python $targetVer already installed via pyenv." -ForegroundColor Green
}
$currentGlobal = ((& $pyenv global 2>&1) | Out-String).Trim()
if ($currentGlobal -notmatch [regex]::Escape($versionPrefix)) {
& $pyenv global $targetVer
Write-Host "pyenv-win global version set to $targetVer" -ForegroundColor Green
} else {
Write-Host "pyenv-win global already set to $currentGlobal" -ForegroundColor Green
}
Write-Host "=== Bootstrap complete ===" -ForegroundColor Cyan
@@ -1,10 +1,13 @@
import os
block_cipher = None
SPEC_DIR = os.path.dirname(os.path.abspath(SPEC))
SRC_DIR = os.path.normpath(os.path.join(SPEC_DIR, '..', '..', 'src'))
a = Analysis(
['../../src/main.py'],
pathex=['src'],
[os.path.join(SRC_DIR, 'main.py')],
pathex=[SRC_DIR],
binaries=[],
datas=[
('../../src/gui', 'gui'),
(os.path.join(SRC_DIR, 'gui'), 'gui'),
],
hiddenimports=[
# GUI modules
@@ -33,7 +36,8 @@ a = Analysis(
'managers.transfer_manager',
# Utility modules
'utils',
'utils.file_deduplication'
'utils.file_deduplication',
'utils.security_utils'
],
hookspath=[],
hooksconfig={},
@@ -1,12 +1,15 @@
# -*- mode: python ; coding: utf-8 -*-
import os
block_cipher = None
SPEC_DIR = os.path.dirname(os.path.abspath(SPEC))
SRC_DIR = os.path.normpath(os.path.join(SPEC_DIR, '..', '..', 'src'))
a = Analysis(
['../../src/main.py'],
pathex=['src'],
[os.path.join(SRC_DIR, 'main.py')],
pathex=[SRC_DIR],
binaries=[],
datas=[
('../../src/gui', 'gui'),
('../../scripts/debian_postinst.sh', 'scripts'),
(os.path.join(SRC_DIR, 'gui'), 'gui'),
(os.path.join(SPEC_DIR, '..', 'debian_postinst.sh'), 'scripts'),
],
hiddenimports=[
# GUI modules
@@ -35,7 +38,8 @@ a = Analysis(
'managers.transfer_manager',
# Utility modules
'utils',
'utils.file_deduplication'
'utils.file_deduplication',
'utils.security_utils'
],
hookspath=[],
hooksconfig={},
@@ -1,11 +1,14 @@
import os
block_cipher = None
SPEC_DIR = os.path.dirname(os.path.abspath(SPEC))
SRC_DIR = os.path.normpath(os.path.join(SPEC_DIR, '..', '..', 'src'))
a = Analysis(
['../../src/main.py'],
pathex=['src'],
[os.path.join(SRC_DIR, 'main.py')],
pathex=[SRC_DIR],
binaries=[],
datas=[
('../../src/gui', 'gui'),
('../../scripts/rhel_postinst.sh', 'scripts'),
(os.path.join(SRC_DIR, 'gui'), 'gui'),
(os.path.join(SPEC_DIR, '..', 'rhel_postinst.sh'), 'scripts'),
],
hiddenimports=[
# GUI modules
@@ -34,7 +37,8 @@ a = Analysis(
'managers.transfer_manager',
# Utility modules
'utils',
'utils.file_deduplication'
'utils.file_deduplication',
'utils.security_utils'
],
hookspath=[],
hooksconfig={},
@@ -1,11 +1,14 @@
# -*- mode: python ; coding: utf-8 -*-
import os
block_cipher = None
SPEC_DIR = os.path.dirname(os.path.abspath(SPEC))
SRC_DIR = os.path.normpath(os.path.join(SPEC_DIR, '..', '..', 'src'))
a = Analysis(
['../../src/main.py'],
pathex=['../..'],
[os.path.join(SRC_DIR, 'main.py')],
pathex=[SRC_DIR],
binaries=[],
datas=[
('../../src/gui', 'gui'),
(os.path.join(SRC_DIR, 'gui'), 'gui'),
],
hiddenimports=[
# GUI modules
@@ -34,7 +37,8 @@ a = Analysis(
'managers.transfer_manager',
# Utility modules
'utils',
'utils.file_deduplication'
'utils.file_deduplication',
'utils.security_utils'
],
hookspath=[],
hooksconfig={},
@@ -66,5 +70,5 @@ exe = EXE(
target_arch=None,
codesign_identity=None,
entitlements_file=None,
icon='../../icon_media/robot_files_256.ico',
icon=os.path.join(SPEC_DIR, '..', '..', 'icon_media', 'robot_files_256.ico'),
)
+298
View File
@@ -0,0 +1,298 @@
# run_vagrant_tests.ps1 — Host-side orchestrator for Vagrant Windows 11 test cycle.
#
# Workflow:
# 1. Build the installer locally (PyInstaller + Inno Setup)
# 2. Ensure the Vagrant VM is running and has a "clean" snapshot
# 3. Restore to the clean snapshot
# 4. Push the installer to the VM
# 5. Run the test script inside the VM via WinRM
# 6. Retrieve screenshots and results to the host
# 7. Print results summary to CLI
# 8. Revert VM back to clean snapshot
#
# Usage (from repo root):
# .\scripts\windows\run_vagrant_tests.ps1
# .\scripts\windows\run_vagrant_tests.ps1 -SkipBuild
# .\scripts\windows\run_vagrant_tests.ps1 -SkipRevert
#
# Prerequisites:
# - Vagrant and Hyper-V enabled on the host
# - Inno Setup 6 installed (default path or in PATH)
# - Poetry environment set up
# - The VM created and "clean" snapshot saved:
# cd vagrant && vagrant up --provider=hyperv && vagrant snapshot save clean
[CmdletBinding()]
param(
[switch]$SkipBuild,
[switch]$SkipRevert,
[string]$SnapshotName = "clean",
[string]$LocalResultsDir = ""
)
Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"
$RepoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..\..")).Path
$VagrantDir = Join-Path $RepoRoot "vagrant"
$Timestamp = Get-Date -Format "yyyyMMdd_HHmmss"
if (-not $LocalResultsDir) {
$LocalResultsDir = Join-Path $RepoRoot "vagrant_test_results\$Timestamp"
}
function Write-Step {
param([string]$Message)
Write-Host "`n>>> $Message" -ForegroundColor Cyan
}
# ── Ensure we're in the vagrant directory ────────────────────────────────────
Push-Location $VagrantDir
try {
# ── Step 1: Build the installer locally ──────────────────────────────────
$installerPath = Join-Path $RepoRoot "dist\android-file-handler-setup.exe"
if ($SkipBuild) {
Write-Step "Skipping build (-SkipBuild specified)"
if (-not (Test-Path $installerPath)) {
Write-Host "ERROR: -SkipBuild was specified but installer not found at $installerPath" -ForegroundColor Red
exit 1
}
}
else {
Write-Step "Building installer locally (PyInstaller + Inno Setup)"
# PyInstaller — compile the exe
Push-Location $RepoRoot
try {
Write-Host " Running PyInstaller..." -ForegroundColor Yellow
poetry run pyinstaller scripts\spec_scripts\android-file-handler-windows.spec --noconfirm
if ($LASTEXITCODE -ne 0) {
Write-Host "ERROR: PyInstaller failed with exit code $LASTEXITCODE" -ForegroundColor Red
exit 1
}
$pyinstallerExe = Join-Path $RepoRoot "dist\android-file-handler-windows.exe"
if (-not (Test-Path $pyinstallerExe)) {
Write-Host "ERROR: PyInstaller output not found at $pyinstallerExe" -ForegroundColor Red
exit 1
}
Write-Host " PyInstaller output: $pyinstallerExe" -ForegroundColor Green
# Inno Setup — compile the installer
Write-Host " Running Inno Setup..." -ForegroundColor Yellow
$isccPaths = @(
"C:\Program Files (x86)\Inno Setup 6\ISCC.exe",
"C:\Program Files\Inno Setup 6\ISCC.exe"
)
$iscc = $isccPaths | Where-Object { Test-Path $_ } | Select-Object -First 1
if (-not $iscc) {
$iscc = Get-Command ISCC.exe -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Source
}
if (-not $iscc) {
Write-Host "ERROR: Inno Setup (ISCC.exe) not found. Install Inno Setup 6 or add it to PATH." -ForegroundColor Red
exit 1
}
& $iscc "scripts\windows\android-file-handler-setup.iss"
if ($LASTEXITCODE -ne 0) {
Write-Host "ERROR: Inno Setup failed with exit code $LASTEXITCODE" -ForegroundColor Red
exit 1
}
if (-not (Test-Path $installerPath)) {
Write-Host "ERROR: Installer not found at $installerPath after Inno Setup" -ForegroundColor Red
exit 1
}
Write-Host " Installer built: $installerPath" -ForegroundColor Green
}
finally {
Pop-Location
}
}
# ── Step 2: Verify VM and snapshot exist ─────────────────────────────────
Write-Step "Checking Vagrant VM status"
$status = vagrant status --machine-readable 2>&1 |
Select-String "state,running" -Quiet
if (-not $status) {
Write-Host "VM is not running. Starting it..." -ForegroundColor Yellow
vagrant up --provider=hyperv
}
Write-Step "Verifying '$SnapshotName' snapshot exists"
$snapshots = vagrant snapshot list 2>&1
if ($snapshots -notmatch [regex]::Escape($SnapshotName)) {
Write-Host "Snapshot '$SnapshotName' not found. Creating it now..." -ForegroundColor Yellow
vagrant snapshot save $SnapshotName
}
# ── Step 3: Restore to clean snapshot ────────────────────────────────────
Write-Step "Restoring VM to '$SnapshotName' snapshot"
vagrant snapshot restore $SnapshotName
# ── Step 4: Push installer to the VM ─────────────────────────────────────
Write-Step "Pushing installer to the VM"
$remoteInstallerDir = "C:\vagrant_installer"
$remoteInstallerPath = "$remoteInstallerDir\android-file-handler-setup.exe"
vagrant winrm --shell powershell --command `
"New-Item -ItemType Directory -Path '$remoteInstallerDir' -Force | Out-Null"
# Base64-encode the installer, transfer via WinRM, decode on the VM
$installerBytes = [IO.File]::ReadAllBytes($installerPath)
$installerSizeMB = [math]::Round($installerBytes.Length / 1MB, 1)
Write-Host " Installer size: ${installerSizeMB} MB — transferring via WinRM..." -ForegroundColor Yellow
# Split into chunks to avoid WinRM message size limits (< 2MB per command)
$chunkSize = 1 * 1024 * 1024 # 1 MB chunks
$totalChunks = [math]::Ceiling($installerBytes.Length / $chunkSize)
vagrant winrm --shell powershell --command `
"if (Test-Path '$remoteInstallerPath') { Remove-Item '$remoteInstallerPath' -Force }"
for ($idx = 0; $idx -lt $totalChunks; $idx++) {
$offset = $idx * $chunkSize
$length = [math]::Min($chunkSize, $installerBytes.Length - $offset)
$chunk = New-Object byte[] $length
[Array]::Copy($installerBytes, $offset, $chunk, 0, $length)
$b64Chunk = [Convert]::ToBase64String($chunk)
$appendScript = if ($idx -eq 0) {
"[IO.File]::WriteAllBytes('$remoteInstallerPath', [Convert]::FromBase64String('$b64Chunk'))"
}
else {
"`$bytes = [Convert]::FromBase64String('$b64Chunk'); " +
"`$stream = [IO.File]::Open('$remoteInstallerPath', 'Append'); " +
"`$stream.Write(`$bytes, 0, `$bytes.Length); `$stream.Close()"
}
vagrant winrm --shell powershell --command $appendScript
Write-Host " Chunk $($idx + 1)/$totalChunks transferred" -ForegroundColor DarkGray
}
# Verify transfer
$remoteSize = vagrant winrm --shell powershell --command `
"(Get-Item '$remoteInstallerPath').Length"
Write-Host " Remote file size: $($remoteSize.Trim()) bytes (expected: $($installerBytes.Length))" -ForegroundColor DarkGray
# ── Step 5: Run tests inside the VM ──────────────────────────────────────
Write-Step "Running test suite inside the VM"
$testExitCode = 0
try {
vagrant winrm --shell powershell --command `
"Set-ExecutionPolicy -ExecutionPolicy Bypass -Scope Process -Force; & C:\vagrant_project\vagrant\test_installer.ps1 -InstallerPath 'C:\vagrant_installer\android-file-handler-setup.exe'"
}
catch {
$testExitCode = 1
Write-Host "Test script returned failures." -ForegroundColor Yellow
}
# ── Step 6: Retrieve results and screenshots ─────────────────────────────
Write-Step "Retrieving test results and screenshots"
if (-not (Test-Path $LocalResultsDir)) {
New-Item -ItemType Directory -Path $LocalResultsDir -Force | Out-Null
}
# Copy results from shared folder on VM via vagrant scp or direct file copy
# The test writes to C:\vagrant_test_results inside the VM.
# We retrieve via WinRM by reading files.
try {
# Get list of result files
$remoteFiles = vagrant winrm --shell powershell --command `
"if (Test-Path 'C:\vagrant_test_results') { Get-ChildItem 'C:\vagrant_test_results' -Name }" 2>&1 |
Where-Object { $_ -is [string] -and $_.Trim() -ne "" }
foreach ($fileName in $remoteFiles) {
$trimmedName = $fileName.Trim()
if (-not $trimmedName) { continue }
$remotePath = "C:\vagrant_test_results\$trimmedName"
$localPath = Join-Path $LocalResultsDir $trimmedName
if ($trimmedName -match '\.csv$') {
# Text file — read content and save
$content = vagrant winrm --shell powershell --command `
"Get-Content -Path '$remotePath' -Raw"
$content | Out-File -FilePath $localPath -Encoding utf8
Write-Host " Retrieved: $trimmedName" -ForegroundColor DarkGray
}
elseif ($trimmedName -match '\.(png|jpg|bmp)$') {
# Binary file — base64 encode, transfer, decode
$b64 = vagrant winrm --shell powershell --command `
"[Convert]::ToBase64String([IO.File]::ReadAllBytes('$remotePath'))"
[IO.File]::WriteAllBytes($localPath, [Convert]::FromBase64String($b64.Trim()))
Write-Host " Retrieved: $trimmedName" -ForegroundColor DarkGray
}
}
}
catch {
Write-Host " Warning: Could not retrieve some result files: $_" -ForegroundColor Yellow
}
# ── Step 7: Print local summary ──────────────────────────────────────────
Write-Step "Test Results"
$csvPath = Join-Path $LocalResultsDir "test_results.csv"
if (Test-Path $csvPath) {
$results = Import-Csv $csvPath
$results | Format-Table -AutoSize
$passCount = ($results | Where-Object { $_.Status -eq "PASS" }).Count
$failCount = ($results | Where-Object { $_.Status -eq "FAIL" }).Count
Write-Host " Passed: $passCount | Failed: $failCount | Total: $($passCount + $failCount)" -ForegroundColor $(if ($failCount -gt 0) { "Red" } else { "Green" })
}
else {
Write-Host " No results CSV found. Check VM output above." -ForegroundColor Yellow
}
Write-Host "`n Results directory: $LocalResultsDir" -ForegroundColor White
# List screenshots
$screenshots = Get-ChildItem $LocalResultsDir -Filter "*.png" -ErrorAction SilentlyContinue
if ($screenshots) {
Write-Host " Screenshots:" -ForegroundColor White
foreach ($shot in $screenshots) {
Write-Host " - $($shot.FullName)" -ForegroundColor DarkGray
}
}
# ── Step 8: Revert to clean snapshot ─────────────────────────────────────
if (-not $SkipRevert) {
Write-Step "Reverting VM to '$SnapshotName' snapshot"
vagrant snapshot restore $SnapshotName
Write-Host "VM reverted to clean state." -ForegroundColor Green
}
else {
Write-Host "`n -SkipRevert specified, VM left in current state." -ForegroundColor Yellow
}
# ── Exit ─────────────────────────────────────────────────────────────────
Write-Host ""
if ($testExitCode -ne 0) {
Write-Host "One or more tests FAILED." -ForegroundColor Red
exit 1
}
else {
Write-Host "All tests PASSED." -ForegroundColor Green
exit 0
}
}
finally {
Pop-Location
}
+89
View File
@@ -0,0 +1,89 @@
# Vagrant Windows 11 Test Environment
Automated testing of the Android File Handler installer on a clean Windows 11 VM using Hyper-V.
## Prerequisites
- [Vagrant](https://www.vagrantup.com/) >= 2.4
- Hyper-V enabled (`Enable-WindowsOptionalFeature -Online -FeatureName Microsoft-Hyper-V -All`)
- [Inno Setup 6](https://jrsoftware.org/isdl.php) installed (default path or ISCC.exe in PATH)
- Poetry environment set up (`poetry install`)
## Setup (one-time)
```powershell
cd vagrant
vagrant up --provider=hyperv
vagrant snapshot save clean
```
> **Note:** Vagrant will prompt for your Windows credentials to set up the SMB synced folder.
> You can set `VAGRANT_SMB_USERNAME` and `VAGRANT_SMB_PASSWORD` environment variables to skip the prompt.
## Running Tests
From the repo root:
```powershell
.\scripts\windows\run_vagrant_tests.ps1
```
This will:
1. Build the installer locally (PyInstaller + Inno Setup)
2. Restore the VM to the `clean` snapshot
3. Push the installer to the VM via WinRM
4. Run the installer silently inside the VM
5. Verify installation (directory, exe, shortcuts, registry)
6. Launch the app, find the window, take a GUI screenshot
7. Run silent uninstall and verify cleanup
8. Retrieve results CSV + screenshots to `vagrant_test_results\<timestamp>\`
9. Print a summary table to the CLI
10. Revert the VM back to the `clean` snapshot
### Options
| Flag | Description |
|---|---|
| `-SkipBuild` | Skip the local build step (use an existing installer in `dist\`) |
| `-SkipRevert` | Leave the VM in post-test state (useful for debugging) |
| `-SnapshotName <name>` | Use a different snapshot name (default: `clean`) |
| `-LocalResultsDir <path>` | Override where results are saved |
## What Gets Tested
| # | Test | Description |
|---|---|---|
| 1 | Installer exists | Checks the built `.exe` is present |
| 2 | Silent install | Runs the Inno Setup installer with `/VERYSILENT` |
| 3 | Install directory | Verifies `C:\Program Files\Android File Handler` exists |
| 4 | Executable present | Checks the app `.exe` is in the install directory |
| 5 | Start Menu shortcut | Verifies the Start Menu entry was created |
| 6 | Registry entry | Checks Add/Remove Programs registration |
| 7 | App launches | Starts the app and confirms it stays running |
| 8 | Window found | Finds the main window by its title |
| 9 | Silent uninstall | Runs the uninstaller and verifies cleanup |
## File Structure
```
vagrant/
├── Vagrantfile # VM definition (Windows 11 Enterprise, Hyper-V)
├── provision.ps1 # VM provisioning (nircmd for screenshots)
├── test_installer.ps1 # Test script that runs inside the VM
└── README.md # This file
scripts/windows/
└── run_vagrant_tests.ps1 # Host-side orchestrator (builds, pushes, tests, reverts)
```
## Troubleshooting
**VM won't start**: Ensure Hyper-V is enabled and you are running the terminal as Administrator.
**WinRM connection fails**: The box uses plaintext WinRM on port 55985. Ensure no firewall blocks it.
**SMB share prompt**: Set `VAGRANT_SMB_USERNAME` and `VAGRANT_SMB_PASSWORD` environment variables to avoid interactive credential prompts.
**Inno Setup not found**: Install Inno Setup 6 to the default path or add `ISCC.exe` to your PATH.
**Want to inspect the VM manually**: Use `vagrant rdp` or pass `-SkipRevert`.
+54
View File
@@ -0,0 +1,54 @@
# -*- mode: ruby -*-
# vi: set ft=ruby :
#
# Vagrant Windows 11 test machine for Android File Handler installer testing.
#
# Prerequisites:
# - Vagrant >= 2.4
# - Hyper-V enabled (Windows host)
#
# Usage:
# cd vagrant
# vagrant up --provider=hyperv
# vagrant snapshot save clean
# ..\scripts\windows\run_vagrant_tests.ps1
#
Vagrant.configure("2") do |config|
config.vm.box = "gusztavvargadr/windows-11-24h2-enterprise"
config.vm.hostname = "afh-test"
config.vm.communicator = "winrm"
config.winrm.username = "vagrant"
config.winrm.password = "vagrant"
config.winrm.transport = :plaintext
config.winrm.basic_auth_only = true
config.vm.guest = :windows
config.vm.boot_timeout = 900
config.vm.graceful_halt_timeout = 120
config.vm.network "forwarded_port", guest: 5985, host: 55985
config.vm.provider "hyperv" do |hv|
hv.vmname = "afh-win11-test"
hv.memory = 4096
hv.maxmemory = 4096
hv.cpus = 2
hv.enable_enhanced_session_mode = true
hv.linked_clone = true
end
# SMB synced folder — Hyper-V does not support VirtualBox shared folders.
# Vagrant will prompt for host credentials on first `vagrant up` to set up the SMB share.
config.vm.synced_folder "..", "/vagrant_project",
type: "smb",
smb_username: ENV["VAGRANT_SMB_USERNAME"],
smb_password: ENV["VAGRANT_SMB_PASSWORD"],
mount_options: ["ro"]
config.vm.provision "shell",
path: "provision.ps1",
privileged: true
end
+30
View File
@@ -0,0 +1,30 @@
# provision.ps1 — Vagrant provisioning script for Windows 11 test VM.
# Installs prerequisites needed to test the Android File Handler installer.
Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"
Write-Host "=== Provisioning Android File Handler test VM ===" -ForegroundColor Cyan
# --- Enable auto-logon so GUI tests work after reboot ---
$RegPath = "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon"
Set-ItemProperty -Path $RegPath -Name AutoAdminLogon -Value "1"
Set-ItemProperty -Path $RegPath -Name DefaultUserName -Value "vagrant"
Set-ItemProperty -Path $RegPath -Name DefaultPassword -Value "vagrant"
# --- Install Chocolatey (package manager) ---
if (-not (Get-Command choco -ErrorAction SilentlyContinue)) {
Write-Host "Installing Chocolatey..." -ForegroundColor Yellow
[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12
Invoke-Expression ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1'))
$env:Path = [System.Environment]::GetEnvironmentVariable("Path", "Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path", "User")
}
# --- Install nircmd for screenshot capture ---
Write-Host "Installing nircmd for screenshot capture..." -ForegroundColor Yellow
choco install nircmd -y --no-progress
# Refresh PATH
$env:Path = [System.Environment]::GetEnvironmentVariable("Path", "Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path", "User")
Write-Host "=== Provisioning complete ===" -ForegroundColor Green
+201
View File
@@ -0,0 +1,201 @@
# test_installer.ps1 — Runs INSIDE the Vagrant VM via WinRM.
# Tests installation and basic function of Android File Handler.
#
# Exit codes: 0 = all passed, 1 = one or more failures.
param(
[string]$InstallerPath = "C:\vagrant_project\dist\android-file-handler-setup.exe",
[string]$ScreenshotDir = "C:\vagrant_test_results"
)
Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"
# ── Helpers ──────────────────────────────────────────────────────────────────
$Script:Passed = 0
$Script:Failed = 0
$Script:Results = @()
function Write-TestResult {
param([string]$Name, [bool]$Pass, [string]$Detail = "")
$status = if ($Pass) { "PASS" } else { "FAIL" }
$color = if ($Pass) { "Green" } else { "Red" }
$msg = "[$status] $Name"
if ($Detail) { $msg += " — $Detail" }
Write-Host $msg -ForegroundColor $color
if ($Pass) { $Script:Passed++ } else { $Script:Failed++ }
$Script:Results += [PSCustomObject]@{ Test = $Name; Status = $status; Detail = $Detail }
}
function Take-Screenshot {
param([string]$Name)
if (-not (Test-Path $ScreenshotDir)) {
New-Item -ItemType Directory -Path $ScreenshotDir -Force | Out-Null
}
$outFile = Join-Path $ScreenshotDir "$Name.png"
$nircmd = Get-Command nircmd -ErrorAction SilentlyContinue
if ($nircmd) {
Start-Process -FilePath $nircmd.Source -ArgumentList "savescreenshot", $outFile -NoNewWindow -Wait
return $outFile
}
# Fallback: .NET screenshot
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing
$screen = [System.Windows.Forms.Screen]::PrimaryScreen.Bounds
$bitmap = New-Object System.Drawing.Bitmap($screen.Width, $screen.Height)
$graphics = [System.Drawing.Graphics]::FromImage($bitmap)
$graphics.CopyFromScreen($screen.Location, [System.Drawing.Point]::Empty, $screen.Size)
$bitmap.Save($outFile, [System.Drawing.Imaging.ImageFormat]::Png)
$graphics.Dispose()
$bitmap.Dispose()
return $outFile
}
# ── Setup ────────────────────────────────────────────────────────────────────
$AppName = "Android File Handler"
$ExeName = "android-file-handler-windows.exe"
$DefaultInstDir = Join-Path $env:ProgramFiles $AppName
Write-Host ""
Write-Host "=============================================" -ForegroundColor Cyan
Write-Host " Android File Handler — Vagrant Test Suite " -ForegroundColor Cyan
Write-Host "=============================================" -ForegroundColor Cyan
Write-Host ""
if (-not (Test-Path $ScreenshotDir)) {
New-Item -ItemType Directory -Path $ScreenshotDir -Force | Out-Null
}
# ── Test 1: Installer exists ────────────────────────────────────────────────
$installerExists = Test-Path $InstallerPath
Write-TestResult -Name "Installer file exists" -Pass $installerExists -Detail $InstallerPath
if (-not $installerExists) {
Write-Host "`nFATAL: Installer not found at $InstallerPath. Build the installer first." -ForegroundColor Red
Write-Host " poetry run pyinstaller scripts\spec_scripts\android-file-handler-windows.spec"
Write-Host " iscc scripts\windows\android-file-handler-setup.iss"
exit 1
}
# ── Test 2: Silent install ──────────────────────────────────────────────────
Write-Host "`n--- Running silent install ---" -ForegroundColor Yellow
$installProc = Start-Process -FilePath $InstallerPath `
-ArgumentList "/VERYSILENT", "/SUPPRESSMSGBOXES", "/NORESTART", "/SP-" `
-Wait -PassThru
$installOk = $installProc.ExitCode -eq 0
Write-TestResult -Name "Silent install completed" -Pass $installOk -Detail "Exit code: $($installProc.ExitCode)"
# ── Test 3: Install directory created ────────────────────────────────────────
$dirExists = Test-Path $DefaultInstDir
Write-TestResult -Name "Install directory exists" -Pass $dirExists -Detail $DefaultInstDir
# ── Test 4: Executable present ──────────────────────────────────────────────
$exePath = Join-Path $DefaultInstDir $ExeName
$exeExists = Test-Path $exePath
Write-TestResult -Name "Executable exists" -Pass $exeExists -Detail $exePath
# ── Test 5: Start Menu shortcut ─────────────────────────────────────────────
$startMenuDir = Join-Path $env:ProgramData "Microsoft\Windows\Start Menu\Programs\$AppName"
$shortcutPath = Join-Path $startMenuDir "$AppName.lnk"
$shortcutExists = Test-Path $shortcutPath
Write-TestResult -Name "Start Menu shortcut exists" -Pass $shortcutExists -Detail $shortcutPath
# ── Test 6: Registry uninstall entry ────────────────────────────────────────
$uninstallKey = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall"
$registryEntry = Get-ChildItem $uninstallKey -ErrorAction SilentlyContinue |
Where-Object { (Get-ItemProperty $_.PSPath -ErrorAction SilentlyContinue).DisplayName -eq $AppName }
$registryOk = $null -ne $registryEntry
Write-TestResult -Name "Registry uninstall entry" -Pass $registryOk
# ── Test 7: Launch application & capture GUI ─────────────────────────────────
if ($exeExists) {
Write-Host "`n--- Launching application ---" -ForegroundColor Yellow
$appProc = Start-Process -FilePath $exePath -PassThru
Start-Sleep -Seconds 8
# Check if process is still running (GUI app should stay alive)
$appRunning = -not $appProc.HasExited
Write-TestResult -Name "Application launches and stays running" -Pass $appRunning
# Look for the window by title
Add-Type @"
using System;
using System.Runtime.InteropServices;
public class Win32 {
[DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)]
public static extern IntPtr FindWindow(string lpClassName, string lpWindowName);
}
"@
$hwnd = [Win32]::FindWindow($null, $AppName)
$windowFound = $hwnd -ne [IntPtr]::Zero
Write-TestResult -Name "Main window found by title" -Pass $windowFound -Detail "HWND: $hwnd"
# Take screenshot of the running application
Start-Sleep -Seconds 2
$screenshotFile = Take-Screenshot -Name "app_running"
Write-Host " Screenshot saved: $screenshotFile" -ForegroundColor DarkGray
# Gracefully close
if (-not $appProc.HasExited) {
$appProc.CloseMainWindow() | Out-Null
Start-Sleep -Seconds 3
if (-not $appProc.HasExited) {
$appProc.Kill()
}
}
}
else {
Write-TestResult -Name "Application launches and stays running" -Pass $false -Detail "Skipped — exe not found"
Write-TestResult -Name "Main window found by title" -Pass $false -Detail "Skipped — exe not found"
}
# ── Test 8: Silent uninstall ────────────────────────────────────────────────
Write-Host "`n--- Running silent uninstall ---" -ForegroundColor Yellow
$uninstallerPath = Join-Path $DefaultInstDir "unins000.exe"
if (Test-Path $uninstallerPath) {
$uninstProc = Start-Process -FilePath $uninstallerPath `
-ArgumentList "/VERYSILENT", "/SUPPRESSMSGBOXES", "/NORESTART" `
-Wait -PassThru
$uninstallOk = $uninstProc.ExitCode -eq 0
Write-TestResult -Name "Silent uninstall completed" -Pass $uninstallOk -Detail "Exit code: $($uninstProc.ExitCode)"
Start-Sleep -Seconds 2
$dirGone = -not (Test-Path $DefaultInstDir)
Write-TestResult -Name "Install directory removed after uninstall" -Pass $dirGone
}
else {
Write-TestResult -Name "Silent uninstall completed" -Pass $false -Detail "Uninstaller not found"
Write-TestResult -Name "Install directory removed after uninstall" -Pass $false -Detail "Skipped"
}
# ── Summary ──────────────────────────────────────────────────────────────────
Write-Host ""
Write-Host "=============================================" -ForegroundColor Cyan
Write-Host " Test Summary" -ForegroundColor Cyan
Write-Host "=============================================" -ForegroundColor Cyan
Write-Host " Passed : $Script:Passed" -ForegroundColor Green
Write-Host " Failed : $Script:Failed" -ForegroundColor $(if ($Script:Failed -gt 0) { "Red" } else { "Green" })
Write-Host " Total : $($Script:Passed + $Script:Failed)" -ForegroundColor White
Write-Host ""
$Script:Results | Format-Table -AutoSize
# Export results as CSV for retrieval
$csvPath = Join-Path $ScreenshotDir "test_results.csv"
$Script:Results | Export-Csv -Path $csvPath -NoTypeInformation
Write-Host "Results saved to $csvPath" -ForegroundColor DarkGray
exit $(if ($Script:Failed -gt 0) { 1 } else { 0 })