mirror of
https://github.com/TinyTapeout/tt-support-tools.git
synced 2026-11-03 04:36:09 +01:00
Previously, it only printed the first error it encountered, causing more iterations when fixing info.yaml and info.md errors
618 lines
23 KiB
Python
618 lines
23 KiB
Python
import pytest
|
|
import yaml
|
|
|
|
from project_info import YAML_VERSION, ProjectInfo, ProjectYamlError
|
|
|
|
|
|
@pytest.fixture
|
|
def valid_yaml_data():
|
|
"""Base valid YAML data for testing"""
|
|
return {
|
|
"yaml_version": YAML_VERSION,
|
|
"project": {
|
|
"title": "Test Project",
|
|
"author": "Test Author",
|
|
"description": "Test Description",
|
|
"tiles": "1x1",
|
|
"analog_pins": 0,
|
|
"uses_3v3": False,
|
|
"language": "Verilog",
|
|
"top_module": "tt_um_test_project",
|
|
"source_files": ["test.v"],
|
|
"clock_hz": 10000000,
|
|
},
|
|
"pinout": {
|
|
"ui[0]": "input_0",
|
|
"ui[1]": "input_1",
|
|
"ui[2]": "input_2",
|
|
"ui[3]": "input_3",
|
|
"ui[4]": "input_4",
|
|
"ui[5]": "input_5",
|
|
"ui[6]": "input_6",
|
|
"ui[7]": "input_7",
|
|
"uo[0]": "output_0",
|
|
"uo[1]": "output_1",
|
|
"uo[2]": "output_2",
|
|
"uo[3]": "output_3",
|
|
"uo[4]": "output_4",
|
|
"uo[5]": "output_5",
|
|
"uo[6]": "output_6",
|
|
"uo[7]": "output_7",
|
|
"uio[0]": "bidir_0",
|
|
"uio[1]": "bidir_1",
|
|
"uio[2]": "bidir_2",
|
|
"uio[3]": "bidir_3",
|
|
"uio[4]": "bidir_4",
|
|
"uio[5]": "bidir_5",
|
|
"uio[6]": "bidir_6",
|
|
"uio[7]": "bidir_7",
|
|
},
|
|
}
|
|
|
|
|
|
@pytest.fixture
|
|
def tile_sizes():
|
|
"""Valid tile sizes for testing"""
|
|
return {
|
|
"1x1": "0 0 161 111.52",
|
|
"1x2": "0 0 161 225.76",
|
|
"2x2": "0 0 322 225.76",
|
|
"8x2": "0 0 1292 225.76",
|
|
}
|
|
|
|
|
|
class TestProjectInfoYamlVersion:
|
|
"""Test YAML version validation"""
|
|
|
|
def test_missing_yaml_version(self, valid_yaml_data, tile_sizes):
|
|
del valid_yaml_data["yaml_version"]
|
|
with pytest.raises(ProjectYamlError, match="Missing 'yaml_version'"):
|
|
ProjectInfo(valid_yaml_data, tile_sizes)
|
|
|
|
def test_wrong_yaml_version(self, valid_yaml_data, tile_sizes):
|
|
valid_yaml_data["yaml_version"] = YAML_VERSION - 1
|
|
with pytest.raises(
|
|
ProjectYamlError,
|
|
match=f"Unsupported YAML version: {YAML_VERSION - 1}, expected {YAML_VERSION}",
|
|
):
|
|
ProjectInfo(valid_yaml_data, tile_sizes)
|
|
|
|
def test_valid_yaml_version(self, valid_yaml_data, tile_sizes):
|
|
project_info = ProjectInfo(valid_yaml_data, tile_sizes)
|
|
assert project_info is not None
|
|
|
|
|
|
class TestProjectInfoProjectSection:
|
|
"""Test project section validation"""
|
|
|
|
def test_missing_project_section(self, valid_yaml_data, tile_sizes):
|
|
del valid_yaml_data["project"]
|
|
with pytest.raises(ProjectYamlError, match="Missing 'project' section"):
|
|
ProjectInfo(valid_yaml_data, tile_sizes)
|
|
|
|
def test_missing_title(self, valid_yaml_data, tile_sizes):
|
|
del valid_yaml_data["project"]["title"]
|
|
with pytest.raises(
|
|
ProjectYamlError, match="Missing key 'title' in 'project' section"
|
|
):
|
|
ProjectInfo(valid_yaml_data, tile_sizes)
|
|
|
|
def test_empty_title(self, valid_yaml_data, tile_sizes):
|
|
valid_yaml_data["project"]["title"] = ""
|
|
with pytest.raises(ProjectYamlError, match="Project title cannot be empty"):
|
|
ProjectInfo(valid_yaml_data, tile_sizes)
|
|
|
|
def test_missing_author(self, valid_yaml_data, tile_sizes):
|
|
del valid_yaml_data["project"]["author"]
|
|
with pytest.raises(
|
|
ProjectYamlError, match="Missing key 'author' in 'project' section"
|
|
):
|
|
ProjectInfo(valid_yaml_data, tile_sizes)
|
|
|
|
def test_empty_author(self, valid_yaml_data, tile_sizes):
|
|
valid_yaml_data["project"]["author"] = ""
|
|
with pytest.raises(ProjectYamlError, match="Project author cannot be empty"):
|
|
ProjectInfo(valid_yaml_data, tile_sizes)
|
|
|
|
def test_missing_description(self, valid_yaml_data, tile_sizes):
|
|
del valid_yaml_data["project"]["description"]
|
|
with pytest.raises(
|
|
ProjectYamlError, match="Missing key 'description' in 'project' section"
|
|
):
|
|
ProjectInfo(valid_yaml_data, tile_sizes)
|
|
|
|
def test_empty_description(self, valid_yaml_data, tile_sizes):
|
|
valid_yaml_data["project"]["description"] = ""
|
|
with pytest.raises(
|
|
ProjectYamlError, match="Project description cannot be empty"
|
|
):
|
|
ProjectInfo(valid_yaml_data, tile_sizes)
|
|
|
|
def test_missing_tiles(self, valid_yaml_data, tile_sizes):
|
|
del valid_yaml_data["project"]["tiles"]
|
|
with pytest.raises(
|
|
ProjectYamlError, match="Missing key 'tiles' in 'project' section"
|
|
):
|
|
ProjectInfo(valid_yaml_data, tile_sizes)
|
|
|
|
def test_invalid_tiles(self, valid_yaml_data, tile_sizes):
|
|
valid_yaml_data["project"]["tiles"] = "invalid_size"
|
|
with pytest.raises(
|
|
ProjectYamlError,
|
|
match="Invalid value for 'tiles' in 'project' section: invalid_size",
|
|
):
|
|
ProjectInfo(valid_yaml_data, tile_sizes)
|
|
|
|
def test_analog_pins_not_integer(self, valid_yaml_data, tile_sizes):
|
|
valid_yaml_data["project"]["analog_pins"] = "not_int"
|
|
with pytest.raises(
|
|
ProjectYamlError,
|
|
match="Invalid value for 'analog_pins' in 'project' section, must be an integer",
|
|
):
|
|
ProjectInfo(valid_yaml_data, tile_sizes)
|
|
|
|
def test_analog_pins_negative(self, valid_yaml_data, tile_sizes):
|
|
valid_yaml_data["project"]["analog_pins"] = -1
|
|
with pytest.raises(
|
|
ProjectYamlError,
|
|
match="Invalid value for 'analog_pins' in 'project' section, must be between 0 and 6",
|
|
):
|
|
ProjectInfo(valid_yaml_data, tile_sizes)
|
|
|
|
def test_analog_pins_too_high(self, valid_yaml_data, tile_sizes):
|
|
valid_yaml_data["project"]["analog_pins"] = 7
|
|
with pytest.raises(
|
|
ProjectYamlError,
|
|
match="Invalid value for 'analog_pins' in 'project' section, must be between 0 and 6",
|
|
):
|
|
ProjectInfo(valid_yaml_data, tile_sizes)
|
|
|
|
def test_uses_3v3_without_analog_pins(self, valid_yaml_data, tile_sizes):
|
|
valid_yaml_data["project"]["uses_3v3"] = True
|
|
valid_yaml_data["project"]["analog_pins"] = 0
|
|
with pytest.raises(
|
|
ProjectYamlError,
|
|
match="Projects with 3v3 power need at least one analog pin",
|
|
):
|
|
ProjectInfo(valid_yaml_data, tile_sizes)
|
|
|
|
def test_missing_language(self, valid_yaml_data, tile_sizes):
|
|
del valid_yaml_data["project"]["language"]
|
|
with pytest.raises(
|
|
ProjectYamlError, match="Missing key 'language' in 'project' section"
|
|
):
|
|
ProjectInfo(valid_yaml_data, tile_sizes)
|
|
|
|
def test_empty_language(self, valid_yaml_data, tile_sizes):
|
|
valid_yaml_data["project"]["language"] = ""
|
|
with pytest.raises(ProjectYamlError, match="Project language cannot be empty"):
|
|
ProjectInfo(valid_yaml_data, tile_sizes)
|
|
|
|
|
|
class TestProjectInfoWokwiLanguage:
|
|
"""Test Wokwi language specific validation"""
|
|
|
|
def test_wokwi_missing_wokwi_id(self, valid_yaml_data, tile_sizes):
|
|
valid_yaml_data["project"]["language"] = "Wokwi"
|
|
del valid_yaml_data["project"]["top_module"]
|
|
del valid_yaml_data["project"]["source_files"]
|
|
with pytest.raises(
|
|
ProjectYamlError, match="Missing key 'wokwi_id' in 'project' section"
|
|
):
|
|
ProjectInfo(valid_yaml_data, tile_sizes)
|
|
|
|
def test_wokwi_empty_wokwi_id(self, valid_yaml_data, tile_sizes):
|
|
valid_yaml_data["project"]["language"] = "Wokwi"
|
|
valid_yaml_data["project"]["wokwi_id"] = ""
|
|
del valid_yaml_data["project"]["top_module"]
|
|
del valid_yaml_data["project"]["source_files"]
|
|
with pytest.raises(
|
|
ProjectYamlError, match="Please provide a valid Wokwi project ID"
|
|
):
|
|
ProjectInfo(valid_yaml_data, tile_sizes)
|
|
|
|
def test_wokwi_zero_wokwi_id(self, valid_yaml_data, tile_sizes):
|
|
valid_yaml_data["project"]["language"] = "Wokwi"
|
|
valid_yaml_data["project"]["wokwi_id"] = "0"
|
|
del valid_yaml_data["project"]["top_module"]
|
|
del valid_yaml_data["project"]["source_files"]
|
|
with pytest.raises(
|
|
ProjectYamlError, match="Please provide a valid Wokwi project ID"
|
|
):
|
|
ProjectInfo(valid_yaml_data, tile_sizes)
|
|
|
|
def test_wokwi_valid(self, valid_yaml_data, tile_sizes):
|
|
valid_yaml_data["project"]["language"] = "Wokwi"
|
|
valid_yaml_data["project"]["wokwi_id"] = 123456789
|
|
del valid_yaml_data["project"]["top_module"]
|
|
del valid_yaml_data["project"]["source_files"]
|
|
project_info = ProjectInfo(valid_yaml_data, tile_sizes)
|
|
assert project_info.top_module == "tt_um_wokwi_123456789"
|
|
assert project_info.source_files == ["tt_um_wokwi_123456789.v", "cells.v"]
|
|
|
|
|
|
class TestProjectInfoNonWokwiLanguage:
|
|
"""Test non-Wokwi language validation"""
|
|
|
|
def test_missing_top_module(self, valid_yaml_data, tile_sizes):
|
|
del valid_yaml_data["project"]["top_module"]
|
|
with pytest.raises(
|
|
ProjectYamlError, match="Missing key 'top_module' in 'project' section"
|
|
):
|
|
ProjectInfo(valid_yaml_data, tile_sizes)
|
|
|
|
def test_top_module_without_tt_um_prefix(self, valid_yaml_data, tile_sizes):
|
|
valid_yaml_data["project"]["top_module"] = "invalid_module"
|
|
with pytest.raises(
|
|
ProjectYamlError, match="Top module must start with 'tt_um_'"
|
|
):
|
|
ProjectInfo(valid_yaml_data, tile_sizes)
|
|
|
|
def test_missing_source_files(self, valid_yaml_data, tile_sizes):
|
|
del valid_yaml_data["project"]["source_files"]
|
|
with pytest.raises(
|
|
ProjectYamlError, match="Missing key 'source_files' in 'project' section"
|
|
):
|
|
ProjectInfo(valid_yaml_data, tile_sizes)
|
|
|
|
def test_empty_source_files(self, valid_yaml_data, tile_sizes):
|
|
valid_yaml_data["project"]["source_files"] = []
|
|
with pytest.raises(ProjectYamlError, match="No source files specified"):
|
|
ProjectInfo(valid_yaml_data, tile_sizes)
|
|
|
|
|
|
class TestProjectInfoClockHz:
|
|
"""Test clock_hz validation"""
|
|
|
|
def test_missing_clock_hz(self, valid_yaml_data, tile_sizes):
|
|
del valid_yaml_data["project"]["clock_hz"]
|
|
with pytest.raises(
|
|
ProjectYamlError, match="Missing key 'clock_hz' in 'project' section"
|
|
):
|
|
ProjectInfo(valid_yaml_data, tile_sizes)
|
|
|
|
def test_clock_hz_not_integer(self, valid_yaml_data, tile_sizes):
|
|
valid_yaml_data["project"]["clock_hz"] = "not_int"
|
|
with pytest.raises(
|
|
ProjectYamlError,
|
|
match="Invalid value for 'clock_hz' in 'project' section, must be an integer",
|
|
):
|
|
ProjectInfo(valid_yaml_data, tile_sizes)
|
|
|
|
|
|
class TestProjectInfoPinoutSection:
|
|
"""Test pinout section validation"""
|
|
|
|
def test_missing_pinout_section(self, valid_yaml_data, tile_sizes):
|
|
del valid_yaml_data["pinout"]
|
|
with pytest.raises(ProjectYamlError, match="Missing 'pinout' section"):
|
|
ProjectInfo(valid_yaml_data, tile_sizes)
|
|
|
|
def test_missing_ui_pin(self, valid_yaml_data, tile_sizes):
|
|
del valid_yaml_data["pinout"]["ui[0]"]
|
|
with pytest.raises(
|
|
ProjectYamlError, match="Missing 'ui\\[0\\]' in 'pinout' section"
|
|
):
|
|
ProjectInfo(valid_yaml_data, tile_sizes)
|
|
|
|
def test_missing_uo_pin(self, valid_yaml_data, tile_sizes):
|
|
del valid_yaml_data["pinout"]["uo[3]"]
|
|
with pytest.raises(
|
|
ProjectYamlError, match="Missing 'uo\\[3\\]' in 'pinout' section"
|
|
):
|
|
ProjectInfo(valid_yaml_data, tile_sizes)
|
|
|
|
def test_missing_uio_pin(self, valid_yaml_data, tile_sizes):
|
|
del valid_yaml_data["pinout"]["uio[7]"]
|
|
with pytest.raises(
|
|
ProjectYamlError, match="Missing 'uio\\[7\\]' in 'pinout' section"
|
|
):
|
|
ProjectInfo(valid_yaml_data, tile_sizes)
|
|
|
|
def test_all_empty_pinout(self, valid_yaml_data, tile_sizes):
|
|
for pin in valid_yaml_data["pinout"]:
|
|
valid_yaml_data["pinout"][pin] = ""
|
|
with pytest.raises(
|
|
ProjectYamlError, match="Please fill in the 'pinout' section"
|
|
):
|
|
ProjectInfo(valid_yaml_data, tile_sizes)
|
|
|
|
def test_invalid_pinout_keys(self, valid_yaml_data, tile_sizes):
|
|
valid_yaml_data["pinout"]["invalid_key"] = "value"
|
|
with pytest.raises(
|
|
ProjectYamlError,
|
|
match="Invalid keys \\['invalid_key'\\] in 'pinout' section. Please remove them.",
|
|
):
|
|
ProjectInfo(valid_yaml_data, tile_sizes)
|
|
|
|
def test_ua_pins_optional_when_not_analog(self, valid_yaml_data, tile_sizes):
|
|
# ua pins should be optional when analog_pins is 0
|
|
project_info = ProjectInfo(valid_yaml_data, tile_sizes)
|
|
assert project_info.pinout.ua == []
|
|
|
|
def test_ua_pins_required_when_analog(self, valid_yaml_data, tile_sizes):
|
|
valid_yaml_data["project"]["analog_pins"] = 2
|
|
pinout = valid_yaml_data["pinout"]
|
|
pinout["ua[0]"] = "analog_0"
|
|
pinout["ua[1]"] = "analog_1"
|
|
pinout["ua[2]"] = ""
|
|
pinout["ua[3]"] = ""
|
|
pinout["ua[4]"] = ""
|
|
pinout["ua[5]"] = ""
|
|
|
|
project_info = ProjectInfo(valid_yaml_data, tile_sizes)
|
|
expected_ua = ["analog_0", "analog_1", "", "", "", ""]
|
|
assert project_info.pinout.ua == expected_ua
|
|
|
|
def test_missing_ua_pin_when_analog(self, valid_yaml_data, tile_sizes):
|
|
valid_yaml_data["project"]["analog_pins"] = 1
|
|
# If ua[0] is present, but ua[1] is missing, it should fail
|
|
valid_yaml_data["pinout"]["ua[0]"] = "analog_0"
|
|
# Missing ua[1] should fail since ua[0] exists
|
|
with pytest.raises(
|
|
ProjectYamlError, match="Missing 'ua\\[1\\]' in 'pinout' section"
|
|
):
|
|
ProjectInfo(valid_yaml_data, tile_sizes)
|
|
|
|
def test_ua_pins_completely_optional(self, valid_yaml_data, tile_sizes):
|
|
# If no ua[0] is present, then ua pins are completely optional
|
|
valid_yaml_data["project"]["analog_pins"] = 2 # Even with analog_pins > 0
|
|
project_info = ProjectInfo(valid_yaml_data, tile_sizes)
|
|
assert project_info.pinout.ua == []
|
|
|
|
|
|
class TestProjectInfoValidCombinations:
|
|
"""Test valid combinations and edge cases"""
|
|
|
|
def test_valid_analog_project(self, valid_yaml_data, tile_sizes):
|
|
valid_yaml_data["project"]["analog_pins"] = 2
|
|
valid_yaml_data["project"]["uses_3v3"] = True
|
|
# Include all required ua pins
|
|
for i in range(6):
|
|
valid_yaml_data["pinout"][f"ua[{i}]"] = f"analog_{i}" if i < 2 else ""
|
|
|
|
project_info = ProjectInfo(valid_yaml_data, tile_sizes)
|
|
assert project_info.analog_pins == 2
|
|
assert project_info.is_analog is True
|
|
assert project_info.uses_3v3 is True
|
|
|
|
def test_valid_digital_project(self, valid_yaml_data, tile_sizes):
|
|
project_info = ProjectInfo(valid_yaml_data, tile_sizes)
|
|
assert project_info.analog_pins == 0
|
|
assert project_info.is_analog is False
|
|
assert project_info.uses_3v3 is False
|
|
|
|
def test_optional_fields(self, valid_yaml_data, tile_sizes):
|
|
# Test that optional fields work
|
|
valid_yaml_data["project"]["discord"] = "test_discord"
|
|
valid_yaml_data["project"]["doc_link"] = "https://example.com"
|
|
|
|
project_info = ProjectInfo(valid_yaml_data, tile_sizes)
|
|
assert project_info.discord == "test_discord"
|
|
assert project_info.doc_link == "https://example.com"
|
|
|
|
def test_default_analog_pins(self, valid_yaml_data, tile_sizes):
|
|
# Test that analog_pins defaults to 0 when not specified
|
|
del valid_yaml_data["project"]["analog_pins"]
|
|
project_info = ProjectInfo(valid_yaml_data, tile_sizes)
|
|
assert project_info.analog_pins == 0
|
|
|
|
def test_default_uses_3v3(self, valid_yaml_data, tile_sizes):
|
|
# Test that uses_3v3 defaults to False when not specified
|
|
del valid_yaml_data["project"]["uses_3v3"]
|
|
project_info = ProjectInfo(valid_yaml_data, tile_sizes)
|
|
assert project_info.uses_3v3 is False
|
|
|
|
def test_all_tile_sizes(self, valid_yaml_data, tile_sizes):
|
|
# Test all valid tile sizes
|
|
for tile_size in tile_sizes.keys():
|
|
valid_yaml_data["project"]["tiles"] = tile_size
|
|
project_info = ProjectInfo(valid_yaml_data, tile_sizes)
|
|
assert project_info.tiles == tile_size
|
|
|
|
def test_empty_pinout_descriptions_allowed(self, valid_yaml_data, tile_sizes):
|
|
# Some pins can be empty (just not ALL pins)
|
|
valid_yaml_data["pinout"]["ui[0]"] = ""
|
|
valid_yaml_data["pinout"]["uo[0]"] = ""
|
|
valid_yaml_data["pinout"]["uio[0]"] = ""
|
|
|
|
project_info = ProjectInfo(valid_yaml_data, tile_sizes)
|
|
assert project_info.pinout.ui[0] == ""
|
|
assert project_info.pinout.uo[0] == ""
|
|
assert project_info.pinout.uio[0] == ""
|
|
|
|
|
|
class TestMultipleErrors:
|
|
"""Test that multiple errors are collected and reported together"""
|
|
|
|
def test_multiple_project_errors(self, valid_yaml_data, tile_sizes):
|
|
"""Test that multiple errors in project section are collected"""
|
|
del valid_yaml_data["project"]["title"]
|
|
del valid_yaml_data["project"]["author"]
|
|
valid_yaml_data["project"]["description"] = ""
|
|
|
|
with pytest.raises(ProjectYamlError) as exc_info:
|
|
ProjectInfo(valid_yaml_data, tile_sizes)
|
|
|
|
assert sorted(exc_info.value.errors) == sorted(
|
|
[
|
|
"Missing key 'title' in 'project' section",
|
|
"Missing key 'author' in 'project' section",
|
|
"Project description cannot be empty",
|
|
]
|
|
)
|
|
|
|
def test_multiple_pinout_errors(self, valid_yaml_data, tile_sizes):
|
|
"""Test that multiple errors in pinout section are collected"""
|
|
del valid_yaml_data["pinout"]["ui[0]"]
|
|
del valid_yaml_data["pinout"]["uo[3]"]
|
|
del valid_yaml_data["pinout"]["uio[7]"]
|
|
|
|
with pytest.raises(ProjectYamlError) as exc_info:
|
|
ProjectInfo(valid_yaml_data, tile_sizes)
|
|
|
|
assert sorted(exc_info.value.errors) == sorted(
|
|
[
|
|
"Missing 'ui[0]' in 'pinout' section",
|
|
"Missing 'uo[3]' in 'pinout' section",
|
|
"Missing 'uio[7]' in 'pinout' section",
|
|
]
|
|
)
|
|
|
|
def test_errors_across_sections(self, valid_yaml_data, tile_sizes):
|
|
"""Test that errors across different sections are all collected"""
|
|
del valid_yaml_data["yaml_version"]
|
|
del valid_yaml_data["project"]["title"]
|
|
valid_yaml_data["project"]["analog_pins"] = -1
|
|
del valid_yaml_data["pinout"]["ui[0]"]
|
|
|
|
with pytest.raises(ProjectYamlError) as exc_info:
|
|
ProjectInfo(valid_yaml_data, tile_sizes)
|
|
|
|
assert sorted(exc_info.value.errors) == sorted(
|
|
[
|
|
"Missing 'yaml_version'",
|
|
"Missing key 'title' in 'project' section",
|
|
"Invalid value for 'analog_pins' in 'project' section, must be between 0 and 6",
|
|
"Missing 'ui[0]' in 'pinout' section",
|
|
]
|
|
)
|
|
|
|
def test_comprehensive_validation_errors(self, valid_yaml_data, tile_sizes):
|
|
"""Test a comprehensive set of validation errors"""
|
|
valid_yaml_data["yaml_version"] = 999
|
|
valid_yaml_data["project"]["title"] = ""
|
|
valid_yaml_data["project"]["author"] = ""
|
|
valid_yaml_data["project"]["tiles"] = "invalid"
|
|
valid_yaml_data["project"]["analog_pins"] = 10
|
|
del valid_yaml_data["project"]["clock_hz"]
|
|
del valid_yaml_data["pinout"]["uo[0]"]
|
|
valid_yaml_data["pinout"]["invalid_key"] = "value"
|
|
|
|
with pytest.raises(ProjectYamlError) as exc_info:
|
|
ProjectInfo(valid_yaml_data, tile_sizes)
|
|
|
|
assert sorted(exc_info.value.errors) == sorted(
|
|
[
|
|
"Unsupported YAML version: 999, expected 6",
|
|
"Project title cannot be empty",
|
|
"Project author cannot be empty",
|
|
"Invalid value for 'tiles' in 'project' section: invalid",
|
|
"Invalid value for 'analog_pins' in 'project' section, must be between 0 and 6",
|
|
"Missing key 'clock_hz' in 'project' section",
|
|
"Missing 'uo[0]' in 'pinout' section",
|
|
"Invalid keys ['invalid_key'] in 'pinout' section. Please remove them.",
|
|
]
|
|
)
|
|
|
|
def test_error_message_formatting(self, valid_yaml_data, tile_sizes):
|
|
"""Test that error message contains all errors separated by commas"""
|
|
del valid_yaml_data["project"]["title"]
|
|
del valid_yaml_data["project"]["author"]
|
|
|
|
with pytest.raises(ProjectYamlError) as exc_info:
|
|
ProjectInfo(valid_yaml_data, tile_sizes)
|
|
|
|
error_message = str(exc_info.value)
|
|
assert (
|
|
"Missing key 'title' in 'project' section, Missing key 'author' in 'project' section"
|
|
== error_message
|
|
)
|
|
|
|
|
|
class TestProjectInfoIntegration:
|
|
"""Integration tests with real YAML files"""
|
|
|
|
def test_load_from_yaml_string(self, tile_sizes):
|
|
yaml_content = f"""
|
|
yaml_version: {YAML_VERSION}
|
|
project:
|
|
title: "Integration Test"
|
|
author: "Test Author"
|
|
description: "Integration test description"
|
|
tiles: "1x1"
|
|
language: "Verilog"
|
|
top_module: "tt_um_integration_test"
|
|
source_files: ["integration_test.v"]
|
|
clock_hz: 25000000
|
|
pinout:
|
|
ui[0]: "clk"
|
|
ui[1]: "rst_n"
|
|
ui[2]: "enable"
|
|
ui[3]: "data_in[0]"
|
|
ui[4]: "data_in[1]"
|
|
ui[5]: "data_in[2]"
|
|
ui[6]: "data_in[3]"
|
|
ui[7]: "unused"
|
|
uo[0]: "data_out[0]"
|
|
uo[1]: "data_out[1]"
|
|
uo[2]: "data_out[2]"
|
|
uo[3]: "data_out[3]"
|
|
uo[4]: "valid"
|
|
uo[5]: "ready"
|
|
uo[6]: "error"
|
|
uo[7]: "unused"
|
|
uio[0]: "sda"
|
|
uio[1]: "scl"
|
|
uio[2]: "cs"
|
|
uio[3]: "mosi"
|
|
uio[4]: "miso"
|
|
uio[5]: "sck"
|
|
uio[6]: "interrupt"
|
|
uio[7]: "debug"
|
|
"""
|
|
yaml_data = yaml.safe_load(yaml_content)
|
|
project_info = ProjectInfo(yaml_data, tile_sizes)
|
|
|
|
assert project_info.title == "Integration Test"
|
|
assert project_info.top_module == "tt_um_integration_test"
|
|
assert project_info.clock_hz == 25000000
|
|
assert len(project_info.source_files) == 1
|
|
assert project_info.source_files[0] == "integration_test.v"
|
|
|
|
def test_load_wokwi_from_yaml_string(self, tile_sizes):
|
|
yaml_content = f"""
|
|
yaml_version: {YAML_VERSION}
|
|
project:
|
|
title: "Wokwi Integration Test"
|
|
author: "Wokwi User"
|
|
description: "Wokwi integration test"
|
|
tiles: "1x1"
|
|
language: "Wokwi"
|
|
wokwi_id: 987654321
|
|
clock_hz: 10000000
|
|
pinout:
|
|
ui[0]: "button_0"
|
|
ui[1]: "button_1"
|
|
ui[2]: "switch_0"
|
|
ui[3]: "switch_1"
|
|
ui[4]: "unused"
|
|
ui[5]: "unused"
|
|
ui[6]: "unused"
|
|
ui[7]: "unused"
|
|
uo[0]: "led_0"
|
|
uo[1]: "led_1"
|
|
uo[2]: "led_2"
|
|
uo[3]: "led_3"
|
|
uo[4]: "segment_a"
|
|
uo[5]: "segment_b"
|
|
uo[6]: "segment_c"
|
|
uo[7]: "segment_d"
|
|
uio[0]: "unused"
|
|
uio[1]: "unused"
|
|
uio[2]: "unused"
|
|
uio[3]: "unused"
|
|
uio[4]: "unused"
|
|
uio[5]: "unused"
|
|
uio[6]: "unused"
|
|
uio[7]: "unused"
|
|
"""
|
|
yaml_data = yaml.safe_load(yaml_content)
|
|
project_info = ProjectInfo(yaml_data, tile_sizes)
|
|
|
|
assert project_info.title == "Wokwi Integration Test"
|
|
assert project_info.top_module == "tt_um_wokwi_987654321"
|
|
assert project_info.source_files == ["tt_um_wokwi_987654321.v", "cells.v"]
|
|
assert hasattr(project_info, "wokwi_id")
|
|
assert project_info.wokwi_id == 987654321
|