mirror of
https://github.com/vladmandic/automatic
synced 2026-08-27 07:31:01 +02:00
daf8c0d9bd
Pins the cases that separate a complete slice from a truncated one, including the zero-argument script whose empty slice is complete, and asserts no hook runner slices the vector on its own.
287 lines
9.3 KiB
Python
287 lines
9.3 KiB
Python
#!/usr/bin/env python
|
|
"""
|
|
Offline unit tests for script argument resolution in modules.scripts_manager.
|
|
|
|
A hook is driven by a slice of the processing object's script args, taken from the range the
|
|
script recorded when its UI was built. The vector is only as long as the caller made it, so
|
|
the range and the vector can disagree:
|
|
|
|
- a truncated slice would splat fewer positionals than the hook signature takes
|
|
- a script that declares no arguments has a complete slice even when the vector is empty
|
|
- a caller-supplied override replaces the slice outright
|
|
|
|
Covers ``resolve_script_args`` over those cases, and asserts no hook runner slices the vector
|
|
on its own.
|
|
|
|
No running server required.
|
|
|
|
Usage:
|
|
python test/test-script-args.py
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
import inspect
|
|
|
|
script_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
sys.path.insert(0, script_dir)
|
|
os.chdir(script_dir)
|
|
|
|
os.environ['SD_INSTALL_QUIET'] = '1'
|
|
|
|
# Bootstrap cmd_args before any module that pulls in shared.py.
|
|
import modules.cmd_args # pylint: disable=wrong-import-position
|
|
import installer # pylint: disable=wrong-import-position
|
|
orig_argv = sys.argv
|
|
sys.argv = [sys.argv[0]]
|
|
try:
|
|
modules.cmd_args.parse_args()
|
|
finally:
|
|
sys.argv = orig_argv
|
|
installer.add_args(modules.cmd_args.parser)
|
|
modules.cmd_args.parsed, _ = modules.cmd_args.parser.parse_known_args([])
|
|
|
|
from modules.errors import log # pylint: disable=wrong-import-position
|
|
|
|
|
|
results: dict[str, dict] = {}
|
|
|
|
|
|
def category(name: str):
|
|
if name not in results:
|
|
results[name] = {'passed': 0, 'failed': 0, 'skipped': 0, 'tests': []}
|
|
return name
|
|
|
|
|
|
def record(cat: str, passed: bool, name: str, detail: str = ''):
|
|
status = 'PASS' if passed else 'FAIL'
|
|
results[cat]['passed' if passed else 'failed'] += 1
|
|
results[cat]['tests'].append((status, name))
|
|
msg = f' {status}: {name}'
|
|
if detail:
|
|
msg += f' ({detail})'
|
|
if passed:
|
|
log.info(msg)
|
|
else:
|
|
log.error(msg)
|
|
|
|
|
|
def skip(cat: str, name: str, reason: str):
|
|
results[cat]['skipped'] += 1
|
|
results[cat]['tests'].append(('SKIP', name))
|
|
log.warning(f' SKIP: {name} ({reason})')
|
|
|
|
|
|
def run_test(cat: str, fn):
|
|
name = fn.__name__
|
|
try:
|
|
ok = fn()
|
|
if ok is False:
|
|
record(cat, False, name)
|
|
elif isinstance(ok, str):
|
|
skip(cat, name, ok)
|
|
else:
|
|
record(cat, True, name)
|
|
except AssertionError as e:
|
|
record(cat, False, name, str(e))
|
|
except Exception as e: # pylint: disable=broad-except
|
|
record(cat, False, name, f'exception: {type(e).__name__}: {e}')
|
|
|
|
|
|
class StubScript:
|
|
"""What resolve_script_args reads off a script: a title and the range its ui claimed."""
|
|
def __init__(self, args_from=None, args_to=None, name='stub'):
|
|
self.name = name
|
|
if args_from is not None:
|
|
self.args_from = args_from
|
|
if args_to is not None:
|
|
self.args_to = args_to
|
|
|
|
def title(self):
|
|
return self.name
|
|
|
|
|
|
def resolver():
|
|
from modules import scripts_manager
|
|
return scripts_manager.resolve_script_args
|
|
|
|
|
|
# ============================================================
|
|
# The declared range against the vector
|
|
# ============================================================
|
|
|
|
def test_full_vector_gives_the_declared_slice():
|
|
resolve = resolver()
|
|
assert resolve(StubScript(0, 3), ['a', 'b', 'c']) == ['a', 'b', 'c']
|
|
assert resolve(StubScript(1, 3), ['a', 'b', 'c']) == ['b', 'c']
|
|
|
|
|
|
def test_longer_vector_still_gives_the_declared_slice():
|
|
resolve = resolver()
|
|
assert resolve(StubScript(2, 4), ['a', 'b', 'c', 'd', 'e']) == ['c', 'd']
|
|
|
|
|
|
def test_truncated_slice_is_refused():
|
|
resolve = resolver()
|
|
assert resolve(StubScript(2, 5), ['a', 'b', 'c', 'd']) is None
|
|
|
|
|
|
def test_empty_vector_against_declared_args_is_refused():
|
|
resolve = resolver()
|
|
assert resolve(StubScript(0, 3), []) is None
|
|
assert resolve(StubScript(0, 3), ()) is None
|
|
|
|
|
|
def test_script_declaring_no_args_still_runs():
|
|
"""The case a plain empty-slice rule would break: nothing to fill, so nothing is missing."""
|
|
resolve = resolver()
|
|
assert resolve(StubScript(5, 5), []) == []
|
|
assert resolve(StubScript(5, 5), ['a', 'b']) == []
|
|
|
|
|
|
def test_uninitialized_range_is_refused():
|
|
resolve = resolver()
|
|
assert resolve(StubScript(0, 0), ['a', 'b']) is None
|
|
|
|
|
|
def test_inverted_range_is_refused():
|
|
resolve = resolver()
|
|
assert resolve(StubScript(5, 3), ['a', 'b', 'c', 'd', 'e', 'f']) is None
|
|
|
|
|
|
def test_missing_range_attributes_are_refused():
|
|
resolve = resolver()
|
|
assert resolve(StubScript(), ['a', 'b']) is None
|
|
assert resolve(StubScript(args_from=0), ['a', 'b']) is None
|
|
assert resolve(StubScript(args_to=2), ['a', 'b']) is None
|
|
|
|
|
|
# ============================================================
|
|
# Caller-supplied overrides
|
|
# ============================================================
|
|
|
|
def test_override_wins_over_the_slice():
|
|
resolve = resolver()
|
|
script = StubScript(0, 3, name='over')
|
|
assert resolve(script, ['a', 'b', 'c'], {'over': ['x']}) == ['x']
|
|
|
|
|
|
def test_override_wins_even_when_the_vector_is_short():
|
|
"""An override is the whole point when the caller never built a vector."""
|
|
resolve = resolver()
|
|
script = StubScript(0, 3, name='over')
|
|
assert resolve(script, [], {'over': ['x', 'y', 'z']}) == ['x', 'y', 'z']
|
|
|
|
|
|
def test_override_holding_none_falls_through():
|
|
resolve = resolver()
|
|
script = StubScript(0, 2, name='over')
|
|
assert resolve(script, ['a', 'b'], {'over': None}) == ['a', 'b']
|
|
|
|
|
|
def test_override_holding_an_empty_list_is_honored():
|
|
resolve = resolver()
|
|
script = StubScript(0, 2, name='over')
|
|
assert resolve(script, ['a', 'b'], {'over': []}) == []
|
|
|
|
|
|
def test_override_for_another_script_is_ignored():
|
|
resolve = resolver()
|
|
script = StubScript(0, 2, name='mine')
|
|
assert resolve(script, ['a', 'b'], {'other': ['x']}) == ['a', 'b']
|
|
|
|
|
|
def test_no_override_table_falls_through():
|
|
resolve = resolver()
|
|
script = StubScript(0, 2)
|
|
assert resolve(script, ['a', 'b'], None) == ['a', 'b']
|
|
assert resolve(script, ['a', 'b'], {}) == ['a', 'b']
|
|
|
|
|
|
# ============================================================
|
|
# The runners route through the resolver
|
|
# ============================================================
|
|
|
|
def test_no_hook_runner_slices_the_vector_itself():
|
|
from modules import scripts_manager
|
|
source = inspect.getsource(scripts_manager.ScriptRunner)
|
|
assert 'p.script_args[' not in source, 'a hook runner slices the vector instead of resolving it'
|
|
assert 'args[script.args_from:script.args_to]' not in source, 'a runner slices a vector instead of resolving it'
|
|
|
|
|
|
def test_every_alwayson_runner_resolves():
|
|
from modules import scripts_manager
|
|
hooks = ['before_process', 'process', 'process_images', 'before_process_batch', 'process_batch',
|
|
'postprocess', 'postprocess_batch', 'postprocess_batch_list', 'postprocess_image']
|
|
for hook in hooks:
|
|
fn = getattr(scripts_manager.ScriptRunner, hook, None)
|
|
assert fn is not None, f'{hook} is missing'
|
|
source = inspect.getsource(fn)
|
|
assert 'resolve_script_args' in source, f'{hook} does not resolve its args'
|
|
|
|
|
|
def test_selectable_paths_resolve():
|
|
from modules import scripts_manager
|
|
for hook in ['run', 'after']:
|
|
source = inspect.getsource(getattr(scripts_manager.ScriptRunner, hook))
|
|
assert 'resolve_script_args' in source, f'{hook} does not resolve its args'
|
|
|
|
|
|
def run_all():
|
|
log.warning('=== declared range ===')
|
|
cat = category('range')
|
|
for fn in [
|
|
test_full_vector_gives_the_declared_slice,
|
|
test_longer_vector_still_gives_the_declared_slice,
|
|
test_truncated_slice_is_refused,
|
|
test_empty_vector_against_declared_args_is_refused,
|
|
test_script_declaring_no_args_still_runs,
|
|
test_uninitialized_range_is_refused,
|
|
test_inverted_range_is_refused,
|
|
test_missing_range_attributes_are_refused,
|
|
]:
|
|
run_test(cat, fn)
|
|
|
|
log.warning('=== overrides ===')
|
|
cat = category('override')
|
|
for fn in [
|
|
test_override_wins_over_the_slice,
|
|
test_override_wins_even_when_the_vector_is_short,
|
|
test_override_holding_none_falls_through,
|
|
test_override_holding_an_empty_list_is_honored,
|
|
test_override_for_another_script_is_ignored,
|
|
test_no_override_table_falls_through,
|
|
]:
|
|
run_test(cat, fn)
|
|
|
|
log.warning('=== runners ===')
|
|
cat = category('runners')
|
|
for fn in [
|
|
test_no_hook_runner_slices_the_vector_itself,
|
|
test_every_alwayson_runner_resolves,
|
|
test_selectable_paths_resolve,
|
|
]:
|
|
run_test(cat, fn)
|
|
|
|
log.warning('=== Results ===')
|
|
total_passed = 0
|
|
total_failed = 0
|
|
total_skipped = 0
|
|
for cat_name, info in results.items():
|
|
ok = info['failed'] == 0
|
|
status = 'PASS' if ok else 'FAIL'
|
|
log.info(f" {cat_name}: {info['passed']} passed, {info['failed']} failed, {info['skipped']} skipped [{status}]")
|
|
total_passed += info['passed']
|
|
total_failed += info['failed']
|
|
total_skipped += info['skipped']
|
|
log.warning(f'Total: {total_passed} passed, {total_failed} failed, {total_skipped} skipped')
|
|
return total_failed == 0
|
|
|
|
|
|
if __name__ == '__main__':
|
|
import time
|
|
t0 = time.time()
|
|
success = run_all()
|
|
log.warning(f'Total time: {time.time() - t0:.2f}s')
|
|
sys.exit(0 if success else 1)
|