Repository: ethereum/execution-spec-tests
Pull request #1439: feat(static tests) : do not compile test fillers source code while loading the models
Author: login: winsvega
Labels: name: type:feat
description: type: Feature
name: scope:fill
description: Scope: fill command
Created: 2025-04-11T09:19:10Z | Merged: 2025-04-23T22:23:04Z

@marioevz  please don't merge there are bullet points to complete:

- [x] When reading Filler.json into pydantic do not compile test source right away. this speeds up test generation
- [x] Support lllc container docker image, so no need to have lllc compiled locally. can import from docker hub: winsvega/lllc:latest
- [x] After all tests generated or on exception fail stop, need to call stop_lllc_containers() function 

_- [x] Make sure exceptions are working correctly after mapper refactoring (too many things to fix)_ https://github.com/ethereum/execution-specs/issues/1538


the test generation is supported with lllc from docker hub. 
https://hub.docker.com/r/winsvega/lllc

it works fast using docker exec. all we need is to pull a docker image in our CI


Diff:
diff --git a/src/ethereum_test_fixtures/collector.py b/src/ethereum_test_fixtures/collector.py
index 10ed2376319..d93cc1fd7bb 100644
--- a/src/ethereum_test_fixtures/collector.py
+++ b/src/ethereum_test_fixtures/collector.py
@@ -109,6 +109,7 @@ class FixtureCollector:
 
     output_dir: Path
     flat_output: bool
+    fill_static_tests: bool
     single_fixture_per_file: bool
     filler_path: Path
     base_dump_dir: Optional[Path] = None
@@ -125,6 +126,12 @@ def get_fixture_basename(self, info: TestInfo) -> Path:
             return Path(info.get_single_test_name(mode="module"))
         else:
             module_relative_output_dir = info.get_module_relative_output_dir(self.filler_path)
+
+            # Each legacy test filler has only 1 test per file if it's a !state test!
+            # So no need to create directory Add11/add11.json it can be plain add11.json
+            if self.fill_static_tests:
+                return module_relative_output_dir.parent / info.original_name
+
             if self.single_fixture_per_file:
                 return module_relative_output_dir / info.get_single_test_name(mode="test")
             return module_relative_output_dir / info.get_single_test_name(mode="module")
diff --git a/src/ethereum_test_specs/static_state/account.py b/src/ethereum_test_specs/static_state/account.py
index 779087fb02e..3a2e2d3854a 100644
--- a/src/ethereum_test_specs/static_state/account.py
+++ b/src/ethereum_test_specs/static_state/account.py
@@ -19,3 +19,4 @@ class Config:
         """Model Config."""
 
         extra = "forbid"
+        arbitrary_types_allowed = True  # For CodeInFiller
diff --git a/src/ethereum_test_specs/static_state/common/common.py b/src/ethereum_test_specs/static_state/common/common.py
index e7f46b94c81..15e0d53ea2d 100644
--- a/src/ethereum_test_specs/static_state/common/common.py
+++ b/src/ethereum_test_specs/static_state/common/common.py
@@ -3,15 +3,15 @@
 import re
 import subprocess
 import tempfile
-from typing import Tuple
+from functools import cached_property
+from typing import Any
 
 from eth_abi import encode
 from eth_utils import function_signature_to_4byte_selector
-from pydantic import BaseModel, Field
 from pydantic.functional_validators import BeforeValidator
 from typing_extensions import Annotated
 
-from ethereum_test_base_types import Address, Bytes, Hash, HexNumber
+from ethereum_test_base_types import Address, Hash, HexNumber
 
 from .compile_yul import compile_yul
 
@@ -30,12 +30,6 @@ def parse_hex_number(i: str | int) -> int:
     return int(i, 10)
 
 
-class CodeOptions(BaseModel):
-    """Define options of the code."""
-
-    label: str = Field("")
-
-
 def parse_args_from_string_into_array(stream: str, pos: int, delim: str = " "):
     """Parse YUL options into array."""
     args = []
@@ -53,116 +47,146 @@ def parse_args_from_string_into_array(stream: str, pos: int, delim: str = " "):
     return args, pos
 
 
-def parse_code(code: str) -> Tuple[bytes, CodeOptions]:
-    """Check if the given string is a valid code."""
-    # print("parse `" + str(code) + "`")
-    if isinstance(code, int):
-        # Users pass code as int (very bad)
-        hex_str = format(code, "02x")
-        return (bytes.fromhex(hex_str), CodeOptions())
-    if not isinstance(code, str):
-        raise ValueError(f"parse_code(code: str) code is not string: {code}")
-    if len(code) == 0:
-        return (bytes.fromhex(""), CodeOptions())
-
-    compiled_code = ""
-    code_options: CodeOptions = CodeOptions()
-
-    raw_marker = ":raw 0x"
-    raw_index = code.find(raw_marker)
-    abi_marker = ":abi"
-    abi_index = code.find(abi_marker)
+class CodeInFillerSource:
+    """Not compiled code source in test filler."""
+
+    code_label: str | None
+    code_raw: Any
+
+    def __init__(self, code: Any, label: str | None = None):
+        """Instantiate."""
+        self.code_label = label
+        self.code_raw = code
+
+    @cached_property
+    def compiled(self) -> bytes:
+        """Compile the code from source to bytes."""
+        if isinstance(self.code_raw, int):
+            # Users pass code as int (very bad)
+            hex_str = format(self.code_raw, "02x")
+            return bytes.fromhex(hex_str)
+
+        if not isinstance(self.code_raw, str):
+            raise ValueError(f"parse_code(code: str) code is not string: {self.code_raw}")
+        if len(self.code_raw) == 0:
+            return b""
+
+        compiled_code = ""
+
+        raw_marker = ":raw 0x"
+        raw_index = self.code_raw.find(raw_marker)
+        abi_marker = ":abi"
+        abi_index = self.code_raw.find(abi_marker)
+        yul_marker = ":yul"
+        yul_index = self.code_raw.find(yul_marker)
+
+        # Parse :raw
+        if raw_index != -1:
+            compiled_code = self.code_raw[raw_index + len(raw_marker) :]
+
+        # Parse :yul
+        elif yul_index != -1:
+            option_start = yul_index + len(yul_marker)
+            options: list[str] = []
+            native_yul_options: str = ""
+
+            if self.code_raw[option_start:].lstrip().startswith("{"):
+                # No yul options, proceed to code parsing
+                source_start = option_start
+            else:
+                opt, source_start = parse_args_from_string_into_array(
+                    self.code_raw, option_start + 1
+                )
+                for arg in opt:
+                    if arg == "object" or arg == '"C"':
+                        native_yul_options += arg + " "
+                    else:
+                        options.append(arg)
+
+            with tempfile.NamedTemporaryFile(mode="w+", delete=False) as tmp:
+                tmp.write(native_yul_options + self.code_raw[source_start:])
+                tmp_path = tmp.name
+            compiled_code = compile_yul(
+                source_file=tmp_path,
+                evm_version=options[0] if len(options) >= 1 else None,
+                optimize=options[1] if len(options) >= 2 else None,
+            )[2:]
+
+        # Parse :abi
+        elif abi_index != -1:
+            abi_encoding = self.code_raw[abi_index + len(abi_marker) + 1 :]
+            tokens = abi_encoding.strip().split()
+            abi = tokens[0]
+            function_signature = function_signature_to_4byte_selector(abi)
+            parameter_str = re.sub(r"^\w+", "", abi).strip()
+
+            parameter_types = parameter_str.strip("()").split(",")
+            if len(tokens) > 1:
+                function_parameters = encode(
+                    [parameter_str],
+                    [
+                        [
+                            int(t.lower(), 0) & ((1 << 256) - 1)  # treat big ints as 256bits
+                            if parameter_types[t_index] == "uint"
+                            else int(t.lower(), 0) > 0  # treat positive values as True
+                            if parameter_types[t_index] == "bool"
+                            else False and ValueError("unhandled parameter_types")
+                            for t_index, t in enumerate(tokens[1:])
+                        ]
+                    ],
+                )
+                return function_signature + function_parameters
+            return function_signature
+
+        # Parse plain code 0x
+        elif self.code_raw.lstrip().startswith("0x"):
+            compiled_code = self.code_raw[2:].lower()
+
+        # Parse lllc code
+        elif self.code_raw.lstrip().startswith("{") or self.code_raw.lstrip().startswith("(asm"):
+            with tempfile.NamedTemporaryFile(mode="w+", delete=False) as tmp:
+                tmp.write(self.code_raw)
+                tmp_path = tmp.name
+
+            # - using lllc
+            result = subprocess.run(["lllc", tmp_path], capture_output=True, text=True)
+
+            # - using docker:
+            #   If the running machine does not have lllc installed, we can use docker to run lllc,
+            #   but we need to start a container first, and the process is generally slower.
+            # from .docker import get_lllc_container_id
+            # result = subprocess.run(
+            #     ["docker", "exec", get_lllc_container_id(), "lllc", tmp_path[5:]],
+            #     capture_output=True,
+            #     text=True,
+            # )
+            compiled_code = "".join(result.stdout.splitlines())
+        else:
+            raise Exception(f'Error parsing code: "{self.code_raw}"')
+
+        try:
+            return bytes.fromhex(compiled_code)
+        except ValueError as e:
+            raise Exception(f'Error parsing compile code: "{self.code_raw}"') from e
+
+
+def parse_code_label(code) -> CodeInFillerSource:
+    """Parse label from code."""
     label_marker = ":label"
     label_index = code.find(label_marker)
-    yul_marker = ":yul"
-    yul_index = code.find(yul_marker)
 
     # Parse :label into code options
+    label = None
     if label_index != -1:
         space_index = code.find(" ", label_index + len(label_marker) + 1)
         if space_index == -1:
             label = code[label_index + len(label_marker) + 1 :]
         else:
             label = code[label_index + len(label_marker) + 1 : space_index]
-        code_options.label = label
-
-    # Prase :raw
-    if raw_index != -1:
-        compiled_code = code[raw_index + len(raw_marker) :]
-
-    elif yul_index != -1:
-        option_start = yul_index + len(yul_marker)
-        options: list[str] = []
-        native_yul_options: str = ""
-
-        if code[option_start:].lstrip().startswith("{"):
-            # No yul options, proceed to code parsing
-            source_start = option_start
-        else:
-            opt, source_start = parse_args_from_string_into_array(code, option_start + 1)
-            for arg in opt:
-                if arg == "object" or arg == '"C"':
-                    native_yul_options += arg + " "
-                else:
-                    options.append(arg)
-
-        with tempfile.NamedTemporaryFile(mode="w+", delete=False) as tmp:
-            tmp.write(native_yul_options + code[source_start:])
-            tmp_path = tmp.name
-        compiled_code = compile_yul(
-            source_file=tmp_path,
-            evm_version=options[0] if len(options) >= 1 else None,
-            optimize=options[1] if len(options) >= 2 else None,
-        )[2:]
-
-    # Prase :abi
-    elif abi_index != -1:
-        abi_encoding = code[abi_index + len(abi_marker) + 1 :]
-        tokens = abi_encoding.strip().split()
-        abi = tokens[0]
-        function_signature = function_signature_to_4byte_selector(abi)
-        parameter_str = re.sub(r"^\w+", "", abi).strip()
-
-        parameter_types = parameter_str.strip("()").split(",")
-        if len(tokens) > 1:
-            function_parameters = encode(
-                [parameter_str],
-                [
-                    [
-                        int(t.lower(), 0) & ((1 << 256) - 1)  # treat big ints as 256bits
-                        if parameter_types[t_index] == "uint"
-                        else int(t.lower(), 0) > 0  # treat positive values as True
-                        if parameter_types[t_index] == "bool"
-                        else False and ValueError("unhandled parameter_types")
-                        for t_index, t in enumerate(tokens[1:])
-                    ]
-                ],
-            )
-            return (function_signature + function_parameters, code_options)
-        return (function_signature, code_options)
-
-    # Prase plain code 0x
-    elif code.lstrip().startswith("0x"):
-        compiled_code = code[2:].lower()
-
-    # Prase lllc code
-    elif code.lstrip().startswith("{") or code.lstrip().startswith("(asm"):
-        binary_path = "lllc"
-        with tempfile.NamedTemporaryFile(mode="w+", delete=False) as tmp:
-            tmp.write(code)
-            tmp_path = tmp.name
-        result = subprocess.run([binary_path, tmp_path], capture_output=True, text=True)
-        compiled_code = "".join(result.stdout.splitlines())
-    else:
-        raise Exception(f'Error parsing code: "{code}"')
-
-    try:
-        return (bytes.fromhex(compiled_code), code_options)
-    except ValueError as e:
-        raise Exception(f'Error parsing compile code: "{code}"') from e
+    return CodeInFillerSource(code, label)
 
 
 AddressInFiller = Annotated[Address, BeforeValidator(lambda a: Address(a, left_padding=True))]
 ValueInFiller = Annotated[HexNumber, BeforeValidator(parse_hex_number)]
-CodeInFiller = Annotated[Tuple[Bytes, CodeOptions], BeforeValidator(parse_code)]
+CodeInFiller = Annotated[CodeInFillerSource, BeforeValidator(parse_code_label)]
 Hash32InFiller = Annotated[Hash, BeforeValidator(lambda h: Hash(h, left_padding=True))]
diff --git a/src/ethereum_test_specs/static_state/common/docker.py b/src/ethereum_test_specs/static_state/common/docker.py
new file mode 100644
index 00000000000..1b6d95e2d2c
--- /dev/null
+++ b/src/ethereum_test_specs/static_state/common/docker.py
@@ -0,0 +1,72 @@
+"""Manage lllc docker container."""
+
+import subprocess
+import threading
+
+# Global variable to store the container id once instantiated.
+container_id = None
+
+# A global lock to control concurrent access.
+container_lock = threading.Lock()
+
+
+def get_lllc_container_id():
+    """
+    Return the container ID. If the container is not yet instantiated,
+    it acquires the lock and runs the Docker command to instantiate the container.
+    """
+    global container_id
+    # Acquire the lock so that only one thread can instantiate the container.
+    with container_lock:
+        if container_id is None:
+            try:
+                # Run the docker command using subprocess. The command is equivalent to:
+                # docker run -d --entrypoint tail -v /tmp:/tests -w /tests lllc -f /dev/null
+                result = subprocess.run(
+                    [
+                        "docker",
+                        "run",
+                        "-d",
+                        "--entrypoint",
+                        "tail",
+                        "-v",
+                        "/tmp:/tests",
+                        "-w",
+                        "/tests",
+                        "lllc",
+                        "-f",
+                        "/dev/null",
+                    ],
+                    check=True,
+                    stdout=subprocess.PIPE,
+                    stderr=subprocess.PIPE,
+                    text=True,
+                )
+                # The container id is expected to be printed to stdout.
+                container_id = result.stdout.strip()
+                print(f"Container instantiated with id: {container_id}")
+            except subprocess.CalledProcessError as e:
+                # In case of error, print the error message and raise the exception.
+                raise Exception("Error instantiating container:", e.stderr) from e
+    return container_id
+
+
+def stop_lllc_containers():
+    """Stop all running Docker containers that were started from the 'lllc' image."""
+    try:
+        # Retrieve container IDs for all running containers with the image 'lllc'
+        result = subprocess.check_output(
+            ["docker", "ps", "-q", "--filter", "ancestor=lllc"], text=True
+        )
+        container_ids = result.strip().splitlines()
+
+        if not container_ids:
+            print("No running containers for image 'lllc' found.")
+            return
+
+        # Iterate over each container ID and stop it.
+        for cid in container_ids:
+            subprocess.run(["docker", "stop", cid], check=True)
+
+    except subprocess.CalledProcessError as e:
+        raise Exception("Error while stopping containers:", e.stderr) from e
diff --git a/src/ethereum_test_specs/static_state/expect_section.py b/src/ethereum_test_specs/static_state/expect_section.py
index a427cf88f6a..a488edf072a 100644
--- a/src/ethereum_test_specs/static_state/expect_section.py
+++ b/src/ethereum_test_specs/static_state/expect_section.py
@@ -38,6 +38,7 @@ class Config:
         """Model Config."""
 
         extra = "forbid"
+        arbitrary_types_allowed = True  # For CodeInFiller
 
 
 class CMP(Enum):
diff --git a/src/ethereum_test_specs/static_state/general_transaction.py b/src/ethereum_test_specs/static_state/general_transaction.py
index 9462bfbc680..912c0d289e3 100644
--- a/src/ethereum_test_specs/static_state/general_transaction.py
+++ b/src/ethereum_test_specs/static_state/general_transaction.py
@@ -19,6 +19,7 @@ class Config:
         """Model Config."""
 
         extra = "forbid"
+        arbitrary_types_allowed = True  # For CodeInFiller
 
     @field_validator("access_list", mode="before")
     def convert_keys_to_hash(cls, access_list):  # noqa: N805
diff --git a/src/ethereum_test_specs/static_state/state_static.py b/src/ethereum_test_specs/static_state/state_static.py
index 43e2fc2a430..35d9dc890b2 100644
--- a/src/ethereum_test_specs/static_state/state_static.py
+++ b/src/ethereum_test_specs/static_state/state_static.py
@@ -4,6 +4,7 @@
 from typing import Any, Callable, ClassVar, Dict, List, Tuple
 
 import pytest
+from _pytest.mark.structures import ParameterSet
 
 from ethereum_test_base_types import Address, Hash, HexNumber, Storage, ZeroPaddedHexNumber
 from ethereum_test_exceptions import TransactionExceptionInstanceOrList
@@ -32,11 +33,23 @@ def model_post_init(self, context):
 
     def fill_function(self) -> Callable:
         """Return a StateTest spec from a static file."""
+        d_g_v_parameters: List[ParameterSet] = []
+        for d in range(len(self.transaction.data)):
+            for g in range(len(self.transaction.gas_limit)):
+                for v in range(len(self.transaction.value)):
+                    exception_test = False
+                    for expect in self.expect:
+                        if expect.has_index(d, g, v) and expect.expect_exception is not None:
+                            exception_test = True
+                    # TODO: This does not take into account exceptions that only happen on
+                    #       specific forks, but this requires a covariant parametrize
+                    marks = [pytest.mark.exception_test] if exception_test else []
+                    d_g_v_parameters.append(
+                        pytest.param(d, g, v, marks=marks, id=f"d{d}-g{g}-v{v}")
+                    )
 
         @pytest.mark.valid_at(*self.get_valid_at_forks())
-        @pytest.mark.parametrize("d", range(len(self.transaction.data)))
-        @pytest.mark.parametrize("g", range(len(self.transaction.gas_limit)))
-        @pytest.mark.parametrize("v", range(len(self.transaction.value)))
+        @pytest.mark.parametrize("d,g,v", d_g_v_parameters)
         def test_state_vectors(
             state_test: StateTestFiller,
             fork: Fork,
@@ -64,6 +77,11 @@ def test_state_vectors(
                         )
             pytest.fail(f"Expectation not found for d={d}, g={g}, v={v}, fork={fork}")
 
+        if self.info and self.info.pytest_marks:
+            for mark in self.info.pytest_marks:
+                apply_mark = getattr(pytest.mark, mark)
+                test_state_vectors = apply_mark(test_state_vectors)
+
         return test_state_vectors
 
     def get_valid_at_forks(self) -> List[str]:
@@ -110,11 +128,10 @@ def _get_pre(self) -> Alloc:
             for key, value in account.storage.items():
                 storage[key] = value
 
-            acc_code, acc_code_opt = account.code
             pre[account_address] = Account(
                 balance=account.balance,
                 nonce=account.nonce,
-                code=acc_code,
+                code=account.code.compiled,
                 storage=storage,
             )
         return pre
@@ -129,13 +146,11 @@ def _make_vector(
     ) -> Tuple[Alloc, Transaction]:
         """Compose test vector from test data."""
         general_tr = self.transaction
-        data = general_tr.data[d]
-
-        data_code, options = data.data
+        data_box = general_tr.data[d]
 
         tr: Transaction = Transaction(
-            data=data_code,
-            access_list=data.access_list,
+            data=data_box.data.compiled,
+            access_list=data_box.access_list,
             gas_limit=HexNumber(general_tr.gas_limit[g]),
             value=HexNumber(general_tr.value[v]),
             gas_price=general_tr.gas_price,
@@ -165,8 +180,7 @@ def _make_vector(
                         storage.set_expect_any(key)
                 account_kwargs["storage"] = storage
             if account.code is not None:
-                code_bytes, code_options = account.code
-                account_kwargs["code"] = code_bytes
+                account_kwargs["code"] = account.code.compiled
             if account.balance is not None:
                 account_kwargs["balance"] = account.balance
             if account.nonce is not None:
diff --git a/src/ethereum_test_specs/static_state/state_test_filler.py b/src/ethereum_test_specs/static_state/state_test_filler.py
index cf13469cd0c..f0d0174766a 100644
--- a/src/ethereum_test_specs/static_state/state_test_filler.py
+++ b/src/ethereum_test_specs/static_state/state_test_filler.py
@@ -17,7 +17,8 @@
 class Info(BaseModel):
     """Class that represents an info filler."""
 
-    comment: str
+    comment: str | None = Field(None)
+    pytest_marks: List[str] = Field(default_factory=list)
 
 
 class StateTestInFiller(BaseModel):
@@ -46,8 +47,7 @@ def parse_string_indexes(indexes: str) -> List[int]:
                 indexes = indexes.replace(":label ", "")
                 tx_matches: List[int] = []
                 for idx, d in enumerate(self.transaction.data):
-                    _, code_opt = d.data
-                    if indexes == code_opt.label:
+                    if indexes == d.data.code_label:
                         tx_matches.append(idx)
                 return tx_matches
             else:
diff --git a/src/pytest_plugins/filler/filler.py b/src/pytest_plugins/filler/filler.py
index c1201911a56..e90c8f29686 100644
--- a/src/pytest_plugins/filler/filler.py
+++ b/src/pytest_plugins/filler/filler.py
@@ -617,6 +617,7 @@ def fixture_collector(
     fixture_collector = FixtureCollector(
         output_dir=output_dir,
         flat_output=request.config.getoption("flat_output"),
+        fill_static_tests=request.config.getoption("fill_static_tests_enabled"),
         single_fixture_per_file=request.config.getoption("single_fixture_per_file"),
         filler_path=filler_path,
         base_dump_dir=base_dump_dir,
diff --git a/src/pytest_plugins/filler/static_filler.py b/src/pytest_plugins/filler/static_filler.py
index 456dede1643..c21aa29d3ce 100644
--- a/src/pytest_plugins/filler/static_filler.py
+++ b/src/pytest_plugins/filler/static_filler.py
@@ -86,8 +86,10 @@ def get_all_combinations_from_parametrize_marks(
     test_ids = set()
     for combination in itertools.product(*list_of_values):
         values: List[Any] = []
+        marks: List[pytest.Mark | pytest.MarkDecorator] = []
         for param_set in combination:
             values.extend(param_set.values)
+            marks.extend(param_set.marks)
         test_id = "-".join([param.id or "" for param in combination])
         if test_id in test_ids:
             current_int = 2
@@ -97,7 +99,7 @@ def get_all_combinations_from_parametrize_marks(
         all_value_combinations.append(
             ParameterSet(
                 values=values,
-                marks=[],
+                marks=marks,
                 id=test_id,
             )
         )
@@ -190,6 +192,13 @@ def collect(self: "FillerFile") -> Generator["FillerTestItem", None, None]:
                     )
                     intersection_set = get_intersection_set(key, validity_markers, self.config)
 
+                    extra_function_marks: List[pytest.Mark] = [
+                        mark
+                        for mark in function_marks
+                        if mark.name != "parametrize"
+                        and (mark.name not in [v.mark.name for v in validity_markers])
+                    ]
+
                     for format_with_or_without_label in fixture_formats:
                         fixture_format_parameter_set = labeled_format_parameter_set(
                             format_with_or_without_label
@@ -199,7 +208,7 @@ def collect(self: "FillerFile") -> Generator["FillerTestItem", None, None]:
                             if isinstance(format_with_or_without_label, LabeledFixtureFormat)
                             else format_with_or_without_label
                         )
-                        for fork in intersection_set:
+                        for fork in sorted(intersection_set):
                             params: Dict[str, Any] = {spec_parameter_name: fixture_format}
                             fixturenames = [
                                 spec_parameter_name,
@@ -221,11 +230,15 @@ def collect(self: "FillerFile") -> Generator["FillerTestItem", None, None]:
                                 )
                                 for parameter_set in parameter_set_list:
                                     # Copy and extend the params with the parameter set
-                                    case_marks = marks[:] + [
-                                        mark
-                                        for mark in parameter_set.marks
-                                        if mark.name != "parametrize"
-                                    ]
+                                    case_marks = (
+                                        marks[:]
+                                        + [
+                                            mark
+                                            for mark in parameter_set.marks
+                                            if mark.name != "parametrize"
+                                        ]
+                                        + extra_function_marks
+                                    )
                                     case_params = params.copy() | dict(
                                         zip(parameter_names, parameter_set.values, strict=True)
                                     )
@@ -291,7 +304,10 @@ def __init__(
         self.fork = fork
         self.fixture_format = fixture_format
         for marker in marks:
-            self.add_marker(marker)  # type: ignore
+            if type(marker) is pytest.Mark:
+                self.own_markers.append(marker)
+            else:
+                self.add_marker(marker)  # type: ignore
 
     def setup(self):
         """Resolve and apply fixtures before test execution."""


Reviews:
user: login: marioevz
body: Looks great, just a few comments, let me know what you think about the docker alternative.
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-14T22:24:15Z
user: login: marioevz
body: I verified and all legacy tests are filling locally, I think we can go ahead and merge this plus #1442.
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-23T22:05:02Z

Review comments:
diff_hunk: @@ -53,116 +48,142 @@ def parse_args_from_string_into_array(stream: str, pos: int, delim: str = " "):
     return args, pos
 
 
-def parse_code(code: str) -> Tuple[bytes, CodeOptions]:
-    """Check if the given string is a valid code."""
-    # print("parse `" + str(code) + "`")
-    if isinstance(code, int):
-        # Users pass code as int (very bad)
-        hex_str = format(code, "02x")
-        return (bytes.fromhex(hex_str), CodeOptions())
-    if not isinstance(code, str):
-        raise ValueError(f"parse_code(code: str) code is not string: {code}")
-    if len(code) == 0:
-        return (bytes.fromhex(""), CodeOptions())
-
-    compiled_code = ""
-    code_options: CodeOptions = CodeOptions()
-
-    raw_marker = ":raw 0x"
-    raw_index = code.find(raw_marker)
-    abi_marker = ":abi"
-    abi_index = code.find(abi_marker)
+class CodeInFillerSource:
+    """Not compiled code source in test filler."""
+
+    code_label: str | None
+    code_raw: Any
+
+    def __init__(self, code: Any, label: str | None = None):
+        """Instantiate."""
+        self.code_label = label
+        self.code_raw = code
+
+    @cached_property
+    def compiled(self) -> bytes:
+        """Compile the code from source to bytes."""
+        if isinstance(self.code_raw, int):
+            # Users pass code as int (very bad)
+            hex_str = format(self.code_raw, "02x")
+            return bytes.fromhex(hex_str)
+
+        if not isinstance(self.code_raw, str):
+            raise ValueError(f"parse_code(code: str) code is not string: {self.code_raw}")
+        if len(self.code_raw) == 0:
+            return bytes.fromhex("")
+
+        compiled_code = ""
+
+        raw_marker = ":raw 0x"
+        raw_index = self.code_raw.find(raw_marker)
+        abi_marker = ":abi"
+        abi_index = self.code_raw.find(abi_marker)
+        yul_marker = ":yul"
+        yul_index = self.code_raw.find(yul_marker)
+
+        # Prase :raw
+        if raw_index != -1:
+            compiled_code = self.code_raw[raw_index + len(raw_marker) :]
+
+        # Prase :yul
path: src/ethereum_test_specs/static_state/common/common.py
user: login: marioevz
body: ```suggestion
        # Parse :yul
```
created_at: 2025-04-14T21:52:03Z
author_association: MEMBER
diff_hunk: @@ -53,116 +48,142 @@ def parse_args_from_string_into_array(stream: str, pos: int, delim: str = " "):
     return args, pos
 
 
-def parse_code(code: str) -> Tuple[bytes, CodeOptions]:
-    """Check if the given string is a valid code."""
-    # print("parse `" + str(code) + "`")
-    if isinstance(code, int):
-        # Users pass code as int (very bad)
-        hex_str = format(code, "02x")
-        return (bytes.fromhex(hex_str), CodeOptions())
-    if not isinstance(code, str):
-        raise ValueError(f"parse_code(code: str) code is not string: {code}")
-    if len(code) == 0:
-        return (bytes.fromhex(""), CodeOptions())
-
-    compiled_code = ""
-    code_options: CodeOptions = CodeOptions()
-
-    raw_marker = ":raw 0x"
-    raw_index = code.find(raw_marker)
-    abi_marker = ":abi"
-    abi_index = code.find(abi_marker)
+class CodeInFillerSource:
+    """Not compiled code source in test filler."""
+
+    code_label: str | None
+    code_raw: Any
+
+    def __init__(self, code: Any, label: str | None = None):
+        """Instantiate."""
+        self.code_label = label
+        self.code_raw = code
+
+    @cached_property
+    def compiled(self) -> bytes:
+        """Compile the code from source to bytes."""
+        if isinstance(self.code_raw, int):
+            # Users pass code as int (very bad)
+            hex_str = format(self.code_raw, "02x")
+            return bytes.fromhex(hex_str)
+
+        if not isinstance(self.code_raw, str):
+            raise ValueError(f"parse_code(code: str) code is not string: {self.code_raw}")
+        if len(self.code_raw) == 0:
+            return bytes.fromhex("")
path: src/ethereum_test_specs/static_state/common/common.py
user: login: marioevz
body: ```suggestion
            return b""
```
Tiny improvement.
created_at: 2025-04-14T22:02:09Z
author_association: MEMBER
diff_hunk: @@ -53,116 +48,142 @@ def parse_args_from_string_into_array(stream: str, pos: int, delim: str = " "):
     return args, pos
 
 
-def parse_code(code: str) -> Tuple[bytes, CodeOptions]:
-    """Check if the given string is a valid code."""
-    # print("parse `" + str(code) + "`")
-    if isinstance(code, int):
-        # Users pass code as int (very bad)
-        hex_str = format(code, "02x")
-        return (bytes.fromhex(hex_str), CodeOptions())
-    if not isinstance(code, str):
-        raise ValueError(f"parse_code(code: str) code is not string: {code}")
-    if len(code) == 0:
-        return (bytes.fromhex(""), CodeOptions())
-
-    compiled_code = ""
-    code_options: CodeOptions = CodeOptions()
-
-    raw_marker = ":raw 0x"
-    raw_index = code.find(raw_marker)
-    abi_marker = ":abi"
-    abi_index = code.find(abi_marker)
+class CodeInFillerSource:
+    """Not compiled code source in test filler."""
+
+    code_label: str | None
+    code_raw: Any
+
+    def __init__(self, code: Any, label: str | None = None):
+        """Instantiate."""
+        self.code_label = label
+        self.code_raw = code
+
+    @cached_property
+    def compiled(self) -> bytes:
+        """Compile the code from source to bytes."""
+        if isinstance(self.code_raw, int):
+            # Users pass code as int (very bad)
+            hex_str = format(self.code_raw, "02x")
+            return bytes.fromhex(hex_str)
+
+        if not isinstance(self.code_raw, str):
+            raise ValueError(f"parse_code(code: str) code is not string: {self.code_raw}")
+        if len(self.code_raw) == 0:
+            return bytes.fromhex("")
+
+        compiled_code = ""
+
+        raw_marker = ":raw 0x"
+        raw_index = self.code_raw.find(raw_marker)
+        abi_marker = ":abi"
+        abi_index = self.code_raw.find(abi_marker)
+        yul_marker = ":yul"
+        yul_index = self.code_raw.find(yul_marker)
+
+        # Prase :raw
+        if raw_index != -1:
+            compiled_code = self.code_raw[raw_index + len(raw_marker) :]
+
+        # Prase :yul
+        elif yul_index != -1:
+            option_start = yul_index + len(yul_marker)
+            options: list[str] = []
+            native_yul_options: str = ""
+
+            if self.code_raw[option_start:].lstrip().startswith("{"):
+                # No yul options, proceed to code parsing
+                source_start = option_start
+            else:
+                opt, source_start = parse_args_from_string_into_array(
+                    self.code_raw, option_start + 1
+                )
+                for arg in opt:
+                    if arg == "object" or arg == '"C"':
+                        native_yul_options += arg + " "
+                    else:
+                        options.append(arg)
+
+            with tempfile.NamedTemporaryFile(mode="w+", delete=False) as tmp:
+                tmp.write(native_yul_options + self.code_raw[source_start:])
+                tmp_path = tmp.name
+            compiled_code = compile_yul(
+                source_file=tmp_path,
+                evm_version=options[0] if len(options) >= 1 else None,
+                optimize=options[1] if len(options) >= 2 else None,
+            )[2:]
+
+        # Prase :abi
+        elif abi_index != -1:
+            abi_encoding = self.code_raw[abi_index + len(abi_marker) + 1 :]
+            tokens = abi_encoding.strip().split()
+            abi = tokens[0]
+            function_signature = function_signature_to_4byte_selector(abi)
+            parameter_str = re.sub(r"^\w+", "", abi).strip()
+
+            parameter_types = parameter_str.strip("()").split(",")
+            if len(tokens) > 1:
+                function_parameters = encode(
+                    [parameter_str],
+                    [
+                        [
+                            int(t.lower(), 0) & ((1 << 256) - 1)  # treat big ints as 256bits
+                            if parameter_types[t_index] == "uint"
+                            else int(t.lower(), 0) > 0  # treat positive values as True
+                            if parameter_types[t_index] == "bool"
+                            else False and ValueError("unhandled parameter_types")
+                            for t_index, t in enumerate(tokens[1:])
+                        ]
+                    ],
+                )
+                return function_signature + function_parameters
+            return function_signature
+
+        # Prase plain code 0x
+        elif self.code_raw.lstrip().startswith("0x"):
+            compiled_code = self.code_raw[2:].lower()
+
+        # Prase lllc code
+        elif self.code_raw.lstrip().startswith("{") or self.code_raw.lstrip().startswith("(asm"):
+            with tempfile.NamedTemporaryFile(mode="w+", delete=False) as tmp:
+                tmp.write(self.code_raw)
+                tmp_path = tmp.name
+
+            # using lllc
+            # result = subprocess.run(["lllc", tmp_path], capture_output=True, text=True)
path: src/ethereum_test_specs/static_state/common/common.py
user: login: marioevz
body: I think it should be ok to just require this to be installed already.

At the moment, we prepare releases and fill tests using out external runners, and all of our current external runners point to our server that has already `lllc` installed, so it should just work.

Downside would be that we would be unable to add unit tests for state static tests that contain lllc, but I think that should be ok.
created_at: 2025-04-14T22:20:01Z
author_association: MEMBER
diff_hunk: @@ -53,116 +48,142 @@ def parse_args_from_string_into_array(stream: str, pos: int, delim: str = " "):
     return args, pos
 
 
-def parse_code(code: str) -> Tuple[bytes, CodeOptions]:
-    """Check if the given string is a valid code."""
-    # print("parse `" + str(code) + "`")
-    if isinstance(code, int):
-        # Users pass code as int (very bad)
-        hex_str = format(code, "02x")
-        return (bytes.fromhex(hex_str), CodeOptions())
-    if not isinstance(code, str):
-        raise ValueError(f"parse_code(code: str) code is not string: {code}")
-    if len(code) == 0:
-        return (bytes.fromhex(""), CodeOptions())
-
-    compiled_code = ""
-    code_options: CodeOptions = CodeOptions()
-
-    raw_marker = ":raw 0x"
-    raw_index = code.find(raw_marker)
-    abi_marker = ":abi"
-    abi_index = code.find(abi_marker)
+class CodeInFillerSource:
+    """Not compiled code source in test filler."""
+
+    code_label: str | None
+    code_raw: Any
+
+    def __init__(self, code: Any, label: str | None = None):
+        """Instantiate."""
+        self.code_label = label
+        self.code_raw = code
+
+    @cached_property
+    def compiled(self) -> bytes:
+        """Compile the code from source to bytes."""
+        if isinstance(self.code_raw, int):
+            # Users pass code as int (very bad)
+            hex_str = format(self.code_raw, "02x")
+            return bytes.fromhex(hex_str)
+
+        if not isinstance(self.code_raw, str):
+            raise ValueError(f"parse_code(code: str) code is not string: {self.code_raw}")
+        if len(self.code_raw) == 0:
+            return bytes.fromhex("")
+
+        compiled_code = ""
+
+        raw_marker = ":raw 0x"
+        raw_index = self.code_raw.find(raw_marker)
+        abi_marker = ":abi"
+        abi_index = self.code_raw.find(abi_marker)
+        yul_marker = ":yul"
+        yul_index = self.code_raw.find(yul_marker)
+
+        # Prase :raw
path: src/ethereum_test_specs/static_state/common/common.py
user: login: marioevz
body: ```suggestion
        # Parse :raw
```
created_at: 2025-04-14T22:22:38Z
author_association: MEMBER
diff_hunk: @@ -53,116 +48,142 @@ def parse_args_from_string_into_array(stream: str, pos: int, delim: str = " "):
     return args, pos
 
 
-def parse_code(code: str) -> Tuple[bytes, CodeOptions]:
-    """Check if the given string is a valid code."""
-    # print("parse `" + str(code) + "`")
-    if isinstance(code, int):
-        # Users pass code as int (very bad)
-        hex_str = format(code, "02x")
-        return (bytes.fromhex(hex_str), CodeOptions())
-    if not isinstance(code, str):
-        raise ValueError(f"parse_code(code: str) code is not string: {code}")
-    if len(code) == 0:
-        return (bytes.fromhex(""), CodeOptions())
-
-    compiled_code = ""
-    code_options: CodeOptions = CodeOptions()
-
-    raw_marker = ":raw 0x"
-    raw_index = code.find(raw_marker)
-    abi_marker = ":abi"
-    abi_index = code.find(abi_marker)
+class CodeInFillerSource:
+    """Not compiled code source in test filler."""
+
+    code_label: str | None
+    code_raw: Any
+
+    def __init__(self, code: Any, label: str | None = None):
+        """Instantiate."""
+        self.code_label = label
+        self.code_raw = code
+
+    @cached_property
+    def compiled(self) -> bytes:
+        """Compile the code from source to bytes."""
+        if isinstance(self.code_raw, int):
+            # Users pass code as int (very bad)
+            hex_str = format(self.code_raw, "02x")
+            return bytes.fromhex(hex_str)
+
+        if not isinstance(self.code_raw, str):
+            raise ValueError(f"parse_code(code: str) code is not string: {self.code_raw}")
+        if len(self.code_raw) == 0:
+            return bytes.fromhex("")
+
+        compiled_code = ""
+
+        raw_marker = ":raw 0x"
+        raw_index = self.code_raw.find(raw_marker)
+        abi_marker = ":abi"
+        abi_index = self.code_raw.find(abi_marker)
+        yul_marker = ":yul"
+        yul_index = self.code_raw.find(yul_marker)
+
+        # Prase :raw
+        if raw_index != -1:
+            compiled_code = self.code_raw[raw_index + len(raw_marker) :]
+
+        # Prase :yul
+        elif yul_index != -1:
+            option_start = yul_index + len(yul_marker)
+            options: list[str] = []
+            native_yul_options: str = ""
+
+            if self.code_raw[option_start:].lstrip().startswith("{"):
+                # No yul options, proceed to code parsing
+                source_start = option_start
+            else:
+                opt, source_start = parse_args_from_string_into_array(
+                    self.code_raw, option_start + 1
+                )
+                for arg in opt:
+                    if arg == "object" or arg == '"C"':
+                        native_yul_options += arg + " "
+                    else:
+                        options.append(arg)
+
+            with tempfile.NamedTemporaryFile(mode="w+", delete=False) as tmp:
+                tmp.write(native_yul_options + self.code_raw[source_start:])
+                tmp_path = tmp.name
+            compiled_code = compile_yul(
+                source_file=tmp_path,
+                evm_version=options[0] if len(options) >= 1 else None,
+                optimize=options[1] if len(options) >= 2 else None,
+            )[2:]
+
+        # Prase :abi
path: src/ethereum_test_specs/static_state/common/common.py
user: login: marioevz
body: ```suggestion
        # Parse :abi
```
created_at: 2025-04-14T22:22:44Z
author_association: MEMBER
diff_hunk: @@ -53,116 +48,142 @@ def parse_args_from_string_into_array(stream: str, pos: int, delim: str = " "):
     return args, pos
 
 
-def parse_code(code: str) -> Tuple[bytes, CodeOptions]:
-    """Check if the given string is a valid code."""
-    # print("parse `" + str(code) + "`")
-    if isinstance(code, int):
-        # Users pass code as int (very bad)
-        hex_str = format(code, "02x")
-        return (bytes.fromhex(hex_str), CodeOptions())
-    if not isinstance(code, str):
-        raise ValueError(f"parse_code(code: str) code is not string: {code}")
-    if len(code) == 0:
-        return (bytes.fromhex(""), CodeOptions())
-
-    compiled_code = ""
-    code_options: CodeOptions = CodeOptions()
-
-    raw_marker = ":raw 0x"
-    raw_index = code.find(raw_marker)
-    abi_marker = ":abi"
-    abi_index = code.find(abi_marker)
+class CodeInFillerSource:
+    """Not compiled code source in test filler."""
+
+    code_label: str | None
+    code_raw: Any
+
+    def __init__(self, code: Any, label: str | None = None):
+        """Instantiate."""
+        self.code_label = label
+        self.code_raw = code
+
+    @cached_property
+    def compiled(self) -> bytes:
+        """Compile the code from source to bytes."""
+        if isinstance(self.code_raw, int):
+            # Users pass code as int (very bad)
+            hex_str = format(self.code_raw, "02x")
+            return bytes.fromhex(hex_str)
+
+        if not isinstance(self.code_raw, str):
+            raise ValueError(f"parse_code(code: str) code is not string: {self.code_raw}")
+        if len(self.code_raw) == 0:
+            return bytes.fromhex("")
+
+        compiled_code = ""
+
+        raw_marker = ":raw 0x"
+        raw_index = self.code_raw.find(raw_marker)
+        abi_marker = ":abi"
+        abi_index = self.code_raw.find(abi_marker)
+        yul_marker = ":yul"
+        yul_index = self.code_raw.find(yul_marker)
+
+        # Prase :raw
+        if raw_index != -1:
+            compiled_code = self.code_raw[raw_index + len(raw_marker) :]
+
+        # Prase :yul
+        elif yul_index != -1:
+            option_start = yul_index + len(yul_marker)
+            options: list[str] = []
+            native_yul_options: str = ""
+
+            if self.code_raw[option_start:].lstrip().startswith("{"):
+                # No yul options, proceed to code parsing
+                source_start = option_start
+            else:
+                opt, source_start = parse_args_from_string_into_array(
+                    self.code_raw, option_start + 1
+                )
+                for arg in opt:
+                    if arg == "object" or arg == '"C"':
+                        native_yul_options += arg + " "
+                    else:
+                        options.append(arg)
+
+            with tempfile.NamedTemporaryFile(mode="w+", delete=False) as tmp:
+                tmp.write(native_yul_options + self.code_raw[source_start:])
+                tmp_path = tmp.name
+            compiled_code = compile_yul(
+                source_file=tmp_path,
+                evm_version=options[0] if len(options) >= 1 else None,
+                optimize=options[1] if len(options) >= 2 else None,
+            )[2:]
+
+        # Prase :abi
+        elif abi_index != -1:
+            abi_encoding = self.code_raw[abi_index + len(abi_marker) + 1 :]
+            tokens = abi_encoding.strip().split()
+            abi = tokens[0]
+            function_signature = function_signature_to_4byte_selector(abi)
+            parameter_str = re.sub(r"^\w+", "", abi).strip()
+
+            parameter_types = parameter_str.strip("()").split(",")
+            if len(tokens) > 1:
+                function_parameters = encode(
+                    [parameter_str],
+                    [
+                        [
+                            int(t.lower(), 0) & ((1 << 256) - 1)  # treat big ints as 256bits
+                            if parameter_types[t_index] == "uint"
+                            else int(t.lower(), 0) > 0  # treat positive values as True
+                            if parameter_types[t_index] == "bool"
+                            else False and ValueError("unhandled parameter_types")
+                            for t_index, t in enumerate(tokens[1:])
+                        ]
+                    ],
+                )
+                return function_signature + function_parameters
+            return function_signature
+
+        # Prase plain code 0x
+        elif self.code_raw.lstrip().startswith("0x"):
+            compiled_code = self.code_raw[2:].lower()
+
+        # Prase lllc code
path: src/ethereum_test_specs/static_state/common/common.py
user: login: marioevz
body: ```suggestion
        # Parse lllc code
```
created_at: 2025-04-14T22:22:51Z
author_association: MEMBER
diff_hunk: @@ -53,116 +47,146 @@ def parse_args_from_string_into_array(stream: str, pos: int, delim: str = " "):
     return args, pos
 
 
-def parse_code(code: str) -> Tuple[bytes, CodeOptions]:
-    """Check if the given string is a valid code."""
-    # print("parse `" + str(code) + "`")
-    if isinstance(code, int):
-        # Users pass code as int (very bad)
-        hex_str = format(code, "02x")
-        return (bytes.fromhex(hex_str), CodeOptions())
-    if not isinstance(code, str):
-        raise ValueError(f"parse_code(code: str) code is not string: {code}")
-    if len(code) == 0:
-        return (bytes.fromhex(""), CodeOptions())
-
-    compiled_code = ""
-    code_options: CodeOptions = CodeOptions()
-
-    raw_marker = ":raw 0x"
-    raw_index = code.find(raw_marker)
-    abi_marker = ":abi"
-    abi_index = code.find(abi_marker)
+class CodeInFillerSource:
+    """Not compiled code source in test filler."""
+
+    code_label: str | None
+    code_raw: Any
+
+    def __init__(self, code: Any, label: str | None = None):
+        """Instantiate."""
+        self.code_label = label
+        self.code_raw = code
+
+    @cached_property
+    def compiled(self) -> bytes:
+        """Compile the code from source to bytes."""
+        if isinstance(self.code_raw, int):
+            # Users pass code as int (very bad)
+            hex_str = format(self.code_raw, "02x")
+            return bytes.fromhex(hex_str)
+
+        if not isinstance(self.code_raw, str):
+            raise ValueError(f"parse_code(code: str) code is not string: {self.code_raw}")
+        if len(self.code_raw) == 0:
+            return b""
+
+        compiled_code = ""
+
+        raw_marker = ":raw 0x"
+        raw_index = self.code_raw.find(raw_marker)
+        abi_marker = ":abi"
+        abi_index = self.code_raw.find(abi_marker)
+        yul_marker = ":yul"
+        yul_index = self.code_raw.find(yul_marker)
+
+        # Parse :raw
+        if raw_index != -1:
+            compiled_code = self.code_raw[raw_index + len(raw_marker) :]
+
+        # Parse :yul
+        elif yul_index != -1:
+            option_start = yul_index + len(yul_marker)
+            options: list[str] = []
+            native_yul_options: str = ""
+
+            if self.code_raw[option_start:].lstrip().startswith("{"):
+                # No yul options, proceed to code parsing
+                source_start = option_start
+            else:
+                opt, source_start = parse_args_from_string_into_array(
+                    self.code_raw, option_start + 1
+                )
+                for arg in opt:
+                    if arg == "object" or arg == '"C"':
+                        native_yul_options += arg + " "
+                    else:
+                        options.append(arg)
+
+            with tempfile.NamedTemporaryFile(mode="w+", delete=False) as tmp:
+                tmp.write(native_yul_options + self.code_raw[source_start:])
+                tmp_path = tmp.name
+            compiled_code = compile_yul(
+                source_file=tmp_path,
+                evm_version=options[0] if len(options) >= 1 else None,
+                optimize=options[1] if len(options) >= 2 else None,
+            )[2:]
+
+        # Parse :abi
+        elif abi_index != -1:
+            abi_encoding = self.code_raw[abi_index + len(abi_marker) + 1 :]
+            tokens = abi_encoding.strip().split()
+            abi = tokens[0]
+            function_signature = function_signature_to_4byte_selector(abi)
+            parameter_str = re.sub(r"^\w+", "", abi).strip()
+
+            parameter_types = parameter_str.strip("()").split(",")
+            if len(tokens) > 1:
+                function_parameters = encode(
+                    [parameter_str],
+                    [
+                        [
+                            int(t.lower(), 0) & ((1 << 256) - 1)  # treat big ints as 256bits
+                            if parameter_types[t_index] == "uint"
+                            else int(t.lower(), 0) > 0  # treat positive values as True
+                            if parameter_types[t_index] == "bool"
+                            else False and ValueError("unhandled parameter_types")
+                            for t_index, t in enumerate(tokens[1:])
+                        ]
+                    ],
+                )
+                return function_signature + function_parameters
+            return function_signature
+
+        # Prase plain code 0x
path: src/ethereum_test_specs/static_state/common/common.py
user: login: marioevz
body: ```suggestion
        # Parse plain code 0x
```
created_at: 2025-04-23T22:02:45Z
author_association: MEMBER

PR comments:
user: login: winsvega
created_at: 2025-04-14T10:29:58Z
body: restored this logic
https://github.com/ethereum/execution-spec-tests/pull/1362/commits/a092ecb07d2d79c58d73799c3f681438da18f507
author_association: NONE
user: login: winsvega
created_at: 2025-04-14T10:35:20Z
body: @marioevz 

```
execution-spec-tests/src/ethereum_test_specs/static_state/common/docker.py
def stop_lllc_containers():
```

needs to be called after filling of the fixtures has finished. or in case of exception or keyboard interrupt and we are exiting. 
where this part of logic is defined in pytest plugin of static test filler?
author_association: NONE
user: login: marioevz
created_at: 2025-04-23T22:06:32Z
body: I marked the last checkbox in the description because we are now using locally built `lllc`, which doesn't require for us to stop any container.
author_association: MEMBER