"""Dependency direction, cache coverage and explicit composition inputs.""" import ast import json import pathlib import subprocess import sys import unittest import unittest.mock import generator.cache import generator.composition import generator.role_cache import generator.source import generator.vocabulary import tests.solo_fixtures ROOT = pathlib.Path(__file__).resolve().parents[1] class GeneratorBoundaries(unittest.TestCase): def test_generator_imports_are_acyclic(self): files = {'.'.join(path.relative_to(ROOT).with_suffix('').parts): path for path in (ROOT / 'generator').rglob('*.py')} graph = {name: set(generator.cache.imports(path.read_bytes())) & files.keys() for name, path in files.items()} visited, active = set(), [] def visit(name): self.assertNotIn(name, active, ' -> '.join([*active, name])) if name in visited: return active.append(name) for dependency in graph[name]: visit(dependency) active.pop() visited.add(name) for name in graph: visit(name) def test_composition_import_does_not_load_rendering_or_publication(self): code = "import generator.composition, sys, json; print(json.dumps(sorted(sys.modules)))" result = subprocess.run([sys.executable, '-c', code], cwd=ROOT, capture_output=True, text=True, check=True) forbidden = ('PIL', 'fontTools', 'generator.engraving', 'generator.production', 'generator.score_io') self.assertFalse([name for name in json.loads(result.stdout) if name.startswith(forbidden)]) def test_all_previously_missing_role_dependencies_change_the_fingerprint(self): baseline = generator.role_cache.code_key() read = pathlib.Path.read_bytes for name in ('solo/policy.py', 'source.py', 'accompaniment.py'): changed = ROOT / 'generator' / name with self.subTest(dependency=name): def contents(path): return read(path) + (b'\n# changed implementation\n' if path == changed else b'') with unittest.mock.patch.object(pathlib.Path, 'read_bytes', contents): self.assertNotEqual(generator.role_cache.code_key(), baseline) def test_context_owns_immutable_resources_and_survives_caller_mutation(self): resources = generator.vocabulary.load() context = generator.vocabulary.CompositionContext.create(resources, 'electric_accompaniment') expected = context.role()['center'] resources['roles']['roles']['electric_accompaniment']['center'] = 999 self.assertEqual(context.role()['center'], expected) with self.assertRaises(TypeError): context.data('roles')['roles']['electric_accompaniment']['center'] = 999 self.assertNotEqual(generator.vocabulary.load()['roles']['roles']['electric_accompaniment']['center'], 999) def test_role_generation_does_not_consult_ambient_policy(self): source = generator.source.SourceAnalysis.read(tests.solo_fixtures.song(count=1, chords=('CM7',))) with unittest.mock.patch('generator.vocabulary.current', side_effect=AssertionError('implicit policy dependency')): for role in generator.composition_types.ROLES: with self.subTest(role=role): generator.composition.generate_role(source, role, ROOT / 'data', {'predict': True})