From 79737a84fe67cde99090a21c535d5e3993b22d88 Mon Sep 17 00:00:00 2001 From: Kelvin Chung Date: Wed, 28 May 2025 13:26:46 +0100 Subject: [PATCH 01/24] Add force mode and ensure exception is caught correctly --- FABulous/FABulous.py | 9 ++++++++- FABulous/FABulous_CLI/FABulous_CLI.py | 15 +++++++++++++++ FABulous/FABulous_CLI/helper.py | 7 +++++-- 3 files changed, 28 insertions(+), 3 deletions(-) diff --git a/FABulous/FABulous.py b/FABulous/FABulous.py index 3d7dbfe9..fc4c89ed 100644 --- a/FABulous/FABulous.py +++ b/FABulous/FABulous.py @@ -138,11 +138,17 @@ def main(): default=False, ) + parser.add_argument( + "--force", + action="store_true", + help="Force the command to run and ignore any errors", + ) + parser.add_argument("--debug", action="store_true", help="Enable debug mode") args = parser.parse_args() - setup_logger(args.verbose) + setup_logger(args.verbose, args.debug) setup_global_env_vars(args) @@ -182,6 +188,7 @@ def main(): Path().cwd(), FABulousScript=args.FABulousScript, TCLScript=args.TCLScript, + force=args.force, ) fab_CLI.debug = args.debug diff --git a/FABulous/FABulous_CLI/FABulous_CLI.py b/FABulous/FABulous_CLI/FABulous_CLI.py index 702a2044..fbf2228a 100644 --- a/FABulous/FABulous_CLI/FABulous_CLI.py +++ b/FABulous/FABulous_CLI/FABulous_CLI.py @@ -23,6 +23,7 @@ import sys import tkinter as tk from pathlib import Path +import traceback from cmd2 import ( Cmd, @@ -107,6 +108,7 @@ class FABulous_CLI(Cmd): csvFile: Path extension: str = "v" script: str = "" + force: bool = False def __init__( self, @@ -115,6 +117,7 @@ def __init__( enteringDir: Path, FABulousScript: Path = Path(), TCLScript: Path = Path(), + force: bool = False, ): """Initialises the FABulous shell instance. @@ -164,6 +167,9 @@ def __init__( self.verbose = False self.add_settable(Settable("verbose", bool, "verbose output", self)) + self.force = force + self.add_settable(Settable("force", bool, "force execution", self)) + if isinstance(self.fabulousAPI.writer, VHDLWriter): self.extension = "vhdl" else: @@ -222,6 +228,15 @@ def __init__( logger.error(f"Cannot find {FABulousScript}") exit(1) + def onecmd(self, *arg, **kwargs): + """Override the onecmd method to handle exceptions.""" + try: + return super().onecmd(*arg, **kwargs) + except Exception: + logger.debug(traceback.format_exc()) + self.exit_code = 1 + return not self.force + def do_exit(self, *ignored): """Exits the FABulous shell and logs info message.""" logger.info("Exiting FABulous shell") diff --git a/FABulous/FABulous_CLI/helper.py b/FABulous/FABulous_CLI/helper.py index 75f8848c..d558d35d 100644 --- a/FABulous/FABulous_CLI/helper.py +++ b/FABulous/FABulous_CLI/helper.py @@ -16,7 +16,7 @@ MAX_BITBYTES = 16384 -def setup_logger(verbosity: int): +def setup_logger(verbosity: int, debug: bool): # Remove the default logger to avoid duplicate logs logger.remove() @@ -32,7 +32,10 @@ def setup_logger(verbosity: int): log_format = "{level:} | {message}" # Add logger to write logs to stdout - logger.add(sys.stdout, format=log_format, level="DEBUG", colorize=True) + if debug: + logger.add(sys.stdout, format=log_format, level="DEBUG", colorize=True) + else: + logger.add(sys.stdout, format=log_format, level="INFO", colorize=True) def setup_global_env_vars(args: argparse.Namespace) -> None: From 76d20ccefbc037f6296abb1477b641c44c2cd8ac Mon Sep 17 00:00:00 2001 From: Kelvin Chung Date: Wed, 28 May 2025 19:42:54 +0100 Subject: [PATCH 02/24] Fix script command --- FABulous/FABulous.py | 65 ++++++++++++++++++--------- FABulous/FABulous_CLI/FABulous_CLI.py | 36 ++++----------- 2 files changed, 53 insertions(+), 48 deletions(-) diff --git a/FABulous/FABulous.py b/FABulous/FABulous.py index fc4c89ed..07231c40 100644 --- a/FABulous/FABulous.py +++ b/FABulous/FABulous.py @@ -50,6 +50,8 @@ def main(): description="The command line interface for FABulous" ) + script_group = parser.add_mutually_exclusive_group() + parser.add_argument( "project_dir", help="The directory to the project folder", @@ -63,7 +65,7 @@ def main(): help="Create a new project", ) - parser.add_argument( + script_group.add_argument( "-fs", "--FABulousScript", default="", @@ -71,7 +73,8 @@ def main(): "This will automatically exit the CLI once the command finish execution, and the exit will always happen gracefully.", type=Path, ) - parser.add_argument( + + script_group.add_argument( "-ts", "--TCLScript", default="", @@ -80,6 +83,12 @@ def main(): type=Path, ) + script_group.add_argument( + "-p", + "--commands", + help="execute (to chain commands, separate them with semicolon + whitespace: 'cmd1; cmd2')", + ) + parser.add_argument( "-log", default=False, @@ -186,29 +195,45 @@ def main(): os.getenv("FAB_PROJ_LANG"), projectDir, Path().cwd(), - FABulousScript=args.FABulousScript, - TCLScript=args.TCLScript, force=args.force, ) fab_CLI.debug = args.debug - if args.verbose == 2: - fab_CLI.verbose = True - if args.metaDataDir: - if Path(args.metaDataDir).exists(): - metaDataDir = args.metaDataDir - - if args.log: - with open(args.log, "w") as log: - with redirect_stdout(log): - logger.info("Logging to file: " + args.log) - logger.info(f"Setting current working directory to: {projectDir}") - os.chdir(projectDir) - fab_CLI.cmdloop() + if commands := args.commands: + commands = commands.split("; ") + for c in commands: + if fab_CLI.onecmd_plus_hooks(c): + exit(1) + else: + logger.info( + f'Commands "{'; '.join(i.strip() for i in commands)}" executed successfully' + ) + exit(0) + elif args.FABulousScript != Path(""): + if fab_CLI.onecmd_plus_hooks(f"run_script {args.FABulousScript}"): + exit(1) + else: + logger.info( + f"FABulous script {args.FABulousScript} executed successfully" + ) + exit(0) + elif args.TCLScript != Path(""): + if fab_CLI.onecmd_plus_hooks(f"run_script {args.TCLScript}"): + exit(1) + else: + logger.info(f"TCL script {args.TCLScript} executed successfully") + exit(0) else: - logger.info(f"Setting current working directory to: {projectDir}") - os.chdir(projectDir) - fab_CLI.cmdloop() + if args.verbose == 2: + fab_CLI.verbose = True + + if args.log: + with open(args.log, "w") as log: + with redirect_stdout(log): + fab_CLI.cmdloop() + else: + exit_code = fab_CLI.cmdloop() + exit(exit_code) if __name__ == "__main__": diff --git a/FABulous/FABulous_CLI/FABulous_CLI.py b/FABulous/FABulous_CLI/FABulous_CLI.py index fbf2228a..9999df05 100644 --- a/FABulous/FABulous_CLI/FABulous_CLI.py +++ b/FABulous/FABulous_CLI/FABulous_CLI.py @@ -115,8 +115,6 @@ def __init__( writerType: str | None, projectDir: Path, enteringDir: Path, - FABulousScript: Path = Path(), - TCLScript: Path = Path(), force: bool = False, ): """Initialises the FABulous shell instance. @@ -136,7 +134,6 @@ def __init__( super().__init__( persistent_history_file=f"{os.getenv('FAB_PROJ_DIR')}/{META_DATA_DIR}/.fabulous_history", allow_cli_args=False, - startup_script=str(FABulousScript) if not FABulousScript.is_dir() else "", ) self.enteringDir = enteringDir @@ -212,22 +209,6 @@ def __init__( CMD_HELPER, "Helper commands are disabled until fabric is loaded" ) - if not TCLScript.is_dir() and TCLScript.exists(): - self._startup_commands.append(f"run_tcl {Path(TCLScript).absolute()}") - self._startup_commands.append("exit") - elif not TCLScript.is_dir() and not TCLScript.exists(): - logger.error(f"Cannot find {TCLScript}") - exit(1) - - if not FABulousScript.is_dir() and FABulousScript.exists(): - self._startup_commands.append( - f"run_script {Path(FABulousScript).absolute()}" - ) - self._startup_commands.append("exit") - elif not FABulousScript.is_dir() and not FABulousScript.exists(): - logger.error(f"Cannot find {FABulousScript}") - exit(1) - def onecmd(self, *arg, **kwargs): """Override the onecmd method to handle exceptions.""" try: @@ -637,7 +618,6 @@ def do_gen_top_wrapper(self, *ignored): logger.info("Top wrapper generation complete") @with_category(CMD_FABRIC_FLOW) - @allow_blank def do_run_FABulous_fabric(self, *ignored): """Generates the fabric based on the CSV file, creates bitstream specification of the fabric, top wrapper of the fabric, Nextpnr model of the fabric and @@ -646,11 +626,11 @@ def do_run_FABulous_fabric(self, *ignored): Does this by calling the respective functions 'do_gen_[function]'. """ logger.info("Running FABulous") - self.do_gen_fabric() - self.do_gen_bitStream_spec() - self.do_gen_top_wrapper() - self.do_gen_model_npnr() - self.do_gen_geometry() + self.onecmd_plus_hooks("gen_fabric") + self.onecmd_plus_hooks("gen_bitStream_spec") + self.onecmd_plus_hooks("gen_top_wrapper") + self.onecmd_plus_hooks("gen_model_npnr") + self.onecmd_plus_hooks("gen_geometry") logger.info("FABulous fabric flow complete") return @@ -970,9 +950,9 @@ def do_run_FABulous_bitstream(self, args): else: logger.info("No external primsLib found.") - self.do_synthesis(do_synth_args) - self.do_place_and_route(str(json_file_path)) - self.do_gen_bitStream_binary(str(fasm_file_path)) + self.onecmd_plus_hooks(f"synthesis {do_synth_args}") + self.onecmd_plus_hooks(f"place_and_route {json_file_path}") + self.onecmd_plus_hooks(f"gen_bitStream_binary {fasm_file_path}") @with_category(CMD_SCRIPT) @with_argparser(filePathRequireParser) From d54edcfb7ea413d5af0e76916f793638315de7c1 Mon Sep 17 00:00:00 2001 From: Kelvin Chung Date: Thu, 29 May 2025 00:39:50 +0100 Subject: [PATCH 03/24] fix with path resolve --- FABulous/FABulous.py | 7 ++++--- FABulous/FABulous_CLI/FABulous_CLI.py | 1 - 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/FABulous/FABulous.py b/FABulous/FABulous.py index 07231c40..f339b430 100644 --- a/FABulous/FABulous.py +++ b/FABulous/FABulous.py @@ -190,7 +190,6 @@ def main(): exit(1) else: setup_project_env_vars(args) - fab_CLI = FABulous_CLI( os.getenv("FAB_PROJ_LANG"), projectDir, @@ -198,6 +197,8 @@ def main(): force=args.force, ) fab_CLI.debug = args.debug + logger.info(f"Setting current working directory to: {projectDir}") + os.chdir(projectDir) if commands := args.commands: commands = commands.split("; ") @@ -210,7 +211,7 @@ def main(): ) exit(0) elif args.FABulousScript != Path(""): - if fab_CLI.onecmd_plus_hooks(f"run_script {args.FABulousScript}"): + if fab_CLI.onecmd_plus_hooks(f"run_script { projectDir / args.FABulousScript.absolute()}"): exit(1) else: logger.info( @@ -218,7 +219,7 @@ def main(): ) exit(0) elif args.TCLScript != Path(""): - if fab_CLI.onecmd_plus_hooks(f"run_script {args.TCLScript}"): + if fab_CLI.onecmd_plus_hooks(f"run_script {projectDir / args.TCLScript.absolute()}"): exit(1) else: logger.info(f"TCL script {args.TCLScript} executed successfully") diff --git a/FABulous/FABulous_CLI/FABulous_CLI.py b/FABulous/FABulous_CLI/FABulous_CLI.py index 9999df05..1cd912ce 100644 --- a/FABulous/FABulous_CLI/FABulous_CLI.py +++ b/FABulous/FABulous_CLI/FABulous_CLI.py @@ -949,7 +949,6 @@ def do_run_FABulous_bitstream(self, args): do_synth_args += f" -extra-plib {primsLib}" else: logger.info("No external primsLib found.") - self.onecmd_plus_hooks(f"synthesis {do_synth_args}") self.onecmd_plus_hooks(f"place_and_route {json_file_path}") self.onecmd_plus_hooks(f"gen_bitStream_binary {fasm_file_path}") From 8d9a7cfc3313696e2cc08a4c5c53b5a19434c20f Mon Sep 17 00:00:00 2001 From: Kelvin Chung Date: Thu, 29 May 2025 10:58:13 +0100 Subject: [PATCH 04/24] Correct stopping --- FABulous/FABulous.py | 3 +- FABulous/FABulous_CLI/FABulous_CLI.py | 194 ++++++++++---------------- FABulous/FABulous_CLI/helper.py | 103 +++++++------- 3 files changed, 122 insertions(+), 178 deletions(-) diff --git a/FABulous/FABulous.py b/FABulous/FABulous.py index f339b430..cdfe7a4a 100644 --- a/FABulous/FABulous.py +++ b/FABulous/FABulous.py @@ -219,12 +219,13 @@ def main(): ) exit(0) elif args.TCLScript != Path(""): - if fab_CLI.onecmd_plus_hooks(f"run_script {projectDir / args.TCLScript.absolute()}"): + if fab_CLI.onecmd_plus_hooks(f"run_tcl {projectDir / args.TCLScript.absolute()}"): exit(1) else: logger.info(f"TCL script {args.TCLScript} executed successfully") exit(0) else: + fab_CLI.interactive = True if args.verbose == 2: fab_CLI.verbose = True diff --git a/FABulous/FABulous_CLI/FABulous_CLI.py b/FABulous/FABulous_CLI/FABulous_CLI.py index 1cd912ce..97e55cf2 100644 --- a/FABulous/FABulous_CLI/FABulous_CLI.py +++ b/FABulous/FABulous_CLI/FABulous_CLI.py @@ -22,8 +22,8 @@ import subprocess as sp import sys import tkinter as tk -from pathlib import Path import traceback +from pathlib import Path from cmd2 import ( Cmd, @@ -35,21 +35,22 @@ ) from loguru import logger +from FABulous.custom_exception import CommandError, EnvironmentNotSet, FileTypeError from FABulous.fabric_generator.code_generation_Verilog import VerilogWriter from FABulous.fabric_generator.code_generation_VHDL import VHDLWriter +from FABulous.fabric_generator.fabric_automation import generateCustomTileConfig +from FABulous.fabric_generator.file_parser import parseTiles from FABulous.FABulous_API import FABulous_API from FABulous.FABulous_CLI import cmd_synthesis -from FABulous.fabric_generator.fabric_automation import generateCustomTileConfig from FABulous.FABulous_CLI.helper import ( allow_blank, check_if_application_exists, copy_verilog_files, + install_oss_cad_suite, make_hex, remove_dir, wrap_with_except_handling, - install_oss_cad_suite, ) -from FABulous.fabric_generator.file_parser import parseTiles META_DATA_DIR = ".FABulous" @@ -109,6 +110,7 @@ class FABulous_CLI(Cmd): extension: str = "v" script: str = "" force: bool = False + interactive: bool = True def __init__( self, @@ -116,6 +118,7 @@ def __init__( projectDir: Path, enteringDir: Path, force: bool = False, + interactive: bool = False, ): """Initialises the FABulous shell instance. @@ -142,24 +145,16 @@ def __init__( elif writerType == "vhdl": self.fabulousAPI = FABulous_API(VHDLWriter()) else: - logger.critical( - f"Invalid writer type: {writerType}\n Valid options are 'verilog' or 'vhdl'" - ) + logger.critical(f"Invalid writer type: {writerType}\n Valid options are 'verilog' or 'vhdl'") sys.exit(1) self.projectDir = projectDir.absolute() - self.add_settable( - Settable("projectDir", Path, "The directory of the project", self) - ) + self.add_settable(Settable("projectDir", Path, "The directory of the project", self)) self.tiles = [] self.superTiles = [] self.csvFile = Path(projectDir / "fabric.csv") - self.add_settable( - Settable( - "csvFile", Path, "The fabric file ", self, completer=Cmd.path_complete - ) - ) + self.add_settable(Settable("csvFile", Path, "The fabric file ", self, completer=Cmd.path_complete)) self.verbose = False self.add_settable(Settable("verbose", bool, "verbose output", self)) @@ -167,6 +162,8 @@ def __init__( self.force = force self.add_settable(Settable("force", bool, "force execution", self)) + self.interactive = interactive + if isinstance(self.fabulousAPI.writer, VHDLWriter): self.extension = "vhdl" else: @@ -195,28 +192,25 @@ def __init__( name = fun.strip("do_") self.tcl.createcommand(name, wrap_with_except_handling(f)) - self.disable_category( - CMD_FABRIC_FLOW, "Fabric Flow commands are disabled until fabric is loaded" - ) + self.disable_category(CMD_FABRIC_FLOW, "Fabric Flow commands are disabled until fabric is loaded") self.disable_category( CMD_USER_DESIGN_FLOW, "User Design Flow commands are disabled until fabric is loaded", ) - self.disable_category( - CMD_GUI, "GUI commands are disabled until gen_gen_geometry is run" - ) - self.disable_category( - CMD_HELPER, "Helper commands are disabled until fabric is loaded" - ) + self.disable_category(CMD_GUI, "GUI commands are disabled until gen_gen_geometry is run") + self.disable_category(CMD_HELPER, "Helper commands are disabled until fabric is loaded") - def onecmd(self, *arg, **kwargs): + def onecmd(self, *args, **kwargs) -> bool: """Override the onecmd method to handle exceptions.""" try: - return super().onecmd(*arg, **kwargs) + return super().onecmd(*args, **kwargs) except Exception: logger.debug(traceback.format_exc()) self.exit_code = 1 - return not self.force + if not self.interactive: + return not self.force + else: + return False def do_exit(self, *ignored): """Exits the FABulous shell and logs info message.""" @@ -240,9 +234,7 @@ def do_exit(self, *ignored): ) filePathRequireParser = Cmd2ArgumentParser() - filePathRequireParser.add_argument( - "file", type=Path, help="Path to the target file", completer=Cmd.path_complete - ) + filePathRequireParser.add_argument("file", type=Path, help="Path to the target file", completer=Cmd.path_complete) userDesignRequireParser = Cmd2ArgumentParser() userDesignRequireParser.add_argument( @@ -328,19 +320,16 @@ def do_load_fabric(self, args): ) self.fabulousAPI.loadFabric(self.csvFile) else: - logger.error( + logger.opt(exception=FileExistsError()).error( "No argument is given and the csv file is set or the file does not exist" ) - return else: self.fabulousAPI.loadFabric(args.file) self.csvFile = args.file self.fabricLoaded = True # self.projectDir = os.path.split(self.csvFile)[0] - tileByPath = [ - f.stem for f in (self.projectDir / "Tile/").iterdir() if f.is_dir() - ] + tileByPath = [f.stem for f in (self.projectDir / "Tile/").iterdir() if f.is_dir()] tileByFabric = list(self.fabulousAPI.fabric.tileDic.keys()) superTileByFabric = list(self.fabulousAPI.fabric.superTileDic.keys()) self.allTile = list(set(tileByPath) & set(tileByFabric + superTileByFabric)) @@ -353,19 +342,17 @@ def do_load_fabric(self, args): def do_print_bel(self, args): """Prints a Bel object to the console.""" if len(args) != 1: - logger.error("Please provide a Bel name") - return + logger.opt(exception=CommandError()).error("Please provide a Bel name") if not self.fabricLoaded: - logger.error("Need to load fabric first") - return + logger.opt(exception=CommandError()).error("Need to load fabric first") bels = self.fabulousAPI.getBels() for i in bels: if i.name == args[0]: logger.info(f"\n{pprint.pformat(i, width=200)}") return - logger.error("Bel not found") + logger.opt(exception=CommandError()).error("Bel not found") @with_category(CMD_HELPER) @with_argparser(tile_single_parser) @@ -373,7 +360,7 @@ def do_print_tile(self, args): """Prints a tile object to the console.""" if not self.fabricLoaded: - logger.error("Need to load fabric first") + logger.opt(exception=CommandError()).error("Need to load fabric first") return if tile := self.fabulousAPI.getTile(args.tile): @@ -381,7 +368,7 @@ def do_print_tile(self, args): elif tile := self.fabulousAPI.getSuperTile(args[0]): logger.info(f"\n{pprint.pformat(tile, width=200)}") else: - logger.error("Tile not found") + logger.opt(exception=CommandError()).error("Tile not found") @with_category(CMD_FABRIC_FLOW) @with_argparser(tile_list_parser) @@ -394,13 +381,9 @@ def do_gen_config_mem(self, args): logger.info(f"Generating Config Memory for {' '.join(args.tiles)}") for i in args.tiles: logger.info(f"Generating configMem for {i}") - self.fabulousAPI.setWriterOutputFile( - self.projectDir / f"Tile/{i}/{i}_ConfigMem.{self.extension}" - ) - self.fabulousAPI.genConfigMem( - i, self.projectDir / f"Tile/{i}/{i}_ConfigMem.csv" - ) - logger.info("ConfigMem generation complete") + self.fabulousAPI.setWriterOutputFile(self.projectDir / f"Tile/{i}/{i}_ConfigMem.{self.extension}") + self.fabulousAPI.genConfigMem(i, self.projectDir / f"Tile/{i}/{i}_ConfigMem.csv") + logger.info("Generating configMem complete") @with_category(CMD_FABRIC_FLOW) @with_argparser(tile_list_parser) @@ -413,9 +396,7 @@ def do_gen_switch_matrix(self, args): logger.info(f"Generating switch matrix for {' '.join(args.tiles)}") for i in args.tiles: logger.info(f"Generating switch matrix for {i}") - self.fabulousAPI.setWriterOutputFile( - self.projectDir / f"Tile/{i}/{i}_switch_matrix.{self.extension}" - ) + self.fabulousAPI.setWriterOutputFile(self.projectDir / f"Tile/{i}/{i}_switch_matrix.{self.extension}") self.fabulousAPI.genSwitchMatrix(i) logger.info("Switch matrix generation complete") @@ -431,12 +412,8 @@ def do_gen_tile(self, args): logger.info(f"Generating tile {' '.join(args.tiles)}") for t in args.tiles: - if subTiles := [ - f.stem for f in (self.projectDir / f"Tile/{t}").iterdir() if f.is_dir() - ]: - logger.info( - f"{t} is a super tile, generating {t} with sub tiles {' '.join(subTiles)}" - ) + if subTiles := [f.stem for f in (self.projectDir / f"Tile/{t}").iterdir() if f.is_dir()]: + logger.info(f"{t} is a super tile, generating {t} with sub tiles {' '.join(subTiles)}") for st in subTiles: # Gen switch matrix logger.info(f"Generating switch matrix for tile {t}") @@ -453,25 +430,19 @@ def do_gen_tile(self, args): self.fabulousAPI.setWriterOutputFile( f"{self.projectDir}/Tile/{t}/{st}/{st}_ConfigMem.{self.extension}" ) - self.fabulousAPI.genConfigMem( - st, self.projectDir / f"Tile/{t}/{st}/{st}_ConfigMem.csv" - ) + self.fabulousAPI.genConfigMem(st, self.projectDir / f"Tile/{t}/{st}/{st}_ConfigMem.csv") logger.info(f"Generated configMem for {st}") # Gen tile logger.info(f"Generating subtile for tile {t}") logger.info(f"Generating subtile {st}") - self.fabulousAPI.setWriterOutputFile( - f"{self.projectDir}/Tile/{t}/{st}/{st}.{self.extension}" - ) + self.fabulousAPI.setWriterOutputFile(f"{self.projectDir}/Tile/{t}/{st}/{st}.{self.extension}") self.fabulousAPI.genTile(st) logger.info(f"Generated subtile {st}") # Gen super tile logger.info(f"Generating super tile {t}") - self.fabulousAPI.setWriterOutputFile( - f"{self.projectDir}/Tile/{t}/{t}.{self.extension}" - ) + self.fabulousAPI.setWriterOutputFile(f"{self.projectDir}/Tile/{t}/{t}.{self.extension}") self.fabulousAPI.genSuperTile(t) logger.info(f"Generated super tile {t}") continue @@ -484,9 +455,7 @@ def do_gen_tile(self, args): logger.info(f"Generating tile {t}") # Gen tile - self.fabulousAPI.setWriterOutputFile( - f"{self.projectDir}/Tile/{t}/{t}.{self.extension}" - ) + self.fabulousAPI.setWriterOutputFile(f"{self.projectDir}/Tile/{t}/{t}.{self.extension}") self.fabulousAPI.genTile(t) logger.info(f"Generated tile {t}") @@ -562,7 +531,7 @@ def do_start_FABulator(self, *ignored): return if not os.path.exists(fabulatorRoot): - logger.error( + logger.opt(exception=EnvironmentNotSet()).error( f"FABULATOR_ROOT environment variable set to {fabulatorRoot} but the directory does not exist." ) return @@ -579,8 +548,8 @@ def do_start_FABulator(self, *ignored): # discard FABulator output sp.Popen(startupCmd, stdout=sp.DEVNULL, stderr=sp.DEVNULL) - except sp.SubprocessError: - logger.error("Startup of FABulator failed.") + except sp.SubprocessError as e: + logger.opt(exception=e).error("Startup of FABulator failed.") @with_category(CMD_FABRIC_FLOW) def do_gen_bitStream_spec(self, *ignored): @@ -593,9 +562,7 @@ def do_gen_bitStream_spec(self, *ignored): specObject = self.fabulousAPI.genBitStreamSpec() logger.info(f"output file: {self.projectDir}/{META_DATA_DIR}/bitStreamSpec.bin") - with open( - f"{self.projectDir}/{META_DATA_DIR}/bitStreamSpec.bin", "wb" - ) as outFile: + with open(f"{self.projectDir}/{META_DATA_DIR}/bitStreamSpec.bin", "wb") as outFile: pickle.dump(specObject, outFile) logger.info(f"output file: {self.projectDir}/{META_DATA_DIR}/bitStreamSpec.csv") @@ -674,22 +641,19 @@ def do_place_and_route(self, args): Also logs place and route error, file not found error and type error. """ - logger.info( - f"Running Placement and Routing with Nextpnr for design {args.file}" - ) + logger.info(f"Running Placement and Routing with Nextpnr for design {args.file}") path = Path(args.file) parent = path.parent json_file = path.name top_module_name = path.stem if path.suffix != ".json": - logger.error( + logger.opt(exception=FileTypeError()).error( """ No json file provided. Usage: place_and_route ( is generated by Yosys. Generate it by running `synthesis`.) """ ) - return fasm_file = top_module_name + ".fasm" log_file = top_module_name + "_npnr_log.txt" @@ -697,19 +661,16 @@ def do_place_and_route(self, args): if parent == "": parent = "." - if not os.path.exists( - f"{self.projectDir}/.FABulous/pips.txt" - ) or not os.path.exists(f"{self.projectDir}/.FABulous/bel.txt"): - logger.error( + if not os.path.exists(f"{self.projectDir}/.FABulous/pips.txt") or not os.path.exists( + f"{self.projectDir}/.FABulous/bel.txt" + ): + logger.opt(exception=FileNotFoundError()).error( "Pips and Bel files are not found, please run model_gen_npnr first" ) - raise FileNotFoundError if os.path.exists(f"{self.projectDir}/{parent}"): # TODO rewriting the fab_arch script so no need to copy file for work around - npnr = check_if_application_exists( - os.getenv("FAB_NEXTPNR_PATH", "nextpnr-generic") - ) + npnr = check_if_application_exists(os.getenv("FAB_NEXTPNR_PATH", "nextpnr-generic")) if f"{json_file}" in os.listdir(f"{self.projectDir}/{parent}"): runCmd = [ f"FAB_ROOT={self.projectDir}", @@ -732,19 +693,17 @@ def do_place_and_route(self, args): check=True, shell=True, ) - except sp.CalledProcessError: - logger.error("Placement and Routing failed.") + except sp.CalledProcessError as e: + logger.opt(exception=e).error("Placement and Routing failed.") else: - logger.error( + logger.opt(exception=FileNotFoundError()).error( f'Cannot find file "{json_file}" in path "./{parent}/", which is generated by running Yosys with Nextpnr backend (e.g. synthesis).' ) - raise FileNotFoundError logger.info("Placement and Routing completed") else: - logger.error(f"Directory {self.projectDir}/{parent} does not exist.") - raise FileNotFoundError + logger.opt(exception=FileNotFoundError()).error(f"Directory {self.projectDir}/{parent} does not exist.") @with_category(CMD_USER_DESIGN_FLOW) @with_argparser(filePathRequireParser) @@ -762,7 +721,7 @@ def do_gen_bitStream_binary(self, args): top_module_name = args.file.stem if args.file.suffix != ".fasm": - logger.error( + logger.opt(exception=FileTypeError()).error( """ No fasm file provided. Usage: gen_bitStream_binary @@ -773,16 +732,14 @@ def do_gen_bitStream_binary(self, args): bitstream_file = top_module_name + ".bin" if not (self.projectDir / ".FABulous/bitStreamSpec.bin").exists(): - logger.error( + logger.opt(exception=FileNotFoundError()).error( "Cannot find bitStreamSpec.bin file, which is generated by running gen_bitStream_spec" ) - return if not (self.projectDir / f"{parent}/{fasm_file}").exists(): - logger.error( + logger.opt(exception=FileNotFoundError()).error( f"Cannot find {self.projectDir}/{parent}/{fasm_file} file which is generated by running place_and_route. Potentially Place and Route Failed." ) - return logger.info(f"Generating Bitstream for design {self.projectDir}/{args.file}") logger.info(f"Outputting to {self.projectDir}/{parent}/{bitstream_file}") @@ -795,8 +752,8 @@ def do_gen_bitStream_binary(self, args): ] try: sp.run(runCmd, check=True) - except sp.CalledProcessError: - logger.error("Bitstream generation failed") + except sp.CalledProcessError as e: + logger.opt(exception=e).error("Bitstream generation failed") logger.info("Bitstream generated") @@ -836,10 +793,9 @@ def do_run_simulation(self, args): logger.error("No bitstream file specified.") return if not bitstreamPath.exists(): - logger.error( + logger.opt(exception=FileNotFoundError()).error( f"Cannot find {bitstreamPath} file which is generated by running gen_bitStream_binary. Potentially the bitstream generation failed." ) - return waveform_format = args.format defined_option = f"CREATE_{waveform_format.upper()}" @@ -862,9 +818,7 @@ def do_run_simulation(self, args): copy_verilog_files(self.projectDir / "Fabric", fabricFilesDir) file_list = [str(i) for i in fabricFilesDir.glob("*.v")] - iverilog = check_if_application_exists( - os.getenv("FAB_IVERILOG_PATH", "iverilog") - ) + iverilog = check_if_application_exists(os.getenv("FAB_IVERILOG_PATH", "iverilog")) try: runCmd = [ f"{iverilog}", @@ -883,10 +837,9 @@ def do_run_simulation(self, args): logger.info(f"Running command: {' '.join(runCmd)}") sp.run(runCmd, check=True) - except sp.CalledProcessError: - logger.error("Simulation failed") + except sp.CalledProcessError as e: remove_dir(buildDir) - return + logger.opt(exception=e).error("Simulation failed") # bitstream hex file is used for simulation so it'll be created in the test directory bitstreamHexPath = (buildDir.parent / bitstreamPath.stem).with_suffix(".hex") @@ -909,11 +862,11 @@ def do_run_simulation(self, args): if self.verbose or self.debug: logger.info(f"Running command: {' '.join(runCmd)}") sp.run(runCmd, check=True) - except sp.CalledProcessError: - logger.error("Simulation failed") - return + remove_dir(buildDir) + except sp.CalledProcessError as e: + remove_dir(buildDir) + logger.opt(exception=e).error("Simulation failed") - remove_dir(buildDir) logger.info("Simulation finished") @with_category(CMD_USER_DESIGN_FLOW) @@ -931,13 +884,12 @@ def do_run_FABulous_bitstream(self, args): file_path_no_suffix = args.file.parent / args.file.stem if args.file.suffix != ".v": - logger.error( + logger.opt(exception=FileTypeError()).error( """ No verilog file provided. Usage: run_FABulous_bitstream """ ) - return json_file_path = file_path_no_suffix.with_suffix(".json") fasm_file_path = file_path_no_suffix.with_suffix(".fasm") @@ -962,8 +914,7 @@ def do_run_tcl(self, args): Also logs usage errors and file not found errors. """ if not args.file.exists(): - logger.error(f"Cannot find {args.file}") - return + logger.opt(exception=FileNotFoundError()).error(f"Cannot find {args.file}") logger.info(f"Execute TCL script {args.file}") @@ -979,14 +930,11 @@ def do_run_tcl(self, args): @with_category(CMD_USER_DESIGN_FLOW) @with_argparser(userDesignRequireParser) def do_gen_user_design_wrapper(self, args): - if not self.fabricLoaded: - logger.error("Need to load fabric first") + logger.opt(exception=CommandError()).error("Need to load fabric first") return - self.fabulousAPI.generateUserDesignTopWrapper( - args.user_design, args.user_design_top_wrapper - ) + self.fabulousAPI.generateUserDesignTopWrapper(args.user_design, args.user_design_top_wrapper) gen_tile_parser = Cmd2ArgumentParser() gen_tile_parser.add_argument( diff --git a/FABulous/FABulous_CLI/helper.py b/FABulous/FABulous_CLI/helper.py index d558d35d..00cbf52e 100644 --- a/FABulous/FABulous_CLI/helper.py +++ b/FABulous/FABulous_CLI/helper.py @@ -2,7 +2,6 @@ import functools import os import platform -import requests import re import shutil import sys @@ -10,6 +9,7 @@ from pathlib import Path from typing import Literal +import requests from dotenv import load_dotenv from loguru import logger @@ -20,22 +20,29 @@ def setup_logger(verbosity: int, debug: bool): # Remove the default logger to avoid duplicate logs logger.remove() - # Define logger format - if verbosity >= 1: - log_format = ( - "{level:} | " - "[{time:DD-MM-YYYY HH:mm:ss]} | " - "[{name}:{function}:{line}] - " - "{message}" - ) - else: - log_format = "{level:} | {message}" + # Define a custom formatting function that has access to 'verbosity' + def custom_format_function(record): + # Construct the standard part of the log message based on verbosity + level = f"{record["level"].name} | " + time = f"[{record["time"]:DD-MM-YYYY HH:mm:ss}] | " + name = f"[{record["name"]}" + func = f"{record["function"]}" + line = f"{record["line"]}" + msg = f"{record["message"]}" + exc = f"{record["exception"].type.__name__} | " if record["exception"] else "" + + if verbosity >= 1: + final_log = f"{level}{time}{name}:{func}:{line} - {exc}{msg}\n" + else: + final_log = f"{level}{exc}{msg}\n" - # Add logger to write logs to stdout - if debug: - logger.add(sys.stdout, format=log_format, level="DEBUG", colorize=True) - else: - logger.add(sys.stdout, format=log_format, level="INFO", colorize=True) + return final_log + + # Determine the log level for the sink + log_level_to_set = "DEBUG" if debug else "INFO" + + # Add logger to write logs to stdout using the custom formatter + logger.add(sys.stdout, format=custom_format_function, level=log_level_to_set, colorize=True) def setup_global_env_vars(args: argparse.Namespace) -> None: @@ -60,9 +67,7 @@ def setup_global_env_vars(args: argparse.Namespace) -> None: fabulousRoot = str(Path(fabulousRoot).joinpath("FABulous")) os.environ["FAB_ROOT"] = fabulousRoot else: - logger.error( - f"FAB_ROOT environment variable set to {fabulousRoot} but the directory does not exist" - ) + logger.error(f"FAB_ROOT environment variable set to {fabulousRoot} but the directory does not exist") sys.exit() logger.info(f"FAB_ROOT set to {fabulousRoot}") @@ -85,10 +90,7 @@ def setup_global_env_vars(args: argparse.Namespace) -> None: elif fabDir.joinpath(".env").exists() and fabDir.joinpath(".env").is_file(): load_dotenv(fabDir.joinpath(".env")) logger.info(f"Loaded global .env file from {fabulousRoot}/.env") - elif ( - fabDir.parent.joinpath(".env").exists() - and fabDir.parent.joinpath(".env").is_file() - ): + elif fabDir.parent.joinpath(".env").exists() and fabDir.parent.joinpath(".env").is_file(): load_dotenv(fabDir.parent.joinpath(".env")) logger.info(f"Loaded global .env file from {fabDir.parent.joinpath('.env')}") else: @@ -126,10 +128,7 @@ def setup_project_env_vars(args: argparse.Namespace) -> None: elif fabDir.joinpath(".env").exists() and fabDir.joinpath(".env").is_file(): load_dotenv(fabDir.joinpath(".env")) logger.info(f"Loaded project .env file from {fabDir}/.env')") - elif ( - fabDir.parent.joinpath(".env").exists() - and fabDir.parent.joinpath(".env").is_file() - ): + elif fabDir.parent.joinpath(".env").exists() and fabDir.parent.joinpath(".env").is_file(): load_dotenv(fabDir.parent.joinpath(".env")) logger.info(f"Loaded project .env file from {fabDir.parent.joinpath('.env')}") else: @@ -171,7 +170,11 @@ def create_project(project_dir: Path, lang: Literal["verilog", "vhdl"] = "verilo if lang not in ["verilog", "vhdl"]: lang = "verilog" - fabulousRoot = Path(os.getenv("FAB_ROOT")) + fab_root_env = os.getenv("FAB_ROOT") + if fab_root_env is None: + logger.error("FAB_ROOT environment variable is not set. Cannot create project.") + sys.exit(1) + fabulousRoot = Path(fab_root_env) # Copy the project template common_template = fabulousRoot / "fabric_files/FABulous_project_template_common" @@ -284,11 +287,11 @@ def check_if_application_exists(application: str, throw_exception: bool = True) if path is not None: return Path(path) else: - logger.error( - f"{application} is not installed. Please install it or set FAB__PATH in the .env file." - ) - if throw_exception: - raise Exception(f"{application} is not installed.") + error_msg = f"{application} is not installed. Please install it or set FAB__PATH in the .env file." + logger.error(error_msg) + # To satisfy the `-> Path` return type, an exception must be raised if no path is found. + # The throw_exception parameter's original intent might need review if non-exception paths were desired. + raise FileNotFoundError(error_msg) def wrap_with_except_handling(fun_to_wrap): @@ -317,7 +320,7 @@ def inter(*args, **varargs): import traceback traceback.print_exc() - sys.exit(1) + raise Exception("TCL command failed. Please check the logs for details.") return inter @@ -356,9 +359,7 @@ def install_oss_cad_suite(destination_folder: Path, update: bool = False): No valid archive of OSS-CAD-Suite found in the latest release. If the file format of the downloaded archive is unsupported. """ - github_releases_url = ( - "https://api.github.com/repos/YosysHQ/oss-cad-suite-build/releases/latest" - ) + github_releases_url = "https://api.github.com/repos/YosysHQ/oss-cad-suite-build/releases/latest" response = requests.get(github_releases_url) system = platform.system().lower() machine = platform.machine().lower() @@ -385,30 +386,22 @@ def install_oss_cad_suite(destination_folder: Path, update: bool = False): logger.info(f"Creating folder {destination_folder.absolute()}") os.makedirs(destination_folder, exist_ok=True) else: - logger.info( - f"Installing OSS-CAD-Suite to folder {destination_folder.absolute()}" - ) + logger.info(f"Installing OSS-CAD-Suite to folder {destination_folder.absolute()}") # format system and machine to match the OSS-CAD-Suite release naming if system not in ["linux", "windows", "darwin"]: - raise ValueError( - f"Unsupported operating system {system}. Please install OSS-CAD-Suite manually." - ) + raise ValueError(f"Unsupported operating system {system}. Please install OSS-CAD-Suite manually.") if machine in ["x86_64", "amd64"]: machine = "x64" elif machine in ["aarch64", "arm64"]: machine = "arm64" else: - raise ValueError( - f"Unsupported architecture {machine}. Please install OSS-CAD-Suite manually." - ) + raise ValueError(f"Unsupported architecture {machine}. Please install OSS-CAD-Suite manually.") if response.status_code == 200: latest_release = response.json() else: - raise Exception( - f"Failed to fetch latest OSS-CAD-Suite release: {response.status_code}" - ) + raise Exception(f"Failed to fetch latest OSS-CAD-Suite release: {response.status_code}") # find the right release for the current system for asset in latest_release.get("assets", []): @@ -416,7 +409,7 @@ def install_oss_cad_suite(destination_folder: Path, update: bool = False): if machine in asset["name"].lower() and system in asset["name"].lower(): url = asset["browser_download_url"] break # we assume that the first match is the right one - if url == None or url == "": + if url is None or url == "": # Changed == None to is None raise ValueError("No valid archive found in the latest release.") # Download the file @@ -437,14 +430,16 @@ def install_oss_cad_suite(destination_folder: Path, update: bool = False): with tarfile.open(ocs_archive, "r:gz") as tar: tar.extractall(path=destination_folder) else: - raise ValueError( - f"Unsupported file format. Please extract {ocs_archive} manually." - ) + raise ValueError(f"Unsupported file format. Please extract {ocs_archive} manually.") logger.info(f"Remove archive {ocs_archive}") ocs_archive.unlink() - env_file = Path(os.getenv("FAB_ROOT")) / ".env" + fab_root_env = os.getenv("FAB_ROOT") + if fab_root_env is None: + logger.error("FAB_ROOT environment variable is not set. Cannot update .env file for OSS CAD Suite.") + raise EnvironmentError("FAB_ROOT is not set, cannot determine .env file path for OSS CAD Suite.") + env_file = Path(fab_root_env) / ".env" env_cont = "" if env_file.is_file(): logger.info(f"Updating FAB_OSS_CAD_SUITE in .env file {env_file}") From f0f8ab453f3a16f27aa36acb15aec77be795901b Mon Sep 17 00:00:00 2001 From: Kelvin Chung Date: Thu, 29 May 2025 11:05:36 +0100 Subject: [PATCH 05/24] add missing exception --- FABulous/custom_exception.py | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 FABulous/custom_exception.py diff --git a/FABulous/custom_exception.py b/FABulous/custom_exception.py new file mode 100644 index 00000000..6b04277d --- /dev/null +++ b/FABulous/custom_exception.py @@ -0,0 +1,11 @@ +class CommandError(Exception): + """Exception raised for errors in the command execution.""" + pass + +class EnvironmentNotSet(Exception): + """Exception raised when the environment is not set.""" + pass + +class FileTypeError(Exception): + """Exception raised for unsupported file types.""" + pass \ No newline at end of file From b2eabdd3a445cba005d9b7efe176163700b96451 Mon Sep 17 00:00:00 2001 From: Kelvin Chung Date: Thu, 29 May 2025 12:04:28 +0100 Subject: [PATCH 06/24] Correct path resolve --- FABulous/FABulous.py | 17 ++++++++++++----- FABulous/FABulous_CLI/FABulous_CLI.py | 2 -- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/FABulous/FABulous.py b/FABulous/FABulous.py index cdfe7a4a..56abf630 100644 --- a/FABulous/FABulous.py +++ b/FABulous/FABulous.py @@ -161,7 +161,7 @@ def main(): setup_global_env_vars(args) - projectDir = Path(os.getenv("FAB_PROJ_DIR", args.project_dir)).absolute() + projectDir = Path(os.getenv("FAB_PROJ_DIR", args.project_dir)).absolute().resolve() args.top = projectDir.stem @@ -197,6 +197,8 @@ def main(): force=args.force, ) fab_CLI.debug = args.debug + fabScript: Path = args.FABulousScript.absolute() + tclScript: Path = args.TCLScript.absolute() logger.info(f"Setting current working directory to: {projectDir}") os.chdir(projectDir) @@ -210,20 +212,25 @@ def main(): f'Commands "{'; '.join(i.strip() for i in commands)}" executed successfully' ) exit(0) - elif args.FABulousScript != Path(""): - if fab_CLI.onecmd_plus_hooks(f"run_script { projectDir / args.FABulousScript.absolute()}"): + elif fabScript.is_file(): + if fab_CLI.onecmd_plus_hooks(f"run_script {fabScript}"): exit(1) else: logger.info( f"FABulous script {args.FABulousScript} executed successfully" ) exit(0) - elif args.TCLScript != Path(""): - if fab_CLI.onecmd_plus_hooks(f"run_tcl {projectDir / args.TCLScript.absolute()}"): + elif tclScript.is_file(): + if fab_CLI.onecmd_plus_hooks(f"run_tcl {tclScript}"): exit(1) else: logger.info(f"TCL script {args.TCLScript} executed successfully") exit(0) + elif fabScript or tclScript: + logger.error( + "You have provided a FABulous script or a TCL script, but you have provided a path but not a file." + ) + exit(1) else: fab_CLI.interactive = True if args.verbose == 2: diff --git a/FABulous/FABulous_CLI/FABulous_CLI.py b/FABulous/FABulous_CLI/FABulous_CLI.py index 97e55cf2..8d4d2b03 100644 --- a/FABulous/FABulous_CLI/FABulous_CLI.py +++ b/FABulous/FABulous_CLI/FABulous_CLI.py @@ -924,8 +924,6 @@ def do_run_tcl(self, args): logger.info("TCL script executed") - if "exit" in script: - return True @with_category(CMD_USER_DESIGN_FLOW) @with_argparser(userDesignRequireParser) From 4eb5c52dd34f5c0a16d2f02229d821582a33b1fa Mon Sep 17 00:00:00 2001 From: Kelvin Chung Date: Thu, 29 May 2025 12:05:27 +0100 Subject: [PATCH 07/24] format --- FABulous/FABulous_CLI/FABulous_CLI.py | 101 +++++++++++++++++++------- FABulous/FABulous_CLI/helper.py | 58 +++++++++++---- FABulous/custom_exception.py | 7 +- 3 files changed, 124 insertions(+), 42 deletions(-) diff --git a/FABulous/FABulous_CLI/FABulous_CLI.py b/FABulous/FABulous_CLI/FABulous_CLI.py index 8d4d2b03..bbcf4b74 100644 --- a/FABulous/FABulous_CLI/FABulous_CLI.py +++ b/FABulous/FABulous_CLI/FABulous_CLI.py @@ -145,16 +145,24 @@ def __init__( elif writerType == "vhdl": self.fabulousAPI = FABulous_API(VHDLWriter()) else: - logger.critical(f"Invalid writer type: {writerType}\n Valid options are 'verilog' or 'vhdl'") + logger.critical( + f"Invalid writer type: {writerType}\n Valid options are 'verilog' or 'vhdl'" + ) sys.exit(1) self.projectDir = projectDir.absolute() - self.add_settable(Settable("projectDir", Path, "The directory of the project", self)) + self.add_settable( + Settable("projectDir", Path, "The directory of the project", self) + ) self.tiles = [] self.superTiles = [] self.csvFile = Path(projectDir / "fabric.csv") - self.add_settable(Settable("csvFile", Path, "The fabric file ", self, completer=Cmd.path_complete)) + self.add_settable( + Settable( + "csvFile", Path, "The fabric file ", self, completer=Cmd.path_complete + ) + ) self.verbose = False self.add_settable(Settable("verbose", bool, "verbose output", self)) @@ -192,13 +200,19 @@ def __init__( name = fun.strip("do_") self.tcl.createcommand(name, wrap_with_except_handling(f)) - self.disable_category(CMD_FABRIC_FLOW, "Fabric Flow commands are disabled until fabric is loaded") + self.disable_category( + CMD_FABRIC_FLOW, "Fabric Flow commands are disabled until fabric is loaded" + ) self.disable_category( CMD_USER_DESIGN_FLOW, "User Design Flow commands are disabled until fabric is loaded", ) - self.disable_category(CMD_GUI, "GUI commands are disabled until gen_gen_geometry is run") - self.disable_category(CMD_HELPER, "Helper commands are disabled until fabric is loaded") + self.disable_category( + CMD_GUI, "GUI commands are disabled until gen_gen_geometry is run" + ) + self.disable_category( + CMD_HELPER, "Helper commands are disabled until fabric is loaded" + ) def onecmd(self, *args, **kwargs) -> bool: """Override the onecmd method to handle exceptions.""" @@ -234,7 +248,9 @@ def do_exit(self, *ignored): ) filePathRequireParser = Cmd2ArgumentParser() - filePathRequireParser.add_argument("file", type=Path, help="Path to the target file", completer=Cmd.path_complete) + filePathRequireParser.add_argument( + "file", type=Path, help="Path to the target file", completer=Cmd.path_complete + ) userDesignRequireParser = Cmd2ArgumentParser() userDesignRequireParser.add_argument( @@ -329,7 +345,9 @@ def do_load_fabric(self, args): self.fabricLoaded = True # self.projectDir = os.path.split(self.csvFile)[0] - tileByPath = [f.stem for f in (self.projectDir / "Tile/").iterdir() if f.is_dir()] + tileByPath = [ + f.stem for f in (self.projectDir / "Tile/").iterdir() if f.is_dir() + ] tileByFabric = list(self.fabulousAPI.fabric.tileDic.keys()) superTileByFabric = list(self.fabulousAPI.fabric.superTileDic.keys()) self.allTile = list(set(tileByPath) & set(tileByFabric + superTileByFabric)) @@ -381,8 +399,12 @@ def do_gen_config_mem(self, args): logger.info(f"Generating Config Memory for {' '.join(args.tiles)}") for i in args.tiles: logger.info(f"Generating configMem for {i}") - self.fabulousAPI.setWriterOutputFile(self.projectDir / f"Tile/{i}/{i}_ConfigMem.{self.extension}") - self.fabulousAPI.genConfigMem(i, self.projectDir / f"Tile/{i}/{i}_ConfigMem.csv") + self.fabulousAPI.setWriterOutputFile( + self.projectDir / f"Tile/{i}/{i}_ConfigMem.{self.extension}" + ) + self.fabulousAPI.genConfigMem( + i, self.projectDir / f"Tile/{i}/{i}_ConfigMem.csv" + ) logger.info("Generating configMem complete") @with_category(CMD_FABRIC_FLOW) @@ -396,7 +418,9 @@ def do_gen_switch_matrix(self, args): logger.info(f"Generating switch matrix for {' '.join(args.tiles)}") for i in args.tiles: logger.info(f"Generating switch matrix for {i}") - self.fabulousAPI.setWriterOutputFile(self.projectDir / f"Tile/{i}/{i}_switch_matrix.{self.extension}") + self.fabulousAPI.setWriterOutputFile( + self.projectDir / f"Tile/{i}/{i}_switch_matrix.{self.extension}" + ) self.fabulousAPI.genSwitchMatrix(i) logger.info("Switch matrix generation complete") @@ -412,8 +436,12 @@ def do_gen_tile(self, args): logger.info(f"Generating tile {' '.join(args.tiles)}") for t in args.tiles: - if subTiles := [f.stem for f in (self.projectDir / f"Tile/{t}").iterdir() if f.is_dir()]: - logger.info(f"{t} is a super tile, generating {t} with sub tiles {' '.join(subTiles)}") + if subTiles := [ + f.stem for f in (self.projectDir / f"Tile/{t}").iterdir() if f.is_dir() + ]: + logger.info( + f"{t} is a super tile, generating {t} with sub tiles {' '.join(subTiles)}" + ) for st in subTiles: # Gen switch matrix logger.info(f"Generating switch matrix for tile {t}") @@ -430,19 +458,25 @@ def do_gen_tile(self, args): self.fabulousAPI.setWriterOutputFile( f"{self.projectDir}/Tile/{t}/{st}/{st}_ConfigMem.{self.extension}" ) - self.fabulousAPI.genConfigMem(st, self.projectDir / f"Tile/{t}/{st}/{st}_ConfigMem.csv") + self.fabulousAPI.genConfigMem( + st, self.projectDir / f"Tile/{t}/{st}/{st}_ConfigMem.csv" + ) logger.info(f"Generated configMem for {st}") # Gen tile logger.info(f"Generating subtile for tile {t}") logger.info(f"Generating subtile {st}") - self.fabulousAPI.setWriterOutputFile(f"{self.projectDir}/Tile/{t}/{st}/{st}.{self.extension}") + self.fabulousAPI.setWriterOutputFile( + f"{self.projectDir}/Tile/{t}/{st}/{st}.{self.extension}" + ) self.fabulousAPI.genTile(st) logger.info(f"Generated subtile {st}") # Gen super tile logger.info(f"Generating super tile {t}") - self.fabulousAPI.setWriterOutputFile(f"{self.projectDir}/Tile/{t}/{t}.{self.extension}") + self.fabulousAPI.setWriterOutputFile( + f"{self.projectDir}/Tile/{t}/{t}.{self.extension}" + ) self.fabulousAPI.genSuperTile(t) logger.info(f"Generated super tile {t}") continue @@ -455,7 +489,9 @@ def do_gen_tile(self, args): logger.info(f"Generating tile {t}") # Gen tile - self.fabulousAPI.setWriterOutputFile(f"{self.projectDir}/Tile/{t}/{t}.{self.extension}") + self.fabulousAPI.setWriterOutputFile( + f"{self.projectDir}/Tile/{t}/{t}.{self.extension}" + ) self.fabulousAPI.genTile(t) logger.info(f"Generated tile {t}") @@ -562,7 +598,9 @@ def do_gen_bitStream_spec(self, *ignored): specObject = self.fabulousAPI.genBitStreamSpec() logger.info(f"output file: {self.projectDir}/{META_DATA_DIR}/bitStreamSpec.bin") - with open(f"{self.projectDir}/{META_DATA_DIR}/bitStreamSpec.bin", "wb") as outFile: + with open( + f"{self.projectDir}/{META_DATA_DIR}/bitStreamSpec.bin", "wb" + ) as outFile: pickle.dump(specObject, outFile) logger.info(f"output file: {self.projectDir}/{META_DATA_DIR}/bitStreamSpec.csv") @@ -641,7 +679,9 @@ def do_place_and_route(self, args): Also logs place and route error, file not found error and type error. """ - logger.info(f"Running Placement and Routing with Nextpnr for design {args.file}") + logger.info( + f"Running Placement and Routing with Nextpnr for design {args.file}" + ) path = Path(args.file) parent = path.parent json_file = path.name @@ -661,16 +701,18 @@ def do_place_and_route(self, args): if parent == "": parent = "." - if not os.path.exists(f"{self.projectDir}/.FABulous/pips.txt") or not os.path.exists( - f"{self.projectDir}/.FABulous/bel.txt" - ): + if not os.path.exists( + f"{self.projectDir}/.FABulous/pips.txt" + ) or not os.path.exists(f"{self.projectDir}/.FABulous/bel.txt"): logger.opt(exception=FileNotFoundError()).error( "Pips and Bel files are not found, please run model_gen_npnr first" ) if os.path.exists(f"{self.projectDir}/{parent}"): # TODO rewriting the fab_arch script so no need to copy file for work around - npnr = check_if_application_exists(os.getenv("FAB_NEXTPNR_PATH", "nextpnr-generic")) + npnr = check_if_application_exists( + os.getenv("FAB_NEXTPNR_PATH", "nextpnr-generic") + ) if f"{json_file}" in os.listdir(f"{self.projectDir}/{parent}"): runCmd = [ f"FAB_ROOT={self.projectDir}", @@ -703,7 +745,9 @@ def do_place_and_route(self, args): logger.info("Placement and Routing completed") else: - logger.opt(exception=FileNotFoundError()).error(f"Directory {self.projectDir}/{parent} does not exist.") + logger.opt(exception=FileNotFoundError()).error( + f"Directory {self.projectDir}/{parent} does not exist." + ) @with_category(CMD_USER_DESIGN_FLOW) @with_argparser(filePathRequireParser) @@ -818,7 +862,9 @@ def do_run_simulation(self, args): copy_verilog_files(self.projectDir / "Fabric", fabricFilesDir) file_list = [str(i) for i in fabricFilesDir.glob("*.v")] - iverilog = check_if_application_exists(os.getenv("FAB_IVERILOG_PATH", "iverilog")) + iverilog = check_if_application_exists( + os.getenv("FAB_IVERILOG_PATH", "iverilog") + ) try: runCmd = [ f"{iverilog}", @@ -924,7 +970,6 @@ def do_run_tcl(self, args): logger.info("TCL script executed") - @with_category(CMD_USER_DESIGN_FLOW) @with_argparser(userDesignRequireParser) def do_gen_user_design_wrapper(self, args): @@ -932,7 +977,9 @@ def do_gen_user_design_wrapper(self, args): logger.opt(exception=CommandError()).error("Need to load fabric first") return - self.fabulousAPI.generateUserDesignTopWrapper(args.user_design, args.user_design_top_wrapper) + self.fabulousAPI.generateUserDesignTopWrapper( + args.user_design, args.user_design_top_wrapper + ) gen_tile_parser = Cmd2ArgumentParser() gen_tile_parser.add_argument( diff --git a/FABulous/FABulous_CLI/helper.py b/FABulous/FABulous_CLI/helper.py index 00cbf52e..07f4c439 100644 --- a/FABulous/FABulous_CLI/helper.py +++ b/FABulous/FABulous_CLI/helper.py @@ -23,13 +23,17 @@ def setup_logger(verbosity: int, debug: bool): # Define a custom formatting function that has access to 'verbosity' def custom_format_function(record): # Construct the standard part of the log message based on verbosity - level = f"{record["level"].name} | " + level = f"{record["level"].name} | " time = f"[{record["time"]:DD-MM-YYYY HH:mm:ss}] | " name = f"[{record["name"]}" func = f"{record["function"]}" line = f"{record["line"]}" msg = f"{record["message"]}" - exc = f"{record["exception"].type.__name__} | " if record["exception"] else "" + exc = ( + f"{record["exception"].type.__name__} | " + if record["exception"] + else "" + ) if verbosity >= 1: final_log = f"{level}{time}{name}:{func}:{line} - {exc}{msg}\n" @@ -42,7 +46,9 @@ def custom_format_function(record): log_level_to_set = "DEBUG" if debug else "INFO" # Add logger to write logs to stdout using the custom formatter - logger.add(sys.stdout, format=custom_format_function, level=log_level_to_set, colorize=True) + logger.add( + sys.stdout, format=custom_format_function, level=log_level_to_set, colorize=True + ) def setup_global_env_vars(args: argparse.Namespace) -> None: @@ -67,7 +73,9 @@ def setup_global_env_vars(args: argparse.Namespace) -> None: fabulousRoot = str(Path(fabulousRoot).joinpath("FABulous")) os.environ["FAB_ROOT"] = fabulousRoot else: - logger.error(f"FAB_ROOT environment variable set to {fabulousRoot} but the directory does not exist") + logger.error( + f"FAB_ROOT environment variable set to {fabulousRoot} but the directory does not exist" + ) sys.exit() logger.info(f"FAB_ROOT set to {fabulousRoot}") @@ -90,7 +98,10 @@ def setup_global_env_vars(args: argparse.Namespace) -> None: elif fabDir.joinpath(".env").exists() and fabDir.joinpath(".env").is_file(): load_dotenv(fabDir.joinpath(".env")) logger.info(f"Loaded global .env file from {fabulousRoot}/.env") - elif fabDir.parent.joinpath(".env").exists() and fabDir.parent.joinpath(".env").is_file(): + elif ( + fabDir.parent.joinpath(".env").exists() + and fabDir.parent.joinpath(".env").is_file() + ): load_dotenv(fabDir.parent.joinpath(".env")) logger.info(f"Loaded global .env file from {fabDir.parent.joinpath('.env')}") else: @@ -128,7 +139,10 @@ def setup_project_env_vars(args: argparse.Namespace) -> None: elif fabDir.joinpath(".env").exists() and fabDir.joinpath(".env").is_file(): load_dotenv(fabDir.joinpath(".env")) logger.info(f"Loaded project .env file from {fabDir}/.env')") - elif fabDir.parent.joinpath(".env").exists() and fabDir.parent.joinpath(".env").is_file(): + elif ( + fabDir.parent.joinpath(".env").exists() + and fabDir.parent.joinpath(".env").is_file() + ): load_dotenv(fabDir.parent.joinpath(".env")) logger.info(f"Loaded project .env file from {fabDir.parent.joinpath('.env')}") else: @@ -359,7 +373,9 @@ def install_oss_cad_suite(destination_folder: Path, update: bool = False): No valid archive of OSS-CAD-Suite found in the latest release. If the file format of the downloaded archive is unsupported. """ - github_releases_url = "https://api.github.com/repos/YosysHQ/oss-cad-suite-build/releases/latest" + github_releases_url = ( + "https://api.github.com/repos/YosysHQ/oss-cad-suite-build/releases/latest" + ) response = requests.get(github_releases_url) system = platform.system().lower() machine = platform.machine().lower() @@ -386,22 +402,30 @@ def install_oss_cad_suite(destination_folder: Path, update: bool = False): logger.info(f"Creating folder {destination_folder.absolute()}") os.makedirs(destination_folder, exist_ok=True) else: - logger.info(f"Installing OSS-CAD-Suite to folder {destination_folder.absolute()}") + logger.info( + f"Installing OSS-CAD-Suite to folder {destination_folder.absolute()}" + ) # format system and machine to match the OSS-CAD-Suite release naming if system not in ["linux", "windows", "darwin"]: - raise ValueError(f"Unsupported operating system {system}. Please install OSS-CAD-Suite manually.") + raise ValueError( + f"Unsupported operating system {system}. Please install OSS-CAD-Suite manually." + ) if machine in ["x86_64", "amd64"]: machine = "x64" elif machine in ["aarch64", "arm64"]: machine = "arm64" else: - raise ValueError(f"Unsupported architecture {machine}. Please install OSS-CAD-Suite manually.") + raise ValueError( + f"Unsupported architecture {machine}. Please install OSS-CAD-Suite manually." + ) if response.status_code == 200: latest_release = response.json() else: - raise Exception(f"Failed to fetch latest OSS-CAD-Suite release: {response.status_code}") + raise Exception( + f"Failed to fetch latest OSS-CAD-Suite release: {response.status_code}" + ) # find the right release for the current system for asset in latest_release.get("assets", []): @@ -430,15 +454,21 @@ def install_oss_cad_suite(destination_folder: Path, update: bool = False): with tarfile.open(ocs_archive, "r:gz") as tar: tar.extractall(path=destination_folder) else: - raise ValueError(f"Unsupported file format. Please extract {ocs_archive} manually.") + raise ValueError( + f"Unsupported file format. Please extract {ocs_archive} manually." + ) logger.info(f"Remove archive {ocs_archive}") ocs_archive.unlink() fab_root_env = os.getenv("FAB_ROOT") if fab_root_env is None: - logger.error("FAB_ROOT environment variable is not set. Cannot update .env file for OSS CAD Suite.") - raise EnvironmentError("FAB_ROOT is not set, cannot determine .env file path for OSS CAD Suite.") + logger.error( + "FAB_ROOT environment variable is not set. Cannot update .env file for OSS CAD Suite." + ) + raise EnvironmentError( + "FAB_ROOT is not set, cannot determine .env file path for OSS CAD Suite." + ) env_file = Path(fab_root_env) / ".env" env_cont = "" if env_file.is_file(): diff --git a/FABulous/custom_exception.py b/FABulous/custom_exception.py index 6b04277d..592668dc 100644 --- a/FABulous/custom_exception.py +++ b/FABulous/custom_exception.py @@ -1,11 +1,16 @@ class CommandError(Exception): """Exception raised for errors in the command execution.""" + pass + class EnvironmentNotSet(Exception): """Exception raised when the environment is not set.""" + pass + class FileTypeError(Exception): """Exception raised for unsupported file types.""" - pass \ No newline at end of file + + pass From c95b7777d40ffcda1a6ce62f7ebe6a8e0b404799 Mon Sep 17 00:00:00 2001 From: Kelvin Chung Date: Thu, 29 May 2025 13:04:41 +0100 Subject: [PATCH 08/24] More robust exit --- FABulous/FABulous.py | 45 +++++++++++++-------------- FABulous/FABulous_CLI/FABulous_CLI.py | 5 +-- 2 files changed, 24 insertions(+), 26 deletions(-) diff --git a/FABulous/FABulous.py b/FABulous/FABulous.py index 56abf630..cc552f1d 100644 --- a/FABulous/FABulous.py +++ b/FABulous/FABulous.py @@ -8,10 +8,10 @@ from FABulous.FABulous_CLI.FABulous_CLI import FABulous_CLI from FABulous.FABulous_CLI.helper import ( create_project, + install_oss_cad_suite, setup_global_env_vars, setup_logger, setup_project_env_vars, - install_oss_cad_suite, ) @@ -46,9 +46,7 @@ def main(): -iocd, --install_oss_cad_suite : str, optional Install the oss-cad-suite in the project directory. """ - parser = argparse.ArgumentParser( - description="The command line interface for FABulous" - ) + parser = argparse.ArgumentParser(description="The command line interface for FABulous") script_group = parser.add_mutually_exclusive_group() @@ -166,9 +164,7 @@ def main(): args.top = projectDir.stem if args.createProject and args.install_oss_cad_suite: - logger.error( - f"You cannot create a new project and install the oss-cad-suite at the same time." - ) + logger.error("You cannot create a new project and install the oss-cad-suite at the same time.") exit(1) if args.createProject: @@ -184,9 +180,7 @@ def main(): exit(0) if not (projectDir / ".FABulous").exists(): - logger.error( - "The directory provided is not a FABulous project as it does not have a .FABulous folder" - ) + logger.error("The directory provided is not a FABulous project as it does not have a .FABulous folder") exit(1) else: setup_project_env_vars(args) @@ -208,24 +202,27 @@ def main(): if fab_CLI.onecmd_plus_hooks(c): exit(1) else: - logger.info( - f'Commands "{'; '.join(i.strip() for i in commands)}" executed successfully' - ) + logger.info(f'Commands "{"; ".join(i.strip() for i in commands)}" executed successfully') exit(0) elif fabScript.is_file(): - if fab_CLI.onecmd_plus_hooks(f"run_script {fabScript}"): - exit(1) - else: - logger.info( - f"FABulous script {args.FABulousScript} executed successfully" + fab_CLI.onecmd_plus_hooks(f"run_script {fabScript}") + if fab_CLI.exit_code: + logger.error( + f"FABulous script {args.FABulousScript} execution failed with exit code {fab_CLI.exit_code}" ) - exit(0) + else: + logger.info(f"FABulous script {args.FABulousScript} executed successfully") + + exit(fab_CLI.exit_code) elif tclScript.is_file(): - if fab_CLI.onecmd_plus_hooks(f"run_tcl {tclScript}"): - exit(1) + fab_CLI.onecmd_plus_hooks(f"run_tcl {tclScript}") + if fab_CLI.exit_code: + logger.error( + f"TCL script {args.TCLScript} execution failed with exit code {fab_CLI.exit_code}" + ) else: logger.info(f"TCL script {args.TCLScript} executed successfully") - exit(0) + exit(fab_CLI.exit_code) elif fabScript or tclScript: logger.error( "You have provided a FABulous script or a TCL script, but you have provided a path but not a file." @@ -241,8 +238,8 @@ def main(): with redirect_stdout(log): fab_CLI.cmdloop() else: - exit_code = fab_CLI.cmdloop() - exit(exit_code) + fab_CLI.cmdloop() + exit(0) if __name__ == "__main__": diff --git a/FABulous/FABulous_CLI/FABulous_CLI.py b/FABulous/FABulous_CLI/FABulous_CLI.py index bbcf4b74..a6192b5a 100644 --- a/FABulous/FABulous_CLI/FABulous_CLI.py +++ b/FABulous/FABulous_CLI/FABulous_CLI.py @@ -29,6 +29,7 @@ Cmd, Cmd2ArgumentParser, Settable, + Statement, categorize, with_argparser, with_category, @@ -214,10 +215,10 @@ def __init__( CMD_HELPER, "Helper commands are disabled until fabric is loaded" ) - def onecmd(self, *args, **kwargs) -> bool: + def onecmd(self, statement: Statement | str, *, add_to_history: bool = True) -> bool: """Override the onecmd method to handle exceptions.""" try: - return super().onecmd(*args, **kwargs) + return super().onecmd(statement, add_to_history=add_to_history) except Exception: logger.debug(traceback.format_exc()) self.exit_code = 1 From cda07746e4910479c49cfc19efbcf4e2c5539f06 Mon Sep 17 00:00:00 2001 From: Kelvin Chung Date: Thu, 29 May 2025 13:05:36 +0100 Subject: [PATCH 09/24] lint --- FABulous/FABulous.py | 20 +++++++++++++++----- FABulous/FABulous_CLI/FABulous_CLI.py | 4 +++- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/FABulous/FABulous.py b/FABulous/FABulous.py index cc552f1d..5b733238 100644 --- a/FABulous/FABulous.py +++ b/FABulous/FABulous.py @@ -46,7 +46,9 @@ def main(): -iocd, --install_oss_cad_suite : str, optional Install the oss-cad-suite in the project directory. """ - parser = argparse.ArgumentParser(description="The command line interface for FABulous") + parser = argparse.ArgumentParser( + description="The command line interface for FABulous" + ) script_group = parser.add_mutually_exclusive_group() @@ -164,7 +166,9 @@ def main(): args.top = projectDir.stem if args.createProject and args.install_oss_cad_suite: - logger.error("You cannot create a new project and install the oss-cad-suite at the same time.") + logger.error( + "You cannot create a new project and install the oss-cad-suite at the same time." + ) exit(1) if args.createProject: @@ -180,7 +184,9 @@ def main(): exit(0) if not (projectDir / ".FABulous").exists(): - logger.error("The directory provided is not a FABulous project as it does not have a .FABulous folder") + logger.error( + "The directory provided is not a FABulous project as it does not have a .FABulous folder" + ) exit(1) else: setup_project_env_vars(args) @@ -202,7 +208,9 @@ def main(): if fab_CLI.onecmd_plus_hooks(c): exit(1) else: - logger.info(f'Commands "{"; ".join(i.strip() for i in commands)}" executed successfully') + logger.info( + f'Commands "{"; ".join(i.strip() for i in commands)}" executed successfully' + ) exit(0) elif fabScript.is_file(): fab_CLI.onecmd_plus_hooks(f"run_script {fabScript}") @@ -211,7 +219,9 @@ def main(): f"FABulous script {args.FABulousScript} execution failed with exit code {fab_CLI.exit_code}" ) else: - logger.info(f"FABulous script {args.FABulousScript} executed successfully") + logger.info( + f"FABulous script {args.FABulousScript} executed successfully" + ) exit(fab_CLI.exit_code) elif tclScript.is_file(): diff --git a/FABulous/FABulous_CLI/FABulous_CLI.py b/FABulous/FABulous_CLI/FABulous_CLI.py index a6192b5a..b98fd252 100644 --- a/FABulous/FABulous_CLI/FABulous_CLI.py +++ b/FABulous/FABulous_CLI/FABulous_CLI.py @@ -215,7 +215,9 @@ def __init__( CMD_HELPER, "Helper commands are disabled until fabric is loaded" ) - def onecmd(self, statement: Statement | str, *, add_to_history: bool = True) -> bool: + def onecmd( + self, statement: Statement | str, *, add_to_history: bool = True + ) -> bool: """Override the onecmd method to handle exceptions.""" try: return super().onecmd(statement, add_to_history=add_to_history) From c625c90ab719136077d0fb7e5b8da6e4bebef26b Mon Sep 17 00:00:00 2001 From: Kelvin Chung Date: Wed, 4 Jun 2025 20:32:45 +0100 Subject: [PATCH 10/24] load_fabric command on start --- FABulous/FABulous.py | 6 +----- FABulous/FABulous_CLI/FABulous_CLI.py | 3 +-- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/FABulous/FABulous.py b/FABulous/FABulous.py index 5b733238..ff4add68 100644 --- a/FABulous/FABulous.py +++ b/FABulous/FABulous.py @@ -201,6 +201,7 @@ def main(): tclScript: Path = args.TCLScript.absolute() logger.info(f"Setting current working directory to: {projectDir}") os.chdir(projectDir) + fab_CLI.onecmd_plus_hooks("load_fabric") if commands := args.commands: commands = commands.split("; ") @@ -233,11 +234,6 @@ def main(): else: logger.info(f"TCL script {args.TCLScript} executed successfully") exit(fab_CLI.exit_code) - elif fabScript or tclScript: - logger.error( - "You have provided a FABulous script or a TCL script, but you have provided a path but not a file." - ) - exit(1) else: fab_CLI.interactive = True if args.verbose == 2: diff --git a/FABulous/FABulous_CLI/FABulous_CLI.py b/FABulous/FABulous_CLI/FABulous_CLI.py index b98fd252..d39b58b9 100644 --- a/FABulous/FABulous_CLI/FABulous_CLI.py +++ b/FABulous/FABulous_CLI/FABulous_CLI.py @@ -92,7 +92,6 @@ The shell support tab completion for commands and files To run the complete FABulous flow with the default project, run the following command: - load_fabric run_FABulous_fabric run_FABulous_bitstream ./user_design/sequential_16bit_en.v run_simulation fst ./user_design/sequential_16bit_en.bin @@ -340,7 +339,7 @@ def do_load_fabric(self, args): self.fabulousAPI.loadFabric(self.csvFile) else: logger.opt(exception=FileExistsError()).error( - "No argument is given and the csv file is set or the file does not exist" + "No argument is given and the csv file is set but the file does not exist" ) else: self.fabulousAPI.loadFabric(args.file) From 27939b0f19a703d53bc10413e73c6d05e3fc9d1b Mon Sep 17 00:00:00 2001 From: Kelvin Chung Date: Wed, 4 Jun 2025 20:35:36 +0100 Subject: [PATCH 11/24] Better invalid script path --- FABulous/FABulous.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/FABulous/FABulous.py b/FABulous/FABulous.py index ff4add68..fc4d476e 100644 --- a/FABulous/FABulous.py +++ b/FABulous/FABulous.py @@ -213,7 +213,7 @@ def main(): f'Commands "{"; ".join(i.strip() for i in commands)}" executed successfully' ) exit(0) - elif fabScript.is_file(): + elif fabScript != Path().cwd(): fab_CLI.onecmd_plus_hooks(f"run_script {fabScript}") if fab_CLI.exit_code: logger.error( @@ -225,7 +225,7 @@ def main(): ) exit(fab_CLI.exit_code) - elif tclScript.is_file(): + elif tclScript != Path().cwd(): fab_CLI.onecmd_plus_hooks(f"run_tcl {tclScript}") if fab_CLI.exit_code: logger.error( From a02d25ef32ef41b44875f4fe75ebb3b108c42674 Mon Sep 17 00:00:00 2001 From: Kelvin Chung Date: Wed, 4 Jun 2025 22:48:19 +0100 Subject: [PATCH 12/24] Add testing for current start up script --- FABulous/FABulous.py | 23 ++++++++++---------- FABulous/FABulous_CLI/FABulous_CLI.py | 2 +- FABulous/FABulous_CLI/helper.py | 30 ++++++++++++++++----------- tests/CLI_test/conftest.py | 2 +- 4 files changed, 32 insertions(+), 25 deletions(-) diff --git a/FABulous/FABulous.py b/FABulous/FABulous.py index fc4d476e..27bc4f97 100644 --- a/FABulous/FABulous.py +++ b/FABulous/FABulous.py @@ -91,7 +91,8 @@ def main(): parser.add_argument( "-log", - default=False, + default="", + type=Path, nargs="?", const="FABulous.log", help="Log all the output from the terminal", @@ -157,7 +158,7 @@ def main(): args = parser.parse_args() - setup_logger(args.verbose, args.debug) + setup_logger(args.verbose, args.debug, log_file=args.log) setup_global_env_vars(args) @@ -200,20 +201,25 @@ def main(): fabScript: Path = args.FABulousScript.absolute() tclScript: Path = args.TCLScript.absolute() logger.info(f"Setting current working directory to: {projectDir}") + cwd = Path().cwd() os.chdir(projectDir) fab_CLI.onecmd_plus_hooks("load_fabric") if commands := args.commands: commands = commands.split("; ") for c in commands: - if fab_CLI.onecmd_plus_hooks(c): + fab_CLI.onecmd_plus_hooks(c) + if fab_CLI.exit_code: + logger.error( + f"Command '{c}' execution failed with exit code {fab_CLI.exit_code}" + ) exit(1) else: logger.info( f'Commands "{"; ".join(i.strip() for i in commands)}" executed successfully' ) exit(0) - elif fabScript != Path().cwd(): + elif fabScript != cwd: fab_CLI.onecmd_plus_hooks(f"run_script {fabScript}") if fab_CLI.exit_code: logger.error( @@ -225,7 +231,7 @@ def main(): ) exit(fab_CLI.exit_code) - elif tclScript != Path().cwd(): + elif tclScript != cwd: fab_CLI.onecmd_plus_hooks(f"run_tcl {tclScript}") if fab_CLI.exit_code: logger.error( @@ -239,12 +245,7 @@ def main(): if args.verbose == 2: fab_CLI.verbose = True - if args.log: - with open(args.log, "w") as log: - with redirect_stdout(log): - fab_CLI.cmdloop() - else: - fab_CLI.cmdloop() + fab_CLI.cmdloop() exit(0) diff --git a/FABulous/FABulous_CLI/FABulous_CLI.py b/FABulous/FABulous_CLI/FABulous_CLI.py index d39b58b9..3bf2a2b0 100644 --- a/FABulous/FABulous_CLI/FABulous_CLI.py +++ b/FABulous/FABulous_CLI/FABulous_CLI.py @@ -407,7 +407,7 @@ def do_gen_config_mem(self, args): self.fabulousAPI.genConfigMem( i, self.projectDir / f"Tile/{i}/{i}_ConfigMem.csv" ) - logger.info("Generating configMem complete") + logger.info("ConfigMem generation complete") @with_category(CMD_FABRIC_FLOW) @with_argparser(tile_list_parser) diff --git a/FABulous/FABulous_CLI/helper.py b/FABulous/FABulous_CLI/helper.py index 07f4c439..1d1ba462 100644 --- a/FABulous/FABulous_CLI/helper.py +++ b/FABulous/FABulous_CLI/helper.py @@ -16,21 +16,21 @@ MAX_BITBYTES = 16384 -def setup_logger(verbosity: int, debug: bool): +def setup_logger(verbosity: int, debug: bool, log_file: Path = Path()): # Remove the default logger to avoid duplicate logs logger.remove() # Define a custom formatting function that has access to 'verbosity' def custom_format_function(record): # Construct the standard part of the log message based on verbosity - level = f"{record["level"].name} | " - time = f"[{record["time"]:DD-MM-YYYY HH:mm:ss}] | " - name = f"[{record["name"]}" - func = f"{record["function"]}" - line = f"{record["line"]}" - msg = f"{record["message"]}" + level = f"{record['level'].name} | " + time = f"[{record['time']:DD-MM-YYYY HH:mm:ss}] | " + name = f"[{record['name']}" + func = f"{record['function']}" + line = f"{record['line']}" + msg = f"{record['message']}" exc = ( - f"{record["exception"].type.__name__} | " + f"{record['exception'].type.__name__} | " if record["exception"] else "" ) @@ -46,9 +46,15 @@ def custom_format_function(record): log_level_to_set = "DEBUG" if debug else "INFO" # Add logger to write logs to stdout using the custom formatter - logger.add( - sys.stdout, format=custom_format_function, level=log_level_to_set, colorize=True - ) + if log_file != Path(): + logger.add(log_file, format=custom_format_function, level=log_level_to_set) + else: + logger.add( + sys.stdout, + format=custom_format_function, + level=log_level_to_set, + colorize=True, + ) def setup_global_env_vars(args: argparse.Namespace) -> None: @@ -176,7 +182,7 @@ def create_project(project_dir: Path, lang: Literal["verilog", "vhdl"] = "verilo """ if project_dir.exists(): logger.error("Project directory already exists!") - sys.exit() + sys.exit(1) else: project_dir.mkdir(parents=True, exist_ok=True) (project_dir / ".FABulous").mkdir(parents=True, exist_ok=True) diff --git a/tests/CLI_test/conftest.py b/tests/CLI_test/conftest.py index aa6eb6cd..05957ab8 100644 --- a/tests/CLI_test/conftest.py +++ b/tests/CLI_test/conftest.py @@ -44,7 +44,7 @@ def cli(tmp_path): os.environ["FAB_ROOT"] = fabulousRoot os.environ["FAB_PROJ_DIR"] = str(projectDir) create_project(projectDir) - setup_logger(0) + setup_logger(0, False) cli = FABulous_CLI( writerType="verilog", projectDir=projectDir, enteringDir=tmp_path ) From 1feb42928e21dcc3aec4777e44e815070d64dd3e Mon Sep 17 00:00:00 2001 From: Kelvin Chung Date: Wed, 4 Jun 2025 23:11:13 +0100 Subject: [PATCH 13/24] Actually adding the test --- tests/CLI_test/test_arguments.py | 388 +++++++++++++++++++++++++++++++ 1 file changed, 388 insertions(+) create mode 100644 tests/CLI_test/test_arguments.py diff --git a/tests/CLI_test/test_arguments.py b/tests/CLI_test/test_arguments.py new file mode 100644 index 00000000..30eb4e41 --- /dev/null +++ b/tests/CLI_test/test_arguments.py @@ -0,0 +1,388 @@ +from subprocess import run + + +def test_create_project(tmp_path): + result = run( + ["FABulous", "--createProject", str(tmp_path / "test_prj")], + capture_output=True, + text=True, + ) + assert result.returncode == 0 + + +def test_create_project_existing_dir(tmp_path): + existing_dir = tmp_path / "existing_dir" + existing_dir.mkdir() + result = run( + ["FABulous", "--createProject", str(existing_dir)], + capture_output=True, + text=True, + ) + assert "already exists" in result.stdout + assert result.returncode != 0 + + +def test_create_project_with_no_name(tmp_path): + result = run(["FABulous", "--createProject"], capture_output=True, text=True) + assert result.returncode != 0 + + +def test_fabulous_script(tmp_path): + # Create project first + project_dir = tmp_path / "test_prj" + result = run( + ["FABulous", "--createProject", str(project_dir)], + capture_output=True, + text=True, + ) + assert result.returncode == 0 + + # Create a test FABulous script file + script_file = tmp_path / "test_script.fab" + script_file.write_text("# Test FABulous script\nhelp\n") + + result = run( + ["FABulous", str(project_dir), "--FABulousScript", str(script_file)], + capture_output=True, + text=True, + ) + assert result.returncode == 0 + + +def test_fabulous_script_nonexistent_file(tmp_path): + nonexistent_script = tmp_path / "nonexistent_script.fab" + project_dir = tmp_path / "test_prj" + + result = run( + ["FABulous", str(project_dir), "--FABulousScript", str(nonexistent_script)], + capture_output=True, + text=True, + ) + assert result.returncode != 0 + + +def test_fabulous_script_with_no_project_dir(tmp_path): + script_file = tmp_path / "test_script.fab" + script_file.write_text("# Test FABulous script\n") + + result = run( + ["FABulous", "--FABulousScript", str(script_file)], + capture_output=True, + text=True, + ) + assert result.returncode != 0 + + +def test_tcl_script_execution(tmp_path): + """Test TCL script execution on a valid project""" + project_dir = tmp_path / "test_tcl_project" + + # Create project first + result = run( + ["FABulous", "--createProject", str(project_dir)], + capture_output=True, + text=True, + ) + assert result.returncode == 0 + + # Create a TCL script + tcl_script = tmp_path / "test_script.tcl" + tcl_script.write_text( + '# TCL script with FABulous commands\nputs "Hello from TCL"\n' + ) + + # Run TCL script + result = run( + ["FABulous", str(project_dir), "--TCLScript", str(tcl_script)], + capture_output=True, + text=True, + ) + assert result.returncode == 0 + + +def test_commands_execution(tmp_path): + """Test direct command execution with -p/--commands""" + project_dir = tmp_path / "test_cmd_project" + + # Create project + result = run( + ["FABulous", "--createProject", str(project_dir)], + capture_output=True, + text=True, + ) + assert result.returncode == 0 + + # Run commands directly + result = run( + ["FABulous", str(project_dir), "--commands", "help; help"], + capture_output=True, + text=True, + ) + assert result.returncode == 0 + + +def test_create_project_with_vhdl_writer(tmp_path): + """Test project creation with VHDL writer""" + project_dir = tmp_path / "test_vhdl_project" + + result = run( + ["FABulous", "--createProject", str(project_dir), "--writer", "vhdl"], + capture_output=True, + text=True, + ) + assert result.returncode == 0 + assert project_dir.exists() + assert (project_dir / ".FABulous").exists() + assert "vhdl" in (project_dir / ".FABulous" / ".env").read_text() + + +def test_create_project_with_verilog_writer(tmp_path): + """Test project creation with Verilog writer""" + project_dir = tmp_path / "test_verilog_project" + + result = run( + ["FABulous", "--createProject", str(project_dir), "--writer", "verilog"], + capture_output=True, + text=True, + ) + assert result.returncode == 0 + assert project_dir.exists() + assert (project_dir / ".FABulous").exists() + assert "verilog" in (project_dir / ".FABulous" / ".env").read_text() + + +def test_logging_functionality(tmp_path): + """Test log file creation and output""" + project_dir = tmp_path / "test_log_project" + log_file = tmp_path / "test.log" + + # Create project + result = run( + ["FABulous", "--createProject", str(project_dir)], + capture_output=True, + text=True, + ) + assert result.returncode == 0 + + # Run with logging using commands instead of script to avoid file handling issues + result = run( + ["FABulous", str(project_dir), "--commands", "help", "-log", str(log_file)], + capture_output=True, + text=True, + ) + assert result.returncode == 0 + assert log_file.exists() + assert log_file.stat().st_size > 0 # Check if log file is not empty + + +def test_verbose_mode(tmp_path): + """Test verbose mode execution""" + project_dir = tmp_path / "test_verbose_project" + + # Create project + result = run( + ["FABulous", "--createProject", str(project_dir)], + capture_output=True, + text=True, + ) + assert result.returncode == 0 + + # Run with verbose mode + result = run( + ["FABulous", str(project_dir), "--commands", "help", "-v"], + capture_output=True, + text=True, + ) + assert result.returncode == 0 + + +def test_force_flag(tmp_path): + """Test force flag functionality""" + project_dir = tmp_path / "test_force_project" + + # Create project + result = run( + ["FABulous", "--createProject", str(project_dir)], + capture_output=True, + text=True, + ) + assert result.returncode == 0 + + # Run with force flag + result = run( + [ + "FABulous", + str(project_dir), + "--commands", + "load_fabric non_existent", + "--force", + ], + capture_output=True, + text=True, + ) + assert result.returncode == 1 + + result = run( + [ + "FABulous", + str(project_dir), + "--commands", + "load_fabric non_existent; load_fabric", + "--force", + ], + capture_output=True, + text=True, + ) + assert result.returncode == 1 + + +def test_debug_mode(tmp_path): + """Test debug mode functionality""" + project_dir = tmp_path / "test_debug_project" + + # Create project + result = run( + ["FABulous", "--createProject", str(project_dir)], + capture_output=True, + text=True, + ) + assert result.returncode == 0 + + # Run with debug mode + result = run( + ["FABulous", str(project_dir), "--commands", "help", "--debug"], + capture_output=True, + text=True, + ) + assert result.returncode == 0 + + +def test_install_oss_cad_suite(tmp_path, mocker): + """Test oss-cad-suite installation""" + install_dir = tmp_path / "oss_install_test" + install_dir.mkdir() + + # Test installation (may fail if network unavailable, but should handle gracefully) + class MockRequest: + status_code = 200 + + mocker.patch( + "requests.get", return_value=MockRequest() + ) # Mock network request for testing + result = run( + ["FABulous", str(install_dir), "--install_oss_cad_suite"], + capture_output=True, + text=True, + ) + assert result.returncode == 0 + + +def test_script_mutually_exclusive(tmp_path): + """Test that FABulous script and TCL script are mutually exclusive""" + project_dir = tmp_path / "test_exclusive_project" + + # Create project + result = run( + ["FABulous", "--createProject", str(project_dir)], + capture_output=True, + text=True, + ) + assert result.returncode == 0 + + # Create both script types + fab_script = tmp_path / "test.fab" + fab_script.write_text("help\n") + tcl_script = tmp_path / "test.tcl" + tcl_script.write_text("puts hello\n") + + # Try to use both - should fail + result = run( + [ + "FABulous", + str(project_dir), + "--FABulousScript", + str(fab_script), + "--TCLScript", + str(tcl_script), + ], + capture_output=True, + text=True, + ) + assert result.returncode != 0 + + +def test_invalid_project_directory(): + """Test error handling for invalid project directory""" + invalid_dir = "/nonexistent/path/to/project" + + result = run( + ["FABulous", invalid_dir, "--commands", "help"], capture_output=True, text=True + ) + assert result.returncode != 0 + assert "does not exist" in result.stdout + + +def test_project_without_fabulous_folder(tmp_path): + """Test error handling for directory without .FABulous folder""" + regular_dir = tmp_path / "regular_directory" + regular_dir.mkdir() + + result = run( + ["FABulous", str(regular_dir), "--commands", "help"], + capture_output=True, + text=True, + ) + assert result.returncode != 0 + assert "not a FABulous project" in result.stdout + + +def test_nonexistent_script_file(tmp_path): + """Test error handling for nonexistent script files""" + project_dir = tmp_path / "test_project" + + # Create project + result = run( + ["FABulous", "--createProject", str(project_dir)], + capture_output=True, + text=True, + ) + assert result.returncode == 0 + + # Try to run nonexistent FABulous script - FABulous handles this gracefully + result = run( + ["FABulous", str(project_dir), "--FABulousScript", "/nonexistent/script.fab"], + capture_output=True, + text=True, + ) + # FABulous appears to handle missing script files gracefully and still executes successfully + assert result.returncode == 0 + assert "Problem accessing script" in result.stderr + + # Try to run nonexistent TCL script + result = run( + ["FABulous", str(project_dir), "--TCLScript", "/nonexistent/script.tcl"], + capture_output=True, + text=True, + ) + # Check that it at least attempts to handle the missing file + assert "nonexistent" in result.stdout or "Problem" in result.stderr + + +def test_empty_commands(tmp_path): + """Test handling of empty command string""" + project_dir = tmp_path / "test_empty_cmd_project" + + # Create project + result = run( + ["FABulous", "--createProject", str(project_dir)], + capture_output=True, + text=True, + ) + assert result.returncode == 0 + + # Run with empty commands + result = run( + ["FABulous", str(project_dir), "--commands", ""], capture_output=True, text=True + ) + # Should handle gracefully + assert result.returncode == 0 From fe7d16b76c231f5ecc4e187c0491f8340755d1b2 Mon Sep 17 00:00:00 2001 From: Kelvin Chung Date: Thu, 5 Jun 2025 18:37:35 +0100 Subject: [PATCH 14/24] Update on test --- FABulous/FABulous.py | 181 +++++++++++++------------- FABulous/FABulous_CLI/FABulous_CLI.py | 19 ++- FABulous/FABulous_CLI/helper.py | 3 +- tests/CLI_test/conftest.py | 8 +- tests/CLI_test/test_arguments.py | 170 +++++++----------------- 5 files changed, 167 insertions(+), 214 deletions(-) diff --git a/FABulous/FABulous.py b/FABulous/FABulous.py index 27bc4f97..42bc6158 100644 --- a/FABulous/FABulous.py +++ b/FABulous/FABulous.py @@ -50,14 +50,9 @@ def main(): description="The command line interface for FABulous" ) - script_group = parser.add_mutually_exclusive_group() - - parser.add_argument( - "project_dir", - help="The directory to the project folder", - ) + create_group = parser.add_mutually_exclusive_group() - parser.add_argument( + create_group.add_argument( "-c", "--createProject", default=False, @@ -65,6 +60,26 @@ def main(): help="Create a new project", ) + create_group.add_argument( + "-iocs", + "--install_oss_cad_suite", + help="Install the oss-cad-suite in the directory." + "This will create a new directory called oss-cad-suite in the provided" + "directory and install the oss-cad-suite there." + "If there is already a directory called oss-cad-suite, it will be removed and replaced with a new one." + "This will also automatically add the FAB_OSS_CAD_SUITE env var in the global FABulous .env file. ", + action="store_true", + default=False, + ) + + script_group = parser.add_mutually_exclusive_group() + + parser.add_argument( + "project_dir", + help="The directory to the project folder", + ) + + script_group.add_argument( "-fs", "--FABulousScript", @@ -136,17 +151,6 @@ def main(): help="Set the project .env file path. Default is $FAB_PROJ_DIR/.env", ) - parser.add_argument( - "-iocs", - "--install_oss_cad_suite", - help="Install the oss-cad-suite in the directory." - "This will create a new directory called oss-cad-suite in the provided" - "directory and install the oss-cad-suite there." - "If there is already a directory called oss-cad-suite, it will be removed and replaced with a new one." - "This will also automatically add the FAB_OSS_CAD_SUITE env var in the global FABulous .env file. ", - action="store_true", - default=False, - ) parser.add_argument( "--force", @@ -160,93 +164,94 @@ def main(): setup_logger(args.verbose, args.debug, log_file=args.log) + if args.createProject: + create_project(Path(args.project_dir).absolute(), args.writer) + exit(0) + + if not (Path(args.project_dir).absolute() / ".FABulous").exists(): + logger.error( + "The directory provided is not a FABulous project as it does not have a .FABulous folder" + ) + exit(1) + + if not Path(args.project_dir).absolute().exists(): + logger.error(f"The directory provided does not exist: {args.project_dir}") + exit(1) + setup_global_env_vars(args) projectDir = Path(os.getenv("FAB_PROJ_DIR", args.project_dir)).absolute().resolve() - args.top = projectDir.stem - - if args.createProject and args.install_oss_cad_suite: - logger.error( - "You cannot create a new project and install the oss-cad-suite at the same time." + if projectDir != Path(args.project_dir).absolute().resolve(): + logger.warning( + f"The project directory provided ({args.project_dir}) does not match the FAB_PROJ_DIR environment variable ({projectDir})." + "Overriding user provided project directory with FAB_PROJ_DIR environment variable value." ) - exit(1) - if args.createProject: - create_project(projectDir, args.writer) - exit(0) - if not projectDir.exists(): - logger.error(f"The directory provided does not exist: {projectDir}") - exit(1) + args.top = projectDir.stem if args.install_oss_cad_suite: install_oss_cad_suite(projectDir, True) exit(0) - if not (projectDir / ".FABulous").exists(): - logger.error( - "The directory provided is not a FABulous project as it does not have a .FABulous folder" - ) - exit(1) - else: - setup_project_env_vars(args) - fab_CLI = FABulous_CLI( - os.getenv("FAB_PROJ_LANG"), - projectDir, - Path().cwd(), - force=args.force, - ) - fab_CLI.debug = args.debug - fabScript: Path = args.FABulousScript.absolute() - tclScript: Path = args.TCLScript.absolute() - logger.info(f"Setting current working directory to: {projectDir}") - cwd = Path().cwd() - os.chdir(projectDir) - fab_CLI.onecmd_plus_hooks("load_fabric") - - if commands := args.commands: - commands = commands.split("; ") - for c in commands: - fab_CLI.onecmd_plus_hooks(c) - if fab_CLI.exit_code: - logger.error( - f"Command '{c}' execution failed with exit code {fab_CLI.exit_code}" - ) - exit(1) - else: - logger.info( - f'Commands "{"; ".join(i.strip() for i in commands)}" executed successfully' - ) - exit(0) - elif fabScript != cwd: - fab_CLI.onecmd_plus_hooks(f"run_script {fabScript}") - if fab_CLI.exit_code: - logger.error( - f"FABulous script {args.FABulousScript} execution failed with exit code {fab_CLI.exit_code}" - ) - else: - logger.info( - f"FABulous script {args.FABulousScript} executed successfully" - ) - - exit(fab_CLI.exit_code) - elif tclScript != cwd: - fab_CLI.onecmd_plus_hooks(f"run_tcl {tclScript}") + setup_project_env_vars(args) + fab_CLI = FABulous_CLI( + os.getenv("FAB_PROJ_LANG"), + projectDir, + Path().cwd(), + force=args.force, + ) + fab_CLI.debug = args.debug + fabScript: Path = args.FABulousScript.absolute() + tclScript: Path = args.TCLScript.absolute() + logger.info(f"Setting current working directory to: {projectDir}") + cwd = Path().cwd() + os.chdir(projectDir) + fab_CLI.onecmd_plus_hooks("load_fabric") + + if commands := args.commands: + commands = commands.split("; ") + for c in commands: + fab_CLI.onecmd_plus_hooks(c) if fab_CLI.exit_code: logger.error( - f"TCL script {args.TCLScript} execution failed with exit code {fab_CLI.exit_code}" + f"Command '{c}' execution failed with exit code {fab_CLI.exit_code}" ) - else: - logger.info(f"TCL script {args.TCLScript} executed successfully") - exit(fab_CLI.exit_code) + exit(1) else: - fab_CLI.interactive = True - if args.verbose == 2: - fab_CLI.verbose = True - - fab_CLI.cmdloop() + logger.info( + f'Commands "{"; ".join(i.strip() for i in commands)}" executed successfully' + ) exit(0) + elif fabScript != cwd: + fab_CLI.onecmd_plus_hooks(f"run_script {fabScript}") + if fab_CLI.exit_code: + logger.error( + f"FABulous script {args.FABulousScript} execution failed with exit code {fab_CLI.exit_code}" + ) + else: + logger.info( + f"FABulous script {args.FABulousScript} executed successfully" + ) + + exit(fab_CLI.exit_code) + elif tclScript != cwd: + fab_CLI.onecmd_plus_hooks(f"run_tcl {tclScript}") + if fab_CLI.exit_code: + logger.error( + f"TCL script {args.TCLScript} execution failed with exit code {fab_CLI.exit_code}" + ) + else: + logger.info(f"TCL script {args.TCLScript} executed successfully") + exit(fab_CLI.exit_code) + else: + fab_CLI.interactive = True + if args.verbose == 2: + fab_CLI.verbose = True + + fab_CLI.cmdloop() + exit(0) if __name__ == "__main__": diff --git a/FABulous/FABulous_CLI/FABulous_CLI.py b/FABulous/FABulous_CLI/FABulous_CLI.py index 3bf2a2b0..48d177e5 100644 --- a/FABulous/FABulous_CLI/FABulous_CLI.py +++ b/FABulous/FABulous_CLI/FABulous_CLI.py @@ -188,8 +188,6 @@ def __init__( categorize(self.do_shortcuts, CMD_OTHER) categorize(self.do_help, CMD_OTHER) categorize(self.do_macro, CMD_OTHER) - - categorize(self.do_run_script, CMD_SCRIPT) categorize(self.do_run_tcl, CMD_SCRIPT) categorize(self.do_run_pyscript, CMD_SCRIPT) @@ -972,6 +970,23 @@ def do_run_tcl(self, args): logger.info("TCL script executed") + + @with_category(CMD_SCRIPT) + @with_argparser(filePathRequireParser) + def do_run_script(self, args): + """Executes script + """ + if not args.file.exists(): + logger.opt(exception=FileNotFoundError()).error(f"Cannot find {args.file}") + + logger.info(f"Execute script {args.file}") + + with open(args.file, "r") as f: + for i in f.readlines(): + self.onecmd_plus_hooks(i.strip()) + + logger.info("Script executed") + @with_category(CMD_USER_DESIGN_FLOW) @with_argparser(userDesignRequireParser) def do_gen_user_design_wrapper(self, args): diff --git a/FABulous/FABulous_CLI/helper.py b/FABulous/FABulous_CLI/helper.py index 1d1ba462..5cc656e1 100644 --- a/FABulous/FABulous_CLI/helper.py +++ b/FABulous/FABulous_CLI/helper.py @@ -16,7 +16,7 @@ MAX_BITBYTES = 16384 -def setup_logger(verbosity: int, debug: bool, log_file: Path = Path()): +def setup_logger(verbosity: int, debug: bool, log_file: Path = Path(), testing: bool = False): # Remove the default logger to avoid duplicate logs logger.remove() @@ -180,6 +180,7 @@ def create_project(project_dir: Path, lang: Literal["verilog", "vhdl"] = "verilo lang : Literal["verilog", "vhdl"], optional The language of project to create ("verilog" or "vhdl"), by default "verilog". """ + logger.info(project_dir) if project_dir.exists(): logger.error("Project directory already exists!") sys.exit(1) diff --git a/tests/CLI_test/conftest.py b/tests/CLI_test/conftest.py index 05957ab8..70102a65 100644 --- a/tests/CLI_test/conftest.py +++ b/tests/CLI_test/conftest.py @@ -50,7 +50,13 @@ def cli(tmp_path): ) cli.debug = True run_cmd(cli, "load_fabric") - return cli + yield cli + +@pytest.fixture +def project(tmp_path): + project_dir = tmp_path / "test_project" + create_project(project_dir) + return project_dir @pytest.fixture diff --git a/tests/CLI_test/test_arguments.py b/tests/CLI_test/test_arguments.py index 30eb4e41..7087bb69 100644 --- a/tests/CLI_test/test_arguments.py +++ b/tests/CLI_test/test_arguments.py @@ -22,39 +22,29 @@ def test_create_project_existing_dir(tmp_path): assert result.returncode != 0 -def test_create_project_with_no_name(tmp_path): +def test_create_project_with_no_name(): result = run(["FABulous", "--createProject"], capture_output=True, text=True) assert result.returncode != 0 -def test_fabulous_script(tmp_path): - # Create project first - project_dir = tmp_path / "test_prj" - result = run( - ["FABulous", "--createProject", str(project_dir)], - capture_output=True, - text=True, - ) - assert result.returncode == 0 - +def test_fabulous_script(tmp_path, project): # Create a test FABulous script file script_file = tmp_path / "test_script.fab" script_file.write_text("# Test FABulous script\nhelp\n") result = run( - ["FABulous", str(project_dir), "--FABulousScript", str(script_file)], + ["FABulous", str(project), "--FABulousScript", str(script_file)], capture_output=True, text=True, ) assert result.returncode == 0 -def test_fabulous_script_nonexistent_file(tmp_path): +def test_fabulous_script_nonexistent_file(tmp_path, project): nonexistent_script = tmp_path / "nonexistent_script.fab" - project_dir = tmp_path / "test_prj" result = run( - ["FABulous", str(project_dir), "--FABulousScript", str(nonexistent_script)], + ["FABulous", str(project), "--FABulousScript", str(nonexistent_script)], capture_output=True, text=True, ) @@ -73,17 +63,8 @@ def test_fabulous_script_with_no_project_dir(tmp_path): assert result.returncode != 0 -def test_tcl_script_execution(tmp_path): +def test_tcl_script_execution(tmp_path, project): """Test TCL script execution on a valid project""" - project_dir = tmp_path / "test_tcl_project" - - # Create project first - result = run( - ["FABulous", "--createProject", str(project_dir)], - capture_output=True, - text=True, - ) - assert result.returncode == 0 # Create a TCL script tcl_script = tmp_path / "test_script.tcl" @@ -93,28 +74,18 @@ def test_tcl_script_execution(tmp_path): # Run TCL script result = run( - ["FABulous", str(project_dir), "--TCLScript", str(tcl_script)], + ["FABulous", str(project), "--TCLScript", str(tcl_script)], capture_output=True, text=True, ) assert result.returncode == 0 -def test_commands_execution(tmp_path): +def test_commands_execution(tmp_path, project): """Test direct command execution with -p/--commands""" - project_dir = tmp_path / "test_cmd_project" - - # Create project - result = run( - ["FABulous", "--createProject", str(project_dir)], - capture_output=True, - text=True, - ) - assert result.returncode == 0 - # Run commands directly result = run( - ["FABulous", str(project_dir), "--commands", "help; help"], + ["FABulous", str(project), "--commands", "help; help"], capture_output=True, text=True, ) @@ -151,22 +122,13 @@ def test_create_project_with_verilog_writer(tmp_path): assert "verilog" in (project_dir / ".FABulous" / ".env").read_text() -def test_logging_functionality(tmp_path): +def test_logging_functionality(tmp_path, project): """Test log file creation and output""" - project_dir = tmp_path / "test_log_project" log_file = tmp_path / "test.log" - # Create project - result = run( - ["FABulous", "--createProject", str(project_dir)], - capture_output=True, - text=True, - ) - assert result.returncode == 0 - # Run with logging using commands instead of script to avoid file handling issues result = run( - ["FABulous", str(project_dir), "--commands", "help", "-log", str(log_file)], + ["FABulous", str(project), "--commands", "help", "-log", str(log_file)], capture_output=True, text=True, ) @@ -175,44 +137,26 @@ def test_logging_functionality(tmp_path): assert log_file.stat().st_size > 0 # Check if log file is not empty -def test_verbose_mode(tmp_path): +def test_verbose_mode(project): """Test verbose mode execution""" - project_dir = tmp_path / "test_verbose_project" - - # Create project - result = run( - ["FABulous", "--createProject", str(project_dir)], - capture_output=True, - text=True, - ) - assert result.returncode == 0 # Run with verbose mode result = run( - ["FABulous", str(project_dir), "--commands", "help", "-v"], + ["FABulous", str(project), "--commands", "help", "-v"], capture_output=True, text=True, ) assert result.returncode == 0 -def test_force_flag(tmp_path): +def test_force_flag(project): """Test force flag functionality""" - project_dir = tmp_path / "test_force_project" - - # Create project - result = run( - ["FABulous", "--createProject", str(project_dir)], - capture_output=True, - text=True, - ) - assert result.returncode == 0 # Run with force flag result = run( [ "FABulous", - str(project_dir), + str(project), "--commands", "load_fabric non_existent", "--force", @@ -225,7 +169,7 @@ def test_force_flag(tmp_path): result = run( [ "FABulous", - str(project_dir), + str(project), "--commands", "load_fabric non_existent; load_fabric", "--force", @@ -236,31 +180,20 @@ def test_force_flag(tmp_path): assert result.returncode == 1 -def test_debug_mode(tmp_path): +def test_debug_mode(project): """Test debug mode functionality""" - project_dir = tmp_path / "test_debug_project" - - # Create project - result = run( - ["FABulous", "--createProject", str(project_dir)], - capture_output=True, - text=True, - ) - assert result.returncode == 0 # Run with debug mode result = run( - ["FABulous", str(project_dir), "--commands", "help", "--debug"], + ["FABulous", str(project), "--commands", "help", "--debug"], capture_output=True, text=True, ) assert result.returncode == 0 -def test_install_oss_cad_suite(tmp_path, mocker): +def test_install_oss_cad_suite(project, mocker): """Test oss-cad-suite installation""" - install_dir = tmp_path / "oss_install_test" - install_dir.mkdir() # Test installation (may fail if network unavailable, but should handle gracefully) class MockRequest: @@ -270,24 +203,15 @@ class MockRequest: "requests.get", return_value=MockRequest() ) # Mock network request for testing result = run( - ["FABulous", str(install_dir), "--install_oss_cad_suite"], + ["FABulous", str(project), "--install_oss_cad_suite"], capture_output=True, text=True, ) assert result.returncode == 0 -def test_script_mutually_exclusive(tmp_path): +def test_script_mutually_exclusive(tmp_path, project): """Test that FABulous script and TCL script are mutually exclusive""" - project_dir = tmp_path / "test_exclusive_project" - - # Create project - result = run( - ["FABulous", "--createProject", str(project_dir)], - capture_output=True, - text=True, - ) - assert result.returncode == 0 # Create both script types fab_script = tmp_path / "test.fab" @@ -299,7 +223,7 @@ def test_script_mutually_exclusive(tmp_path): result = run( [ "FABulous", - str(project_dir), + str(project), "--FABulousScript", str(fab_script), "--TCLScript", @@ -319,7 +243,6 @@ def test_invalid_project_directory(): ["FABulous", invalid_dir, "--commands", "help"], capture_output=True, text=True ) assert result.returncode != 0 - assert "does not exist" in result.stdout def test_project_without_fabulous_folder(tmp_path): @@ -336,31 +259,21 @@ def test_project_without_fabulous_folder(tmp_path): assert "not a FABulous project" in result.stdout -def test_nonexistent_script_file(tmp_path): +def test_nonexistent_script_file(project): """Test error handling for nonexistent script files""" - project_dir = tmp_path / "test_project" - - # Create project - result = run( - ["FABulous", "--createProject", str(project_dir)], - capture_output=True, - text=True, - ) - assert result.returncode == 0 # Try to run nonexistent FABulous script - FABulous handles this gracefully result = run( - ["FABulous", str(project_dir), "--FABulousScript", "/nonexistent/script.fab"], + ["FABulous", str(project), "--FABulousScript", "/nonexistent/script.fab"], capture_output=True, text=True, ) # FABulous appears to handle missing script files gracefully and still executes successfully - assert result.returncode == 0 - assert "Problem accessing script" in result.stderr + assert result.returncode == 1 # Try to run nonexistent TCL script result = run( - ["FABulous", str(project_dir), "--TCLScript", "/nonexistent/script.tcl"], + ["FABulous", str(project), "--TCLScript", "/nonexistent/script.tcl"], capture_output=True, text=True, ) @@ -368,21 +281,34 @@ def test_nonexistent_script_file(tmp_path): assert "nonexistent" in result.stdout or "Problem" in result.stderr -def test_empty_commands(tmp_path): +def test_empty_commands(project): """Test handling of empty command string""" - project_dir = tmp_path / "test_empty_cmd_project" + # Run with empty commands + result = run( + ["FABulous", str(project), "--commands", ""], capture_output=True, text=True + ) + # Should handle gracefully + assert result.returncode == 0 + +def test_create_project_with_invalid_writer(tmp_path, project): + """Test project creation with an invalid writer""" + project_dir = tmp_path / "test_invalid_writer_project" - # Create project result = run( - ["FABulous", "--createProject", str(project_dir)], + ["FABulous", "--createProject", str(project_dir), "--writer", "invalid_writer"], capture_output=True, text=True, ) - assert result.returncode == 0 + assert result.returncode != 0 + + +def test_create_project_with_install_oss_cad_suite(tmp_path): + """Test project creation with install_oss_cad_suite flag""" + project_dir = tmp_path / "test_install_oss_cad_suite_project" - # Run with empty commands result = run( - ["FABulous", str(project_dir), "--commands", ""], capture_output=True, text=True + ["FABulous", "--createProject", str(project_dir), "--install_oss_cad_suite"], + capture_output=True, + text=True, ) - # Should handle gracefully - assert result.returncode == 0 + assert result.returncode != 0 \ No newline at end of file From 2a3d72ffc6e10aff73e0369b0e7b630e434fd26b Mon Sep 17 00:00:00 2001 From: Kelvin Chung Date: Thu, 5 Jun 2025 18:39:55 +0100 Subject: [PATCH 15/24] format --- FABulous/FABulous.py | 7 +------ FABulous/FABulous_CLI/FABulous_CLI.py | 4 +--- FABulous/FABulous_CLI/helper.py | 4 +++- tests/CLI_test/conftest.py | 1 + tests/CLI_test/test_arguments.py | 3 ++- 5 files changed, 8 insertions(+), 11 deletions(-) diff --git a/FABulous/FABulous.py b/FABulous/FABulous.py index 42bc6158..de149d76 100644 --- a/FABulous/FABulous.py +++ b/FABulous/FABulous.py @@ -79,7 +79,6 @@ def main(): help="The directory to the project folder", ) - script_group.add_argument( "-fs", "--FABulousScript", @@ -151,7 +150,6 @@ def main(): help="Set the project .env file path. Default is $FAB_PROJ_DIR/.env", ) - parser.add_argument( "--force", action="store_true", @@ -188,7 +186,6 @@ def main(): "Overriding user provided project directory with FAB_PROJ_DIR environment variable value." ) - args.top = projectDir.stem if args.install_oss_cad_suite: @@ -231,9 +228,7 @@ def main(): f"FABulous script {args.FABulousScript} execution failed with exit code {fab_CLI.exit_code}" ) else: - logger.info( - f"FABulous script {args.FABulousScript} executed successfully" - ) + logger.info(f"FABulous script {args.FABulousScript} executed successfully") exit(fab_CLI.exit_code) elif tclScript != cwd: diff --git a/FABulous/FABulous_CLI/FABulous_CLI.py b/FABulous/FABulous_CLI/FABulous_CLI.py index 48d177e5..bb9621e9 100644 --- a/FABulous/FABulous_CLI/FABulous_CLI.py +++ b/FABulous/FABulous_CLI/FABulous_CLI.py @@ -970,12 +970,10 @@ def do_run_tcl(self, args): logger.info("TCL script executed") - @with_category(CMD_SCRIPT) @with_argparser(filePathRequireParser) def do_run_script(self, args): - """Executes script - """ + """Executes script""" if not args.file.exists(): logger.opt(exception=FileNotFoundError()).error(f"Cannot find {args.file}") diff --git a/FABulous/FABulous_CLI/helper.py b/FABulous/FABulous_CLI/helper.py index 5cc656e1..534bd6cb 100644 --- a/FABulous/FABulous_CLI/helper.py +++ b/FABulous/FABulous_CLI/helper.py @@ -16,7 +16,9 @@ MAX_BITBYTES = 16384 -def setup_logger(verbosity: int, debug: bool, log_file: Path = Path(), testing: bool = False): +def setup_logger( + verbosity: int, debug: bool, log_file: Path = Path(), testing: bool = False +): # Remove the default logger to avoid duplicate logs logger.remove() diff --git a/tests/CLI_test/conftest.py b/tests/CLI_test/conftest.py index 70102a65..cbfbab40 100644 --- a/tests/CLI_test/conftest.py +++ b/tests/CLI_test/conftest.py @@ -52,6 +52,7 @@ def cli(tmp_path): run_cmd(cli, "load_fabric") yield cli + @pytest.fixture def project(tmp_path): project_dir = tmp_path / "test_project" diff --git a/tests/CLI_test/test_arguments.py b/tests/CLI_test/test_arguments.py index 7087bb69..5786a5d1 100644 --- a/tests/CLI_test/test_arguments.py +++ b/tests/CLI_test/test_arguments.py @@ -290,6 +290,7 @@ def test_empty_commands(project): # Should handle gracefully assert result.returncode == 0 + def test_create_project_with_invalid_writer(tmp_path, project): """Test project creation with an invalid writer""" project_dir = tmp_path / "test_invalid_writer_project" @@ -311,4 +312,4 @@ def test_create_project_with_install_oss_cad_suite(tmp_path): capture_output=True, text=True, ) - assert result.returncode != 0 \ No newline at end of file + assert result.returncode != 0 From d87d330db3654327e0a4007dae56dfb4c9a6b51a Mon Sep 17 00:00:00 2001 From: Kelvin Chung Date: Thu, 5 Jun 2025 21:51:15 +0100 Subject: [PATCH 16/24] Make integration test working with pytest --- FABulous/FABulous.py | 4 +-- tests/CLI_test/test_CLI.py | 2 +- tests/{CLI_test => }/conftest.py | 16 ++++++++--- tests/fabric_gen_test/test_intergration.py | 31 ++++++++++++++++++++++ 4 files changed, 46 insertions(+), 7 deletions(-) rename tests/{CLI_test => }/conftest.py (84%) create mode 100644 tests/fabric_gen_test/test_intergration.py diff --git a/FABulous/FABulous.py b/FABulous/FABulous.py index de149d76..88542e56 100644 --- a/FABulous/FABulous.py +++ b/FABulous/FABulous.py @@ -162,6 +162,8 @@ def main(): setup_logger(args.verbose, args.debug, log_file=args.log) + setup_global_env_vars(args) + if args.createProject: create_project(Path(args.project_dir).absolute(), args.writer) exit(0) @@ -176,8 +178,6 @@ def main(): logger.error(f"The directory provided does not exist: {args.project_dir}") exit(1) - setup_global_env_vars(args) - projectDir = Path(os.getenv("FAB_PROJ_DIR", args.project_dir)).absolute().resolve() if projectDir != Path(args.project_dir).absolute().resolve(): diff --git a/tests/CLI_test/test_CLI.py b/tests/CLI_test/test_CLI.py index 6c3423d3..6d7a6333 100644 --- a/tests/CLI_test/test_CLI.py +++ b/tests/CLI_test/test_CLI.py @@ -1,6 +1,6 @@ from pathlib import Path import pytest -from tests.CLI_test.conftest import ( +from tests.conftest import ( TILE, normalize_and_check_for_errors, run_cmd, diff --git a/tests/CLI_test/conftest.py b/tests/conftest.py similarity index 84% rename from tests/CLI_test/conftest.py rename to tests/conftest.py index cbfbab40..7c5488f6 100644 --- a/tests/CLI_test/conftest.py +++ b/tests/conftest.py @@ -34,14 +34,18 @@ def normalize_and_check_for_errors(caplog_text: str): TILE = "LUT4AB" -os.environ["FAB_ROOT"] = str(Path(__file__).resolve().parent.parent.parent / "FABulous") + +@pytest.fixture(autouse=True) +def env(): + fabulousRoot = str(Path(__file__).resolve().parent.parent / "FABulous") + os.environ["FAB_ROOT"] = fabulousRoot + yield + os.environ.pop("FAB_ROOT", None) @pytest.fixture def cli(tmp_path): projectDir = tmp_path / "test_project" - fabulousRoot = str(Path(__file__).resolve().parent.parent.parent / "FABulous") - os.environ["FAB_ROOT"] = fabulousRoot os.environ["FAB_PROJ_DIR"] = str(projectDir) create_project(projectDir) setup_logger(0, False) @@ -51,13 +55,17 @@ def cli(tmp_path): cli.debug = True run_cmd(cli, "load_fabric") yield cli + os.environ.pop("FAB_ROOT", None) + os.environ.pop("FAB_PROJ_DIR", None) @pytest.fixture def project(tmp_path): project_dir = tmp_path / "test_project" + os.environ["FAB_PROJ_DIR"] = str(project_dir) create_project(project_dir) - return project_dir + yield project_dir + os.environ.pop("FAB_PROJ_DIR", None) @pytest.fixture diff --git a/tests/fabric_gen_test/test_intergration.py b/tests/fabric_gen_test/test_intergration.py new file mode 100644 index 00000000..04451fcc --- /dev/null +++ b/tests/fabric_gen_test/test_intergration.py @@ -0,0 +1,31 @@ +from subprocess import run +import os + + +def test_run_verilog_simulation_CIL(tmp_path): + project_dir = tmp_path / "demo" + result = run(["FABulous", "-c", str(project_dir)]) + assert result.returncode == 0 + + result = run( + ["FABulous", str(project_dir), "-fs", "./demo/FABulous.tcl"], cwd=tmp_path + ) + assert result.returncode == 0 + + +def test_run_verilog_simulation_makefile(tmp_path): + project_dir = tmp_path / "demo" + result = run(["FABulous", "-c", str(project_dir)]) + assert result.returncode == 0 + + result = run(["make", "FAB_sim"], cwd=project_dir / "Test") + assert result.returncode == 0 + + +def test_run_vhdl_simulation_makefile(tmp_path): + project_dir = tmp_path / "demo_vhdl" + result = run(["FABulous", "-c", str(project_dir), "-w", "vhdl"]) + assert result.returncode == 0 + + result = run(["make", "full_sim"], cwd=project_dir / "Test") + assert result.returncode == 0 From 53c3768c2393ffeec292675f945927c784b58cb6 Mon Sep 17 00:00:00 2001 From: Kelvin Chung Date: Tue, 10 Jun 2025 16:42:56 +0100 Subject: [PATCH 17/24] Add test for priority order and test for command stop --- FABulous/FABulous.py | 35 +++++++------ FABulous/FABulous_CLI/FABulous_CLI.py | 54 ++++++++++++++++++-- FABulous/FABulous_CLI/helper.py | 7 +-- tests/CLI_test/test_CLI.py | 12 +++++ tests/CLI_test/test_arguments.py | 73 +++++++++++++++++++++++++++ tests/conftest.py | 2 + 6 files changed, 159 insertions(+), 24 deletions(-) diff --git a/FABulous/FABulous.py b/FABulous/FABulous.py index 88542e56..cbf84991 100644 --- a/FABulous/FABulous.py +++ b/FABulous/FABulous.py @@ -1,6 +1,5 @@ import argparse import os -from contextlib import redirect_stdout from pathlib import Path from loguru import logger @@ -76,6 +75,8 @@ def main(): parser.add_argument( "project_dir", + default="", + nargs="?", help="The directory to the project folder", ) @@ -162,37 +163,39 @@ def main(): setup_logger(args.verbose, args.debug, log_file=args.log) + # Start with default value + projectDir = Path().cwd() + + # Setup global and project env vars to load .env files setup_global_env_vars(args) + setup_project_env_vars(args) + + # Check if FAB_PROJ_DIR is set in environment (including from .env files) + if fab_proj_dir := os.getenv("FAB_PROJ_DIR", None): + projectDir = Path(fab_proj_dir).absolute().resolve() + + # Finally, user provided argument takes highest priority + if args.project_dir: + projectDir = Path(args.project_dir).absolute().resolve() if args.createProject: - create_project(Path(args.project_dir).absolute(), args.writer) + create_project(projectDir, args.writer) exit(0) - if not (Path(args.project_dir).absolute() / ".FABulous").exists(): + if not (projectDir / ".FABulous").exists(): logger.error( "The directory provided is not a FABulous project as it does not have a .FABulous folder" ) exit(1) - if not Path(args.project_dir).absolute().exists(): - logger.error(f"The directory provided does not exist: {args.project_dir}") + if not projectDir.exists(): + logger.error(f"The directory provided does not exist: {projectDir}") exit(1) - projectDir = Path(os.getenv("FAB_PROJ_DIR", args.project_dir)).absolute().resolve() - - if projectDir != Path(args.project_dir).absolute().resolve(): - logger.warning( - f"The project directory provided ({args.project_dir}) does not match the FAB_PROJ_DIR environment variable ({projectDir})." - "Overriding user provided project directory with FAB_PROJ_DIR environment variable value." - ) - - args.top = projectDir.stem - if args.install_oss_cad_suite: install_oss_cad_suite(projectDir, True) exit(0) - setup_project_env_vars(args) fab_CLI = FABulous_CLI( os.getenv("FAB_PROJ_LANG"), projectDir, diff --git a/FABulous/FABulous_CLI/FABulous_CLI.py b/FABulous/FABulous_CLI/FABulous_CLI.py index bb9621e9..d2cee31f 100644 --- a/FABulous/FABulous_CLI/FABulous_CLI.py +++ b/FABulous/FABulous_CLI/FABulous_CLI.py @@ -222,9 +222,9 @@ def onecmd( logger.debug(traceback.format_exc()) self.exit_code = 1 if not self.interactive: - return not self.force - else: return False + else: + return not self.force def do_exit(self, *ignored): """Exits the FABulous shell and logs info message.""" @@ -512,7 +512,12 @@ def do_gen_fabric(self, *ignored): Logs start and completion of fabric generation process. """ logger.info(f"Generating fabric {self.fabulousAPI.fabric.name}") - self.do_gen_all_tile() + self.onecmd_plus_hooks("gen_all_tile") + if self.exit_code != 0: + logger.opt(exception=CommandError()).error( + "Tile generation failed. Please check the logs for more details." + ) + return self.fabulousAPI.setWriterOutputFile( f"{self.projectDir}/Fabric/{self.fabulousAPI.fabric.name}.{self.extension}" ) @@ -632,12 +637,37 @@ def do_run_FABulous_fabric(self, *ignored): """ logger.info("Running FABulous") self.onecmd_plus_hooks("gen_fabric") + if self.exit_code != 0: + logger.opt(exception=CommandError()).error( + "Fabric generation failed. Please check the logs for more details." + ) + return + self.onecmd_plus_hooks("gen_bitStream_spec") + if self.exit_code != 0: + logger.opt(exception=CommandError()).error( + "Bitstream specification generation failed. Please check the logs for more details." + ) + return self.onecmd_plus_hooks("gen_top_wrapper") + if self.exit_code != 0: + logger.opt(exception=CommandError()).error( + "Top wrapper generation failed. Please check the logs for more details." + ) + return self.onecmd_plus_hooks("gen_model_npnr") + if self.exit_code != 0: + logger.opt(exception=CommandError()).error( + "Nextpnr model generation failed. Please check the logs for more details." + ) + return self.onecmd_plus_hooks("gen_geometry") + if self.exit_code != 0: + logger.opt(exception=CommandError()).error( + "Geometry generation failed. Please check the logs for more details." + ) + return logger.info("FABulous fabric flow complete") - return @with_category(CMD_FABRIC_FLOW) def do_gen_model_npnr(self, *ignored): @@ -771,7 +801,6 @@ def do_gen_bitStream_binary(self, args): Usage: gen_bitStream_binary """ ) - return bitstream_file = top_module_name + ".bin" @@ -948,8 +977,23 @@ def do_run_FABulous_bitstream(self, args): else: logger.info("No external primsLib found.") self.onecmd_plus_hooks(f"synthesis {do_synth_args}") + if self.exit_code != 0: + logger.opt(exception=CommandError()).error( + "Synthesis failed. Please check the logs for more details." + ) + return self.onecmd_plus_hooks(f"place_and_route {json_file_path}") + if self.exit_code != 0: + logger.opt(exception=CommandError()).error( + "Place and Route failed. Please check the logs for more details." + ) + return self.onecmd_plus_hooks(f"gen_bitStream_binary {fasm_file_path}") + if self.exit_code != 0: + logger.opt(exception=CommandError()).error( + "Bitstream generation failed. Please check the logs for more details." + ) + return @with_category(CMD_SCRIPT) @with_argparser(filePathRequireParser) diff --git a/FABulous/FABulous_CLI/helper.py b/FABulous/FABulous_CLI/helper.py index 534bd6cb..3fb09292 100644 --- a/FABulous/FABulous_CLI/helper.py +++ b/FABulous/FABulous_CLI/helper.py @@ -16,9 +16,7 @@ MAX_BITBYTES = 16384 -def setup_logger( - verbosity: int, debug: bool, log_file: Path = Path(), testing: bool = False -): +def setup_logger(verbosity: int, debug: bool, log_file: Path = Path()): # Remove the default logger to avoid duplicate logs logger.remove() @@ -42,6 +40,9 @@ def custom_format_function(record): else: final_log = f"{level}{exc}{msg}\n" + if os.getenv("FABULOUS_TESTING", None): + final_log = f"{record['level'].name}: {record['message']}\n" + return final_log # Determine the log level for the sink diff --git a/tests/CLI_test/test_CLI.py b/tests/CLI_test/test_CLI.py index 6d7a6333..08fa0b97 100644 --- a/tests/CLI_test/test_CLI.py +++ b/tests/CLI_test/test_CLI.py @@ -1,7 +1,10 @@ from pathlib import Path +import subprocess import pytest +from FABulous.FABulous_CLI.FABulous_CLI import FABulous_CLI from tests.conftest import ( TILE, + normalize, normalize_and_check_for_errors, run_cmd, ) @@ -132,3 +135,12 @@ def test_run_tcl(cli, caplog, tmp_path): log = normalize_and_check_for_errors(caplog.text) assert f"Execute TCL script {str(tcl_script_path)}" in log[0] assert "TCL script executed" in log[-1] + + +def test_multi_command_stop(cli, caplog, monkeypatch): + monkeypatch.setattr( + subprocess, "run", lambda: False, raising=ValueError("Command failed") + ) + run_cmd(cli, "run_FABulous_bitstream ./user_design/sequential_16bit_en.v") + log = normalize(caplog.text) + assert "ERROR" in log[-2] diff --git a/tests/CLI_test/test_arguments.py b/tests/CLI_test/test_arguments.py index 5786a5d1..e27e69e3 100644 --- a/tests/CLI_test/test_arguments.py +++ b/tests/CLI_test/test_arguments.py @@ -1,4 +1,5 @@ from subprocess import run +import os def test_create_project(tmp_path): @@ -199,6 +200,9 @@ def test_install_oss_cad_suite(project, mocker): class MockRequest: status_code = 200 + def iter_content(self, chunk_size=1024): + return [] + mocker.patch( "requests.get", return_value=MockRequest() ) # Mock network request for testing @@ -313,3 +317,72 @@ def test_create_project_with_install_oss_cad_suite(tmp_path): text=True, ) assert result.returncode != 0 + + +def test_project_directory_priority_order(tmp_path, monkeypatch, mocker): + """Test that project directory priority order is followed: + 1. User provided argument (highest priority) + 2. Environment variables (FAB_PROJ_DIR) + 3. Project .env file (handled by setup functions) + 4. Global .env file (handled by setup functions) + 5. Default value - current working directory (lowest priority) + """ + # Create multiple project directories for testing + user_provided_dir = tmp_path / "user_provided_project" + env_var_dir = tmp_path / "env_var_project" + default_dir = tmp_path / "default_project" + + # Create all directories with .FABulous folders + for project_dir in [user_provided_dir, env_var_dir, default_dir]: + project_dir.mkdir() + (project_dir / ".FABulous").mkdir() + (project_dir / ".FABulous" / ".env").write_text("FAB_PROJ_LANG=verilog\n") + + # Test 1: User provided argument should take highest priority over environment variable + monkeypatch.setenv("FAB_PROJ_DIR", str(env_var_dir)) + monkeypatch.chdir(default_dir) + + result = run( + ["FABulous", str(user_provided_dir), "--commands", "help"], + capture_output=True, + text=True, + ) + + # The log should show the user provided directory being used + assert ( + f"INFO: Setting current working directory to: {str(user_provided_dir)}" + in result.stdout + ) + + # Test 2: Environment variable should be used when no user argument provided + env_with_fab_proj = os.environ.copy() + env_with_fab_proj["FAB_PROJ_DIR"] = str(env_var_dir) + + result = run( + ["FABulous", "--commands", "help"], + capture_output=True, + text=True, + env=env_with_fab_proj, + ) + # Should use the environment variable directory + assert ( + f"INFO: Setting current working directory to: {str(env_var_dir)}" + in result.stdout + ) + + # Test 3: Default directory (cwd) should be used when no argument or env var + env_without_fab_proj = os.environ.copy() + env_without_fab_proj.pop("FAB_PROJ_DIR", None) + + result = run( + ["FABulous", "--commands", "help"], + capture_output=True, + text=True, + cwd=str(default_dir), + env=env_without_fab_proj, + ) + + assert ( + f"INFO: Setting current working directory to: {str(default_dir)}" + in result.stdout + ) diff --git a/tests/conftest.py b/tests/conftest.py index 7c5488f6..29dd2916 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -39,8 +39,10 @@ def normalize_and_check_for_errors(caplog_text: str): def env(): fabulousRoot = str(Path(__file__).resolve().parent.parent / "FABulous") os.environ["FAB_ROOT"] = fabulousRoot + os.environ["FABULOUS_TESTING"] = "TRUE" yield os.environ.pop("FAB_ROOT", None) + os.environ.pop("FABULOUS_TESTING", None) @pytest.fixture From 05ee045d48291244bb47d8da9d9a40e74e59acba Mon Sep 17 00:00:00 2001 From: Kelvin Chung Date: Tue, 10 Jun 2025 17:01:31 +0100 Subject: [PATCH 18/24] Add doc --- docs/source/Building fabric.rst | 2 +- docs/source/Usage.rst | 13 ++++++++++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/docs/source/Building fabric.rst b/docs/source/Building fabric.rst index 2b22ec38..0892610a 100644 --- a/docs/source/Building fabric.rst +++ b/docs/source/Building fabric.rst @@ -14,7 +14,7 @@ completed. #. Create a new project - .. prompt:: bash FABulous> + .. prompt:: bash FABulous -c demo diff --git a/docs/source/Usage.rst b/docs/source/Usage.rst index 2316c483..1e024189 100644 --- a/docs/source/Usage.rst +++ b/docs/source/Usage.rst @@ -103,6 +103,9 @@ Install FABulous with "editable" option: Building Fabric and Bitstream ----------------------------- +We offer two ways to run the FABulous flow, either via the FABulous Shell or directly supplying the commands via the command line (similar to Vivado Batch mode). + +To use the FABulous Shell, you can run the following command: .. code-block:: console @@ -110,10 +113,18 @@ Building Fabric and Bitstream (venv)$ FABulous # inside the FABulous shell - FABulous> load_fabric FABulous> run_FABulous_fabric FABulous> run_FABulous_bitstream user_design/sequential_16bit_en.v + +To run the FABulous flow directly from the command line, you can use the following commands: +.. code-block:: console + + (venv)$ FABulous -c + (venv)$ FABulous -p "run_FABulous_fabric; run_FABulous_bitstream user_design/sequential_16bit_en.v" + + + .. note:: You will probably receive a warning for the FASM package like the following: From 886cb15dbdfc2760e710988783f19a912dd03f73 Mon Sep 17 00:00:00 2001 From: Kelvin Chung Date: Wed, 11 Jun 2025 16:48:45 +0100 Subject: [PATCH 19/24] Hand tested the command actually finish on the first error --- FABulous/FABulous_CLI/FABulous_CLI.py | 94 +++++++++++++++----------- FABulous/FABulous_CLI/cmd_synthesis.py | 13 ++-- FABulous/FABulous_CLI/helper.py | 5 +- tests/CLI_test/test_CLI.py | 19 ++---- 4 files changed, 72 insertions(+), 59 deletions(-) diff --git a/FABulous/FABulous_CLI/FABulous_CLI.py b/FABulous/FABulous_CLI/FABulous_CLI.py index d2cee31f..1ffc9759 100644 --- a/FABulous/FABulous_CLI/FABulous_CLI.py +++ b/FABulous/FABulous_CLI/FABulous_CLI.py @@ -221,7 +221,7 @@ def onecmd( except Exception: logger.debug(traceback.format_exc()) self.exit_code = 1 - if not self.interactive: + if self.interactive: return False else: return not self.force @@ -757,17 +757,18 @@ def do_place_and_route(self, args): "--log", f"{self.projectDir}/{parent}/{log_file}", ] - try: - sp.run( - " ".join(runCmd), - stdout=sys.stdout, - stderr=sp.STDOUT, - check=True, - shell=True, + result = sp.run( + " ".join(runCmd), + stdout=sys.stdout, + stderr=sp.STDOUT, + check=True, + shell=True, + ) + if result.returncode != 0: + logger.opt(exception=CommandError()).error( + "Nextpnr failed. Please check the logs for more details." ) - except sp.CalledProcessError as e: - logger.opt(exception=e).error("Placement and Routing failed.") - + return else: logger.opt(exception=FileNotFoundError()).error( f'Cannot find file "{json_file}" in path "./{parent}/", which is generated by running Yosys with Nextpnr backend (e.g. synthesis).' @@ -865,6 +866,7 @@ def do_run_simulation(self, args): if bitstreamPath.suffix != ".bin": logger.error("No bitstream file specified.") return + if not bitstreamPath.exists(): logger.opt(exception=FileNotFoundError()).error( f"Cannot find {bitstreamPath} file which is generated by running gen_bitStream_binary. Potentially the bitstream generation failed." @@ -894,27 +896,28 @@ def do_run_simulation(self, args): iverilog = check_if_application_exists( os.getenv("FAB_IVERILOG_PATH", "iverilog") ) - try: - runCmd = [ - f"{iverilog}", - "-D", - f"{defined_option}", - "-s", - f"{topModuleTB}", - "-o", - f"{buildDir}/{vvpFile}", - *file_list, - f"{bitstreamPath.parent}/{designFile}", - f"{testPath}/{testBench}", - ] - if self.verbose or self.debug: - logger.info(f"Running simulation with {args.format} format") - logger.info(f"Running command: {' '.join(runCmd)}") - sp.run(runCmd, check=True) + runCmd = [ + f"{iverilog}", + "-D", + f"{defined_option}", + "-s", + f"{topModuleTB}", + "-o", + f"{buildDir}/{vvpFile}", + *file_list, + f"{bitstreamPath.parent}/{designFile}", + f"{testPath}/{testBench}", + ] + if self.verbose or self.debug: + logger.info(f"Running simulation with {args.format} format") + logger.info(f"Running command: {' '.join(runCmd)}") - except sp.CalledProcessError as e: - remove_dir(buildDir) - logger.opt(exception=e).error("Simulation failed") + result = sp.run(runCmd, check=True) + if result.returncode != 0: + logger.opt(exception=CommandError()).error( + "Simulation failed. Please check the logs for more details." + ) + return # bitstream hex file is used for simulation so it'll be created in the test directory bitstreamHexPath = (buildDir.parent / bitstreamPath.stem).with_suffix(".hex") @@ -931,16 +934,18 @@ def do_run_simulation(self, args): if waveform_format == "fst": vvpArgs.append("-fst") - try: - runCmd = [f"{vvp}", f"{buildDir}/{vvpFile}"] - runCmd.extend(vvpArgs) - if self.verbose or self.debug: - logger.info(f"Running command: {' '.join(runCmd)}") - sp.run(runCmd, check=True) - remove_dir(buildDir) - except sp.CalledProcessError as e: - remove_dir(buildDir) - logger.opt(exception=e).error("Simulation failed") + runCmd = [f"{vvp}", f"{buildDir}/{vvpFile}"] + runCmd.extend(vvpArgs) + if self.verbose or self.debug: + logger.info(f"Running command: {' '.join(runCmd)}") + + result = sp.run(runCmd, check=True) + remove_dir(buildDir) + if result.returncode != 0: + logger.opt(exception=CommandError()).error( + "Simulation failed. Please check the logs for more details." + ) + return logger.info("Simulation finished") @@ -976,12 +981,14 @@ def do_run_FABulous_bitstream(self, args): do_synth_args += f" -extra-plib {primsLib}" else: logger.info("No external primsLib found.") + self.onecmd_plus_hooks(f"synthesis {do_synth_args}") if self.exit_code != 0: logger.opt(exception=CommandError()).error( "Synthesis failed. Please check the logs for more details." ) return + self.onecmd_plus_hooks(f"place_and_route {json_file_path}") if self.exit_code != 0: logger.opt(exception=CommandError()).error( @@ -1026,6 +1033,11 @@ def do_run_script(self, args): with open(args.file, "r") as f: for i in f.readlines(): self.onecmd_plus_hooks(i.strip()) + if self.exit_code != 0: + logger.opt(exception=CommandError()).error( + f"Script execution failed at line: {i.strip()}" + ) + return logger.info("Script executed") diff --git a/FABulous/FABulous_CLI/cmd_synthesis.py b/FABulous/FABulous_CLI/cmd_synthesis.py index 9855ceb2..c2f690c7 100644 --- a/FABulous/FABulous_CLI/cmd_synthesis.py +++ b/FABulous/FABulous_CLI/cmd_synthesis.py @@ -2,6 +2,7 @@ import subprocess as sp from pathlib import Path +from FABulous.custom_exception import CommandError from cmd2 import Cmd, Cmd2ArgumentParser, with_argparser, with_category from loguru import logger @@ -290,8 +291,10 @@ def do_synthesis(self, args): *[str(i) for i in paths], ] logger.debug(f"{runCmd}") - try: - sp.run(runCmd, check=True) - logger.info("Synthesis completed") - except sp.CalledProcessError: - logger.error("Synthesis failed") + result = sp.run(runCmd, check=True) + + if result.returncode != 0: + logger.opt(exception=CommandError()).error( + "Synthesis failed with non-zero return code." + ) + logger.info("Synthesis command executed successfully.") diff --git a/FABulous/FABulous_CLI/helper.py b/FABulous/FABulous_CLI/helper.py index 3fb09292..2d848fee 100644 --- a/FABulous/FABulous_CLI/helper.py +++ b/FABulous/FABulous_CLI/helper.py @@ -50,13 +50,16 @@ def custom_format_function(record): # Add logger to write logs to stdout using the custom formatter if log_file != Path(): - logger.add(log_file, format=custom_format_function, level=log_level_to_set) + logger.add( + log_file, format=custom_format_function, level=log_level_to_set, catch=False + ) else: logger.add( sys.stdout, format=custom_format_function, level=log_level_to_set, colorize=True, + catch=False, ) diff --git a/tests/CLI_test/test_CLI.py b/tests/CLI_test/test_CLI.py index 08fa0b97..41193047 100644 --- a/tests/CLI_test/test_CLI.py +++ b/tests/CLI_test/test_CLI.py @@ -1,10 +1,7 @@ from pathlib import Path -import subprocess -import pytest -from FABulous.FABulous_CLI.FABulous_CLI import FABulous_CLI + from tests.conftest import ( TILE, - normalize, normalize_and_check_for_errors, run_cmd, ) @@ -100,7 +97,7 @@ def test_gen_model_npnr(cli, caplog): def test_run_FABulous_bitstream(cli, caplog, mocker): """Test the run_FABulous_bitstream command""" - m = mocker.patch("subprocess.run", return_value=None) + m = mocker.patch("subprocess.run", side_effect=RuntimeError("Mocked error")) run_cmd(cli, "run_FABulous_fabric") Path(cli.projectDir / "user_design" / "sequential_16bit_en.json").touch() Path(cli.projectDir / "user_design" / "sequential_16bit_en.fasm").touch() @@ -112,7 +109,7 @@ def test_run_FABulous_bitstream(cli, caplog, mocker): def test_run_simulation(cli, caplog, mocker): """Test running simulation""" - m = mocker.patch("subprocess.run", return_value=None) + m = mocker.patch("subprocess.run", side_effect=RuntimeError("Mocked error")) run_cmd(cli, "run_FABulous_fabric") Path(cli.projectDir / "user_design" / "sequential_16bit_en.json").touch() Path(cli.projectDir / "user_design" / "sequential_16bit_en.fasm").touch() @@ -137,10 +134,8 @@ def test_run_tcl(cli, caplog, tmp_path): assert "TCL script executed" in log[-1] -def test_multi_command_stop(cli, caplog, monkeypatch): - monkeypatch.setattr( - subprocess, "run", lambda: False, raising=ValueError("Command failed") - ) +def test_multi_command_stop(cli, mocker): + m = mocker.patch("subprocess.run", side_effect=RuntimeError("Mocked error")) run_cmd(cli, "run_FABulous_bitstream ./user_design/sequential_16bit_en.v") - log = normalize(caplog.text) - assert "ERROR" in log[-2] + + m.assert_called_once() From 2f6dbd0e541a6407a79aa2b258bc68d1e91d7cf1 Mon Sep 17 00:00:00 2001 From: Kelvin Chung Date: Wed, 11 Jun 2025 17:05:30 +0100 Subject: [PATCH 20/24] Fix other test error --- tests/CLI_test/test_CLI.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/tests/CLI_test/test_CLI.py b/tests/CLI_test/test_CLI.py index 41193047..364cfc55 100644 --- a/tests/CLI_test/test_CLI.py +++ b/tests/CLI_test/test_CLI.py @@ -97,7 +97,9 @@ def test_gen_model_npnr(cli, caplog): def test_run_FABulous_bitstream(cli, caplog, mocker): """Test the run_FABulous_bitstream command""" - m = mocker.patch("subprocess.run", side_effect=RuntimeError("Mocked error")) + class MockCompletedProcess: + returncode = 0 + m = mocker.patch("subprocess.run", return_value=MockCompletedProcess()) run_cmd(cli, "run_FABulous_fabric") Path(cli.projectDir / "user_design" / "sequential_16bit_en.json").touch() Path(cli.projectDir / "user_design" / "sequential_16bit_en.fasm").touch() @@ -109,7 +111,9 @@ def test_run_FABulous_bitstream(cli, caplog, mocker): def test_run_simulation(cli, caplog, mocker): """Test running simulation""" - m = mocker.patch("subprocess.run", side_effect=RuntimeError("Mocked error")) + class MockCompletedProcess: + returncode = 0 + m = mocker.patch("subprocess.run", return_value=MockCompletedProcess()) run_cmd(cli, "run_FABulous_fabric") Path(cli.projectDir / "user_design" / "sequential_16bit_en.json").touch() Path(cli.projectDir / "user_design" / "sequential_16bit_en.fasm").touch() @@ -138,4 +142,4 @@ def test_multi_command_stop(cli, mocker): m = mocker.patch("subprocess.run", side_effect=RuntimeError("Mocked error")) run_cmd(cli, "run_FABulous_bitstream ./user_design/sequential_16bit_en.v") - m.assert_called_once() + m.assert_called_once() \ No newline at end of file From 73d0b80b2522cde20e1c6d0d25dfe933915a7a3d Mon Sep 17 00:00:00 2001 From: Kelvin Chung Date: Thu, 12 Jun 2025 10:39:50 +0100 Subject: [PATCH 21/24] Update test to capture failure --- tests/CLI_test/test_arguments.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/CLI_test/test_arguments.py b/tests/CLI_test/test_arguments.py index e27e69e3..5b023349 100644 --- a/tests/CLI_test/test_arguments.py +++ b/tests/CLI_test/test_arguments.py @@ -150,7 +150,7 @@ def test_verbose_mode(project): assert result.returncode == 0 -def test_force_flag(project): +def test_force_flag(project, mocker): """Test force flag functionality""" # Run with force flag @@ -172,12 +172,14 @@ def test_force_flag(project): "FABulous", str(project), "--commands", - "load_fabric non_existent; load_fabric", + "load_fabric non_exist; load_fabric non_exist", "--force", ], capture_output=True, text=True, ) + + assert result.stdout.count("non_exist") == 2 assert result.returncode == 1 From d74f561c9bbcbe581996dcc55aa2bc4eb7122347 Mon Sep 17 00:00:00 2001 From: Kelvin Chung Date: Thu, 12 Jun 2025 11:43:17 +0100 Subject: [PATCH 22/24] Fixes with more test --- FABulous/FABulous.py | 12 +++--- FABulous/FABulous_CLI/FABulous_CLI.py | 44 +++++++++++++------- FABulous/custom_exception.py | 6 +++ FABulous/fabric_generator/file_parser.py | 51 ++++++++++++------------ tests/CLI_test/test_CLI.py | 14 ++++++- tests/CLI_test/test_arguments.py | 21 +++++++++- 6 files changed, 101 insertions(+), 47 deletions(-) diff --git a/FABulous/FABulous.py b/FABulous/FABulous.py index cbf84991..1b0f7c63 100644 --- a/FABulous/FABulous.py +++ b/FABulous/FABulous.py @@ -154,7 +154,7 @@ def main(): parser.add_argument( "--force", action="store_true", - help="Force the command to run and ignore any errors", + help="Force the command to run and ignore any errors. This feature does not work for the TCLScript argument", ) parser.add_argument("--debug", action="store_true", help="Enable debug mode") @@ -214,16 +214,17 @@ def main(): commands = commands.split("; ") for c in commands: fab_CLI.onecmd_plus_hooks(c) - if fab_CLI.exit_code: + if fab_CLI.exit_code and not args.force: logger.error( f"Command '{c}' execution failed with exit code {fab_CLI.exit_code}" ) - exit(1) + exit(fab_CLI.exit_code) else: logger.info( f'Commands "{"; ".join(i.strip() for i in commands)}" executed successfully' ) - exit(0) + exit(fab_CLI.exit_code) + elif fabScript != cwd: fab_CLI.onecmd_plus_hooks(f"run_script {fabScript}") if fab_CLI.exit_code: @@ -232,8 +233,8 @@ def main(): ) else: logger.info(f"FABulous script {args.FABulousScript} executed successfully") - exit(fab_CLI.exit_code) + elif tclScript != cwd: fab_CLI.onecmd_plus_hooks(f"run_tcl {tclScript}") if fab_CLI.exit_code: @@ -243,6 +244,7 @@ def main(): else: logger.info(f"TCL script {args.TCLScript} executed successfully") exit(fab_CLI.exit_code) + else: fab_CLI.interactive = True if args.verbose == 2: diff --git a/FABulous/FABulous_CLI/FABulous_CLI.py b/FABulous/FABulous_CLI/FABulous_CLI.py index 1ffc9759..f770ef26 100644 --- a/FABulous/FABulous_CLI/FABulous_CLI.py +++ b/FABulous/FABulous_CLI/FABulous_CLI.py @@ -636,37 +636,47 @@ def do_run_FABulous_fabric(self, *ignored): Does this by calling the respective functions 'do_gen_[function]'. """ logger.info("Running FABulous") + self.onecmd_plus_hooks("gen_fabric") if self.exit_code != 0: logger.opt(exception=CommandError()).error( "Fabric generation failed. Please check the logs for more details." ) - return + if not self.force: + return self.onecmd_plus_hooks("gen_bitStream_spec") if self.exit_code != 0: logger.opt(exception=CommandError()).error( "Bitstream specification generation failed. Please check the logs for more details." ) - return + if not self.force: + return + self.onecmd_plus_hooks("gen_top_wrapper") if self.exit_code != 0: logger.opt(exception=CommandError()).error( "Top wrapper generation failed. Please check the logs for more details." ) - return + if not self.force: + return + self.onecmd_plus_hooks("gen_model_npnr") if self.exit_code != 0: logger.opt(exception=CommandError()).error( "Nextpnr model generation failed. Please check the logs for more details." ) - return + if not self.force: + return + self.onecmd_plus_hooks("gen_geometry") if self.exit_code != 0: logger.opt(exception=CommandError()).error( "Geometry generation failed. Please check the logs for more details." ) - return + if not self.force: + return + logger.info("FABulous fabric flow complete") @with_category(CMD_FABRIC_FLOW) @@ -768,7 +778,7 @@ def do_place_and_route(self, args): logger.opt(exception=CommandError()).error( "Nextpnr failed. Please check the logs for more details." ) - return + else: logger.opt(exception=FileNotFoundError()).error( f'Cannot find file "{json_file}" in path "./{parent}/", which is generated by running Yosys with Nextpnr backend (e.g. synthesis).' @@ -864,8 +874,7 @@ def do_run_simulation(self, args): bitstreamPath = args.file topModule = bitstreamPath.stem if bitstreamPath.suffix != ".bin": - logger.error("No bitstream file specified.") - return + logger.opt(exception=FileTypeError()).error("No bitstream file specified.") if not bitstreamPath.exists(): logger.opt(exception=FileNotFoundError()).error( @@ -917,7 +926,6 @@ def do_run_simulation(self, args): logger.opt(exception=CommandError()).error( "Simulation failed. Please check the logs for more details." ) - return # bitstream hex file is used for simulation so it'll be created in the test directory bitstreamHexPath = (buildDir.parent / bitstreamPath.stem).with_suffix(".hex") @@ -945,7 +953,6 @@ def do_run_simulation(self, args): logger.opt(exception=CommandError()).error( "Simulation failed. Please check the logs for more details." ) - return logger.info("Simulation finished") @@ -987,20 +994,24 @@ def do_run_FABulous_bitstream(self, args): logger.opt(exception=CommandError()).error( "Synthesis failed. Please check the logs for more details." ) - return + if not self.force: + return self.onecmd_plus_hooks(f"place_and_route {json_file_path}") if self.exit_code != 0: logger.opt(exception=CommandError()).error( "Place and Route failed. Please check the logs for more details." ) - return + if not self.force: + return + self.onecmd_plus_hooks(f"gen_bitStream_binary {fasm_file_path}") if self.exit_code != 0: logger.opt(exception=CommandError()).error( "Bitstream generation failed. Please check the logs for more details." ) - return + if not self.force: + return @with_category(CMD_SCRIPT) @with_argparser(filePathRequireParser) @@ -1013,6 +1024,11 @@ def do_run_tcl(self, args): if not args.file.exists(): logger.opt(exception=FileNotFoundError()).error(f"Cannot find {args.file}") + if self.force: + logger.warning( + "TCL script does not work with force mode, TCL will stop on first error" + ) + logger.info(f"Execute TCL script {args.file}") with open(args.file, "r") as f: @@ -1037,7 +1053,6 @@ def do_run_script(self, args): logger.opt(exception=CommandError()).error( f"Script execution failed at line: {i.strip()}" ) - return logger.info("Script executed") @@ -1046,7 +1061,6 @@ def do_run_script(self, args): def do_gen_user_design_wrapper(self, args): if not self.fabricLoaded: logger.opt(exception=CommandError()).error("Need to load fabric first") - return self.fabulousAPI.generateUserDesignTopWrapper( args.user_design, args.user_design_top_wrapper diff --git a/FABulous/custom_exception.py b/FABulous/custom_exception.py index 592668dc..66ab2be7 100644 --- a/FABulous/custom_exception.py +++ b/FABulous/custom_exception.py @@ -14,3 +14,9 @@ class FileTypeError(Exception): """Exception raised for unsupported file types.""" pass + + +class FabricParsingError(Exception): + """Exception raised for errors in fabric parsing.""" + + pass diff --git a/FABulous/fabric_generator/file_parser.py b/FABulous/fabric_generator/file_parser.py index 8a6b0414..c49d385b 100644 --- a/FABulous/fabric_generator/file_parser.py +++ b/FABulous/fabric_generator/file_parser.py @@ -1,27 +1,29 @@ import csv +import json import os import re import subprocess -import json -from loguru import logger from copy import deepcopy - -from typing import Literal, overload from pathlib import Path -from FABulous.fabric_generator.utilities import expandListPorts +from typing import Literal, overload + +from loguru import logger + +from FABulous.custom_exception import FabricParsingError from FABulous.fabric_definition.Bel import Bel -from FABulous.fabric_definition.Port import Port -from FABulous.fabric_definition.Tile import Tile -from FABulous.fabric_definition.SuperTile import SuperTile -from FABulous.fabric_definition.Fabric import Fabric from FABulous.fabric_definition.ConfigMem import ConfigMem from FABulous.fabric_definition.define import ( IO, - Direction, - Side, ConfigBitMode, + Direction, MultiplexerStyle, + Side, ) +from FABulous.fabric_definition.Fabric import Fabric +from FABulous.fabric_definition.Port import Port +from FABulous.fabric_definition.SuperTile import SuperTile +from FABulous.fabric_definition.Tile import Tile +from FABulous.fabric_generator.utilities import expandListPorts oppositeDic = {"NORTH": "SOUTH", "SOUTH": "NORTH", "EAST": "WEST", "WEST": "EAST"} @@ -136,9 +138,6 @@ def parseFabricCSV(fileName: str) -> Fabric: if i[0].startswith("Tile"): if "GENERATE" in i: # import here to avoid circular import - from FABulous.fabric_generator.fabric_automation import ( - generateCustomTileConfig, - ) # we generate the tile right before we parse everything i[1] = str(generate_custom_tile_config(filePath.joinpath(i[1]))) @@ -1024,14 +1023,14 @@ def parseBelFile( json_file = filename.with_suffix(".json") runCmd = [ "yosys", - "-qp" - f"read_verilog -sv {filename}; proc -noopt; write_json -compat-int {json_file}", + f"-qpread_verilog -sv {filename}; proc -noopt; write_json -compat-int {json_file}", ] - try: - subprocess.run(runCmd, check=True) - except subprocess.CalledProcessError as e: - logger.error(f"Failed to run yosys command: {e}") - raise ValueError + + result = subprocess.run(runCmd, check=True) + if result.returncode != 0: + logger.opt(exception=FabricParsingError()).error( + "Failed to run yosys command: {e}" + ) with open(f"{json_file}", "r") as f: data_dict = json.load(f) @@ -1040,11 +1039,13 @@ def parseBelFile( filtered_ports: dict[str, tuple[IO, list]] = {} if len(modules) == 0: - logger.error(f"File {filename} does not contain any modules.") - raise ValueError + logger.opt(exception=FabricParsingError()).error( + f"File {filename} does not contain any modules." + ) elif len(modules) > 1: - logger.error(f"File {filename} contains more than one module.") - raise ValueError + logger.opt(exception=FabricParsingError()).error( + f"File {filename} contains more than one module." + ) # Gathers port name and direction, filters out configbits as they show in ports. for module_name, module_info in modules.items(): diff --git a/tests/CLI_test/test_CLI.py b/tests/CLI_test/test_CLI.py index 364cfc55..278a9e2c 100644 --- a/tests/CLI_test/test_CLI.py +++ b/tests/CLI_test/test_CLI.py @@ -97,8 +97,10 @@ def test_gen_model_npnr(cli, caplog): def test_run_FABulous_bitstream(cli, caplog, mocker): """Test the run_FABulous_bitstream command""" + class MockCompletedProcess: returncode = 0 + m = mocker.patch("subprocess.run", return_value=MockCompletedProcess()) run_cmd(cli, "run_FABulous_fabric") Path(cli.projectDir / "user_design" / "sequential_16bit_en.json").touch() @@ -111,8 +113,10 @@ class MockCompletedProcess: def test_run_simulation(cli, caplog, mocker): """Test running simulation""" + class MockCompletedProcess: returncode = 0 + m = mocker.patch("subprocess.run", return_value=MockCompletedProcess()) run_cmd(cli, "run_FABulous_fabric") Path(cli.projectDir / "user_design" / "sequential_16bit_en.json").touch() @@ -142,4 +146,12 @@ def test_multi_command_stop(cli, mocker): m = mocker.patch("subprocess.run", side_effect=RuntimeError("Mocked error")) run_cmd(cli, "run_FABulous_bitstream ./user_design/sequential_16bit_en.v") - m.assert_called_once() \ No newline at end of file + m.assert_called_once() + + +def test_multi_command_force(cli, mocker): + m = mocker.patch("subprocess.run", side_effect=RuntimeError("Mocked error")) + cli.force = True + run_cmd(cli, "run_FABulous_bitstream ./user_design/sequential_16bit_en.v") + + assert m.call_count == 2 diff --git a/tests/CLI_test/test_arguments.py b/tests/CLI_test/test_arguments.py index 5b023349..40b6d327 100644 --- a/tests/CLI_test/test_arguments.py +++ b/tests/CLI_test/test_arguments.py @@ -150,7 +150,7 @@ def test_verbose_mode(project): assert result.returncode == 0 -def test_force_flag(project, mocker): +def test_force_flag(project, tmp_path): """Test force flag functionality""" # Run with force flag @@ -182,6 +182,25 @@ def test_force_flag(project, mocker): assert result.stdout.count("non_exist") == 2 assert result.returncode == 1 + with open(tmp_path / "test.fs", "w") as f: + f.write("load_fabric non_exist.csv\n") + f.write("load_fabric non_exist.csv\n") + + result = run( + [ + "FABulous", + str(project), + "--FABulousScript", + str(tmp_path / "test.fs"), + "--force", + ], + capture_output=True, + text=True, + ) + + assert result.stdout.count("INFO: Loading fabric") == 3 + assert result.returncode == 1 + def test_debug_mode(project): """Test debug mode functionality""" From 267e40f1427588818f8357ea463f616d47fbe396 Mon Sep 17 00:00:00 2001 From: Kelvin Chung Date: Thu, 12 Jun 2025 11:50:31 +0100 Subject: [PATCH 23/24] One extra test --- tests/CLI_test/test_arguments.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/CLI_test/test_arguments.py b/tests/CLI_test/test_arguments.py index 40b6d327..d10575c0 100644 --- a/tests/CLI_test/test_arguments.py +++ b/tests/CLI_test/test_arguments.py @@ -407,3 +407,21 @@ def test_project_directory_priority_order(tmp_path, monkeypatch, mocker): f"INFO: Setting current working directory to: {str(default_dir)}" in result.stdout ) + + +def test_command_flag_with_stop_on_first_error(project): + """Test that using --commands with multiple commands raises an error on the first failure""" + # Run with multiple commands, where the first one fails + result = run( + [ + "FABulous", + str(project), + "--commands", + "load_fabric non_exist; load_fabric non_exist", + ], + capture_output=True, + text=True, + ) + + assert result.stdout.count("non_exist") == 1 + assert result.returncode == 1 From 797d5091307dbb4885c869d19b61d474efe8d3f9 Mon Sep 17 00:00:00 2001 From: Kelvin Chung Date: Fri, 13 Jun 2025 15:57:34 +0100 Subject: [PATCH 24/24] Minor fix --- FABulous/fabric_generator/file_parser.py | 3 +++ docs/source/Building fabric.rst | 6 +++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/FABulous/fabric_generator/file_parser.py b/FABulous/fabric_generator/file_parser.py index c49d385b..2df34ba9 100644 --- a/FABulous/fabric_generator/file_parser.py +++ b/FABulous/fabric_generator/file_parser.py @@ -138,6 +138,9 @@ def parseFabricCSV(fileName: str) -> Fabric: if i[0].startswith("Tile"): if "GENERATE" in i: # import here to avoid circular import + from FABulous.fabric_generator.fabric_automation import ( + generate_custom_tile_config, + ) # we generate the tile right before we parse everything i[1] = str(generate_custom_tile_config(filePath.joinpath(i[1]))) diff --git a/docs/source/Building fabric.rst b/docs/source/Building fabric.rst index 0892610a..2e805523 100644 --- a/docs/source/Building fabric.rst +++ b/docs/source/Building fabric.rst @@ -16,15 +16,15 @@ completed. .. prompt:: bash - FABulous -c demo + (venv)$ FABulous -c demo This will create a new project named ``demo`` in the current directory. #. Running the FABulous shell - .. prompt:: bash FABulous> + .. prompt:: bash - FABulous demo + (venv)$ FABulous demo And now, we will be in the FABulous shell. After running the above command, the current working directory will be moved into the project directory, which is ``demo`` in this case.