Merge branch 'mirror_clean' into 'master'

MirrorChooser orders mirrors using location and error counts

See merge request fdroid/fdroidclient!1455
This commit is contained in:
Torsten Grote
2024-12-06 13:42:30 +00:00
14 changed files with 454 additions and 25 deletions

View File

@@ -36,6 +36,7 @@ public class HttpDownloaderTest {
FDroidApp.queryString,
null,
null,
null,
true
);
private static final Collection<Pair<String, String>> URLS;

View File

@@ -35,6 +35,8 @@ import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import androidx.preference.PreferenceManager;
import com.google.common.collect.Lists;
import org.fdroid.fdroid.data.Apk;
import org.fdroid.fdroid.data.DBHelper;
import org.fdroid.fdroid.installer.PrivilegedInstaller;
@@ -43,7 +45,6 @@ import org.fdroid.fdroid.net.ConnectivityMonitorService;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
@@ -125,6 +126,8 @@ public final class Preferences implements SharedPreferences.OnSharedPreferenceCh
public static final String PREF_LANGUAGE = "language";
public static final String PREF_USE_DNS_CACHE = "useDnsCache";
public static final String PREF_DNS_CACHE = "dnsCache";
public static final String PREF_MIRROR_ERROR_DATA = "mirrorErrorData";
public static final String PREF_PREFER_FOREIGN = "preferForeign";
public static final String PREF_USE_TOR = "useTor";
public static final String PREF_ENABLE_PROXY = "enableProxy";
public static final String PREF_PROXY_HOST = "proxyHost";
@@ -576,7 +579,7 @@ public final class Preferences implements SharedPreferences.OnSharedPreferenceCh
ipList.add(InetAddress.getByName(ip));
} catch (UnknownHostException e) {
// should not occur, if an ip address is supplied only the format is checked.
Log.e("Preferences", "Exception thrown when converting " + ip, e);
Log.e(TAG, "Exception thrown when converting " + ip, e);
}
}
dnsMap.put(url, ipList);
@@ -605,13 +608,56 @@ public final class Preferences implements SharedPreferences.OnSharedPreferenceCh
HashMap<String, List<String>> output = new HashMap<String, List<String>>();
for (String line : string.split("\n")) {
String[] items = line.split(" ");
ArrayList<String> list = new ArrayList<>(Arrays.asList(items));
List<String> list = Lists.newArrayList(items);
String key = list.remove(0);
output.put(key, list);
}
return output;
}
private String intMapToString(HashMap<String, Integer> intMap) {
String output = "";
for (String key : intMap.keySet()) {
if (!output.isEmpty()) {
output = output + "\n";
}
output = output + key + " " + intMap.get(key);
}
return output;
}
private HashMap<String, Integer> stringToIntMap(String mapString) {
HashMap<String, Integer> output = new HashMap<String, Integer>();
for (String line : mapString.split("\n")) {
String[] pair = line.split(" ");
output.put(pair[0], Integer.valueOf(pair[1]));
}
return output;
}
public void setPreferForeignValue(boolean newValue) {
preferences.edit().putBoolean(PREF_PREFER_FOREIGN, newValue).apply();
}
public boolean isPreferForeignSet() {
return preferences.getBoolean(PREF_PREFER_FOREIGN, false);
}
public void setMirrorErrorData(HashMap<String, Integer> mirrorErrorMap) {
preferences.edit().putString(PREF_MIRROR_ERROR_DATA, intMapToString(mirrorErrorMap)).apply();
}
public HashMap<String, Integer> getMirrorErrorData() {
HashMap<String, Integer> mirrorDataMap = new HashMap<String, Integer>();
String mapString = preferences.getString(PREF_MIRROR_ERROR_DATA, "");
if (mapString == null || mapString.isEmpty()) {
// no-op, return empty map to avoid null issues
} else {
mirrorDataMap = stringToIntMap(mapString);
}
return mirrorDataMap;
}
/**
* This preference's default is set dynamically based on whether Orbot is
* installed. If Orbot is installed, default to using Tor, the user can still override

View File

@@ -4,7 +4,6 @@ import android.content.Context
import android.os.Handler
import android.os.Looper
import android.util.Log
import android.widget.Toast
import android.widget.Toast.LENGTH_LONG
import androidx.annotation.WorkerThread
import androidx.lifecycle.asLiveData
@@ -155,7 +154,9 @@ class RepoUpdateManager @JvmOverloads constructor(
}
// can't show Toast from background thread, so we need to move this to UiThread
Handler(Looper.getMainLooper()).post {
Toast.makeText(context, msgBuilder.toString(), LENGTH_LONG).show()
// can only post toast messages on the ui thread but this may
// be called from code that is executed by runOffUiThread()
Utils.showToastFromService(context, msgBuilder.toString(), LENGTH_LONG)
}
}

View File

@@ -33,8 +33,12 @@ public class DownloaderFactory extends org.fdroid.download.DownloaderFactory {
private static final String TAG = "DownloaderFactory";
// TODO move to application object or inject where needed
public static final DownloaderFactory INSTANCE = new DownloaderFactory();
public static final HttpManager HTTP_MANAGER =
new HttpManager(Utils.getUserAgent(), FDroidApp.queryString, NetCipher.getProxy(), new DnsWithCache());
public static final HttpManager HTTP_MANAGER = new HttpManager(
Utils.getUserAgent(),
FDroidApp.queryString,
NetCipher.getProxy(),
new DnsWithCache(),
new FDroidMirrorParameterManager());
@NonNull
@Override

View File

@@ -0,0 +1,88 @@
package org.fdroid.fdroid.net;
import android.content.Context;
import android.telephony.TelephonyManager;
import androidx.annotation.NonNull;
import androidx.core.os.LocaleListCompat;
import org.fdroid.download.MirrorParameterManager;
import org.fdroid.fdroid.FDroidApp;
import org.fdroid.fdroid.Preferences;
import org.fdroid.fdroid.data.App;
import java.util.HashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
public class FDroidMirrorParameterManager implements MirrorParameterManager {
private volatile HashMap<String, Integer> errorCache;
private static final int DELAY_TIME = 1;
private static final TimeUnit DELAY_UNIT = TimeUnit.SECONDS;
private volatile boolean writeErrorScheduled = false;
private final Runnable delayedErrorWrite = () -> {
Preferences prefs = Preferences.get();
prefs.setMirrorErrorData(errorCache);
writeErrorScheduled = false;
};
private final ScheduledExecutorService writeErrorExecutor = Executors.newSingleThreadScheduledExecutor();
public FDroidMirrorParameterManager() {
Preferences prefs = Preferences.get();
errorCache = prefs.getMirrorErrorData();
}
public void updateErrorCacheAndPrefs(@NonNull String url, @NonNull Integer errorCount) {
errorCache.put(url, errorCount);
if (!writeErrorScheduled) {
writeErrorScheduled = true;
writeErrorExecutor.schedule(delayedErrorWrite, DELAY_TIME, DELAY_UNIT);
}
}
@Override
public void incrementMirrorErrorCount(@NonNull String mirrorUrl) {
if (errorCache.containsKey(mirrorUrl)) {
updateErrorCacheAndPrefs(mirrorUrl, errorCache.get(mirrorUrl) + 1);
} else {
updateErrorCacheAndPrefs(mirrorUrl, 1);
}
}
@Override
public int getMirrorErrorCount(@NonNull String mirrorUrl) {
if (errorCache.containsKey(mirrorUrl)) {
return errorCache.get(mirrorUrl);
} else {
return 0;
}
}
@Override
public boolean preferForeignMirrors() {
Preferences prefs = Preferences.get();
return prefs.isPreferForeignSet();
}
@NonNull
@Override
public String getCurrentLocation() {
TelephonyManager tm = (TelephonyManager) FDroidApp.getInstance().getSystemService(Context.TELEPHONY_SERVICE);
if (tm.getSimCountryIso() != null) {
return tm.getSimCountryIso();
} else if (tm.getNetworkCountryIso() != null) {
return tm.getNetworkCountryIso();
} else {
LocaleListCompat localeList = App.getLocales();
if (localeList != null && localeList.size() > 0) {
return localeList.get(0).getCountry();
} else {
return "";
}
}
}
}

View File

@@ -118,6 +118,7 @@ public class PreferencesFragment extends PreferenceFragmentCompat
private LiveSeekBarPreference updateIntervalSeekBar;
private SwitchPreferenceCompat enableProxyCheckPref;
private SwitchPreferenceCompat useDnsCacheCheckPref;
private SwitchPreferenceCompat preferForeignCheckPref;
private SwitchPreferenceCompat useTorCheckPref;
private Preference updateAutoDownloadPref;
private SwitchPreferenceCompat keepInstallHistoryPref;
@@ -161,6 +162,8 @@ public class PreferencesFragment extends PreferenceFragmentCompat
}
useDnsCacheCheckPref = ObjectsCompat.requireNonNull(findPreference(Preferences.PREF_USE_DNS_CACHE));
preferForeignCheckPref = ObjectsCompat.requireNonNull(findPreference(Preferences.PREF_PREFER_FOREIGN));
useTorCheckPref = ObjectsCompat.requireNonNull(findPreference(Preferences.PREF_USE_TOR));
useTorCheckPref.setOnPreferenceChangeListener(useTorChangedListener);
enableProxyCheckPref = ObjectsCompat.requireNonNull(findPreference(Preferences.PREF_ENABLE_PROXY));
@@ -548,6 +551,11 @@ public class PreferencesFragment extends PreferenceFragmentCompat
useDnsCacheCheckPref.setChecked(Preferences.get().isDnsCacheEnabled());
}
private void initPreferForeignPreference() {
preferForeignCheckPref.setDefaultValue(false);
preferForeignCheckPref.setChecked(Preferences.get().isPreferForeignSet());
}
/**
* The default for "Use Tor" is dynamically set based on whether Orbot is installed.
*/
@@ -600,6 +608,7 @@ public class PreferencesFragment extends PreferenceFragmentCompat
initAutoFetchUpdatesPreference();
initPrivilegedInstallerPreference();
initUseDnsCachePreference();
initPreferForeignPreference();
initUseTorPreference(requireContext().getApplicationContext());
updateIpfsGatewaySummary();

View File

@@ -372,6 +372,8 @@ This often occurs with apps installed via Google Play or other sources, if they
<string name="useDnsCache">Use DNS Cache</string>
<string name="useDnsCacheSummary">Use cached results to minimize DNS queries.</string>
<string name="preferForeign">Prefer Foreign Mirrors</string>
<string name="preferForeignSummary">Try mirrors that are located outside your country first, e.g. if foreign protections are stronger.</string>
<string name="useTor">Use Tor</string>
<string name="useTorSummary">Force download traffic through Tor for increased privacy. Requires Orbot</string>
<string name="proxy">Proxy</string>

View File

@@ -121,10 +121,6 @@
android:targetClass="org.fdroid.fdroid.views.IpfsGatewaySettingsActivity"
android:targetPackage="@string/applicationId" />
</PreferenceScreen>
<SwitchPreferenceCompat
android:key="useDnsCache"
android:summary="@string/useDnsCacheSummary"
android:title="@string/useDnsCache" />
<SwitchPreferenceCompat
android:key="useTor"
android:summary="@string/useTorSummary"
@@ -160,6 +156,15 @@
android:summary="@string/preventScreenshots_summary"
android:title="@string/preventScreenshots_title" />
<SwitchPreferenceCompat
android:key="preferForeign"
android:summary="@string/preferForeignSummary"
android:title="@string/preferForeign" />
<SwitchPreferenceCompat
android:key="useDnsCache"
android:summary="@string/useDnsCacheSummary"
android:title="@string/useDnsCache" />
<!-- only visible in full flavor -->
<SwitchPreferenceCompat
android:defaultValue="false"

View File

@@ -1,3 +1,4 @@
#Tue Oct 22 15:45:27 PDT 2024
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionSha256Sum=2ab88d6de2c23e6adae7363ae6e29cbdd2a709e992929b48b6530fd0c7133bd6

View File

@@ -37,6 +37,7 @@ kotlin {
dependencies {
implementation kotlin('test')
implementation libs.ktor.client.mock
implementation libs.mockk
}
}
// JVM is disabled for now, because Android app is including it instead of Android library
@@ -74,6 +75,7 @@ kotlin {
implementation project(":libs:sharedTest")
implementation libs.androidx.test.runner
implementation libs.androidx.test.ext.junit
implementation libs.mockk.android
}
}
nativeMain {
@@ -114,6 +116,11 @@ android {
lintConfig file("lint.xml")
}
testOptions {
packaging {
resources.excludes.add("META-INF/*")
}
}
}
signing {

View File

@@ -46,8 +46,9 @@ public open class HttpManager @JvmOverloads constructor(
queryString: String? = null,
proxyConfig: ProxyConfig? = null,
customDns: Dns? = null,
private val mirrorParameterManager: MirrorParameterManager? = null,
private val highTimeouts: Boolean = false,
private val mirrorChooser: MirrorChooser = MirrorChooserRandom(),
private val mirrorChooser: MirrorChooser = MirrorChooserWithParameters(mirrorParameterManager),
private val httpClientEngineFactory: HttpClientEngineFactory<*> = getHttpClientEngineFactory(
customDns
),

View File

@@ -1,6 +1,5 @@
package org.fdroid.download
import io.ktor.client.network.sockets.SocketTimeoutException
import io.ktor.client.plugins.ResponseException
import io.ktor.http.HttpStatusCode.Companion.Forbidden
import io.ktor.http.HttpStatusCode.Companion.NotFound
@@ -30,20 +29,30 @@ internal abstract class MirrorChooserImpl : MirrorChooser {
request: suspend (mirror: Mirror, url: Url) -> T,
): T {
val mirrors = if (downloadRequest.proxy == null) {
// keep ordered mirror list rather than reverting back to raw list from request
val orderedMirrors = orderMirrors(downloadRequest)
// if we don't use a proxy, filter out onion mirrors (won't work without Orbot)
val orderedMirrors =
orderMirrors(downloadRequest).filter { mirror -> !mirror.isOnion() }
// if we only have onion mirrors, take what we have and expect errors
orderedMirrors.ifEmpty { downloadRequest.mirrors }
val filteredMirrors = orderedMirrors.filter { mirror -> !mirror.isOnion() }
if (filteredMirrors.isEmpty()) {
// if we only have onion mirrors, take what we have and expect errors
orderedMirrors
} else {
filteredMirrors
}
} else {
orderMirrors(downloadRequest)
}
if (mirrors.isEmpty()) {
error("No valid mirrors were found. Check settings.")
}
mirrors.forEachIndexed { index, mirror ->
val ipfsCidV1 = downloadRequest.indexFile.ipfsCidV1
val url = if (mirror.isIpfsGateway) {
if (ipfsCidV1 == null) {
val e = IOException("Got IPFS gateway without CID")
throwOnLastMirror(e, index == mirrors.size - 1)
handleException(e, mirror, index, mirrors.size)
return@forEachIndexed
} else mirror.getUrl(ipfsCidV1)
} else {
@@ -57,20 +66,19 @@ internal abstract class MirrorChooserImpl : MirrorChooser {
// don't try other mirrors if we got NotFount response and downloaded a repo
if (downloadRequest.tryFirstMirror != null && e.response.status == NotFound) throw e
// also throw if this is the last mirror to try, otherwise try next
throwOnLastMirror(e, index == mirrors.size - 1)
handleException(e, mirror, index, mirrors.size)
} catch (e: IOException) {
throwOnLastMirror(e, index == mirrors.size - 1)
} catch (e: SocketTimeoutException) {
throwOnLastMirror(e, index == mirrors.size - 1)
handleException(e, mirror, index, mirrors.size)
} catch (e: NoResumeException) {
// continue to next mirror, if we need to resume, but this one doesn't support it
throwOnLastMirror(e, index == mirrors.size - 1)
handleException(e, mirror, index, mirrors.size)
}
}
error("Reached code that was thought to be unreachable.")
}
private fun throwOnLastMirror(e: Exception, wasLastMirror: Boolean) {
open fun handleException(e: Exception, mirror: Mirror, mirrorIndex: Int, mirrorCount: Int) {
val wasLastMirror = mirrorIndex == mirrorCount - 1
log.info {
val info = if (e is ResponseException) e.response.status.toString()
else e::class.simpleName ?: ""
@@ -97,3 +105,95 @@ internal class MirrorChooserRandom : MirrorChooserImpl() {
}
}
internal class MirrorChooserWithParameters(
private val mirrorParameterManager: MirrorParameterManager? = null
) : MirrorChooserImpl() {
override fun orderMirrors(downloadRequest: DownloadRequest): List<Mirror> {
val errorComparator = Comparator { mirror1: Mirror, mirror2: Mirror ->
// if no parameter manager is available, default to 0 (should return equal)
val error1 = mirrorParameterManager?.getMirrorErrorCount(mirror1.baseUrl) ?: 0
val error2 = mirrorParameterManager?.getMirrorErrorCount(mirror2.baseUrl) ?: 0
// prefer mirrors with fewer errors
error1.compareTo(error2)
}
val mirrorList: MutableList<Mirror> = mutableListOf<Mirror>()
if (mirrorParameterManager != null &&
mirrorParameterManager.getCurrentLocation().isNotEmpty()
) {
// if we have access to mirror parameters and the current location,
// then use that information to sort the mirror list
val mirrorFilteredList: List<Mirror> = sortMirrorsByLocation(
mirrorParameterManager.preferForeignMirrors(),
downloadRequest.mirrors,
mirrorParameterManager.getCurrentLocation(),
errorComparator
)
mirrorList.addAll(mirrorFilteredList)
} else {
// shuffle initial list so all viable mirrors will be tried
// then sort list to avoid mirrors that have caused errors
val mirrorCompleteList: List<Mirror> =
downloadRequest.mirrors
.toMutableList()
.apply { shuffle() }
.sortedWith(errorComparator)
mirrorList.addAll(mirrorCompleteList)
}
// respect the mirror to try first, if set
if (downloadRequest.tryFirstMirror != null) {
mirrorList.sortBy { if (it == downloadRequest.tryFirstMirror) 0 else 1 }
}
return mirrorList
}
private fun sortMirrorsByLocation(
foreignMirrorsPreferred: Boolean,
availableMirrorList: List<Mirror>,
currentLocation: String,
mirrorComparator: Comparator<Mirror>
): List<Mirror> {
// shuffle initial list so all viable mirrors will be tried
// then sort list to avoid mirrors that have caused errors
val mirrorList: MutableList<Mirror> = mutableListOf<Mirror>()
val sortedList: List<Mirror> = availableMirrorList
.toMutableList()
.apply { shuffle() }
.sortedWith(mirrorComparator)
val domesticList: List<Mirror> = sortedList.filter { mirror ->
!mirror.countryCode.isNullOrEmpty() && currentLocation == mirror.countryCode
}
val foreignList: List<Mirror> = sortedList.filter { mirror ->
!mirror.countryCode.isNullOrEmpty() && currentLocation != mirror.countryCode
}
val unknownList: List<Mirror> = sortedList.filter { mirror ->
mirror.countryCode.isNullOrEmpty()
}
if (foreignMirrorsPreferred) {
mirrorList.addAll(foreignList)
mirrorList.addAll(unknownList)
mirrorList.addAll(domesticList)
} else {
mirrorList.addAll(domesticList)
mirrorList.addAll(unknownList)
mirrorList.addAll(foreignList)
}
return mirrorList
}
override fun handleException(e: Exception, mirror: Mirror, mirrorIndex: Int, mirrorCount: Int) {
if (e is ResponseException || e is IOException) {
mirrorParameterManager?.incrementMirrorErrorCount(mirror.baseUrl)
}
super.handleException(e, mirror, mirrorIndex, mirrorCount)
}
}

View File

@@ -0,0 +1,32 @@
package org.fdroid.download
/**
* This is an interface for providing access to stored parameters for mirrors without adding
* additional dependencies. The expectation is that this will be used to store and retrieve
* data about mirror performance to use when ordering mirror for subsequent tests.
*
* Currently it supports success and error count, but other parameters could be added later.
*/
public interface MirrorParameterManager {
/**
* Set or get the number of failed attempts to access the specified mirror. The intent
* is to order mirrors for subsequent tests based on the number of failures.
*/
public fun incrementMirrorErrorCount(mirrorUrl: String)
public fun getMirrorErrorCount(mirrorUrl: String): Int
/**
* Returns true or false depending on whether the location preference has been enabled. This
* preference reflects whether mirrors matching your location should get priority.
*/
public fun preferForeignMirrors(): Boolean
/**
* Returns the country code of the user's current location
*/
public fun getCurrentLocation(): String
}

View File

@@ -2,6 +2,8 @@ package org.fdroid.download
import io.ktor.client.network.sockets.SocketTimeoutException
import io.ktor.utils.io.errors.IOException
import io.mockk.every
import io.mockk.mockk
import org.fdroid.getIndexFile
import org.fdroid.runSuspend
import kotlin.random.Random
@@ -12,8 +14,27 @@ import kotlin.test.assertTrue
class MirrorChooserTest {
private val mirrors = listOf(Mirror("foo"), Mirror("bar"), Mirror("42"), Mirror("1337"))
private val mirrors = listOf(
Mirror("foo"),
Mirror("bar"),
Mirror("42"),
Mirror("1337"))
private val mirrorsLocation = listOf(
Mirror(baseUrl = "unknown_1", countryCode = null),
Mirror(baseUrl = "unknown_2", countryCode = null),
Mirror(baseUrl = "unknown_3", countryCode = null),
Mirror(baseUrl = "local_1", countryCode = "HERE"),
Mirror(baseUrl = "local_2", countryCode = "HERE"),
Mirror(baseUrl = "local_3", countryCode = "HERE"),
Mirror(baseUrl = "remote_1", countryCode = "THERE"),
Mirror(baseUrl = "remote_2", countryCode = "THERE"),
Mirror(baseUrl = "remote_3", countryCode = "THERE"))
private val downloadRequest = DownloadRequest("foo", mirrors)
private val downloadRequestLocation = DownloadRequest("location", mirrorsLocation)
private val downloadRequestTryFIrst = DownloadRequest(
path = "location",
mirrors = mirrorsLocation,
tryFirstMirror = Mirror(baseUrl = "remote_1", countryCode = "THERE"))
private val ipfsIndexFile = getIndexFile(name = "foo", ipfsCidV1 = "CIDv1")
@@ -141,4 +162,115 @@ class MirrorChooserTest {
assertEquals("Got IPFS gateway without CID", e.message)
}
@Test
fun testMirrorChooserDomesticLocation() {
val mockManager = mockk<MirrorParameterManager>(relaxed = true)
every { mockManager.getCurrentLocation() } returns "HERE"
every { mockManager.preferForeignMirrors() } returns false
val mirrorChooser = MirrorChooserWithParameters(mockManager)
// test domestic mirror preference
val domesticList = mirrorChooser.orderMirrors(downloadRequestLocation)
// confirm the list contains all mirrors
assertEquals(9, domesticList.size)
// mirrors that are local should be included first
assertEquals("HERE", domesticList[0].countryCode)
assertEquals("HERE", domesticList[1].countryCode)
assertEquals("HERE", domesticList[2].countryCode)
assertEquals(null, domesticList[3].countryCode)
}
@Test
fun testMirrorChooserForeignLocation() {
val mockManager = mockk<MirrorParameterManager>(relaxed = true)
every { mockManager.getCurrentLocation() } returns "HERE"
every { mockManager.preferForeignMirrors() } returns true
val mirrorChooser = MirrorChooserWithParameters(mockManager)
// test foreign mirror preference
val foreignList = mirrorChooser.orderMirrors(downloadRequestLocation)
// confirm the list contains all mirrors
assertEquals(9, foreignList.size)
// mirrors that are remote should be included first
assertEquals("THERE", foreignList[0].countryCode)
assertEquals("THERE", foreignList[1].countryCode)
assertEquals("THERE", foreignList[2].countryCode)
assertEquals(null, foreignList[3].countryCode)
}
@Test
fun testMirrorChooserErrorSort() {
val mockManager = mockk<MirrorParameterManager>(relaxed = true)
every { mockManager.getCurrentLocation() } returns "HERE"
every { mockManager.preferForeignMirrors() } returns false
every { mockManager.getMirrorErrorCount("local_1") } returns 5
every { mockManager.getMirrorErrorCount("local_2") } returns 3
every { mockManager.getMirrorErrorCount("local_3") } returns 1
val mirrorChooser = MirrorChooserWithParameters(mockManager)
// test error sorting with domestic mirror preference
val orderedList = mirrorChooser.orderMirrors(downloadRequestLocation)
// confirm the list contains all mirrors
assertEquals(9, orderedList.size)
// mirrors that have fewer errors should be included first
assertEquals("local_3", orderedList[0].baseUrl)
assertEquals("local_2", orderedList[1].baseUrl)
assertEquals("local_1", orderedList[2].baseUrl)
}
@Test
fun testMirrorChooserDomesticWithTryFirst() {
val mockManager = mockk<MirrorParameterManager>(relaxed = true)
every { mockManager.getCurrentLocation() } returns "HERE"
every { mockManager.preferForeignMirrors() } returns false
val mirrorChooser = MirrorChooserWithParameters(mockManager)
// test tryfirst mirror parameter
val tryFirstList = mirrorChooser.orderMirrors(downloadRequestTryFIrst)
// confirm the list contains all mirrors
assertEquals(9, tryFirstList.size)
// tryfirst mirror should be included before local mirrors
assertEquals("remote_1", tryFirstList[0].baseUrl)
assertEquals("HERE", tryFirstList[1].countryCode)
assertEquals("HERE", tryFirstList[2].countryCode)
assertEquals("HERE", tryFirstList[3].countryCode)
}
@Test
fun testMirrorChooserRandomization() {
val mockManager = mockk<MirrorParameterManager>(relaxed = true)
every { mockManager.getCurrentLocation() } returns "HERE"
every { mockManager.preferForeignMirrors() } returns false
val mirrorChooser = MirrorChooserWithParameters(mockManager)
// repeat test to verify that if error count is equal the order isn't always the same
var count1 = 0
var count2 = 0
var count3 = 0
var countX = 0
repeat(100) {
// test error sorting with domestic mirror preference
val orderedList = mirrorChooser.orderMirrors(downloadRequestLocation)
if (orderedList[0].baseUrl.equals("local_1")) {
count1++
} else if (orderedList[0].baseUrl.equals("local_2")) {
count2++
} else if (orderedList[0].baseUrl.equals("local_3")) {
count3++
} else {
countX++
}
}
// all domestic urls should have appeared first in the list at least once
assertTrue { count1 > 0 }
assertTrue { count2 > 0 }
assertTrue { count3 > 0 }
// no foreign urls should should have appeared first in the list
assertEquals(0, countX)
}
}