mirror of
https://github.com/deepfakes/faceswap.git
synced 2026-10-18 14:34:30 +08:00
837bc2d51d
* FaceSwap 3 (#1515) * Update extract pipeline * Update requirements + setup for nvidia * Remove allow-growth option * tf.keras to keras updates * lib.model.losses - Port + fix all loss functions for Keras3 * lib.model - port initializers, layers. normalization to Keras3 * lib.model.autoclip to Keras 3 * Update mixed precision layer storage * model file to .keras format * Restructure nn_blocks to initialize layers in __init__ * Tensorboard - Trainer: Add Torch compatible Tensorboard callbacks - GUI event reader remove TF dependency * Loss logging - Flush TB logs on save - Replace TB live iterator for GUI * Backup models on total loss drop rather than per side * Update all models to Keras3 Compat * Remove lib.model.session * Update clip ViT to Keras 3 * plugins.extract.mask.unet-dfl - Fix for Keras3/Torch backend * Port AdaBelief to Keras 3 * setup.py: - Add --dev flag for dev tool install * Fix Keras 3 syntax * Fix LR Finder for Keras 3 * Fix mixed precision switching for Keras 3 * Add more optimizers + open up config setting * train: Remove updating FS1 weights to FS2 models * Alignments: Remove support for legacy .json files * tools.model: - Remove TF Saved Format saving - Fix Backup/Restore + Nan-Scan * Fix inference model creation for Keras 3 * Preview tool: Fix for Keras3 * setup.py: Configure keras backend * train: Migration of FS2 models to FS3 * Training: Default coverage to 100% * Remove DirectML backend * Update setup for MacOS * GUI: Force line reading to UTF-8 * Remove redundant Tensorflow references * Remove redundant code * Legacy model loading: Fix TFLamdaOp scalar ops and DepthwiseConv2D * Add vertical offset option for training * Github actions: Add more python versions * Add python version to workflow names * Github workflow: Exclude Python 3.12 for macOS * Implement custom training loop * Fs3 - Add RTX5xxx and ROCm 6.1-6.4 support (#1511) * setup.py: Add Cuda/ROCm version select options * bump minimum python version to 3.11 * Switch from setup.cgf to pyproject.toml * Documentation: Update all docs to use automodapi * Allow sysinfo to run with missing packages + correctly install tk under Linux * Bugfix: dot naming convention in clip models * lib.config: Centralise globally rather than passing as object - Add torch DataParallel for multi-gpu training - GUI: Group switches together when generating cli args - CLI: Remove deprecated multi-character argparse args - Refactor: - Centralise tensorboard reading/writing + unit tests - Create trainer plugin interfaces + add original + distributed * Update installers
378 lines
14 KiB
Python
378 lines
14 KiB
Python
#!/usr/bin python3
|
|
""" Pytest unit tests for :mod:`lib.config.ini` """
|
|
|
|
import os
|
|
import pytest
|
|
|
|
from lib.config import ini as ini_mod
|
|
|
|
from tests.lib.config.helpers import FakeConfigItem
|
|
|
|
# pylint:disable=protected-access,invalid-name
|
|
|
|
|
|
_GROUPS = ("group1", "group2", "group3")
|
|
_CONFIG = ("custom", "custom_missing", "root", "root_missing")
|
|
|
|
|
|
@pytest.mark.parametrize("plugin_group", _GROUPS)
|
|
@pytest.mark.parametrize("config", _CONFIG)
|
|
def test_ConfigFile(tmpdir, mocker, plugin_group, config):
|
|
""" Test that :class:`lib.config.ini.ConfigFile` initializes correctly """
|
|
root_conf = tmpdir.mkdir("root").mkdir("config").join(f"{plugin_group}.ini")
|
|
root_dir = os.path.dirname(os.path.dirname(root_conf))
|
|
if config != "root_missing":
|
|
root_conf.write("")
|
|
mocker.patch("lib.config.ini.PROJECT_ROOT", root_dir)
|
|
|
|
conf_file = None
|
|
if config.startswith("custom"):
|
|
conf_file = tmpdir.mkdir("config").join("test_custom_config.ini")
|
|
if config == "custom":
|
|
conf_file.write("")
|
|
|
|
mock_load = mocker.MagicMock()
|
|
mocker.patch("lib.config.ini.ConfigFile.load", mock_load)
|
|
|
|
if config == "custom_missing": # Error on explicit missing
|
|
with pytest.raises(ValueError):
|
|
ini_mod.ConfigFile("group2test", conf_file)
|
|
return
|
|
|
|
instance = ini_mod.ConfigFile(plugin_group, conf_file)
|
|
file_path = conf_file if config == "custom" else root_conf
|
|
assert instance._file_path == file_path
|
|
assert instance._plugin_group == plugin_group
|
|
assert instance._parser.optionxform is str
|
|
|
|
if config in ("custom", "root"): # load when exists
|
|
mock_load.assert_called_once()
|
|
else:
|
|
mock_load.assert_not_called() # Don't load when it doesn't
|
|
|
|
|
|
def test_ConfigFile_load(mocker):
|
|
""" Test that :class:`lib.config.ini.ConfigFile.load` calls correctly """
|
|
instance = ini_mod.ConfigFile("test")
|
|
|
|
mock_read = mocker.MagicMock()
|
|
instance._parser.read = mock_read
|
|
|
|
instance.load()
|
|
|
|
mock_read.assert_called_once()
|
|
|
|
|
|
def test_ConfigFile_save(mocker):
|
|
""" Test that :class:`lib.config.ini.ConfigFile.save` calls correctly """
|
|
instance = ini_mod.ConfigFile("test")
|
|
|
|
mock_write = mocker.MagicMock()
|
|
instance._parser.write = mock_write
|
|
|
|
instance.save()
|
|
|
|
mock_write.assert_called_once()
|
|
|
|
|
|
class FakeConfigSection: # pylint:disable=too-few-public-methods
|
|
""" Fake config section """
|
|
def __init__(self, num_opts=2):
|
|
self.options = {f"opt{i}": FakeConfigItem(f"test_value{i}") for i in range(num_opts)}
|
|
self.helptext = f"Test helptext for {num_opts} options"
|
|
|
|
|
|
def get_local_remote(sections=[2, 1, 3]): # pylint:disable=dangerous-default-value
|
|
""" Obtain an object representing inputs to a ConfigParser and a matching object representing
|
|
Faceswap Config """
|
|
parser_sections = {f"section{i}": {f"opt{idx}": f"test_value{idx}" for idx in range(s)}
|
|
for i, s in enumerate(sections)}
|
|
fs_sections = {f"section{i}": FakeConfigSection(s) for i, s in enumerate(sections)}
|
|
return parser_sections, fs_sections
|
|
|
|
|
|
def test_ConfigFile_is_synced_structure():
|
|
""" Test that :class:`lib.config.ini.ConfigFile.is_synced_structure` is logical """
|
|
instance = ini_mod.ConfigFile("test")
|
|
|
|
sect_sizes = [2, 1, 3]
|
|
parser_sects, fs_sects = get_local_remote(sect_sizes)
|
|
|
|
# No Config
|
|
test = instance._is_synced_structure(fs_sects)
|
|
assert test is False
|
|
|
|
# Sects exist
|
|
for section in parser_sects:
|
|
instance._parser.add_section(section)
|
|
|
|
test = instance._is_synced_structure(fs_sects)
|
|
assert test is False
|
|
|
|
# Some Options missing
|
|
for section, options in parser_sects.items():
|
|
for opt, val in options.items():
|
|
instance._parser.set(section, opt, val)
|
|
break
|
|
|
|
test = instance._is_synced_structure(fs_sects)
|
|
assert test is False
|
|
|
|
# Structure matches
|
|
for section, options in parser_sects.items():
|
|
for opt, val in options.items():
|
|
instance._parser.set(section, opt, val)
|
|
|
|
test = instance._is_synced_structure(fs_sects)
|
|
assert test is True
|
|
|
|
# Extra saved section
|
|
instance._parser.add_section("text_extra_section")
|
|
test = instance._is_synced_structure(fs_sects)
|
|
assert test is False
|
|
|
|
# Structure matches
|
|
del instance._parser["text_extra_section"]
|
|
test = instance._is_synced_structure(fs_sects)
|
|
assert test is True
|
|
|
|
# Extra Option
|
|
instance._parser.set(section, "opt_test_extra_option", "val_test_extra_option")
|
|
test = instance._is_synced_structure(fs_sects)
|
|
assert test is False
|
|
|
|
|
|
def testConfigFile_format_help():
|
|
""" Test that :class:`lib.config.ini.ConfigFile.format_help` inserts # on each line """
|
|
instance = ini_mod.ConfigFile("test")
|
|
text = "This\nis a test\n\n\nof some text\n"
|
|
result = instance.format_help(text)
|
|
assert all(x.startswith("#") for x in result.splitlines() if x)
|
|
|
|
|
|
@pytest.mark.parametrize("section",
|
|
("section1", "another_section", "section_test"))
|
|
def testConfigFile_insert_section(mocker, section):
|
|
""" Test that :class:`lib.config.ini.ConfigFile._insert_section` calls correctly """
|
|
helptext = f"{section}_helptext"
|
|
|
|
instance = ini_mod.ConfigFile("test")
|
|
instance.format_help = mocker.MagicMock(return_value=helptext)
|
|
|
|
parser = instance._parser
|
|
|
|
assert section not in parser
|
|
|
|
instance._insert_section(section, helptext, parser)
|
|
|
|
instance.format_help.assert_called_once_with(helptext, is_section=True)
|
|
|
|
assert section in parser
|
|
assert helptext in parser[section]
|
|
|
|
|
|
@pytest.mark.parametrize(("section", "name", "value"),
|
|
(("section1", "opt1", "value1"),
|
|
("another_section", "my_option", "what_its_worth")))
|
|
def testConfigFile_insert_option(mocker, section, name, value):
|
|
""" Test that :class:`lib.config.ini.ConfigFile._insert_option` calls correctly """
|
|
helptext = f"{section}_helptext"
|
|
|
|
instance = ini_mod.ConfigFile("test")
|
|
instance.format_help = mocker.MagicMock(return_value=helptext)
|
|
|
|
parser = instance._parser
|
|
parser.add_section(section)
|
|
|
|
assert name not in parser[section]
|
|
|
|
instance._insert_option(section, name, helptext, value, parser)
|
|
|
|
instance.format_help.assert_called_once_with(helptext, is_section=False)
|
|
assert name in parser[section]
|
|
assert parser[section][name] == value
|
|
|
|
|
|
_ini, _app, = get_local_remote([2, 1, 3])
|
|
_ini_extra, _app_extra = get_local_remote(sections=[3, 1, 3])
|
|
_ini_value, _ = get_local_remote(sections=[2, 1, 3])
|
|
_ini_value["section0"]["opt0"] = "updated_value"
|
|
|
|
_SYNC = ((_ini, _app, "synced"),
|
|
(_ini, _app_extra, "new_from_app"),
|
|
(_ini_extra, _app, "del_from_app"),
|
|
(_ini_value, _app, "updated_ini"))
|
|
_SYNC_IDS = [x[-1] for x in _SYNC]
|
|
|
|
|
|
@pytest.mark.parametrize(("ini_config", "app_config", "status"), _SYNC, ids=_SYNC_IDS)
|
|
@pytest.mark.parametrize("exists", (True, False), ids=("exists", "not_exists"))
|
|
def testConfigFile_sync_from_app(ini_config, # pylint:disable=too-many-branches # noqa[C901]
|
|
app_config,
|
|
status,
|
|
exists,
|
|
mocker):
|
|
""" Test :class:`lib.config.ini.ConfigFile._sync_from_app` logic """
|
|
mocker.patch("lib.config.ini.ConfigFile._exists", exists)
|
|
|
|
instance = ini_mod.ConfigFile("test")
|
|
instance.save = mocker.MagicMock()
|
|
|
|
original_parser = instance._parser
|
|
|
|
if exists:
|
|
for section, opts in ini_config.items():
|
|
original_parser.add_section(section)
|
|
for name, opt in opts.items():
|
|
original_parser[section][name] = opt
|
|
|
|
opt_pairs = [({k: v.value for k, v in opts.options.items()},
|
|
dict(original_parser[s].items()))
|
|
for s, opts in app_config.items()]
|
|
# Sanity check that the loaded parser is set correctly
|
|
if status == "synced":
|
|
assert all(set(x[0]) == set(x[1]) for x in opt_pairs)
|
|
elif status == "new_from_app":
|
|
assert any(len(x[1]) < len(x[0]) for x in opt_pairs)
|
|
elif status == "new_from_ini":
|
|
assert any(len(x[0]) < len(x[1]) for x in opt_pairs)
|
|
elif status == "updated_ini":
|
|
vals = [(set(x[0].values()), set(x[1].values())) for x in opt_pairs]
|
|
assert not all(a == i for a, i in vals)
|
|
else:
|
|
for section in ini_config:
|
|
assert section not in instance._parser
|
|
|
|
instance._sync_from_app(app_config) # Sync
|
|
|
|
instance.save.assert_called_once() # Saved
|
|
if exists:
|
|
assert instance._parser is not original_parser # New config Generated
|
|
else:
|
|
assert instance._parser is original_parser # Blank Config pre-exists
|
|
|
|
opt_pairs = [({k: v.value for k, v in opts.options.items()},
|
|
{k: v for k, v in instance._parser[s].items() if k.startswith("opt")})
|
|
for s, opts in app_config.items()]
|
|
|
|
# Test options are now in sync
|
|
assert all(set(x[0]) == set(x[1]) for x in opt_pairs)
|
|
# Test that ini value kept
|
|
vals = [(set(x[0].values()), set(x[1].values())) for x in opt_pairs]
|
|
if exists and status == "updated_ini":
|
|
assert any("updated_value" in i for _, i in vals)
|
|
assert any(a != i for a, i in vals)
|
|
else:
|
|
assert not any("updated_value" in i for _, i in vals)
|
|
assert all(a == i for a, i in vals)
|
|
|
|
|
|
@pytest.mark.parametrize(("section", "option", "value", "datatype"),
|
|
(("section1", "opt_str", "test_str", str),
|
|
("section2", "opt_bool", "True", bool),
|
|
("section3", "opt_int", "42", int),
|
|
("section4", "opt_float", "42.69", float),
|
|
("section5", "opt_other", "[test_other]", str)),
|
|
ids=("str", "bool", "int", "float", "other"))
|
|
def testConfigFile_get_converted_value(section, option, value, datatype):
|
|
""" Test :class:`lib.config.ini.ConfigFile._get_converted_value` logic """
|
|
instance = ini_mod.ConfigFile("test")
|
|
instance._parser.add_section(section)
|
|
instance._parser[section][option] = value
|
|
|
|
result = instance._get_converted_value(section, option, datatype)
|
|
assert isinstance(result, datatype)
|
|
assert datatype(value) == result
|
|
|
|
|
|
_ini, _app, = get_local_remote([2, 1, 3])
|
|
_ini_changed, _ = get_local_remote(sections=[2, 1, 3])
|
|
_ini_changed["section0"]["opt0"] = "updated_value"
|
|
_ini_changed["section2"]["opt1"] = "updated_value"
|
|
|
|
_SYNC_TO = ((_ini, _app, "synced"), (_ini_changed, _app, "updated_ini"))
|
|
_SYNC__TO_IDS = [x[-1] for x in _SYNC_TO]
|
|
|
|
|
|
@pytest.mark.parametrize(("ini_config", "app_config", "status"), _SYNC_TO, ids=_SYNC__TO_IDS)
|
|
def testConfigFile_sync_to_app(ini_config, app_config, status, mocker):
|
|
""" Test :class:`lib.config.ini.ConfigFile._sync_to_app` logic """
|
|
|
|
for sect in app_config.values(): # Add a dummy datatype param to FSConfig
|
|
for opt in sect.options.values():
|
|
setattr(opt, "datatype", str)
|
|
|
|
instance = ini_mod.ConfigFile("test")
|
|
instance._get_converted_value = mocker.MagicMock(return_value="updated_value")
|
|
|
|
for section, opts in ini_config.items(): # Load up the dummy ini info
|
|
instance._parser.add_section(section)
|
|
for name, opt in opts.items():
|
|
instance._parser[section][name] = opt
|
|
|
|
instance._sync_to_app(app_config)
|
|
|
|
app_values = {sname: set(v.value for v in sect.options.values())
|
|
for sname, sect in app_config.items()}
|
|
sect_values = {sname: set(instance._parser[sname].values())
|
|
for sname in instance._parser.sections()}
|
|
|
|
if status == "synced": # No items change
|
|
instance._get_converted_value.assert_not_called()
|
|
else: # 2 items updated in the config.ini
|
|
assert instance._get_converted_value.call_count == 2
|
|
|
|
# App and ini values must now match
|
|
assert set(app_values) == set(sect_values)
|
|
for sect in app_values:
|
|
assert set(app_values[sect]) == set(sect_values[sect])
|
|
|
|
|
|
@pytest.mark.parametrize("structure_synced",
|
|
(True, False),
|
|
ids=("struc_synced", "not_struc_synced"))
|
|
@pytest.mark.parametrize("exists", (True, False), ids=("exists", "not_exists"))
|
|
def testConfigFile_sync_on_load(structure_synced, exists, mocker):
|
|
""" Test :class:`lib.config.ini.ConfigFile.on_load` logic """
|
|
mocker.patch("lib.config.ini.ConfigFile._exists", exists)
|
|
_, app_config = get_local_remote()
|
|
|
|
instance = ini_mod.ConfigFile("test")
|
|
instance._sync_from_app = mocker.MagicMock()
|
|
instance._sync_to_app = mocker.MagicMock()
|
|
instance._is_synced_structure = mocker.MagicMock(return_value=structure_synced)
|
|
|
|
instance.on_load(app_config)
|
|
|
|
instance._is_synced_structure.assert_called_once_with(app_config)
|
|
instance._sync_to_app.assert_called_once_with(app_config)
|
|
|
|
if not exists or not structure_synced:
|
|
instance._sync_from_app.assert_called_with(app_config)
|
|
call_count = 2 if (not exists and not structure_synced) else 1
|
|
else:
|
|
call_count = 0
|
|
assert instance._sync_from_app.call_count == call_count
|
|
|
|
|
|
@pytest.mark.parametrize("app_config",
|
|
(get_local_remote([2, 1, 3])[1],
|
|
get_local_remote([4, 2, 6, 8])[1],
|
|
get_local_remote([3])[1]))
|
|
def testConfigFile_sync_update_from_app(app_config, mocker):
|
|
""" Test :class:`lib.config.ini.ConfigFile.update_from_app` logic """
|
|
instance = ini_mod.ConfigFile("test")
|
|
instance.save = mocker.MagicMock()
|
|
for sect in app_config:
|
|
# Updating from app always replaces the existing parser with a new one
|
|
assert sect not in instance._parser.sections()
|
|
|
|
instance.update_from_app(app_config)
|
|
|
|
instance.save.assert_called_once()
|
|
for sect_name, sect in app_config.items():
|
|
assert sect_name in instance._parser.sections()
|
|
for opt_name, val in sect.options.items():
|
|
assert opt_name in instance._parser[sect_name]
|
|
assert instance._parser[sect_name][opt_name] == val.ini_value
|