Repository: Senth/minecraft-mod-manager (python)

Issue:
Automatically and manually select fabric/forge for download
### Problem
Currently, if a mod has support for both forge and fabric and we're using the install method. It will pick the latest one and you don't have the ability to select whether you want the Fabric or Forge version.

### Solution
- Automatically filter by the majority rule. I.e. if you have 12 fabric and 1 forge mod installed, it will install a fabric mod.
- Add parser option `--fabric` and `--forge` to override the majority role or when using the first time without any pre-installed mods.



Pull request description:
Automatically select fabric/forge for install
### What changed?
Now automatically selects fabric/forge for install depending on majority. If two or more mod loaders have the same count of installments then it doesn't select any of them.

### Testing
- [X] Added unit tests

### Related Issues
Fixes #18


Patch:
diff --git a/minecraft_mod_manager/app/install/install.py b/minecraft_mod_manager/app/install/install.py
index 90daccb..46a4c90 100644
--- a/minecraft_mod_manager/app/install/install.py
+++ b/minecraft_mod_manager/app/install/install.py
@@ -1,6 +1,7 @@
-from typing import List, Sequence
+from typing import Dict, List, Sequence
 
 from ...core.entities.mod import Mod, ModArg
+from ...core.entities.mod_loaders import ModLoaders
 from ...utils.logger import LogColors, Logger
 from ..download.download import Download
 from .install_repo import InstallRepo
@@ -13,6 +14,7 @@ class Install(Download):
 
     def execute(self, mods: Sequence[ModArg]) -> None:
         mods = self._filter_installed_mods(mods)
+        mods = self._set_mod_loader(mods)
         self.find_download_and_install(mods)
 
     def _filter_installed_mods(self, mods: Sequence[ModArg]) -> Sequence[Mod]:
@@ -25,3 +27,37 @@ class Install(Download):
                 Logger.info(f"{mod.id} has already been installed, skipping...", LogColors.skip)
 
         return mods_to_install
+
+    def _set_mod_loader(self, mods: Sequence[Mod]) -> Sequence[Mod]:
+        loader = self._get_mod_loader_to_use()
+        if loader != ModLoaders.unknown:
+            for mod in mods:
+                mod.mod_loader = loader
+        return mods
+
+    def _get_mod_loader_to_use(self) -> ModLoaders:
+        mods = self._repo.get_all_mods()
+
+        # Count
+        counts: Dict[ModLoaders, int] = {}
+        for mod in mods:
+            loader = mod.mod_loader
+            if loader != ModLoaders.unknown:
+                count = 0
+                if loader in counts:
+                    count = counts[loader]
+                counts[loader] = count + 1
+
+        # Sort
+        count_max = 0
+        loader_max = ModLoaders.unknown
+
+        for loader, count in counts.items():
+            if count > count_max:
+                count_max = count
+                loader_max = loader
+            # Multiple max, use none then
+            elif count == count_max:
+                loader_max = ModLoaders.unknown
+
+        return loader_max
diff --git a/minecraft_mod_manager/app/install/install_repo.py b/minecraft_mod_manager/app/install/install_repo.py
index 5644eac..b543b17 100644
--- a/minecraft_mod_manager/app/install/install_repo.py
+++ b/minecraft_mod_manager/app/install/install_repo.py
@@ -1,6 +1,12 @@
+from typing import Sequence
+
+from ...core.entities.mod import Mod
 from ..download.download_repo import DownloadRepo
 
 
 class InstallRepo(DownloadRepo):
     def is_installed(self, id: str) -> bool:
         raise NotImplementedError()
+
+    def get_all_mods(self) -> Sequence[Mod]:
+        raise NotImplementedError()


Test patch:
diff --git a/minecraft_mod_manager/app/install/install_test.py b/minecraft_mod_manager/app/install/install_test.py
index cd21491..2efdb80 100644
--- a/minecraft_mod_manager/app/install/install_test.py
+++ b/minecraft_mod_manager/app/install/install_test.py
@@ -1,7 +1,8 @@
 import pytest
+from minecraft_mod_manager.core.entities.mod_loaders import ModLoaders
 from mockito import mock, unstub, verifyStubbedInvocationsAreUsed, when
 
-from ...core.entities.mod import ModArg
+from ...core.entities.mod import Mod, ModArg
 from ...core.entities.sites import Sites
 from .install import Install
 from .install_repo import InstallRepo
@@ -14,6 +15,7 @@ def mock_repo():
 
 def test_mod_not_installed_when_already_installed(mock_repo):
     when(mock_repo).is_installed(...).thenReturn(True)
+    when(mock_repo).get_all_mods(...).thenReturn([])
 
     input = [ModArg(Sites.unknown, "", "")]
     install = Install(mock_repo)
@@ -25,8 +27,67 @@ def test_mod_not_installed_when_already_installed(mock_repo):
 
 def test_call_find_download_and_install(mock_repo):
     install = Install(mock_repo)
+    when(mock_repo).get_all_mods(...).thenReturn([])
     when(install).find_download_and_install(...)
     install.execute([])
 
     verifyStubbedInvocationsAreUsed()
     unstub()
+
+
+def test_set_mod_loader_by_majority(mock_repo):
+    install = Install(mock_repo)
+    installed_mods = [
+        Mod("", "", mod_loader=ModLoaders.fabric),
+        Mod("", "", mod_loader=ModLoaders.fabric),
+        Mod("", "", mod_loader=ModLoaders.forge),
+        Mod("", "", mod_loader=ModLoaders.unknown),
+        Mod("", "", mod_loader=ModLoaders.unknown),
+        Mod("", "", mod_loader=ModLoaders.unknown),
+    ]
+
+    input = ModArg(Sites.unknown, "", None)
+    expected_mod = [Mod("", "", mod_loader=ModLoaders.fabric)]
+    when(mock_repo).is_installed(...).thenReturn(False)
+    when(mock_repo).get_all_mods(...).thenReturn(installed_mods)
+    when(install).find_download_and_install(expected_mod)
+
+    install.execute([input])
+
+    verifyStubbedInvocationsAreUsed()
+    unstub()
+
+
+def test_dont_set_mod_loader(mock_repo):
+    install = Install(mock_repo)
+    installed_mods = []
+
+    input = ModArg(Sites.unknown, "", None)
+    expected_mod = [Mod("", "")]
+    when(mock_repo).is_installed(...).thenReturn(False)
+    when(mock_repo).get_all_mods(...).thenReturn(installed_mods)
+    when(install).find_download_and_install(expected_mod)
+
+    install.execute([input])
+
+    verifyStubbedInvocationsAreUsed()
+    unstub()
+
+
+def test_dont_set_mod_loader_when_no_majority(mock_repo):
+    install = Install(mock_repo)
+    installed_mods = [
+        Mod("", "", mod_loader=ModLoaders.fabric),
+        Mod("", "", mod_loader=ModLoaders.forge),
+    ]
+
+    input = ModArg(Sites.unknown, "", None)
+    expected_mod = [Mod("", "")]
+    when(mock_repo).is_installed(...).thenReturn(False)
+    when(mock_repo).get_all_mods(...).thenReturn(installed_mods)
+    when(install).find_download_and_install(expected_mod)
+
+    install.execute([input])
+
+    verifyStubbedInvocationsAreUsed()
+    unstub()