From 65b3185b29ab99ae17188197d911d782b2afa9e2 Mon Sep 17 00:00:00 2001 From: Ryan Wolk Date: Thu, 7 May 2026 18:47:08 -0700 Subject: [PATCH 1/4] More utilities for cover-float integration into act4 --- Makefile | 17 ++++++ src/cover_float/cli.py | 52 +++++++++++++++-- src/cover_float/common/constants.py | 4 +- src/cover_float/common/log.py | 22 ++++++-- src/cover_float/reference/impl.py | 8 ++- src/cover_float/scripts/postprocess.py | 3 +- src/cover_float/testgen/model.py | 78 ++++++++++++++++++++------ 7 files changed, 151 insertions(+), 33 deletions(-) diff --git a/Makefile b/Makefile index 3eca004..d302b74 100644 --- a/Makefile +++ b/Makefile @@ -2,6 +2,8 @@ RM_CMD ?= rm -rf AGGRESSIVENESS ?= 1 +PROCESSED_ONLY ?= +SILENT ?= COVER_FLOAT_FLAGS = @@ -9,6 +11,14 @@ ifeq ($(AGGRESSIVENESS), 0) COVER_FLOAT_FLAGS += --partial-output endif +ifneq ($(PROCESSED_ONLY),) + COVER_FLOAT_FLAGS += --only-processed-vectors +endif + +ifneq ($(SILENT),) + COVER_FLOAT_FLAGS += -qq +endif + MODELS := B1 B2 B3 B6 B7 B8 B9 B10 B11 B12 B13 B14 B15 B20 B21 B25 B26 B27 B29 .PHONY: build clean sim all $(MODELS) @@ -18,6 +28,9 @@ MODELS := B1 B2 B3 B6 B7 B8 B9 B10 B11 B12 B13 B14 B15 B20 B21 B25 B26 B27 B29 all: uv run --managed-python cover-float-testgen $(COVER_FLOAT_FLAGS) +processed-tests-only: + uv run --managed-python cover-float-testgen --partial-output --only-processed-vectors --quiet $(COVER_FLOAT_FLAGS) + # Build target to compile the pybind11 module (if necessary) build: @echo "Building python module" @@ -40,6 +53,10 @@ clean: $(RM_CMD) sim/coverfloat_worklib/ $(RM_CMD) sim/transcript $(RM_CMD) sim/coverfloat.ucdb + $(RM_CMD) tests/testvectors/B*_tv.txt + $(RM_CMD) tests/covervectors/B*_cv.txt + $(RM_CMD) tests/readable/B*_parsed.txt + $(RM_CMD) tests/processed/*/B*.csv # --- Include Dependency Files --- # Include auto-generated dependency files if they exist diff --git a/src/cover_float/cli.py b/src/cover_float/cli.py index 77d0314..c93cafa 100644 --- a/src/cover_float/cli.py +++ b/src/cover_float/cli.py @@ -15,17 +15,17 @@ import argparse import logging -from concurrent.futures import ProcessPoolExecutor +from concurrent.futures import Future, ProcessPoolExecutor, as_completed from pathlib import Path +from rich.progress import BarColumn, MofNCompleteColumn, Progress, TextColumn, TimeElapsedColumn + import cover_float.common.log as log import cover_float.testgen as tg from cover_float.common.constants import config from cover_float.common.util import SingleThreadedExecutor from cover_float.reference import run_test_vector -logging.basicConfig(level=logging.INFO) - def main() -> None: parser = argparse.ArgumentParser() @@ -63,22 +63,62 @@ def testgen() -> None: parser.add_argument( "--partial-output", action="store_true", help="Create a Reduced Number of Tests in Test Heavy Models" ) + parser.add_argument( + "--quiet", + "-q", + action="count", + default=0, + help="Applying Once Condenses Info Logging to a Single Progress Bar, Twice Eliminates all Logging", + ) + parser.add_argument("--only-processed-vectors", action="store_true", help="Generate Only Processed Test Vectors") args = parser.parse_args() output_dir = Path(args.output_dir) single_thread = args.single_thread or (args.models is not None and len(args.models) < 2) config.FULL_COVERAGE_TESTGEN = not args.partial_output + config.QUIET = args.quiet > 0 + config.RELEASE = args.only_processed_vectors + if args.quiet > 0: + logging.basicConfig(level=logging.ERROR) + else: + logging.basicConfig(level=logging.INFO) if single_thread: executor = SingleThreadedExecutor() else: executor = ProcessPoolExecutor() if args.jobs is None else ProcessPoolExecutor(max_workers=args.jobs) - with log.StatusReporter() as logger, executor: + with log.StatusReporter(disable=args.quiet) as logger, executor: + futures: list[Future[None]] = [] + if args.models is None: for model in tg.model.GLOBAL_MODELS: - tg.model.GLOBAL_MODELS[model](output_dir, logger, executor) + future = tg.model.GLOBAL_MODELS[model](output_dir, logger, executor) + if future is not None: + futures.append(future) else: for model in args.models: if model in tg.model.GLOBAL_MODELS: - tg.model.GLOBAL_MODELS[model](output_dir, logger, executor) + future = tg.model.GLOBAL_MODELS[model](output_dir, logger, executor) + if future is not None: + futures.append(future) + + if len(futures) == 0: + if args.quiet < 2: + print(f"No work to be done for {'cover-float' if not args.models else ', '.join(args.models)}") + return + + if args.quiet == 1: + with Progress( + TextColumn("{task.description}"), + BarColumn(), + MofNCompleteColumn(), + TimeElapsedColumn(), + ) as progress: + for future in progress.track( + as_completed(futures), total=len(futures), description="[cyan]Generating Cover-Float Tests" + ): + future.result() + else: + for future in as_completed(futures): + future.result() diff --git a/src/cover_float/common/constants.py b/src/cover_float/common/constants.py index 81cac2a..7ca5dfa 100644 --- a/src/cover_float/common/constants.py +++ b/src/cover_float/common/constants.py @@ -148,8 +148,10 @@ @dataclass class Config: - FULL_COVERAGE_TESTGEN: int = 1 + FULL_COVERAGE_TESTGEN: bool = True CACHE_DIR: str = "build/cache" + QUIET: bool = False + RELEASE: bool = False config = Config() diff --git a/src/cover_float/common/log.py b/src/cover_float/common/log.py index e5af36d..939dd00 100644 --- a/src/cover_float/common/log.py +++ b/src/cover_float/common/log.py @@ -162,14 +162,21 @@ def render(self, task: Task) -> console.RenderableType: class AsyncLoggingHandler(logging.handlers.QueueListener): - def __init__(self, reporter: StatusReporter, listen_to: Queue[Any], *handlers: logging.Handler) -> None: + def __init__(self, reporter: StatusReporter, listen_to: Queue[Any], level: int, *handlers: logging.Handler) -> None: super().__init__(listen_to, *handlers) self.reporter = reporter + self.level = level def handle_progress_update(self, record: dict[Any, Any]) -> None: try: action = record["action"] + if self.level > STATUS_LEVEL_NUM: + if action == "add_task": + task_id_pipe: Connection = record["pipe_end"] + task_id_pipe.send(-1) + return + if action == "update": self.reporter.progress.update(*record["args"], **record["kwargs"]) elif action == "advance": @@ -187,7 +194,7 @@ def handle_progress_update(self, record: dict[Any, Any]) -> None: else: logging.info(f"Failed to Log {record}") except Exception as e: - logging.info(f"Failed to Log {record}", exc_info=e) + logging.exception(f"Failed to Log {record}", exc_info=e) def handle(self, record: logging.LogRecord | dict[Any, Any]) -> None: if isinstance(record, logging.LogRecord): @@ -217,7 +224,7 @@ def emit(self, record: logging.LogRecord) -> None: class StatusReporter: - def __init__(self) -> None: + def __init__(self, *, disable: bool = False) -> None: self.active_status_bars: dict[str, TaskID] = {} self.progress = Progress( SpinnerColumn(), @@ -234,7 +241,9 @@ def __init__(self) -> None: # This is the actual handler that prints to console self.rich_handler = ProgressAwareLogHandler(self.progress) - self.queue_listener = AsyncLoggingHandler(self, self.logging_queue, self.rich_handler) + self.queue_listener = AsyncLoggingHandler( + self, self.logging_queue, logging.getLogger().level, self.rich_handler + ) logging.getLogger().addHandler(logging.handlers.QueueHandler(self.logging_queue)) # This keeps all of the refreshes in one thread, eliminating all race conditions @@ -242,6 +251,7 @@ def __init__(self) -> None: self._refresh_thread.daemon = True self.exiting = False + self.disable = disable def _reset_refresh_timer(self) -> None: self.logging_queue.put({"action": "refresh", "args": [], "kwargs": {}}) @@ -252,7 +262,9 @@ def _reset_refresh_timer(self) -> None: self._refresh_thread.start() def __enter__(self) -> StatusReporter: - self.progress.start() + if not self.disable: + self.progress.start() + self.queue_listener.start() self._refresh_thread.start() diff --git a/src/cover_float/reference/impl.py b/src/cover_float/reference/impl.py index f0a7c5f..78b203f 100644 --- a/src/cover_float/reference/impl.py +++ b/src/cover_float/reference/impl.py @@ -17,7 +17,7 @@ import cover_float._reference import cover_float._unmodified_reference -from cover_float.common.constants import TEST_VECTOR_WIDTH_HEX_WITH_SEPARATORS +from cover_float.common.constants import TEST_VECTOR_WIDTH_HEX_WITH_SEPARATORS, config def run_and_store_test_vector(test_vector: str, test_file: TextIO, cover_file: TextIO) -> None: @@ -27,13 +27,15 @@ def run_and_store_test_vector(test_vector: str, test_file: TextIO, cover_file: T generated_test_vector = cover_vector[:TEST_VECTOR_WIDTH_HEX_WITH_SEPARATORS] test_file.write(generated_test_vector + "\n") - cover_file.write(cover_vector.strip() + "\n") + if not config.RELEASE: + cover_file.write(cover_vector.strip() + "\n") def store_cover_vector(cover_vector: str, test_file: TextIO, cover_file: TextIO) -> None: generated_test_vector = cover_vector[:TEST_VECTOR_WIDTH_HEX_WITH_SEPARATORS] test_file.write(generated_test_vector + "\n") - cover_file.write(cover_vector.strip() + "\n") + if not config.RELEASE: + cover_file.write(cover_vector.strip() + "\n") def verify_test_vector(test_vector: str) -> bool: diff --git a/src/cover_float/scripts/postprocess.py b/src/cover_float/scripts/postprocess.py index be23b8a..17250e2 100644 --- a/src/cover_float/scripts/postprocess.py +++ b/src/cover_float/scripts/postprocess.py @@ -130,7 +130,8 @@ def postprocess_testvectors( for line in test_vectors.readlines(): parsed = parse_test_vector(line) if parsed: - readable_vectors.write(format_output(parsed) + "\n") + if not constants.config.RELEASE: + readable_vectors.write(format_output(parsed) + "\n") if not verify_test_vector(line): logger.exception( diff --git a/src/cover_float/testgen/model.py b/src/cover_float/testgen/model.py index 30a332f..21d70e3 100644 --- a/src/cover_float/testgen/model.py +++ b/src/cover_float/testgen/model.py @@ -13,23 +13,30 @@ # either express or implied. See the License for the specific language governing permissions # and limitations under the License. +from __future__ import annotations + import concurrent.futures +import inspect import logging import logging.handlers +import os from pathlib import Path from queue import Queue from typing import Any, Callable, TextIO from rich.progress import TaskID +import cover_float.common.constants as constants import cover_float.common.log as log from cover_float.scripts.postprocess import postprocess_testvectors GLOBAL_MODELS: dict[ - str, Callable[[Path, log.StatusReporter, concurrent.futures.Executor], concurrent.futures.Future[None]] + str, Callable[[Path, log.StatusReporter, concurrent.futures.Executor], concurrent.futures.Future[None] | None] ] = {} GLOBAL_MODEL_FUNCTIONS: dict[str, Callable[[TextIO, TextIO], None]] = {} +PARTIAL_OUTPUT_MESSAGE = "# Generated With --partial-output\n" + class MPLoggingHandler(logging.Handler): def __init__(self, queue: Queue[Any], task_id: TaskID) -> None: @@ -57,7 +64,7 @@ def _run_model_by_name( post_process: bool, ) -> None: tv_path = output_dir / "testvectors" / f"{model_name}_tv.txt" - cv_path = output_dir / "covervectors" / f"{model_name}_cv.txt" + cv_path = output_dir / "covervectors" / f"{model_name}_cv.txt" if not constants.config.RELEASE else Path(os.devnull) model_logger = logging.getLogger(model_name) @@ -80,6 +87,9 @@ def _run_model_by_name( try: with tv_path.open("w") as test_f, cv_path.open("w") as cover_f: + if not constants.config.FULL_COVERAGE_TESTGEN: + test_f.write(PARTIAL_OUTPUT_MESSAGE) + cover_f.write(PARTIAL_OUTPUT_MESSAGE) GLOBAL_MODEL_FUNCTIONS[model_name](test_f, cover_f) if post_process: @@ -96,32 +106,66 @@ def register_model( model_name: str, ) -> Callable[ [Callable[[TextIO, TextIO], None]], - Callable[[Path, log.StatusReporter, concurrent.futures.Executor], concurrent.futures.Future[None]], + Callable[[Path, log.StatusReporter, concurrent.futures.Executor], concurrent.futures.Future[None] | None], ]: def inner( fn: Callable[[TextIO, TextIO], None], - ) -> Callable[[Path, log.StatusReporter, concurrent.futures.Executor], concurrent.futures.Future[None]]: + ) -> Callable[[Path, log.StatusReporter, concurrent.futures.Executor], concurrent.futures.Future[None] | None]: # Store the function in a global dict so it can be accessed by the worker process GLOBAL_MODEL_FUNCTIONS[model_name] = fn + source_file = Path(inspect.getfile(fn)) def wrapper( output_dir: Path, status_reporter: log.StatusReporter, executor: concurrent.futures.Executor, post_process: bool = True, - ) -> concurrent.futures.Future[None]: - task_id = status_reporter.start_model(model_name) - - future = executor.submit( - _run_model_by_name, - model_name, - output_dir, - task_id, - status_reporter.logging_queue, - post_process, - ) - future.add_done_callback(lambda _: status_reporter.stop_model(model_name)) - return future + ) -> concurrent.futures.Future[None] | None: + # Check generation time of target files + tv_path = output_dir / "testvectors" / f"{model_name}_tv.txt" + tv_mod_time = tv_path.stat().st_mtime if tv_path.exists() else 0 + + cv_path = output_dir / "covervectors" / f"{model_name}_cv.txt" + cv_mod_time = cv_path.stat().st_mtime if cv_path.exists() else 0 + + source_mod_time = source_file.stat().st_mtime + + tv_comes_from_partial: bool | None = None + if tv_path.exists(): + with tv_path.open("r") as tvs: + first_line = tvs.readline() + tv_comes_from_partial = first_line == PARTIAL_OUTPUT_MESSAGE + + cv_comes_from_partial: bool | None = None + if cv_path.exists(): + with cv_path.open("r") as cvs: + first_line = cvs.readline() + cv_comes_from_partial = first_line == PARTIAL_OUTPUT_MESSAGE + + if ( + not constants.config.RELEASE + and ( + (source_mod_time > cv_mod_time) + or (cv_comes_from_partial != (not constants.config.FULL_COVERAGE_TESTGEN)) + ) + ) or ( + (source_mod_time > tv_mod_time) + or (tv_comes_from_partial != (not constants.config.FULL_COVERAGE_TESTGEN)) + ): + task_id = status_reporter.start_model(model_name) + + future = executor.submit( + _run_model_by_name, + model_name, + output_dir, + task_id, + status_reporter.logging_queue, + post_process, + ) + future.add_done_callback(lambda _: status_reporter.stop_model(model_name)) + return future + + return None GLOBAL_MODELS[model_name] = wrapper return wrapper From 17171dcadcf26a373626a69bce71f664cedb9136 Mon Sep 17 00:00:00 2001 From: Ryan Wolk Date: Thu, 7 May 2026 21:03:53 -0700 Subject: [PATCH 2/4] Integrate copilot suggestions --- src/cover_float/cli.py | 2 +- src/cover_float/scripts/postprocess.py | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/cover_float/cli.py b/src/cover_float/cli.py index c93cafa..1c5d3b6 100644 --- a/src/cover_float/cli.py +++ b/src/cover_float/cli.py @@ -88,7 +88,7 @@ def testgen() -> None: else: executor = ProcessPoolExecutor() if args.jobs is None else ProcessPoolExecutor(max_workers=args.jobs) - with log.StatusReporter(disable=args.quiet) as logger, executor: + with log.StatusReporter(disable=(args.quiet > 0)) as logger, executor: futures: list[Future[None]] = [] if args.models is None: diff --git a/src/cover_float/scripts/postprocess.py b/src/cover_float/scripts/postprocess.py index 17250e2..e7b1626 100644 --- a/src/cover_float/scripts/postprocess.py +++ b/src/cover_float/scripts/postprocess.py @@ -20,6 +20,7 @@ import csv import logging +import os import pathlib import time from dataclasses import dataclass @@ -111,13 +112,16 @@ def postprocess_testvectors( logger: log.ModelLogger = cast(log.ModelLogger, logging.getLogger(model)) test_vector_file = test_vector_location / f"{model}_tv.txt" - readable_vectors_file = readable_vectors_dir / f"{model}_parsed.txt" + readable_vectors_file = ( + readable_vectors_dir / f"{model}_parsed.txt" if not constants.config.RELEASE else pathlib.Path(os.devnull) + ) processed_vectors: dict[str, tuple[csv.DictWriter[str], TextIO]] = {} total = 0 non_riscv = 0 file_size = test_vector_file.stat().st_size - readable_vectors_file.parent.mkdir(parents=True, exist_ok=True) + if not constants.config.RELEASE: + readable_vectors_file.parent.mkdir(parents=True, exist_ok=True) with ( test_vector_file.open("r") as test_vectors, From d52bb86d91faf03b08cfb944def6a79cebc956f1 Mon Sep 17 00:00:00 2001 From: Ryan Wolk Date: Thu, 7 May 2026 21:06:31 -0700 Subject: [PATCH 3/4] Another copilot change --- src/cover_float/common/log.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/cover_float/common/log.py b/src/cover_float/common/log.py index 939dd00..0de608f 100644 --- a/src/cover_float/common/log.py +++ b/src/cover_float/common/log.py @@ -193,8 +193,8 @@ def handle_progress_update(self, record: dict[Any, Any]) -> None: self.reporter.progress.refresh() else: logging.info(f"Failed to Log {record}") - except Exception as e: - logging.exception(f"Failed to Log {record}", exc_info=e) + except Exception: + logging.exception(f"Failed to Log {record}") def handle(self, record: logging.LogRecord | dict[Any, Any]) -> None: if isinstance(record, logging.LogRecord): From 40b5b1c5f2e4be60db2e93bf8a9a2d35933fc926 Mon Sep 17 00:00:00 2001 From: Ryan Wolk Date: Thu, 7 May 2026 21:31:18 -0700 Subject: [PATCH 4/4] Make regeneration logic aware of other files not name Bxx.py --- .gitignore | 1 + Makefile | 1 + src/cover_float/testgen/model.py | 28 ++++++++++++++++++++++++---- 3 files changed, 26 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index 9f3d19d..e3d2975 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,4 @@ tests/testvectors/B*_tv.txt coverage/covergroups/bins_templates/generated tests/readable/B*_parsed.txt tests/processed/*/B*.csv +tests/.stamp/* diff --git a/Makefile b/Makefile index f535f00..ff1d741 100644 --- a/Makefile +++ b/Makefile @@ -57,6 +57,7 @@ clean: $(RM_CMD) tests/covervectors/B*_cv.txt $(RM_CMD) tests/readable/B*_parsed.txt $(RM_CMD) tests/processed/*/B*.csv + $(RM_CMD) tests/.stamp # --- Include Dependency Files --- # Include auto-generated dependency files if they exist diff --git a/src/cover_float/testgen/model.py b/src/cover_float/testgen/model.py index 21d70e3..e254512 100644 --- a/src/cover_float/testgen/model.py +++ b/src/cover_float/testgen/model.py @@ -20,6 +20,7 @@ import logging import logging.handlers import os +import re from pathlib import Path from queue import Queue from typing import Any, Callable, TextIO @@ -65,6 +66,8 @@ def _run_model_by_name( ) -> None: tv_path = output_dir / "testvectors" / f"{model_name}_tv.txt" cv_path = output_dir / "covervectors" / f"{model_name}_cv.txt" if not constants.config.RELEASE else Path(os.devnull) + tv_stamp_path = output_dir / ".stamp" / f"{model_name}_tv.stamp" + cv_stamp_path = output_dir / ".stamp" / f"{model_name}_cv.stamp" model_logger = logging.getLogger(model_name) @@ -97,6 +100,12 @@ def _run_model_by_name( readable_vectors_dir = output_dir / "readable" processed_vectors_dir = output_dir / "processed" postprocess_testvectors(model_name, test_vectors_dir, processed_vectors_dir, readable_vectors_dir) + + tv_stamp_path.parent.mkdir(parents=True, exist_ok=True) + tv_stamp_path.touch() + if not constants.config.RELEASE: + cv_stamp_path.parent.mkdir(parents=True, exist_ok=True) + cv_stamp_path.touch() except Exception as e: logger = logging.getLogger(model_name) logger.exception(f"[bold red]Fatal Error in {model_name}[/] ", exc_info=e, extra={"markup": True}) @@ -121,14 +130,23 @@ def wrapper( executor: concurrent.futures.Executor, post_process: bool = True, ) -> concurrent.futures.Future[None] | None: + # Check modification of source files + cover_float_sources = Path(__file__).parent.parent.rglob("*.py") + max_supporting_mod_time = 0 + for file in cover_float_sources: + if not re.match(r"^B\d+\.py", file.name): + max_supporting_mod_time = max(max_supporting_mod_time, file.stat().st_mtime) + + source_mod_time = source_file.stat().st_mtime + # Check generation time of target files tv_path = output_dir / "testvectors" / f"{model_name}_tv.txt" - tv_mod_time = tv_path.stat().st_mtime if tv_path.exists() else 0 + tv_stamp_path = output_dir / ".stamp" / f"{model_name}_tv.stamp" + tv_mod_time = tv_stamp_path.stat().st_mtime if tv_stamp_path.exists() else 0 cv_path = output_dir / "covervectors" / f"{model_name}_cv.txt" - cv_mod_time = cv_path.stat().st_mtime if cv_path.exists() else 0 - - source_mod_time = source_file.stat().st_mtime + cv_stamp_path = output_dir / ".stamp" / f"{model_name}_cv.stamp" + cv_mod_time = cv_stamp_path.stat().st_mtime if cv_stamp_path.exists() else 0 tv_comes_from_partial: bool | None = None if tv_path.exists(): @@ -146,10 +164,12 @@ def wrapper( not constants.config.RELEASE and ( (source_mod_time > cv_mod_time) + or (max_supporting_mod_time > cv_mod_time) or (cv_comes_from_partial != (not constants.config.FULL_COVERAGE_TESTGEN)) ) ) or ( (source_mod_time > tv_mod_time) + or (max_supporting_mod_time > tv_mod_time) or (tv_comes_from_partial != (not constants.config.FULL_COVERAGE_TESTGEN)) ): task_id = status_reporter.start_model(model_name)