diff --git a/src/mccode_antlr/translators/target.py b/src/mccode_antlr/translators/target.py index b0f26d9..362bd32 100644 --- a/src/mccode_antlr/translators/target.py +++ b/src/mccode_antlr/translators/target.py @@ -216,8 +216,10 @@ def prefetch_data_files(self): Scans every component instance's string parameters. For each literal string value that matches a registered data file (i.e. its registry path - includes a ``data/`` path component) the file is fetched into the local - Pooch cache so it is available before the compiled C instrument runs. + has a directory named ``data`` somewhere among its ancestors -- directly, + as in ``data/foo.dat``, or nested, as in ``data/ISIS_tables/TS2.imat``) + the file is fetched into the local Pooch cache so it is available before + the compiled C instrument runs. """ from pathlib import Path if not self.registries: @@ -240,13 +242,18 @@ def prefetch_data_files(self): # Only consider exact basename matches inside a data/ directory. # reg.known(name, strict=True) checks Path(x).name == name which is # exact, but doesn't filter by directory. We additionally require the - # matched file to live under data/ to avoid fetching non-data assets. + # matched file to live under a directory named data/ -- at any depth, + # since McStas nests some data families a level deeper (data/ISIS_tables/, + # data/Gas_tables/, ...) -- to avoid fetching non-data assets. if not reg.known(name, strict=True): continue # Use the exact fullname (default exact=True) to avoid the loose # substring fallback that raises on multiple matches. fullname = reg.fullname(name) - if fullname is None or f'data/{name}' not in str(fullname).replace('\\', '/'): + if fullname is None: + continue + parts = str(fullname).replace('\\', '/').split('/') + if 'data' not in parts[:-1]: continue cached = reg.path(name) logger.info( diff --git a/tests/translators/test_prefetch_data_files.py b/tests/translators/test_prefetch_data_files.py new file mode 100644 index 0000000..fd5f321 --- /dev/null +++ b/tests/translators/test_prefetch_data_files.py @@ -0,0 +1,107 @@ +"""TargetVisitor.prefetch_data_files must find data files nested a level +deeper than data/, e.g. McStas's data/ISIS_tables/ and data/Gas_tables/ +families (issue #329).""" +from mccode_antlr import Flavor +from mccode_antlr.translators.target import TargetVisitor + + +class FakeValue: + def __init__(self, value): + self.is_str = True + self.has_value = True + self.value = value + + +class FakeParam: + def __init__(self, value): + self.value = FakeValue(value) + + +class FakeComponent: + def __init__(self, *values): + self.parameters = [FakeParam(v) for v in values] + + +class FakeRegistry: + """Mimics the bits of RemoteRegistry/LocalRegistry that prefetch_data_files + relies on: known()/fullname() resolve a bare name against a fixed mapping + of registered files, and path() records what was actually fetched.""" + + def __init__(self, files: dict[str, str]): + # files: {bare_name: resolved registry path} + self._files = files + self.fetched = [] + + def known(self, name, ext=None, strict=False): + return name in self._files + + def fullname(self, name, ext=None, exact=True): + return self._files.get(name) + + def path(self, name, ext=None, exact=True): + self.fetched.append(name) + return f'/cache/{name}' + + +class DummyInstr: + name = 'dummy' + + def __init__(self, registries, components): + self.registries = registries + self.components = components + + def verify_instance_parameters(self): + return None + + +def _prefetch(registry_files, param_value): + reg = FakeRegistry(registry_files) + instr = DummyInstr([reg], [FakeComponent(param_value)]) + visitor = TargetVisitor(instr, Flavor.MCSTAS) + visitor.prefetch_data_files() + return reg + + +class TestNestedDataDirectory: + def test_file_nested_under_a_data_subdirectory_is_fetched(self): + """TS2.imat lives at mcstas-comps/data/ISIS_tables/TS2.imat -- one + directory deeper than the plain data/ case.""" + reg = _prefetch( + {'TS2.imat': 'mcstas-comps/data/ISIS_tables/TS2.imat'}, + '"TS2.imat"', + ) + assert reg.fetched == ['TS2.imat'] + + def test_gas_tables_family_is_also_fetched(self): + reg = _prefetch( + {'He3.gas': 'mcstas-comps/data/Gas_tables/He3.gas'}, + '"He3.gas"', + ) + assert reg.fetched == ['He3.gas'] + + def test_file_directly_under_data_still_works(self): + """No regression on the original, simpler data/ layout.""" + reg = _prefetch( + {'some_file.dat': 'mcstas-comps/data/some_file.dat'}, + '"some_file.dat"', + ) + assert reg.fetched == ['some_file.dat'] + + def test_file_outside_any_data_directory_is_not_fetched(self): + """A same-named non-data asset elsewhere in the registry must not be + pulled in just because it shares a basename with a string parameter.""" + reg = _prefetch( + {'Something.comp': 'mcstas-comps/contrib/Something.comp'}, + '"Something.comp"', + ) + assert reg.fetched == [] + + def test_a_file_literally_named_data_does_not_spuriously_match(self): + """The check excludes the filename itself when looking for a `data` + ancestor, so a bare top-level file named `data.ext` isn't treated as + living under a directory called `data`.""" + reg = _prefetch( + {'data.ext': 'mcstas-comps/data.ext'}, + '"data.ext"', + ) + assert reg.fetched == []