Skip to content

Commit 9597e89

Browse files
CLI testing (#366)
* CLI testing Create a basic pytest CI for the CLI. Update pre-commit to use python3.12 Some CLI logging for consistency
1 parent 968474b commit 9597e89

File tree

10 files changed

+358
-4
lines changed

10 files changed

+358
-4
lines changed

.github/workflows/CIL_testing.yml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
name: Test CLI with pytest
2+
3+
on: [push, pull_request]
4+
5+
6+
jobs:
7+
run_pytest_CLI:
8+
runs-on: ubuntu-latest
9+
steps:
10+
- uses: actions/checkout@v4
11+
- uses: ./.github/actions/prepare_FABulous_container
12+
- name: Run pytest for FABulous CLI
13+
run: pytest -v

FABulous/FABulous_CLI/FABulous_CLI.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -404,7 +404,7 @@ def do_gen_config_mem(self, args):
404404
self.fabulousAPI.genConfigMem(
405405
i, self.projectDir / f"Tile/{i}/{i}_ConfigMem.csv"
406406
)
407-
logger.info("Generating configMem complete")
407+
logger.info("ConfigMem generation complete")
408408

409409
@with_category(CMD_FABRIC_FLOW)
410410
@with_argparser(tile_list_parser)
@@ -501,7 +501,7 @@ def do_gen_all_tile(self, *ignored):
501501
"""Generates all tiles by calling 'do_gen_tile'."""
502502
logger.info("Generating all tiles")
503503
self.do_gen_tile(" ".join(self.allTile))
504-
logger.info("Generated all tiles")
504+
logger.info("All tiles generation complete")
505505

506506
@with_category(CMD_FABRIC_FLOW)
507507
def do_gen_fabric(self, *ignored):
@@ -609,7 +609,7 @@ def do_gen_bitStream_spec(self, *ignored):
609609
w.writerow([key1])
610610
for key2, val in specObject["TileSpecs"][key1].items():
611611
w.writerow([key2, val])
612-
logger.info("Generated bitstream specification")
612+
logger.info("Bitstream specification generation complete")
613613

614614
@with_category(CMD_FABRIC_FLOW)
615615
def do_gen_top_wrapper(self, *ignored):
@@ -619,7 +619,7 @@ def do_gen_top_wrapper(self, *ignored):
619619
f"{self.projectDir}/Fabric/{self.fabulousAPI.fabric.name}_top.{self.extension}"
620620
)
621621
self.fabulousAPI.genTopWrapper()
622-
logger.info("Generated top wrapper")
622+
logger.info("Top wrapper generation complete")
623623

624624
@with_category(CMD_FABRIC_FLOW)
625625
@allow_blank

FABulous/FABulous_CLI/cmd_synthesis.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,8 @@ def do_synthesis(self, args):
241241
p: Path
242242
paths: list[Path] = []
243243
for p in args.files:
244+
if not p.is_absolute():
245+
p = self.projectDir / p
244246
resolvePath: Path = p.absolute()
245247
if resolvePath.exists():
246248
paths.append(resolvePath)

pytest.ini

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[pytest]
2+
testpaths = tests

requirements.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,5 @@ loguru>=0.7.0
33
fasm>=0.0.2
44
cmd2>=2,<3
55
requests>=2.0.0
6+
pytest>=8.3.5
7+
pytest-mock

tests/CLI_test/conftest.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import os
2+
from pathlib import Path
3+
4+
import pytest
5+
from _pytest.logging import LogCaptureFixture
6+
from loguru import logger
7+
8+
from FABulous.FABulous_CLI.FABulous_CLI import FABulous_CLI
9+
from FABulous.FABulous_CLI.helper import create_project, setup_logger
10+
11+
12+
def normalize(block: str):
13+
"""Normalize a block of text to perform comparison.
14+
15+
Strip newlines from the very beginning and very end, then split into separate lines and strip trailing whitespace
16+
from each line.
17+
"""
18+
assert isinstance(block, str)
19+
block = block.strip("\n")
20+
return [line.rstrip() for line in block.splitlines()]
21+
22+
23+
def run_cmd(app, cmd):
24+
"""Clear stdout, stdin and stderr buffers, run the command, and return stdout and stderr"""
25+
app.onecmd_plus_hooks(cmd)
26+
27+
28+
def normalize_and_check_for_errors(caplog_text: str):
29+
"""Normalize a block of text and check for errors."""
30+
log = normalize(caplog_text)
31+
assert not any("ERROR" in line for line in log), "Error found in log messages"
32+
return log
33+
34+
35+
TILE = "LUT4AB"
36+
37+
os.environ["FAB_ROOT"] = str(Path(__file__).resolve().parent.parent.parent / "FABulous")
38+
39+
40+
@pytest.fixture
41+
def cli(tmp_path):
42+
projectDir = tmp_path / "test_project"
43+
fabulousRoot = str(Path(__file__).resolve().parent.parent.parent / "FABulous")
44+
os.environ["FAB_ROOT"] = fabulousRoot
45+
os.environ["FAB_PROJ_DIR"] = str(projectDir)
46+
create_project(projectDir)
47+
setup_logger(0)
48+
cli = FABulous_CLI(
49+
writerType="verilog", projectDir=projectDir, enteringDir=tmp_path
50+
)
51+
cli.debug = True
52+
run_cmd(cli, "load_fabric")
53+
return cli
54+
55+
56+
@pytest.fixture
57+
def caplog(caplog: LogCaptureFixture):
58+
handler_id = logger.add(
59+
caplog.handler,
60+
format="{message}",
61+
level=0,
62+
filter=lambda record: record["level"].no >= caplog.handler.level,
63+
enqueue=False, # Set to 'True' if your test is spawning child processes.
64+
)
65+
yield caplog
66+
logger.remove(handler_id)

tests/CLI_test/test_CLI.py

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
from pathlib import Path
2+
import pytest
3+
from tests.CLI_test.conftest import (
4+
TILE,
5+
normalize_and_check_for_errors,
6+
run_cmd,
7+
)
8+
9+
10+
def test_load_fabric(cli, caplog):
11+
"""Test loading fabric from CSV file"""
12+
run_cmd(cli, "load_fabric")
13+
log = normalize_and_check_for_errors(caplog.text)
14+
assert "Loading fabric" in log[0]
15+
assert "Complete" in log[-1]
16+
17+
18+
def test_gen_config_mem(cli, caplog):
19+
"""Test generating configuration memory"""
20+
run_cmd(cli, f"gen_config_mem {TILE}")
21+
log = normalize_and_check_for_errors(caplog.text)
22+
assert f"Generating Config Memory for {TILE}" in log[0]
23+
assert "ConfigMem generation complete" in log[-1]
24+
25+
26+
def test_gen_switch_matrix(cli, caplog):
27+
"""Test generating switch matrix"""
28+
run_cmd(cli, f"gen_switch_matrix {TILE}")
29+
log = normalize_and_check_for_errors(caplog.text)
30+
assert f"Generating switch matrix for {TILE}" in log[0]
31+
assert "Switch matrix generation complete" in log[-1]
32+
33+
34+
def test_gen_tile(cli, caplog):
35+
"""Test generating tile"""
36+
run_cmd(cli, f"gen_tile {TILE}")
37+
log = normalize_and_check_for_errors(caplog.text)
38+
assert f"Generating tile {TILE}" in log[0]
39+
assert "Tile generation complete" in log[-1]
40+
41+
42+
def test_gen_all_tile(cli, caplog):
43+
"""Test generating all tiles"""
44+
run_cmd(cli, "gen_all_tile")
45+
log = normalize_and_check_for_errors(caplog.text)
46+
assert "Generating all tiles" in log[0]
47+
assert "All tiles generation complete" in log[-1]
48+
49+
50+
def test_gen_fabric(cli, caplog):
51+
"""Test generating fabric"""
52+
run_cmd(cli, "gen_fabric")
53+
log = normalize_and_check_for_errors(caplog.text)
54+
assert "Generating fabric " in log[0]
55+
assert "Fabric generation complete" in log[-1]
56+
57+
58+
def test_gen_geometry(cli, caplog):
59+
"""Test generating geometry"""
60+
61+
# Test with default padding
62+
run_cmd(cli, "gen_geometry")
63+
log = normalize_and_check_for_errors(caplog.text)
64+
assert "Generating geometry" in log[0]
65+
assert "geometry generation complete" in log[-2].lower()
66+
67+
# Test with custom padding
68+
run_cmd(cli, "gen_geometry 16")
69+
log = normalize_and_check_for_errors(caplog.text)
70+
assert "Generating geometry" in log[0]
71+
assert "can now be imported into fabulator" in log[-1].lower()
72+
73+
74+
def test_gen_top_wrapper(cli, caplog):
75+
"""Test generating top wrapper"""
76+
run_cmd(cli, "gen_top_wrapper")
77+
log = normalize_and_check_for_errors(caplog.text)
78+
assert "Generating top wrapper" in log[0]
79+
assert "Top wrapper generation complete" in log[-1]
80+
81+
82+
def test_run_FABulous_fabric(cli, caplog):
83+
"""Test running FABulous fabric flow"""
84+
run_cmd(cli, "run_FABulous_fabric")
85+
log = normalize_and_check_for_errors(caplog.text)
86+
assert "Running FABulous" in log[0]
87+
assert "FABulous fabric flow complete" in log[-1]
88+
89+
90+
def test_gen_model_npnr(cli, caplog):
91+
"""Test generating Nextpnr model"""
92+
run_cmd(cli, "gen_model_npnr")
93+
log = normalize_and_check_for_errors(caplog.text)
94+
assert "Generating npnr model" in log[0]
95+
assert "Generated npnr model" in log[-1]
96+
97+
98+
def test_run_FABulous_bitstream(cli, caplog, mocker):
99+
"""Test the run_FABulous_bitstream command"""
100+
m = mocker.patch("subprocess.run", return_value=None)
101+
run_cmd(cli, "run_FABulous_fabric")
102+
Path(cli.projectDir / "user_design" / "sequential_16bit_en.json").touch()
103+
Path(cli.projectDir / "user_design" / "sequential_16bit_en.fasm").touch()
104+
run_cmd(cli, "run_FABulous_bitstream ./user_design/sequential_16bit_en.v")
105+
log = normalize_and_check_for_errors(caplog.text)
106+
assert "Bitstream generated" in log[-1]
107+
assert m.call_count == 3
108+
109+
110+
def test_run_simulation(cli, caplog, mocker):
111+
"""Test running simulation"""
112+
m = mocker.patch("subprocess.run", return_value=None)
113+
run_cmd(cli, "run_FABulous_fabric")
114+
Path(cli.projectDir / "user_design" / "sequential_16bit_en.json").touch()
115+
Path(cli.projectDir / "user_design" / "sequential_16bit_en.fasm").touch()
116+
Path(cli.projectDir / "user_design" / "sequential_16bit_en.bin").touch()
117+
run_cmd(cli, "run_FABulous_bitstream ./user_design/sequential_16bit_en.v")
118+
run_cmd(cli, "run_simulation fst ./user_design/sequential_16bit_en.bin")
119+
log = normalize_and_check_for_errors(caplog.text)
120+
assert "Simulation finished" in log[-1]
121+
assert m.call_count == 5
122+
123+
124+
def test_run_tcl(cli, caplog, tmp_path):
125+
"""Test running a Tcl script"""
126+
script_content = '# Dummy Tcl script\nputs "Text from tcl"'
127+
tcl_script_path = tmp_path / "test_script.tcl"
128+
with open(tcl_script_path, "w") as f:
129+
f.write(script_content)
130+
131+
run_cmd(cli, f"run_tcl {str(tcl_script_path)}")
132+
log = normalize_and_check_for_errors(caplog.text)
133+
assert f"Execute TCL script {str(tcl_script_path)}" in log[0]
134+
assert "TCL script executed" in log[-1]

tests/CLI_test/test_helper.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import pytest
2+
3+
from FABulous.FABulous_CLI.helper import create_project
4+
5+
6+
def test_create_project(tmp_path):
7+
# Test Verilog project creation
8+
project_dir = tmp_path / "test_project_verilog"
9+
create_project(project_dir)
10+
11+
# Check if directories exist
12+
assert project_dir.exists()
13+
assert (project_dir / ".FABulous").exists()
14+
15+
# Check if .env file exists and contains correct content
16+
env_file = project_dir / ".FABulous" / ".env"
17+
assert env_file.exists()
18+
assert env_file.read_text() == "FAB_PROJ_LANG=verilog\n"
19+
20+
# Check if template files were copied
21+
assert any(
22+
project_dir.glob("**/*.v")
23+
), "No Verilog files found in project directory"
24+
25+
26+
def test_create_project_vhdl(tmp_path):
27+
# Test VHDL project creation
28+
project_dir = tmp_path / "test_project_vhdl"
29+
create_project(project_dir, lang="vhdl")
30+
31+
# Check if directories exist
32+
assert project_dir.exists()
33+
assert (project_dir / ".FABulous").exists()
34+
35+
# Check if .env file exists and contains correct content
36+
env_file = project_dir / ".FABulous" / ".env"
37+
assert env_file.exists()
38+
assert env_file.read_text() == "FAB_PROJ_LANG=vhdl\n"
39+
40+
# Check if template files were copied
41+
assert any(
42+
project_dir.glob("**/*.vhdl")
43+
), "No VHDL files found in project directory"
44+
45+
46+
def test_create_project_existing_dir(tmp_path):
47+
# Test creating project in existing directory
48+
project_dir = tmp_path / "existing_dir"
49+
project_dir.mkdir()
50+
51+
with pytest.raises(SystemExit):
52+
create_project(project_dir)

tests/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)