diff --git a/FABulous/FABulous.py b/FABulous/FABulous.py index 3d7dbfe9..1b0f7c63 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 @@ -8,10 +7,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, ) @@ -50,12 +49,9 @@ def main(): description="The command line interface for FABulous" ) - 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, @@ -63,7 +59,28 @@ 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", + default="", + nargs="?", + help="The directory to the project folder", + ) + + script_group.add_argument( "-fs", "--FABulousScript", default="", @@ -71,7 +88,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,9 +98,16 @@ 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, + default="", + type=Path, nargs="?", const="FABulous.log", help="Log all the output from the terminal", @@ -127,39 +152,42 @@ def main(): ) 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. ", + "--force", action="store_true", - default=False, + 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") args = parser.parse_args() - setup_logger(args.verbose) + 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) - projectDir = Path(os.getenv("FAB_PROJ_DIR", args.project_dir)).absolute() + # 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() - 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." - ) - exit(1) + # Finally, user provided argument takes highest priority + if args.project_dir: + projectDir = Path(args.project_dir).absolute().resolve() if args.createProject: create_project(projectDir, args.writer) 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) + if not projectDir.exists(): logger.error(f"The directory provided does not exist: {projectDir}") exit(1) @@ -168,40 +196,62 @@ def main(): 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(), - FABulousScript=args.FABulousScript, - TCLScript=args.TCLScript, - ) - fab_CLI.debug = args.debug + 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 and not args.force: + logger.error( + f"Command '{c}' execution failed with exit code {fab_CLI.exit_code}" + ) + exit(fab_CLI.exit_code) + else: + logger.info( + f'Commands "{"; ".join(i.strip() for i in commands)}" executed successfully' + ) + exit(fab_CLI.exit_code) + + 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 - 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() - else: - logger.info(f"Setting current working directory to: {projectDir}") - os.chdir(projectDir) - fab_CLI.cmdloop() + + 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 702a2044..f770ef26 100644 --- a/FABulous/FABulous_CLI/FABulous_CLI.py +++ b/FABulous/FABulous_CLI/FABulous_CLI.py @@ -22,33 +22,36 @@ import subprocess as sp import sys import tkinter as tk +import traceback from pathlib import Path from cmd2 import ( Cmd, Cmd2ArgumentParser, Settable, + Statement, categorize, with_argparser, with_category, ) 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" @@ -89,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 @@ -107,14 +109,16 @@ class FABulous_CLI(Cmd): csvFile: Path extension: str = "v" script: str = "" + force: bool = False + interactive: bool = True def __init__( self, writerType: str | None, projectDir: Path, enteringDir: Path, - FABulousScript: Path = Path(), - TCLScript: Path = Path(), + force: bool = False, + interactive: bool = False, ): """Initialises the FABulous shell instance. @@ -133,7 +137,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 @@ -164,6 +167,11 @@ 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)) + + self.interactive = interactive + if isinstance(self.fabulousAPI.writer, VHDLWriter): self.extension = "vhdl" else: @@ -180,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) @@ -206,21 +212,19 @@ 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, 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) + except Exception: + logger.debug(traceback.format_exc()) + self.exit_code = 1 + if self.interactive: + return False + else: + return not self.force def do_exit(self, *ignored): """Exits the FABulous shell and logs info message.""" @@ -332,10 +336,9 @@ def do_load_fabric(self, args): ) self.fabulousAPI.loadFabric(self.csvFile) else: - logger.error( - "No argument is given and the csv file is set or the file does not exist" + logger.opt(exception=FileExistsError()).error( + "No argument is given and the csv file is set but the file does not exist" ) - return else: self.fabulousAPI.loadFabric(args.file) self.csvFile = args.file @@ -357,19 +360,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) @@ -377,7 +378,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): @@ -385,7 +386,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) @@ -511,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}" ) @@ -566,7 +572,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 @@ -583,8 +589,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): @@ -622,7 +628,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 @@ -631,13 +636,48 @@ 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") + if self.exit_code != 0: + logger.opt(exception=CommandError()).error( + "Fabric generation failed. Please check the logs for more details." + ) + 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." + ) + 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." + ) + 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." + ) + 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." + ) + if not self.force: + return + logger.info("FABulous fabric flow complete") - return @with_category(CMD_FABRIC_FLOW) def do_gen_model_npnr(self, *ignored): @@ -688,13 +728,12 @@ def do_place_and_route(self, args): 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" @@ -705,10 +744,9 @@ def do_place_and_route(self, args): if not os.path.exists( f"{self.projectDir}/.FABulous/pips.txt" ) or not os.path.exists(f"{self.projectDir}/.FABulous/bel.txt"): - logger.error( + 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 @@ -729,27 +767,28 @@ 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: - logger.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) @@ -767,27 +806,24 @@ 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 """ ) - return 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}") @@ -800,8 +836,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") @@ -838,13 +874,12 @@ 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.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()}" @@ -870,28 +905,27 @@ 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: - logger.error("Simulation failed") - remove_dir(buildDir) - return + result = sp.run(runCmd, check=True) + if result.returncode != 0: + logger.opt(exception=CommandError()).error( + "Simulation failed. Please check the logs for more details." + ) # bitstream hex file is used for simulation so it'll be created in the test directory bitstreamHexPath = (buildDir.parent / bitstreamPath.stem).with_suffix(".hex") @@ -908,17 +942,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) - except sp.CalledProcessError: - logger.error("Simulation failed") - return + 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." + ) + logger.info("Simulation finished") @with_category(CMD_USER_DESIGN_FLOW) @@ -936,13 +971,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") @@ -955,9 +989,29 @@ 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}") + if self.exit_code != 0: + logger.opt(exception=CommandError()).error( + "Synthesis failed. Please check the logs for more details." + ) + 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." + ) + 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." + ) + if not self.force: + return @with_category(CMD_SCRIPT) @with_argparser(filePathRequireParser) @@ -968,8 +1022,12 @@ 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}") + + 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}") @@ -979,16 +1037,30 @@ def do_run_tcl(self, args): logger.info("TCL script executed") - if "exit" in script: - return True + @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()) + if self.exit_code != 0: + logger.opt(exception=CommandError()).error( + f"Script execution failed at line: {i.strip()}" + ) + + logger.info("Script executed") @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") - return + logger.opt(exception=CommandError()).error("Need to load fabric first") self.fabulousAPI.generateUserDesignTopWrapper( args.user_design, args.user_design_top_wrapper 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 75f8848c..2d848fee 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,29 +9,58 @@ from pathlib import Path from typing import Literal +import requests from dotenv import load_dotenv from loguru import logger MAX_BITBYTES = 16384 -def setup_logger(verbosity: int): +def setup_logger(verbosity: int, debug: bool, log_file: Path = Path()): # 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}" + # 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 "" ) - else: - log_format = "{level:} | {message}" - # Add logger to write logs to stdout - logger.add(sys.stdout, format=log_format, level="DEBUG", colorize=True) + if verbosity >= 1: + final_log = f"{level}{time}{name}:{func}:{line} - {exc}{msg}\n" + 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 + log_level_to_set = "DEBUG" if debug else "INFO" + + # 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, catch=False + ) + else: + logger.add( + sys.stdout, + format=custom_format_function, + level=log_level_to_set, + colorize=True, + catch=False, + ) def setup_global_env_vars(args: argparse.Namespace) -> None: @@ -158,9 +186,10 @@ 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() + sys.exit(1) else: project_dir.mkdir(parents=True, exist_ok=True) (project_dir / ".FABulous").mkdir(parents=True, exist_ok=True) @@ -168,7 +197,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" @@ -281,11 +314,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): @@ -314,7 +347,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 @@ -413,7 +446,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 @@ -441,7 +474,15 @@ def install_oss_cad_suite(destination_folder: Path, update: bool = False): 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}") diff --git a/FABulous/custom_exception.py b/FABulous/custom_exception.py new file mode 100644 index 00000000..66ab2be7 --- /dev/null +++ b/FABulous/custom_exception.py @@ -0,0 +1,22 @@ +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 + + +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..2df34ba9 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"} @@ -137,7 +139,7 @@ def parseFabricCSV(fileName: str) -> Fabric: if "GENERATE" in i: # import here to avoid circular import from FABulous.fabric_generator.fabric_automation import ( - generateCustomTileConfig, + generate_custom_tile_config, ) # we generate the tile right before we parse everything @@ -1024,14 +1026,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 +1042,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/docs/source/Building fabric.rst b/docs/source/Building fabric.rst index 2b22ec38..2e805523 100644 --- a/docs/source/Building fabric.rst +++ b/docs/source/Building fabric.rst @@ -14,17 +14,17 @@ completed. #. Create a new project - .. prompt:: bash FABulous> + .. 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. 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: diff --git a/tests/CLI_test/test_CLI.py b/tests/CLI_test/test_CLI.py index 6c3423d3..278a9e2c 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, @@ -97,7 +97,11 @@ 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) + + 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 +113,11 @@ 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) + + 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() @@ -132,3 +140,18 @@ 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, 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() + + +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 new file mode 100644 index 00000000..d10575c0 --- /dev/null +++ b/tests/CLI_test/test_arguments.py @@ -0,0 +1,427 @@ +from subprocess import run +import os + + +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(): + result = run(["FABulous", "--createProject"], 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), "--FABulousScript", str(script_file)], + capture_output=True, + text=True, + ) + assert result.returncode == 0 + + +def test_fabulous_script_nonexistent_file(tmp_path, project): + nonexistent_script = tmp_path / "nonexistent_script.fab" + + result = run( + ["FABulous", str(project), "--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, project): + """Test TCL script execution on a valid project""" + + # 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), "--TCLScript", str(tcl_script)], + capture_output=True, + text=True, + ) + assert result.returncode == 0 + + +def test_commands_execution(tmp_path, project): + """Test direct command execution with -p/--commands""" + # Run commands directly + result = run( + ["FABulous", str(project), "--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, project): + """Test log file creation and output""" + log_file = tmp_path / "test.log" + + # Run with logging using commands instead of script to avoid file handling issues + result = run( + ["FABulous", str(project), "--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(project): + """Test verbose mode execution""" + + # Run with verbose mode + result = run( + ["FABulous", str(project), "--commands", "help", "-v"], + capture_output=True, + text=True, + ) + assert result.returncode == 0 + + +def test_force_flag(project, tmp_path): + """Test force flag functionality""" + + # Run with force flag + result = run( + [ + "FABulous", + str(project), + "--commands", + "load_fabric non_existent", + "--force", + ], + capture_output=True, + text=True, + ) + assert result.returncode == 1 + + result = run( + [ + "FABulous", + str(project), + "--commands", + "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 + + 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""" + + # Run with debug mode + result = run( + ["FABulous", str(project), "--commands", "help", "--debug"], + capture_output=True, + text=True, + ) + assert result.returncode == 0 + + +def test_install_oss_cad_suite(project, mocker): + """Test oss-cad-suite installation""" + + # Test installation (may fail if network unavailable, but should handle gracefully) + 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 + result = run( + ["FABulous", str(project), "--install_oss_cad_suite"], + capture_output=True, + text=True, + ) + assert result.returncode == 0 + + +def test_script_mutually_exclusive(tmp_path, project): + """Test that FABulous script and TCL script are mutually exclusive""" + + # 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), + "--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 + + +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(project): + """Test error handling for nonexistent script files""" + + # Try to run nonexistent FABulous script - FABulous handles this gracefully + result = run( + ["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 == 1 + + # Try to run nonexistent TCL script + result = run( + ["FABulous", str(project), "--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(project): + """Test handling of empty command string""" + # 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" + + result = run( + ["FABulous", "--createProject", str(project_dir), "--writer", "invalid_writer"], + capture_output=True, + text=True, + ) + 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" + + result = run( + ["FABulous", "--createProject", str(project_dir), "--install_oss_cad_suite"], + capture_output=True, + 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 + ) + + +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 diff --git a/tests/CLI_test/conftest.py b/tests/conftest.py similarity index 74% rename from tests/CLI_test/conftest.py rename to tests/conftest.py index aa6eb6cd..29dd2916 100644 --- a/tests/CLI_test/conftest.py +++ b/tests/conftest.py @@ -34,23 +34,40 @@ 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 + os.environ["FABULOUS_TESTING"] = "TRUE" + yield + os.environ.pop("FAB_ROOT", None) + os.environ.pop("FABULOUS_TESTING", 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) + setup_logger(0, False) cli = FABulous_CLI( writerType="verilog", projectDir=projectDir, enteringDir=tmp_path ) cli.debug = True run_cmd(cli, "load_fabric") - return cli + 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) + 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