summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--mullvad-api/src/availability.rs4
-rw-r--r--mullvad-cli/src/cmds/relay_constraints.rs1
-rw-r--r--mullvad-types/src/relay_constraints.rs53
-rw-r--r--test/Cargo.lock1
-rwxr-xr-xtest/scripts/test-utils.sh136
-rwxr-xr-xtest/test-by-version.sh6
-rw-r--r--test/test-manager/Cargo.toml1
-rw-r--r--test/test-manager/docs/config.md50
-rw-r--r--test/test-manager/src/config/error.rs15
-rw-r--r--test/test-manager/src/config/io.rs80
-rw-r--r--test/test-manager/src/config/manifest.rs110
-rw-r--r--test/test-manager/src/config/manifest/test_locations.rs105
-rw-r--r--test/test-manager/src/config/mod.rs11
-rw-r--r--test/test-manager/src/config/vm.rs (renamed from test/test-manager/src/config.rs)140
-rw-r--r--test/test-manager/src/main.rs141
-rw-r--r--test/test-manager/src/mullvad_daemon.rs18
-rw-r--r--test/test-manager/src/run_tests.rs28
-rw-r--r--test/test-manager/src/tests/helpers.rs191
-rw-r--r--test/test-manager/src/tests/install.rs4
-rw-r--r--test/test-manager/src/tests/mod.rs72
-rw-r--r--test/test-manager/src/tests/split_tunnel.rs6
-rw-r--r--test/test-manager/src/tests/test_metadata.rs8
-rw-r--r--test/test-manager/src/tests/tunnel.rs42
-rw-r--r--test/test-manager/src/tests/ui.rs28
-rw-r--r--test/test-manager/test_macro/src/lib.rs105
-rw-r--r--test/test-rpc/src/mullvad_daemon.rs6
26 files changed, 743 insertions, 619 deletions
diff --git a/mullvad-api/src/availability.rs b/mullvad-api/src/availability.rs
index 0857a7812b..9d5e135c15 100644
--- a/mullvad-api/src/availability.rs
+++ b/mullvad-api/src/availability.rs
@@ -72,7 +72,7 @@ impl ApiAvailability {
/// starting it if it's not currently running.
pub fn reset_inactivity_timer(&self) {
let mut inner = self.acquire();
- log::debug!("Restarting API inactivity check");
+ log::trace!("Restarting API inactivity check");
inner.stop_inactivity_timer();
let availability_handle = self.clone();
inner.inactivity_timer = Some(tokio::spawn(async move {
@@ -252,7 +252,7 @@ impl ApiAvailabilityState {
}
fn stop_inactivity_timer(&mut self) {
- log::debug!("Stopping API inactivity check");
+ log::trace!("Stopping API inactivity check");
if let Some(timer) = self.inactivity_timer.take() {
timer.abort();
}
diff --git a/mullvad-cli/src/cmds/relay_constraints.rs b/mullvad-cli/src/cmds/relay_constraints.rs
index 97555997fc..72b3bdb88e 100644
--- a/mullvad-cli/src/cmds/relay_constraints.rs
+++ b/mullvad-cli/src/cmds/relay_constraints.rs
@@ -27,7 +27,6 @@ impl From<LocationArgs> for Constraint<GeographicLocationConstraint> {
(country, Some(city), Some(hostname)) => {
GeographicLocationConstraint::Hostname(country, city, hostname)
}
-
_ => unreachable!("invalid location arguments"),
})
}
diff --git a/mullvad-types/src/relay_constraints.rs b/mullvad-types/src/relay_constraints.rs
index 4be7e25a4b..e6eaa7ca55 100644
--- a/mullvad-types/src/relay_constraints.rs
+++ b/mullvad-types/src/relay_constraints.rs
@@ -174,6 +174,12 @@ pub enum GeographicLocationConstraint {
Hostname(CountryCode, CityCode, Hostname),
}
+#[derive(thiserror::Error, Debug)]
+#[error("Failed to parse {input} into a geographic location constraint")]
+pub struct ParseGeoLocationError {
+ input: String,
+}
+
impl GeographicLocationConstraint {
/// Create a new [`GeographicLocationConstraint`] given a country.
pub fn country(country: impl Into<String>) -> Self {
@@ -227,6 +233,27 @@ impl Match<Relay> for GeographicLocationConstraint {
}
}
+impl FromStr for GeographicLocationConstraint {
+ type Err = ParseGeoLocationError;
+
+ // TODO: Implement for country and city as well?
+ fn from_str(input: &str) -> Result<Self, Self::Err> {
+ // A host name, such as "se-got-wg-101" maps to
+ // Country: se
+ // City: got
+ // hostname: se-got-wg-101
+ let x = input.split("-").collect::<Vec<_>>();
+ match x[..] {
+ [country] => Ok(GeographicLocationConstraint::country(country)),
+ [country, city] => Ok(GeographicLocationConstraint::city(country, city)),
+ [country, city, ..] => Ok(GeographicLocationConstraint::hostname(country, city, input)),
+ _ => Err(ParseGeoLocationError {
+ input: input.to_string(),
+ }),
+ }
+ }
+}
+
/// Limits the set of servers to choose based on ownership.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Deserialize, Serialize)]
#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
@@ -677,3 +704,29 @@ impl RelayOverride {
}
}
}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn parse_hostname() {
+ // Parse a country
+ assert_eq!(
+ "se".parse::<GeographicLocationConstraint>().unwrap(),
+ GeographicLocationConstraint::country("se")
+ );
+ // Parse a city
+ assert_eq!(
+ "se-got".parse::<GeographicLocationConstraint>().unwrap(),
+ GeographicLocationConstraint::city("se", "got")
+ );
+ // Parse a hostname
+ assert_eq!(
+ "se-got-wg-101"
+ .parse::<GeographicLocationConstraint>()
+ .unwrap(),
+ GeographicLocationConstraint::hostname("se", "got", "se-got-wg-101")
+ );
+ }
+}
diff --git a/test/Cargo.lock b/test/Cargo.lock
index a4e2ee9df7..064ae149ad 100644
--- a/test/Cargo.lock
+++ b/test/Cargo.lock
@@ -3597,6 +3597,7 @@ dependencies = [
"dirs",
"env_logger",
"futures",
+ "glob",
"hyper-util",
"inventory",
"ipnetwork",
diff --git a/test/scripts/test-utils.sh b/test/scripts/test-utils.sh
index 319c646020..be826a6ef0 100755
--- a/test/scripts/test-utils.sh
+++ b/test/scripts/test-utils.sh
@@ -12,9 +12,9 @@ function get_test_utls_dir {
local script_path="${BASH_SOURCE[0]}"
local script_dir
if [[ -n "$script_path" ]]; then
- script_dir="$(cd "$(dirname "$script_path")" > /dev/null && pwd)"
+ script_dir="$(cd "$(dirname "$script_path")" >/dev/null && pwd)"
else
- script_dir="$(cd "$(dirname "$0")" > /dev/null && pwd)"
+ script_dir="$(cd "$(dirname "$0")" >/dev/null && pwd)"
fi
echo "$script_dir"
}
@@ -54,7 +54,7 @@ export CURRENT_VERSION
export LATEST_STABLE_RELEASE
function print_available_releases {
- for release in $(jq -r '.[].tag_name'<<<"$RELEASES"); do
+ for release in $(jq -r '.[].tag_name' <<<"$RELEASES"); do
echo "$release"
done
}
@@ -73,7 +73,7 @@ function get_package_dir {
exit 1
fi
- mkdir -p "$package_dir" || exit 1
+ mkdir -p "$package_dir" || exit 1
# Clean up old packages
find "$package_dir" -type f -mtime +5 -delete || true
@@ -89,7 +89,7 @@ function nice_time {
result=$?
fi
s=$SECONDS
- echo "\"$*\" completed in $((s/60))m:$((s%60))s"
+ echo "\"$*\" completed in $((s / 60))m:$((s % 60))s"
return $result
}
# Matches $1 with a build version string and sets the following exported variables:
@@ -122,22 +122,22 @@ function get_app_filename {
version="${BUILD_VERSION}${COMMIT_HASH}${TAG:-}"
fi
case $os in
- debian*|ubuntu*)
- echo "MullvadVPN-${version}_amd64.deb"
- ;;
- fedora*)
- echo "MullvadVPN-${version}_x86_64.rpm"
- ;;
- windows*)
- echo "MullvadVPN-${version}.exe"
- ;;
- macos*)
- echo "MullvadVPN-${version}.pkg"
- ;;
- *)
- echo "Unsupported target: $os" 1>&2
- return 1
- ;;
+ debian* | ubuntu*)
+ echo "MullvadVPN-${version}_amd64.deb"
+ ;;
+ fedora*)
+ echo "MullvadVPN-${version}_x86_64.rpm"
+ ;;
+ windows*)
+ echo "MullvadVPN-${version}.exe"
+ ;;
+ macos*)
+ echo "MullvadVPN-${version}.pkg"
+ ;;
+ *)
+ echo "Unsupported target: $os" 1>&2
+ return 1
+ ;;
esac
}
@@ -177,19 +177,19 @@ function get_e2e_filename {
version="${BUILD_VERSION}${COMMIT_HASH}"
fi
case $os in
- debian*|ubuntu*|fedora*)
- echo "app-e2e-tests-${version}-x86_64-unknown-linux-gnu"
- ;;
- windows*)
- echo "app-e2e-tests-${version}-x86_64-pc-windows-msvc.exe"
- ;;
- macos*)
- echo "app-e2e-tests-${version}-aarch64-apple-darwin"
- ;;
- *)
- echo "Unsupported target: $os" 1>&2
- return 1
- ;;
+ debian* | ubuntu* | fedora*)
+ echo "app-e2e-tests-${version}-x86_64-unknown-linux-gnu"
+ ;;
+ windows*)
+ echo "app-e2e-tests-${version}-x86_64-pc-windows-msvc.exe"
+ ;;
+ macos*)
+ echo "app-e2e-tests-${version}-aarch64-apple-darwin"
+ ;;
+ *)
+ echo "Unsupported target: $os" 1>&2
+ return 1
+ ;;
esac
}
@@ -282,38 +282,38 @@ function run_tests_for_os {
test_dir=$(get_test_utls_dir)/..
read -ra test_filters_arg <<<"${TEST_FILTERS:-}" # Split the string by words into an array
pushd "$test_dir"
- if [ -n "${TEST_DIST_DIR+x}" ]; then
- if [ ! -x "${TEST_DIST_DIR%/}/test-manager" ]; then
- executable_not_found_in_dist_error test-manager
- fi
- test_manager="${TEST_DIST_DIR%/}/test-manager"
- runner_dir_flag=("--runner-dir" "$TEST_DIST_DIR")
- else
- test_manager="cargo run --bin test-manager"
- runner_dir_flag=()
+ if [ -n "${TEST_DIST_DIR+x}" ]; then
+ if [ ! -x "${TEST_DIST_DIR%/}/test-manager" ]; then
+ executable_not_found_in_dist_error test-manager
fi
+ test_manager="${TEST_DIST_DIR%/}/test-manager"
+ runner_dir_flag=("--runner-dir" "$TEST_DIST_DIR")
+ else
+ test_manager="cargo run --bin test-manager"
+ runner_dir_flag=()
+ fi
- if [ -n "${MULLVAD_HOST+x}" ]; then
- mullvad_host_arg=("--mullvad-host" "$MULLVAD_HOST")
- else
- mullvad_host_arg=()
- fi
+ if [ -n "${MULLVAD_HOST+x}" ]; then
+ mullvad_host_arg=("--mullvad-host" "$MULLVAD_HOST")
+ else
+ mullvad_host_arg=()
+ fi
- if ! RUST_LOG_STYLE=always $test_manager run-tests \
- --account "${ACCOUNT_TOKEN:?Error: ACCOUNT_TOKEN not set}" \
- --app-package "${APP_PACKAGE:?Error: APP_PACKAGE not set}" \
- "${upgrade_package_arg[@]}" \
- "${test_report_arg[@]}" \
- --package-dir "${package_dir}" \
- --vm "$vm" \
- --openvpn-certificate "${OPENVPN_CERTIFICATE:-"assets/openvpn.ca.crt"}" \
- "${mullvad_host_arg[@]}" \
- "${test_filters_arg[@]}" \
- "${runner_dir_flag[@]}" \
- 2>&1 | sed -r "s/${ACCOUNT_TOKEN}/\{ACCOUNT_TOKEN\}/g"; then
- echo "Test run failed"
- exit 1
- fi
+ if ! RUST_LOG_STYLE=always $test_manager run-tests \
+ --account "${ACCOUNT_TOKEN:?Error: ACCOUNT_TOKEN not set}" \
+ --app-package "${APP_PACKAGE:?Error: APP_PACKAGE not set}" \
+ "${upgrade_package_arg[@]}" \
+ "${test_report_arg[@]}" \
+ --package-dir "${package_dir}" \
+ --vm "$vm" \
+ --openvpn-certificate "${OPENVPN_CERTIFICATE:-"assets/openvpn.ca.crt"}" \
+ "${mullvad_host_arg[@]}" \
+ "${test_filters_arg[@]}" \
+ "${runner_dir_flag[@]}" \
+ 2>&1 | sed -r "s/${ACCOUNT_TOKEN}/\{ACCOUNT_TOKEN\}/g"; then
+ echo "Test run failed"
+ exit 1
+ fi
popd
}
@@ -335,10 +335,10 @@ function build_current_version {
if [ ! -f "$app_package" ]; then
pushd "$app_dir"
- if [[ $(git diff --quiet) ]]; then
- echo "WARNING: the app repository contains uncommitted changes, this script will only rebuild the app package when the git hash changes"
- fi
- ./build.sh
+ if [[ $(git diff --quiet) ]]; then
+ echo "WARNING: the app repository contains uncommitted changes, this script will only rebuild the app package when the git hash changes"
+ fi
+ ./build.sh
popd
echo "Moving '$(realpath "$app_dir/dist/$app_filename")' to '$(realpath "$app_package")'"
mv -n "$app_dir"/dist/"$app_filename" "$app_package"
@@ -348,7 +348,7 @@ function build_current_version {
if [ ! -f "$gui_test_bin" ]; then
pushd "$app_dir"/gui
- npm run build-test-executable
+ npm run build-test-executable
popd
echo "Moving '$(realpath "$app_dir/dist/$gui_test_filename")' to '$(realpath "$gui_test_bin")'"
mv -n "$app_dir"/dist/"$gui_test_filename" "$gui_test_bin"
diff --git a/test/test-by-version.sh b/test/test-by-version.sh
index e9de281bad..f409a214cc 100755
--- a/test/test-by-version.sh
+++ b/test/test-by-version.sh
@@ -7,7 +7,7 @@ usage() {
echo
echo "Required environment variables:"
echo " - ACCOUNT_TOKEN: Valid MullvadVPN account number"
- echo " - TEST_OS: Name of the VM configuration to use. List available configurations with 'cargo run --bin test-manager list'"
+ echo " - TEST_OS: Name of the VM configuration to use. List available configurations with 'cargo run --bin test-manager config vm list'"
echo "Optional environment variables:"
echo " - APP_VERSION: The version of the app to test (defaults to the latest stable release)"
echo " - APP_PACKAGE_TO_UPGRADE_FROM: The package version to upgrade from (defaults to none)"
@@ -18,13 +18,13 @@ usage() {
echo " - TEST_REPORT : path to save the test results in a structured format"
}
-SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR"
# shellcheck source=test/scripts/test-utils.sh
source "scripts/test-utils.sh"
-if [[ ( "$*" == "--help") || "$*" == "-h" ]]; then
+if [[ ("$*" == "--help") || "$*" == "-h" ]]; then
usage
exit 0
fi
diff --git a/test/test-manager/Cargo.toml b/test/test-manager/Cargo.toml
index 2671ea454a..3310ab770f 100644
--- a/test/test-manager/Cargo.toml
+++ b/test/test-manager/Cargo.toml
@@ -32,6 +32,7 @@ async-trait = { workspace = true }
uuid = "1.3"
dirs = "5.0.1"
scopeguard = "1.2"
+glob = "0.3"
serde = { workspace = true }
serde_json = { workspace = true }
diff --git a/test/test-manager/docs/config.md b/test/test-manager/docs/config.md
index 8c4db7fce4..09d4f110ce 100644
--- a/test/test-manager/docs/config.md
+++ b/test/test-manager/docs/config.md
@@ -2,39 +2,68 @@
This document outlines the format of the configuration used by `test-manager` to perform end-to-end tests in virtualized environments.
-# Format
+## Format
+
+The configuration is a JSON document with three values:
-The configuration is a JSON document with two values:
```json
{
"mullvad_host": <optional string>,
- "vms": <document>
+ "vms": <document>,
+ "test_locations": [ {"test_name": ["relay"] }, .. ],
}
```
The configurable values are prone to change, and for the time being it is probably a good idea to get acquainted with the [Rust struct called "Config"](../src/config.rs) from which the configuration is serialized.
-To get started, `test-manager` provides the `test-manager set` command to add and edit VM configurations.
+To get started, `test-manager` provides the `test-manager config vm set` command to add and edit VM configurations.
It is also recommended to view the [example section](#Examples) further down this document.
-# Location
+## Location
The configuration is assumed to exist in `$XDG_CONFIG_HOME/mullvad-test/config.json` (most likely `$HOME/.config/mullvad-test/config.json`) on Linux and `$HOME/Library/Application Support/mullvad-test/config.json` on macOS.
-# Examples
+## Per-test relay selection
+
+It is possible to configure which relay(s) should be selected on a test-per-test basis by providing the `test_locations`
+configuration option. If no explicit configuration is given, no assumption will be made from within the tests themselves.
+
+The format is a list of maps with a single key-value pair, where the key is a [glob pattern](<https://en.wikipedia.org/wiki/Glob_(programming)>)
+that will be matched against the test name, and the value is a list of locations to use for the matching tests.
+The name of the locations are the same as for the `mullvad relay set location` CLI-command.
+
+### Example
+
+```json
+{
+ // other fields
+ "test_locations": [
+ { "*daita*": ["se-got-wg-001", "se-got-wg-002"] },
+ { "*": ["se"] }
+ ]
+}
+```
+
+The above example will set the locations for the test `test_daita` to a custom list
+containing `se-got-wg-001` and `se-got-wg-002`. The `*` is a wildcard that will match
+any test name. The configuration is read from top-to-bottom, and the first match will be used.
+
+## Example configurations
-## Minimal
+### Minimal
The minimal valid configuration does not contain any virtual machines
+
```json
{
- "mullvad_host": "stagemole.eu",
- "vms": { }
+ "mullvad_host": "stagemole.eu",
+ "vms": {}
}
```
-## Complete
+### Complete
A configuration containing one Debian 12 VM and one Windows 11 VM
+
```json
{
"mullvad_host": "stagemole.eu",
@@ -68,5 +97,6 @@ A configuration containing one Debian 12 VM and one Windows 11 VM
"tpm": false
}
}
+ }
}
```
diff --git a/test/test-manager/src/config/error.rs b/test/test-manager/src/config/error.rs
new file mode 100644
index 0000000000..17ad599da9
--- /dev/null
+++ b/test/test-manager/src/config/error.rs
@@ -0,0 +1,15 @@
+use std::io;
+
+#[derive(thiserror::Error, Debug)]
+pub enum Error {
+ #[error("Could not find config dir")]
+ FindConfigDir,
+ #[error("Could not create config dir")]
+ CreateConfigDir(#[source] io::Error),
+ #[error("Failed to read config")]
+ Read(#[source] io::Error),
+ #[error("Failed to parse config")]
+ InvalidConfig(#[from] serde_json::Error),
+ #[error("Failed to write config")]
+ Write(#[source] io::Error),
+}
diff --git a/test/test-manager/src/config/io.rs b/test/test-manager/src/config/io.rs
new file mode 100644
index 0000000000..1aaefde71b
--- /dev/null
+++ b/test/test-manager/src/config/io.rs
@@ -0,0 +1,80 @@
+//! See [ConfigFile].
+
+use std::io;
+use std::ops::Deref;
+use std::path::{Path, PathBuf};
+
+use super::{Config, Error};
+
+/// On-disk representation of [Config].
+pub struct ConfigFile {
+ path: PathBuf,
+ config: Config,
+}
+
+impl ConfigFile {
+ /// Make config changes and save them to disk
+ pub async fn edit(&mut self, edit: impl FnOnce(&mut Config)) -> Result<(), Error> {
+ Self::ensure_config_dir().await?;
+ edit(&mut self.config);
+ self.config_save().await
+ }
+
+ /// Make config changes and save them to disk
+ pub async fn load_or_default() -> Result<Self, Error> {
+ let path = Self::get_config_path()?;
+ let config = Self::config_load_or_default(&path).await?;
+ let config_file = Self { path, config };
+ Ok(config_file)
+ }
+
+ async fn config_load_or_default<P: AsRef<Path>>(path: P) -> Result<Config, Error> {
+ Self::config_load(path).await.or_else(|error| match error {
+ Error::Read(ref io_err) if io_err.kind() == io::ErrorKind::NotFound => {
+ log::trace!("Failed to read config file");
+ Ok(Config::default())
+ }
+ error => Err(error),
+ })
+ }
+
+ async fn config_load<P: AsRef<Path>>(path: P) -> Result<Config, Error> {
+ let data = tokio::fs::read(path).await.map_err(Error::Read)?;
+ serde_json::from_slice(&data).map_err(Error::InvalidConfig)
+ }
+
+ async fn config_save(&self) -> Result<(), Error> {
+ let data = serde_json::to_vec_pretty(&self.config).unwrap();
+ tokio::fs::write(&self.path, &data)
+ .await
+ .map_err(Error::Write)
+ }
+
+ /// Get configuration file path
+ pub fn get_config_path() -> Result<PathBuf, Error> {
+ Ok(Self::get_config_dir()?.join("config.json"))
+ }
+
+ /// Get configuration file directory
+ fn get_config_dir() -> Result<PathBuf, Error> {
+ let dir = dirs::config_dir()
+ .ok_or(Error::FindConfigDir)?
+ .join("mullvad-test");
+ Ok(dir)
+ }
+
+ /// Create configuration file directory if it does not exist
+ async fn ensure_config_dir() -> Result<(), Error> {
+ tokio::fs::create_dir_all(Self::get_config_dir()?)
+ .await
+ .map_err(Error::CreateConfigDir)
+ }
+}
+
+impl Deref for ConfigFile {
+ type Target = Config;
+
+ fn deref(&self) -> &Self::Target {
+ &self.config
+ }
+}
diff --git a/test/test-manager/src/config/manifest.rs b/test/test-manager/src/config/manifest.rs
new file mode 100644
index 0000000000..fdf24a8d5a
--- /dev/null
+++ b/test/test-manager/src/config/manifest.rs
@@ -0,0 +1,110 @@
+//! Config definition, see [`Config`].
+
+mod test_locations;
+use std::collections::BTreeMap;
+
+use serde::{Deserialize, Serialize};
+use test_locations::TestLocationList;
+
+use super::VmConfig;
+use crate::tests::config::DEFAULT_MULLVAD_HOST;
+
+/// Global configuration for the `test-manager`.
+///
+/// Can be modified using either the setting file, see
+/// [`crate::config::io::ConfigFile::get_config_path`] or
+/// the `test-manager config` CLI subcommand.
+#[derive(Debug, Default, Serialize, Deserialize, Clone)]
+pub struct Config {
+ #[serde(skip)]
+ pub runtime_opts: RuntimeOptions,
+ pub vms: BTreeMap<String, VmConfig>,
+ pub mullvad_host: Option<String>,
+ #[serde(default)]
+ pub test_locations: TestLocationList,
+}
+
+#[derive(Debug, Default, Serialize, Deserialize, Clone)]
+pub struct RuntimeOptions {
+ pub display: Display,
+ pub keep_changes: bool,
+}
+
+#[derive(Debug, Default, Serialize, Deserialize, Clone)]
+pub enum Display {
+ #[default]
+ None,
+ Local,
+ Vnc,
+}
+
+impl Config {
+ pub fn get_vm(&self, name: &str) -> Option<&VmConfig> {
+ self.vms.get(name)
+ }
+
+ /// Get the Mullvad host to use.
+ ///
+ /// Defaults to [`DEFAULT_MULLVAD_HOST`] if the host was not provided in the [`ConfigFile`].
+ pub fn get_host(&self) -> String {
+ self.mullvad_host.clone().unwrap_or_else(|| {
+ log::debug!("No Mullvad host has been set explicitly. Falling back to default host");
+ DEFAULT_MULLVAD_HOST.to_owned()
+ })
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn parse_test_location_empty() {
+ let config = r#"
+ {
+ "vms": {},
+ "mullvad_host": "mullvad.net"
+ }"#;
+
+ let config: Config = serde_json::from_str(config).unwrap();
+ assert!(config.test_locations.0.is_empty());
+ }
+
+ #[test]
+ fn parse_test_location_not_empty() {
+ let config = r#"
+ {
+ "vms": {},
+ "mullvad_host": "mullvad.net",
+ "test_locations": [
+ { "*daita": [ "se-got-wg-001", "se-got-wg-002" ] },
+ { "*": [ "se" ] }
+ ]
+ }"#;
+
+ let config: Config = serde_json::from_str(config).unwrap();
+ assert!(config
+ .test_locations
+ .lookup("test_daita")
+ .unwrap()
+ .contains(&"se-got-wg-002".to_string()));
+ assert!(!config.test_locations.0.is_empty());
+ }
+
+ #[test]
+ fn parse_multiple_keys_in_map_should_fail() {
+ let config = r#"
+ {
+ "vms": {},
+ "mullvad_host": "mullvad.net",
+ "test_locations": [
+ {
+ "*daita": [ "se-got-wg-001", "se-got-wg-002" ],
+ "*test": ["se"]
+ },
+ ]
+ }"#;
+
+ let _err = serde_json::from_str::<Config>(config).unwrap_err();
+ }
+}
diff --git a/test/test-manager/src/config/manifest/test_locations.rs b/test/test-manager/src/config/manifest/test_locations.rs
new file mode 100644
index 0000000000..febf9ed460
--- /dev/null
+++ b/test/test-manager/src/config/manifest/test_locations.rs
@@ -0,0 +1,105 @@
+use serde::{
+ de::{Deserialize, Deserializer, Error, MapAccess, Visitor},
+ ser::{Serialize, SerializeMap},
+ Deserialize as DeserDerive, Serialize as SerDerive,
+};
+use std::fmt;
+
+#[derive(Clone, Default)]
+pub struct TestLocation(glob::Pattern, Vec<String>);
+
+impl fmt::Debug for TestLocation {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ write!(f, "{}: {:?}", self.0, &self.1)
+ }
+}
+
+/// Relay/location overrides for tests.
+///
+/// # Deserializing with `serde-json`
+///
+/// The format is a list of maps with a single key-value
+/// pair, where the key is a glob pattern that will be matched against the test name, and the
+/// value is a list of locations to use for that test. The first match will be used.
+///
+/// Example:
+/// ```json
+/// {
+/// // other fields
+/// "test_locations": [
+/// { "*daita*": [ "se-got-wg-001", "se-got-wg-002" ] },
+/// { "*": [ "se" ] }
+/// ]
+/// }
+/// ```
+///
+/// The above example will set the locations for the test `test_daita` to a custom list
+/// containing `se-got-wg-001` and `se-got-wg-002`. The `*` is a wildcard that will match
+/// any test name. The order of the list is important, as the first match will be used.
+#[derive(Debug, DeserDerive, SerDerive, Clone, Default)]
+pub struct TestLocationList(pub Vec<TestLocation>);
+
+impl TestLocationList {
+ pub fn lookup(&self, test: &str) -> Option<&Vec<String>> {
+ self.0
+ .iter()
+ .find(|TestLocation(test_glob, _)| test_glob.matches(test))
+ .map(|TestLocation(_, locations)| locations)
+ }
+}
+
+struct TestLocationVisitor;
+
+impl<'de> Visitor<'de> for TestLocationVisitor {
+ // The type that our Visitor is going to produce.
+ type Value = TestLocation;
+
+ // Format a message stating what data this Visitor expects to receive.
+ fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
+ formatter.write_str("A list of maps")
+ }
+
+ fn visit_map<M>(self, mut access: M) -> Result<Self::Value, M::Error>
+ where
+ M: MapAccess<'de>,
+ {
+ let (key, value) = access
+ .next_entry::<String, Vec<String>>()?
+ .ok_or(M::Error::custom(
+ "Test location map should contain exactly one key-value pair, but it was empty",
+ ))?;
+ let glob = glob::Pattern::new(&key).map_err(|err| {
+ M::Error::custom(format!(
+ "Cannot compile glob pattern from: {key} error: {err:?}"
+ ))
+ })?;
+
+ if let Some((key, value)) = access.next_entry::<String, Vec<String>>()? {
+ return Err(M::Error::custom(format!(
+ "Test location map should contain exactly one key-value pair, but found another key: '{key}' and value: '{value:?}'"
+ )));
+ }
+
+ Ok(TestLocation(glob, value))
+ }
+}
+
+impl<'de> Deserialize<'de> for TestLocation {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ deserializer.deserialize_map(TestLocationVisitor)
+ }
+}
+
+impl Serialize for TestLocation {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: serde::Serializer,
+ {
+ let mut map = serializer.serialize_map(Some(1))?;
+ map.serialize_entry(self.0.as_str(), &self.1)?;
+ map.end()
+ }
+}
diff --git a/test/test-manager/src/config/mod.rs b/test/test-manager/src/config/mod.rs
new file mode 100644
index 0000000000..4b510b87cc
--- /dev/null
+++ b/test/test-manager/src/config/mod.rs
@@ -0,0 +1,11 @@
+//! Test manager configuration.
+
+mod error;
+mod io;
+mod manifest;
+mod vm;
+
+use error::Error;
+pub use io::ConfigFile;
+pub use manifest::{Config, Display};
+pub use vm::{Architecture, OsType, PackageType, Provisioner, VmConfig, VmType};
diff --git a/test/test-manager/src/config.rs b/test/test-manager/src/config/vm.rs
index 95a13c48a6..911d2fcf64 100644
--- a/test/test-manager/src/config.rs
+++ b/test/test-manager/src/config/vm.rs
@@ -1,141 +1,9 @@
-//! Test manager configuration.
+//! Virtual machine configuration.
-use serde::{Deserialize, Serialize};
-use std::{
- collections::BTreeMap,
- env, io,
- ops::Deref,
- path::{Path, PathBuf},
-};
-
-use crate::tests::config::DEFAULT_MULLVAD_HOST;
-
-#[derive(thiserror::Error, Debug)]
-pub enum Error {
- #[error("Could not find config dir")]
- FindConfigDir,
- #[error("Could not create config dir")]
- CreateConfigDir(#[source] io::Error),
- #[error("Failed to read config")]
- Read(#[source] io::Error),
- #[error("Failed to parse config")]
- InvalidConfig(#[from] serde_json::Error),
- #[error("Failed to write config")]
- Write(#[source] io::Error),
-}
-
-#[derive(Default, Serialize, Deserialize, Clone)]
-pub struct Config {
- #[serde(skip)]
- pub runtime_opts: RuntimeOptions,
- pub vms: BTreeMap<String, VmConfig>,
- pub mullvad_host: Option<String>,
-}
-
-#[derive(Default, Serialize, Deserialize, Clone)]
-pub struct RuntimeOptions {
- pub display: Display,
- pub keep_changes: bool,
-}
-
-#[derive(Default, Serialize, Deserialize, Clone)]
-pub enum Display {
- #[default]
- None,
- Local,
- Vnc,
-}
-
-impl Config {
- async fn load_or_default<P: AsRef<Path>>(path: P) -> Result<Self, Error> {
- Self::load(path).await.or_else(|error| match error {
- Error::Read(ref io_err) if io_err.kind() == io::ErrorKind::NotFound => {
- Ok(Self::default())
- }
- error => Err(error),
- })
- }
-
- async fn load<P: AsRef<Path>>(path: P) -> Result<Self, Error> {
- let data = tokio::fs::read(path).await.map_err(Error::Read)?;
- serde_json::from_slice(&data).map_err(Error::InvalidConfig)
- }
-
- async fn save<P: AsRef<Path>>(&self, path: P) -> Result<(), Error> {
- let data = serde_json::to_vec_pretty(self).unwrap();
- tokio::fs::write(path, &data).await.map_err(Error::Write)
- }
-
- pub fn get_vm(&self, name: &str) -> Option<&VmConfig> {
- self.vms.get(name)
- }
-
- /// Get the Mullvad host to use.
- ///
- /// Defaults to [`DEFAULT_MULLVAD_HOST`] if the host was not provided in the [`ConfigFile`].
- pub fn get_host(&self) -> String {
- self.mullvad_host.clone().unwrap_or_else(|| {
- log::debug!("No Mullvad host has been set explicitly. Falling back to default host");
- DEFAULT_MULLVAD_HOST.to_owned()
- })
- }
-}
-
-pub struct ConfigFile {
- path: PathBuf,
- config: Config,
-}
-
-impl ConfigFile {
- /// Make config changes and save them to disk
- pub async fn load_or_default() -> Result<Self, Error> {
- Self::load_or_default_inner(Self::get_config_path()?).await
- }
-
- /// Get configuration file path
- fn get_config_path() -> Result<PathBuf, Error> {
- Ok(Self::get_config_dir()?.join("config.json"))
- }
-
- /// Get configuration file directory
- fn get_config_dir() -> Result<PathBuf, Error> {
- let dir = dirs::config_dir()
- .ok_or(Error::FindConfigDir)?
- .join("mullvad-test");
- Ok(dir)
- }
-
- /// Create configuration file directory if it does not exist
- async fn ensure_config_dir() -> Result<(), Error> {
- tokio::fs::create_dir_all(Self::get_config_dir()?)
- .await
- .map_err(Error::CreateConfigDir)
- }
+use std::env;
+use std::path::{Path, PathBuf};
- /// Make config changes and save them to disk
- async fn load_or_default_inner<P: AsRef<Path>>(path: P) -> Result<Self, Error> {
- Ok(Self {
- path: path.as_ref().to_path_buf(),
- config: Config::load_or_default(path).await?,
- })
- }
-
- /// Make config changes and save them to disk
- pub async fn edit(&mut self, edit: impl FnOnce(&mut Config)) -> Result<(), Error> {
- Self::ensure_config_dir().await?;
-
- edit(&mut self.config);
- self.config.save(&self.path).await
- }
-}
-
-impl Deref for ConfigFile {
- type Target = Config;
-
- fn deref(&self) -> &Self::Target {
- &self.config
- }
-}
+use serde::{Deserialize, Serialize};
#[derive(clap::Args, Debug, Serialize, Deserialize, Clone)]
#[serde(rename_all = "snake_case")]
diff --git a/test/test-manager/src/main.rs b/test/test-manager/src/main.rs
index 95274e9a7a..694af65dc3 100644
--- a/test/test-manager/src/main.rs
+++ b/test/test-manager/src/main.rs
@@ -13,8 +13,9 @@ mod vm;
use std::net::IpAddr;
use std::{net::SocketAddr, path::PathBuf};
-use anyhow::{Context, Result};
+use anyhow::{Context, Ok, Result};
use clap::{builder::PossibleValuesParser, Parser};
+use config::ConfigFile;
use package::TargetInfo;
use tests::{config::TEST_CONFIG, get_filtered_tests};
use vm::provision;
@@ -31,28 +32,13 @@ struct Args {
#[derive(clap::Subcommand, Debug)]
enum Commands {
- /// Create or edit a VM config
- Set {
- /// Name of the VM config
- vm: String,
-
- /// VM config
- #[clap(flatten)]
- config: config::VmConfig,
- },
-
- /// Remove specified VM config
- Remove {
- /// Name of the VM config, run `test-manager list` to see available configs
- vm: String,
- },
-
- /// List available VM configurations
- List,
+ /// Manage configuration for tests and VMs
+ #[clap(subcommand)]
+ Config(ConfigArg),
/// Spawn a runner instance without running any tests
RunVm {
- /// Name of the VM config, run `test-manager list` to see available configs
+ /// Name of the VM config, run `test-manager config vm list` to see configured VMs
vm: String,
/// Run VNC server on a specified port
@@ -69,7 +55,7 @@ enum Commands {
/// Spawn a runner instance and run tests
RunTests {
- /// Name of the VM config, run `test-manager list` to see available configs
+ /// Name of the VM config, run `test-manager config vm list` to see configured VMs
#[arg(long)]
vm: String,
@@ -161,6 +147,39 @@ enum Commands {
},
}
+#[derive(clap::Subcommand, Debug)]
+enum ConfigArg {
+ /// Print the current config
+ Get,
+ /// Print the path to the current config file
+ Which,
+ /// Manage VM-specific setting
+ #[clap(subcommand)]
+ Vm(VmConfig),
+}
+
+#[derive(clap::Subcommand, Debug)]
+enum VmConfig {
+ /// Create or edit a VM config
+ Set {
+ /// Name of the VM config
+ vm: String,
+
+ /// VM config
+ #[clap(flatten)]
+ config: config::VmConfig,
+ },
+
+ /// Remove specified VM config
+ Remove {
+ /// Name of the VM config, run `test-manager config vm list` to see configured VMs
+ vm: String,
+ },
+
+ /// List available VM configurations
+ List,
+}
+
#[cfg(target_os = "linux")]
impl Args {
fn get_vnc_port(&self) -> Option<u16> {
@@ -184,33 +203,50 @@ async fn main() -> Result<()> {
.await
.context("Failed to load config")?;
match args.cmd {
- Commands::Set {
- vm,
- config: vm_config,
- } => vm::set_config(&mut config, &vm, vm_config)
- .await
- .context("Failed to edit or create VM config"),
- Commands::Remove { vm } => {
- if config.get_vm(&vm).is_none() {
- println!("No such configuration");
- return Ok(());
+ Commands::Config(config_subcommand) => match config_subcommand {
+ ConfigArg::Get => {
+ println!("{:#?}", *config);
+ Ok(())
}
- config
- .edit(|config| {
- config.vms.remove_entry(&vm);
- })
- .await
- .context("Failed to remove config entry")?;
- println!("Removed configuration \"{vm}\"");
- Ok(())
- }
- Commands::List => {
- println!("Available configurations:");
- for (vm, config) in config.vms.iter() {
- println!("{vm}: {config:#?}");
+ ConfigArg::Which => {
+ println!(
+ "{}",
+ ConfigFile::get_config_path()
+ .expect("Get config path")
+ .display()
+ );
+ Ok(())
}
- Ok(())
- }
+ ConfigArg::Vm(vm_config) => match vm_config {
+ VmConfig::Set {
+ vm,
+ config: vm_config,
+ } => vm::set_config(&mut config, &vm, vm_config)
+ .await
+ .context("Failed to edit or create VM config"),
+ VmConfig::Remove { vm } => {
+ if config.get_vm(&vm).is_none() {
+ println!("No such configuration");
+ return Ok(());
+ }
+ config
+ .edit(|config| {
+ config.vms.remove_entry(&vm);
+ })
+ .await
+ .context("Failed to remove config entry")?;
+ println!("Removed configuration \"{vm}\"");
+ Ok(())
+ }
+ VmConfig::List => {
+ println!("Configured VMs:");
+ for vm in config.vms.keys() {
+ println!("{vm}");
+ }
+ Ok(())
+ }
+ },
+ },
Commands::RunVm {
vm,
vnc,
@@ -266,7 +302,12 @@ async fn main() -> Result<()> {
};
if let Some(mullvad_host) = mullvad_host {
- log::trace!("Setting Mullvad host using --mullvad-host flag");
+ match config.mullvad_host {
+ Some(old_host) => {
+ log::info!("Overriding Mullvad host from {old_host} to {mullvad_host}",)
+ }
+ None => log::info!("Setting Mullvad host to {mullvad_host}",),
+ };
config.mullvad_host = Some(mullvad_host);
}
let mullvad_host = config.get_host();
@@ -327,7 +368,11 @@ async fn main() -> Result<()> {
test_rpc::meta::Os::from(vm_config.os_type),
openvpn_certificate,
));
- let tests = get_filtered_tests(&test_filters)?;
+
+ let mut tests = get_filtered_tests(&test_filters)?;
+ for test in tests.iter_mut() {
+ test.location = config.test_locations.lookup(test.name).cloned();
+ }
// For convenience, spawn a SOCKS5 server that is reachable for tests that need it
let socks = socks_server::spawn(SocketAddr::new(
diff --git a/test/test-manager/src/mullvad_daemon.rs b/test/test-manager/src/mullvad_daemon.rs
index b039813470..8da149d329 100644
--- a/test/test-manager/src/mullvad_daemon.rs
+++ b/test/test-manager/src/mullvad_daemon.rs
@@ -4,10 +4,7 @@ use std::{io, time::Duration};
use futures::{channel::mpsc, future::BoxFuture, pin_mut, FutureExt, SinkExt, StreamExt};
use hyper_util::rt::TokioIo;
use mullvad_management_interface::{ManagementServiceClient, MullvadProxyClient};
-use test_rpc::{
- mullvad_daemon::MullvadClientVersion,
- transport::{ConnectionHandle, GrpcForwarder},
-};
+use test_rpc::transport::{ConnectionHandle, GrpcForwarder};
use tokio::io::{AsyncReadExt, AsyncWriteExt, DuplexStream};
use tokio_util::codec::{Decoder, LengthDelimitedCodec};
use tower::Service;
@@ -52,20 +49,7 @@ pub struct RpcClientProvider {
service: DummyService,
}
-pub enum MullvadClientArgument {
- WithClient(MullvadProxyClient),
- None,
-}
-
impl RpcClientProvider {
- /// Whether a [test case](test_macro::test_function) needs a [`MullvadProxyClient`].
- pub async fn mullvad_client(&self, client_type: MullvadClientVersion) -> MullvadClientArgument {
- match client_type {
- MullvadClientVersion::New => MullvadClientArgument::WithClient(self.new_client().await),
- MullvadClientVersion::None => MullvadClientArgument::None,
- }
- }
-
pub async fn new_client(&self) -> MullvadProxyClient {
// FIXME: Ugly workaround to ensure that we don't receive stuff from a
// previous RPC session.
diff --git a/test/test-manager/src/run_tests.rs b/test/test-manager/src/run_tests.rs
index 88577fb3b3..54e563f704 100644
--- a/test/test-manager/src/run_tests.rs
+++ b/test/test-manager/src/run_tests.rs
@@ -1,12 +1,13 @@
use crate::{
logging::{Logger, Panic, TestOutput, TestResult},
- mullvad_daemon::{self, MullvadClientArgument, RpcClientProvider},
+ mullvad_daemon::{self, RpcClientProvider},
summary::SummaryLogger,
tests::{self, config::TEST_CONFIG, TestContext, TestMetadata},
vm,
};
use anyhow::{Context, Result};
use futures::FutureExt;
+use mullvad_management_interface::MullvadProxyClient;
use std::{future::Future, panic, time::Duration};
use test_rpc::{logging::Output, ServiceClient};
@@ -33,10 +34,10 @@ impl TestHandler<'_> {
&mut self,
test: &F,
test_name: &'static str,
- mullvad_client: MullvadClientArgument,
+ mullvad_client: Option<MullvadProxyClient>,
) -> Result<(), anyhow::Error>
where
- F: Fn(super::tests::TestContext, ServiceClient, MullvadClientArgument) -> R,
+ F: Fn(super::tests::TestContext, ServiceClient, Option<MullvadProxyClient>) -> R,
R: Future<Output = anyhow::Result<()>>,
{
log::info!("Running {test_name}");
@@ -146,26 +147,23 @@ pub async fn run(
// expected, and to allow for skipping tests on arbitrary conditions.
if TEST_CONFIG.app_package_to_upgrade_from_filename.is_some() {
test_handler
- .run_test(
- &tests::test_upgrade_app,
- "test_upgrade_app",
- MullvadClientArgument::None,
- )
+ .run_test(&tests::test_upgrade_app, "test_upgrade_app", None)
.await?;
} else {
log::warn!("No previous app to upgrade from, skipping upgrade test");
};
for test in tests {
- tests::prepare_daemon(&test_runner_client, &rpc_provider)
+ let mut mullvad_client = tests::prepare_daemon(&test_runner_client, &rpc_provider)
.await
.context("Failed to reset daemon before test")?;
- let mullvad_client = rpc_provider
- .mullvad_client(test.mullvad_client_version)
- .await;
+ tests::set_test_location(&mut mullvad_client, &test)
+ .await
+ .context("Failed to create custom list from test locations")?;
+
test_handler
- .run_test(&test.func, test.name, mullvad_client)
+ .run_test(&test.func, test.name, Some(mullvad_client))
.await?;
}
@@ -204,13 +202,13 @@ async fn register_test_result(
pub async fn run_test_function<F, R>(
runner_rpc: ServiceClient,
- mullvad_rpc: MullvadClientArgument,
+ mullvad_rpc: Option<MullvadProxyClient>,
test: &F,
test_name: &'static str,
test_context: super::tests::TestContext,
) -> TestOutput
where
- F: Fn(super::tests::TestContext, ServiceClient, MullvadClientArgument) -> R,
+ F: Fn(super::tests::TestContext, ServiceClient, Option<MullvadProxyClient>) -> R,
R: Future<Output = anyhow::Result<()>>,
{
let _flushed = runner_rpc.try_poll_output().await;
diff --git a/test/test-manager/src/tests/helpers.rs b/test/test-manager/src/tests/helpers.rs
index ff581c8f62..78a7cfacdf 100644
--- a/test/test-manager/src/tests/helpers.rs
+++ b/test/test-manager/src/tests/helpers.rs
@@ -17,6 +17,7 @@ use mullvad_relay_selector::{
};
use mullvad_types::{
constraints::Constraint,
+ custom_list::CustomList,
relay_constraints::{
GeographicLocationConstraint, LocationConstraint, RelayConstraints, RelaySettings,
},
@@ -1199,160 +1200,48 @@ fn parse_am_i_mullvad(result: String) -> anyhow::Result<bool> {
})
}
-pub mod custom_lists {
- use super::*;
-
- use mullvad_types::custom_list::{CustomList, Id};
- use std::sync::{LazyLock, Mutex};
-
- // Expose all custom list variants as a shorthand.
- pub use List::*;
-
- /// The default custom list to use as location for all tests.
- pub const DEFAULT_LIST: List = List::Nordic;
-
- /// Mapping between [List] to daemon custom lists. Since custom list ids are assigned by the
- /// daemon at the creation of the custom list settings object, we can't map a custom list
- /// name to a specific list before runtime.
- static IDS: LazyLock<Mutex<HashMap<List, Id>>> = LazyLock::new(|| Mutex::new(HashMap::new()));
-
- /// Pre-defined (well-typed) custom lists which may be useful in different test scenarios.
- #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
- pub enum List {
- /// A selection of Nordic servers
- Nordic,
- /// A selection of European servers
- Europe,
- /// This custom list contains relays which are close geographically to the computer running
- /// the test scenarios, which hopefully means there will be little latency between the test
- /// machine and these relays
- LowLatency,
- /// Antithesis of [List::LowLatency], these relays are located far away from the test
- /// server. Use this custom list if you want to simulate scenarios where the probability
- /// of experiencing high latencies is desirable.
- HighLatency,
- }
-
- impl List {
- pub fn name(self) -> String {
- use List::*;
- match self {
- Nordic => "Nordic".to_string(),
- Europe => "Europe".to_string(),
- LowLatency => "Low Latency".to_string(),
- HighLatency => "High Latency".to_string(),
- }
- }
-
- /// Iterator over all custom lists.
- pub fn all() -> impl Iterator<Item = List> {
- use List::*;
- [Nordic, Europe, LowLatency, HighLatency].into_iter()
- }
-
- pub fn locations(self) -> impl Iterator<Item = GeographicLocationConstraint> {
- use List::*;
- let country = GeographicLocationConstraint::country;
- let city = GeographicLocationConstraint::city;
- match self {
- Nordic => {
- vec![country("no"), country("se"), country("fi"), country("dk")].into_iter()
- }
- Europe => vec![
- // North
- country("se"),
- // West
- country("fr"),
- // East
- country("ro"),
- // South
- country("it"),
- ]
- .into_iter(),
- LowLatency => {
- // Assumption: Test server is located in Gothenburg, Sweden.
- vec![city("se", "got")].into_iter()
- }
- HighLatency => {
- // Assumption: Test server is located in Gothenburg, Sweden.
- vec![country("au"), country("ca"), country("za")].into_iter()
- }
- }
- }
-
- pub fn to_constraint(self) -> Option<LocationConstraint> {
- let ids = IDS.lock().unwrap();
- let id = ids.get(&self)?;
- Some(LocationConstraint::CustomList { list_id: *id })
- }
- }
-
- impl From<List> for LocationConstraint {
- fn from(custom_list: List) -> Self {
- // TODO: Is this _too_ unsound ??
- custom_list.to_constraint().unwrap()
- }
- }
-
- /// Add a set of custom lists which can be used in different test scenarios.
- ///
- /// See [`List`] for available custom lists.
- pub async fn add_default_lists(mullvad_client: &mut MullvadProxyClient) -> anyhow::Result<()> {
- for custom_list in List::all() {
- let id = mullvad_client
- .create_custom_list(custom_list.name())
- .await?;
- let mut daemon_dito = find_custom_list(mullvad_client, &custom_list.name()).await?;
- assert_eq!(id, daemon_dito.id);
- for locations in custom_list.locations() {
- daemon_dito.locations.insert(locations);
- }
- mullvad_client.update_custom_list(daemon_dito).await?;
- // Associate this custom list variant with a specific, runtime custom list id.
- IDS.lock().unwrap().insert(custom_list, id);
- }
- Ok(())
- }
-
- /// Set the default location to the custom list specified by `DEFAULT_LIST`. This also includes
- /// entry location for multihop. It does not, however, affect bridge location for OpenVPN.
- /// This is for simplify, as bridges default to using the server closest to the exit anyway, and
- /// OpenVPN is slated for removal.
- pub async fn set_default_location(
- mullvad_client: &mut MullvadProxyClient,
- ) -> anyhow::Result<()> {
- let constraints = get_custom_list_location_relay_constraints(DEFAULT_LIST);
+/// Set the location to the given [`LocationConstraint`]. This also includes
+/// entry location for multihop. It does not, however, affect bridge location for OpenVPN.
+/// This is for simplify, as bridges default to using the server closest to the exit anyway, and
+/// OpenVPN is slated for removal.
+///
+/// NOTE: Calling this from within a test will overwrite the default test lcoation specified in
+/// the settings.
+pub async fn set_location(
+ mullvad_client: &mut MullvadProxyClient,
+ location: impl Into<LocationConstraint>,
+) -> anyhow::Result<()> {
+ let constraints = get_location_relay_constraints(location.into());
- mullvad_client
- .set_relay_settings(constraints.into())
- .await
- .context("Failed to set relay settings")
- }
+ mullvad_client
+ .set_relay_settings(constraints.into())
+ .await
+ .context("Failed to set relay settings")
+}
- fn get_custom_list_location_relay_constraints(custom_list: List) -> RelayConstraints {
- let wireguard_constraints = mullvad_types::relay_constraints::WireguardConstraints {
- entry_location: Constraint::Only(custom_list.into()),
- ..Default::default()
- };
+fn get_location_relay_constraints(custom_list: LocationConstraint) -> RelayConstraints {
+ let wireguard_constraints = mullvad_types::relay_constraints::WireguardConstraints {
+ entry_location: Constraint::Only(custom_list.clone()),
+ ..Default::default()
+ };
- RelayConstraints {
- location: Constraint::Only(custom_list.into()),
- wireguard_constraints,
- ..Default::default()
- }
+ RelayConstraints {
+ location: Constraint::Only(custom_list),
+ wireguard_constraints,
+ ..Default::default()
}
+}
- /// Dig out a custom list from the daemon settings based on the custom list's name.
- /// There should be an rpc for this.
- async fn find_custom_list(
- rpc: &mut MullvadProxyClient,
- name: &str,
- ) -> anyhow::Result<CustomList> {
- rpc.get_settings()
- .await?
- .custom_lists
- .into_iter()
- .find(|list| list.name == name)
- .ok_or(anyhow!("List '{name}' not found"))
- }
+/// Dig out a custom list from the daemon settings based on the custom list's name.
+/// There should be an rpc for this.
+pub async fn find_custom_list(
+ rpc: &mut MullvadProxyClient,
+ name: &str,
+) -> anyhow::Result<CustomList> {
+ rpc.get_settings()
+ .await?
+ .custom_lists
+ .into_iter()
+ .find(|list| list.name == name)
+ .ok_or(anyhow!("List '{name}' not found"))
}
diff --git a/test/test-manager/src/tests/install.rs b/test/test-manager/src/tests/install.rs
index 936f07d65d..d640b26089 100644
--- a/test/test-manager/src/tests/install.rs
+++ b/test/test-manager/src/tests/install.rs
@@ -6,7 +6,7 @@ use mullvad_types::{constraints::Constraint, relay_constraints};
use test_macro::test_function;
use test_rpc::{mullvad_daemon::ServiceStatus, ServiceClient};
-use crate::{mullvad_daemon::MullvadClientArgument, tests::helpers};
+use crate::tests::helpers;
use super::{
config::TEST_CONFIG,
@@ -25,7 +25,7 @@ use super::{
pub async fn test_upgrade_app(
ctx: TestContext,
rpc: ServiceClient,
- _mullvad_client: MullvadClientArgument,
+ _mullvad_client: Option<MullvadProxyClient>,
) -> anyhow::Result<()> {
// Install the older version of the app and verify that it is running.
let old_version = TEST_CONFIG
diff --git a/test/test-manager/src/tests/mod.rs b/test/test-manager/src/tests/mod.rs
index 9eb23fe13e..e2d50d0889 100644
--- a/test/test-manager/src/tests/mod.rs
+++ b/test/test-manager/src/tests/mod.rs
@@ -16,18 +16,16 @@ mod tunnel_state;
mod ui;
use itertools::Itertools;
+use mullvad_types::relay_constraints::{GeographicLocationConstraint, LocationConstraint};
pub use test_metadata::TestMetadata;
use anyhow::Context;
use futures::future::BoxFuture;
use std::time::Duration;
-use crate::{
- mullvad_daemon::{MullvadClientArgument, RpcClientProvider},
- package::get_version_from_path,
-};
+use crate::{mullvad_daemon::RpcClientProvider, package::get_version_from_path};
use config::TEST_CONFIG;
-use helpers::{get_app_env, install_app};
+use helpers::{find_custom_list, get_app_env, install_app, set_location};
pub use install::test_upgrade_app;
use mullvad_management_interface::MullvadProxyClient;
use test_rpc::{meta::Os, ServiceClient};
@@ -39,8 +37,11 @@ pub struct TestContext {
pub rpc_provider: RpcClientProvider,
}
-pub type TestWrapperFunction =
- fn(TestContext, ServiceClient, MullvadClientArgument) -> BoxFuture<'static, anyhow::Result<()>>;
+pub type TestWrapperFunction = fn(
+ TestContext,
+ ServiceClient,
+ Option<MullvadProxyClient>,
+) -> BoxFuture<'static, anyhow::Result<()>>;
#[derive(thiserror::Error, Debug)]
pub enum Error {
@@ -137,7 +138,7 @@ pub fn get_filtered_tests(specified_tests: &[String]) -> Result<Vec<TestMetadata
pub async fn prepare_daemon(
rpc: &ServiceClient,
rpc_provider: &RpcClientProvider,
-) -> anyhow::Result<()> {
+) -> anyhow::Result<MullvadProxyClient> {
// Check if daemon should be restarted
let mut mullvad_client = ensure_daemon_version(rpc, rpc_provider)
.await
@@ -152,9 +153,60 @@ pub async fn prepare_daemon(
.await
.context("Failed to disconnect daemon after test")?;
helpers::ensure_logged_in(&mut mullvad_client).await?;
- helpers::custom_lists::add_default_lists(&mut mullvad_client).await?;
- helpers::custom_lists::set_default_location(&mut mullvad_client).await?;
+ Ok(mullvad_client)
+}
+
+/// Create and selects an "anonymous" custom list for this test. The custom list will
+/// have the same name as the test and contain the locations as specified by
+/// [`TestMetadata`] location field.
+pub async fn set_test_location(
+ mullvad_client: &mut MullvadProxyClient,
+ test: &TestMetadata,
+) -> anyhow::Result<()> {
+ // If no location is specified for the test, don't do anything and use the default value of the app
+ let Some(locations) = test.location.as_ref() else {
+ return Ok(());
+ };
+ // Convert locations from the test config to actual location constraints
+ let locations: Vec<GeographicLocationConstraint> = locations
+ .iter()
+ .map(|input| {
+ input
+ .parse::<GeographicLocationConstraint>()
+ .with_context(|| format!("Failed to parse {input}"))
+ })
+ .try_collect()?;
+
+ log::debug!(
+ "Creating custom list {} with locations '{:?}'",
+ test.name,
+ locations
+ );
+
+ // Add the custom list to the current app instance
+ // NOTE: This const is actually defined in, `mullvad_types::custom_list`, but we cannot import it.
+ const CUSTOM_LIST_NAME_MAX_SIZE: usize = 30;
+ let mut custom_list_name = test.name.to_string();
+ custom_list_name.truncate(CUSTOM_LIST_NAME_MAX_SIZE);
+ log::debug!("Creating custom list {custom_list_name} with locations '{locations:?}'");
+
+ let list_id = mullvad_client
+ .create_custom_list(custom_list_name.clone())
+ .await?;
+
+ let mut custom_list = find_custom_list(mullvad_client, &custom_list_name).await?;
+
+ assert_eq!(list_id, custom_list.id);
+ for location in locations {
+ custom_list.locations.insert(location);
+ }
+ mullvad_client.update_custom_list(custom_list).await?;
+ log::debug!("Added custom list");
+
+ set_location(mullvad_client, LocationConstraint::CustomList { list_id })
+ .await
+ .with_context(|| format!("Failed to set location to custom list with ID '{list_id:?}'"))?;
Ok(())
}
diff --git a/test/test-manager/src/tests/split_tunnel.rs b/test/test-manager/src/tests/split_tunnel.rs
index 435552a460..98cbed7951 100644
--- a/test/test-manager/src/tests/split_tunnel.rs
+++ b/test/test-manager/src/tests/split_tunnel.rs
@@ -86,7 +86,11 @@ pub async fn test_split_tunnel(
/// - A split process should never push traffic through the tunnel.
/// - Splitting/unsplitting should work regardless if process is running.
#[test_function(target_os = "macos")]
-pub async fn test_split_tunnel_ui(_ctx: TestContext, rpc: ServiceClient) -> anyhow::Result<()> {
+pub async fn test_split_tunnel_ui(
+ _ctx: TestContext,
+ rpc: ServiceClient,
+ _: MullvadProxyClient,
+) -> anyhow::Result<()> {
// Skip test on macOS 12, since the feature is unsupported
if is_macos_12_or_lower(&rpc).await? {
return Ok(());
diff --git a/test/test-manager/src/tests/test_metadata.rs b/test/test-manager/src/tests/test_metadata.rs
index 79c7f74def..31a55498f9 100644
--- a/test/test-manager/src/tests/test_metadata.rs
+++ b/test/test-manager/src/tests/test_metadata.rs
@@ -1,13 +1,15 @@
use super::TestWrapperFunction;
-use test_rpc::{meta::Os, mullvad_daemon::MullvadClientVersion};
+use test_rpc::meta::Os;
-#[derive(Clone)]
+#[derive(Clone, Debug)]
pub struct TestMetadata {
pub name: &'static str,
pub targets: &'static [Os],
- pub mullvad_client_version: MullvadClientVersion,
pub func: TestWrapperFunction,
+ /// Priority order of the tests, unless specific tests are given as the `TEST_FILTERS` argument
pub priority: Option<i32>,
+ /// A list of location that will be used for by the test
+ pub location: Option<Vec<String>>,
}
// Register our test metadata struct with inventory to allow submitting tests of this type.
diff --git a/test/test-manager/src/tests/tunnel.rs b/test/test-manager/src/tests/tunnel.rs
index bc15ccb8a3..b64a16d854 100644
--- a/test/test-manager/src/tests/tunnel.rs
+++ b/test/test-manager/src/tests/tunnel.rs
@@ -187,15 +187,7 @@ pub async fn test_wireguard_over_shadowsocks(
rpc: ServiceClient,
mut mullvad_client: MullvadProxyClient,
) -> anyhow::Result<()> {
- // NOTE: We have experienced flakiness due to timeout issues if distant relays are selected.
- // This is an attempt to try to reduce this type of flakiness.
- use helpers::custom_lists::LowLatency;
-
- let query = RelayQueryBuilder::new()
- .wireguard()
- .shadowsocks()
- .location(LowLatency)
- .build();
+ let query = RelayQueryBuilder::new().wireguard().shadowsocks().build();
apply_settings_from_relay_query(&mut mullvad_client, query).await?;
@@ -294,16 +286,7 @@ pub async fn test_multihop(
rpc: ServiceClient,
mut mullvad_client: MullvadProxyClient,
) -> Result<(), Error> {
- // NOTE: We have experienced flakiness due to timeout issues if distant relays are selected.
- // This is an attempt to try to reduce this type of flakiness.
- use helpers::custom_lists::LowLatency;
-
- let query = RelayQueryBuilder::new()
- .wireguard()
- .multihop()
- .location(LowLatency)
- .entry(LowLatency)
- .build();
+ let query = RelayQueryBuilder::new().wireguard().multihop().build();
apply_settings_from_relay_query(&mut mullvad_client, query).await?;
@@ -455,10 +438,6 @@ pub async fn test_quantum_resistant_tunnel(
rpc: ServiceClient,
mut mullvad_client: MullvadProxyClient,
) -> anyhow::Result<()> {
- // NOTE: We have experienced flakiness due to timeout issues if distant relays are selected.
- // This is an attempt to try to reduce this type of flakiness.
- use helpers::custom_lists::LowLatency;
-
mullvad_client
.set_quantum_resistant_tunnel(wireguard::QuantumResistantState::Off)
.await
@@ -472,10 +451,7 @@ pub async fn test_quantum_resistant_tunnel(
log::info!("Setting tunnel protocol to WireGuard");
- let query = RelayQueryBuilder::new()
- .wireguard()
- .location(LowLatency)
- .build();
+ let query = RelayQueryBuilder::new().wireguard().build();
apply_settings_from_relay_query(&mut mullvad_client, query).await?;
@@ -536,10 +512,6 @@ pub async fn test_quantum_resistant_multihop_udp2tcp_tunnel(
rpc: ServiceClient,
mut mullvad_client: MullvadProxyClient,
) -> Result<(), Error> {
- // NOTE: We have experienced flakiness due to timeout issues if distant relays are selected.
- // This is an attempt to try to reduce this type of flakiness.
- use helpers::custom_lists::LowLatency;
-
mullvad_client
.set_quantum_resistant_tunnel(wireguard::QuantumResistantState::On)
.await
@@ -549,8 +521,6 @@ pub async fn test_quantum_resistant_multihop_udp2tcp_tunnel(
.wireguard()
.multihop()
.udp2tcp()
- .entry(LowLatency)
- .location(LowLatency)
.build();
apply_settings_from_relay_query(&mut mullvad_client, query).await?;
@@ -577,10 +547,6 @@ pub async fn test_quantum_resistant_multihop_shadowsocks_tunnel(
rpc: ServiceClient,
mut mullvad_client: MullvadProxyClient,
) -> anyhow::Result<()> {
- // NOTE: We have experienced flakiness due to timeout issues if distant relays are selected.
- // This is an attempt to try to reduce this type of flakiness.
- use helpers::custom_lists::LowLatency;
-
mullvad_client
.set_quantum_resistant_tunnel(wireguard::QuantumResistantState::On)
.await
@@ -590,8 +556,6 @@ pub async fn test_quantum_resistant_multihop_shadowsocks_tunnel(
.wireguard()
.multihop()
.shadowsocks()
- .entry(LowLatency)
- .location(LowLatency)
.build();
apply_settings_from_relay_query(&mut mullvad_client, query).await?;
diff --git a/test/test-manager/src/tests/ui.rs b/test/test-manager/src/tests/ui.rs
index 6b9ce5b3a9..088fd1e55d 100644
--- a/test/test-manager/src/tests/ui.rs
+++ b/test/test-manager/src/tests/ui.rs
@@ -91,19 +91,11 @@ pub async fn test_ui_tunnel_settings(
rpc: ServiceClient,
mut mullvad_client: MullvadProxyClient,
) -> anyhow::Result<()> {
- // NOTE: This test connects multiple times using various settings, some of which may cause a
- // significant increase in connection time, e.g. multihop and OpenVPN. For this reason, it is
- // preferable to only target low latency servers.
- use helpers::custom_lists::LowLatency;
-
// tunnel-state.spec precondition: a single WireGuard relay should be selected
log::info!("Select WireGuard relay");
let entry = helpers::constrain_to_relay(
&mut mullvad_client,
- RelayQueryBuilder::new()
- .wireguard()
- .location(LowLatency)
- .build(),
+ RelayQueryBuilder::new().wireguard().build(),
)
.await?;
@@ -273,7 +265,11 @@ async fn test_custom_bridge_gui(
/// Test settings import / IP overrides in the GUI
#[test_function]
-pub async fn test_import_settings_ui(_: TestContext, rpc: ServiceClient) -> Result<(), Error> {
+pub async fn test_import_settings_ui(
+ _: TestContext,
+ rpc: ServiceClient,
+ _: MullvadProxyClient,
+) -> Result<(), Error> {
let ui_result = run_test(&rpc, &["settings-import.spec"]).await?;
assert!(ui_result.success());
Ok(())
@@ -281,7 +277,11 @@ pub async fn test_import_settings_ui(_: TestContext, rpc: ServiceClient) -> Resu
/// Test obfuscation settings in the GUI
#[test_function]
-pub async fn test_obfuscation_settings_ui(_: TestContext, rpc: ServiceClient) -> Result<(), Error> {
+pub async fn test_obfuscation_settings_ui(
+ _: TestContext,
+ rpc: ServiceClient,
+ _: MullvadProxyClient,
+) -> Result<(), Error> {
let ui_result = run_test(&rpc, &["obfuscation.spec"]).await?;
assert!(ui_result.success());
Ok(())
@@ -289,7 +289,11 @@ pub async fn test_obfuscation_settings_ui(_: TestContext, rpc: ServiceClient) ->
/// Test settings in the GUI
#[test_function]
-pub async fn test_settings_ui(_: TestContext, rpc: ServiceClient) -> Result<(), Error> {
+pub async fn test_settings_ui(
+ _: TestContext,
+ rpc: ServiceClient,
+ _: MullvadProxyClient,
+) -> Result<(), Error> {
let ui_result = run_test(&rpc, &["settings.spec"]).await?;
assert!(ui_result.success());
Ok(())
diff --git a/test/test-manager/test_macro/src/lib.rs b/test/test-manager/test_macro/src/lib.rs
index cddb6c5a2f..048bb1975e 100644
--- a/test/test-manager/test_macro/src/lib.rs
+++ b/test/test-manager/test_macro/src/lib.rs
@@ -91,11 +91,9 @@ fn parse_marked_test_function(
function: &syn::ItemFn,
) -> Result<TestFunction> {
let macro_parameters = get_test_macro_parameters(attributes)?;
- let function_parameters = get_test_function_parameters(&function.sig.inputs)?;
Ok(TestFunction {
name: function.sig.ident.clone(),
- function_parameters,
macro_parameters,
})
}
@@ -153,34 +151,15 @@ fn create_test(test_function: TestFunction) -> proc_macro2::TokenStream {
.collect();
let func_name = test_function.name;
- let function_mullvad_version = test_function.function_parameters.mullvad_client.version();
- let wrapper_closure = match test_function.function_parameters.mullvad_client {
- MullvadClient::New { .. } => {
- quote! {
- |test_context: crate::tests::TestContext,
- rpc: test_rpc::ServiceClient,
- mullvad_client: crate::mullvad_daemon::MullvadClientArgument|
- {
- let mullvad_client = match mullvad_client {
- crate::mullvad_daemon::MullvadClientArgument::WithClient(client) => client,
- crate::mullvad_daemon::MullvadClientArgument::None => unreachable!("invalid mullvad client")
- };
- Box::pin(async move {
- #func_name(test_context, rpc, mullvad_client).await.map_err(Into::into)
- })
- }
- }
- }
- MullvadClient::None { .. } => {
- quote! {
- |test_context: crate::tests::TestContext,
- rpc: test_rpc::ServiceClient,
- _mullvad_client: crate::mullvad_daemon::MullvadClientArgument| {
- Box::pin(async move {
- #func_name(test_context, rpc).await.map_err(Into::into)
- })
- }
- }
+ let wrapper_closure = quote! {
+ |test_context: crate::tests::TestContext,
+ rpc: test_rpc::ServiceClient,
+ mullvad_client: Option<MullvadProxyClient>|
+ {
+ let mullvad_client = mullvad_client.expect("Test functions defined using the macro should be given a mullvad client");
+ Box::pin(async move {
+ #func_name(test_context, rpc, mullvad_client).await.map_err(Into::into)
+ })
}
};
@@ -188,16 +167,15 @@ fn create_test(test_function: TestFunction) -> proc_macro2::TokenStream {
inventory::submit!(crate::tests::test_metadata::TestMetadata {
name: stringify!(#func_name),
targets: &[#targets],
- mullvad_client_version: #function_mullvad_version,
func: #wrapper_closure,
priority: #test_function_priority,
+ location: None,
});
}
}
struct TestFunction {
name: syn::Ident,
- function_parameters: FunctionParameters,
macro_parameters: MacroParameters,
}
@@ -205,66 +183,3 @@ struct MacroParameters {
priority: Option<i32>,
targets: Vec<Os>,
}
-
-enum MullvadClient {
- None {
- mullvad_client_version: proc_macro2::TokenStream,
- },
- New {
- mullvad_client_version: proc_macro2::TokenStream,
- },
-}
-
-impl MullvadClient {
- fn version(&self) -> proc_macro2::TokenStream {
- match self {
- MullvadClient::None {
- mullvad_client_version,
- } => mullvad_client_version.clone(),
- MullvadClient::New {
- mullvad_client_version,
- ..
- } => mullvad_client_version.clone(),
- }
- }
-}
-
-struct FunctionParameters {
- mullvad_client: MullvadClient,
-}
-
-fn get_test_function_parameters(
- args: &syn::punctuated::Punctuated<syn::FnArg, syn::Token![,]>,
-) -> Result<FunctionParameters> {
- if args.len() <= 2 {
- return Ok(FunctionParameters {
- mullvad_client: MullvadClient::None {
- mullvad_client_version: quote! {
- test_rpc::mullvad_daemon::MullvadClientVersion::None
- },
- },
- });
- }
-
- let arg = args[2].clone();
- let syn::FnArg::Typed(pat_type) = arg else {
- bail!(arg, "unexpected 'mullvad_client' arg");
- };
-
- let syn::Type::Path(syn::TypePath { path, .. }) = &*pat_type.ty else {
- bail!(pat_type, "unexpected 'mullvad_client' type");
- };
-
- let mullvad_client = match path.segments[0].ident.to_string().as_str() {
- "mullvad_management_interface" | "MullvadProxyClient" => {
- let mullvad_client_version =
- quote! { test_rpc::mullvad_daemon::MullvadClientVersion::New };
- MullvadClient::New {
- mullvad_client_version,
- }
- }
- _ => bail!(pat_type, "cannot infer mullvad client type"),
- };
-
- Ok(FunctionParameters { mullvad_client })
-}
diff --git a/test/test-rpc/src/mullvad_daemon.rs b/test/test-rpc/src/mullvad_daemon.rs
index 10cc00c3fc..3a62fa7c6a 100644
--- a/test/test-rpc/src/mullvad_daemon.rs
+++ b/test/test-rpc/src/mullvad_daemon.rs
@@ -26,9 +26,3 @@ pub enum Verbosity {
Debug,
Trace,
}
-
-#[derive(Clone, Copy, PartialEq)]
-pub enum MullvadClientVersion {
- None,
- New,
-}