Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions HF_files/aibom-generator/pytest.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[pytest]
testpaths = tests
python_files = test_*.py
python_classes = Test*
python_functions = test_*
addopts = -v --tb=short
filterwarnings =
ignore::DeprecationWarning
ignore::PendingDeprecationWarning
8 changes: 7 additions & 1 deletion HF_files/aibom-generator/requirements.txt
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also added tests for my feature -- I think this should probably be in a different requirements file, e.g. requirements-dev.txt, no? ideally, it would nice to use uv and pyproject.toml to have even more granular control, but at the very least we shouldn't allow for installing dev deps in every pip install, right?

Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,10 @@ jinja2>=3.0.0
datasets>=2.0.0
beautifulsoup4>=4.11.0
nltk>=3.8.0
python-dateutil>=2.8.0
python-dateutil>=2.8.0

# Test dependencies
pytest>=7.0.0
pytest-mock>=3.10.0
pytest-cov>=4.0.0
jsonschema>=4.17.0
1 change: 1 addition & 0 deletions HF_files/aibom-generator/tests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# AIBOM Generator Test Suite
226 changes: 226 additions & 0 deletions HF_files/aibom-generator/tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
"""
Shared pytest fixtures for AIBOM Generator tests.

Provides mock HuggingFace API responses to enable offline testing.
"""
import pytest
from unittest.mock import MagicMock, patch
from datetime import datetime


class MockModelInfo:
"""Mock HuggingFace ModelInfo object."""

def __init__(
self,
model_id="test-org/test-model",
author="test-org",
pipeline_tag="text-generation",
library_name="transformers",
license="apache-2.0",
tags=None,
downloads=1000,
likes=100,
created_at=None,
last_modified=None,
):
self.id = model_id
self.modelId = model_id
self.author = author
self.pipeline_tag = pipeline_tag
self.library_name = library_name
self.license = license
self.tags = tags or ["pytorch", "text-generation"]
self.downloads = downloads
self.likes = likes
self.created_at = created_at or datetime(2024, 1, 1)
self.last_modified = last_modified or datetime(2024, 6, 1)
self.sha = "abc123def456"
self.private = False
self.disabled = False
self.gated = False
self.siblings = []
self.config = {"model_type": "llama", "architectures": ["LlamaForCausalLM"]}
self.card_data = None
self.safetensors = None


class MockModelCard:
"""Mock HuggingFace ModelCard object."""

def __init__(self, content="", data=None):
self.content = content
self.data = data or MockModelCardData()
self.text = content


class MockModelCardData:
"""Mock ModelCard data."""

def __init__(self):
self.license = "apache-2.0"
self.language = ["en"]
self.tags = ["text-generation"]
self.datasets = ["test-dataset"]
self.metrics = []
self.model_name = None
self.eval_results = []
self.library_name = "transformers"
self.pipeline_tag = "text-generation"
self.base_model = None

def to_dict(self):
return {
"license": self.license,
"language": self.language,
"tags": self.tags,
"datasets": self.datasets,
}


@pytest.fixture
def mock_model_info():
"""Return a mock ModelInfo object with default values."""
return MockModelInfo()


@pytest.fixture
def mock_model_info_llama():
"""Return a mock ModelInfo object for a Llama-style model."""
return MockModelInfo(
model_id="meta-llama/Llama-2-7b",
author="meta-llama",
pipeline_tag="text-generation",
library_name="transformers",
license="llama2",
tags=["pytorch", "llama", "text-generation"],
downloads=1000000,
likes=5000,
)


@pytest.fixture
def mock_model_info_whisper():
"""Return a mock ModelInfo object for a Whisper-style model."""
return MockModelInfo(
model_id="openai/whisper-large-v3",
author="openai",
pipeline_tag="automatic-speech-recognition",
library_name="transformers",
license="apache-2.0",
tags=["pytorch", "whisper", "audio"],
downloads=500000,
likes=2000,
)


@pytest.fixture
def mock_model_card():
"""Return a mock ModelCard object."""
content = """
# Test Model

This is a test model for unit testing.

## Model Details

- **Model type:** Transformer
- **Language:** English
- **License:** Apache 2.0

## Training Data

Trained on test dataset.

## Limitations

This model has some limitations.
"""
return MockModelCard(content=content)


@pytest.fixture
def mock_hf_api(mock_model_info, mock_model_card):
"""
Fixture that mocks the HuggingFace Hub API.

Usage:
def test_something(mock_hf_api):
# HfApi calls are now mocked
generator = AIBOMGenerator()
result = generator.generate_aibom("test/model")
"""
with patch('huggingface_hub.HfApi') as mock_api_class:
mock_api = MagicMock()
mock_api.model_info.return_value = mock_model_info
mock_api_class.return_value = mock_api

with patch('huggingface_hub.ModelCard') as mock_card_class:
mock_card_class.load.return_value = mock_model_card

yield {
'api': mock_api,
'api_class': mock_api_class,
'card_class': mock_card_class,
'model_info': mock_model_info,
'model_card': mock_model_card,
}


@pytest.fixture
def sample_aibom():
"""Return a sample valid AIBOM structure for testing."""
return {
"bomFormat": "CycloneDX",
"specVersion": "1.6",
"serialNumber": "urn:uuid:test-uuid",
"version": 1,
"metadata": {
"timestamp": "2024-01-01T00:00:00Z",
"tools": {
"components": [{
"type": "application",
"name": "OWASP AIBOM Generator",
"version": "1.0.0"
}]
},
"component": {
"type": "application",
"name": "test-model",
"version": "1.0"
}
},
"components": [{
"bom-ref": "pkg:huggingface/test-org/test-model@1.0",
"type": "machine-learning-model",
"name": "test-model",
"version": "1.0",
"purl": "pkg:huggingface/test-org%2Ftest-model@1.0",
"licenses": [{"license": {"id": "apache-2.0"}}],
"modelCard": {
"modelParameters": {
"task": "text-generation"
}
}
}],
"dependencies": []
}


@pytest.fixture
def cyclonedx_schema():
"""Return the CycloneDX 1.6 JSON schema for validation."""
return {
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"required": ["bomFormat", "specVersion"],
"properties": {
"bomFormat": {"type": "string", "enum": ["CycloneDX"]},
"specVersion": {"type": "string"},
"serialNumber": {"type": "string"},
"version": {"type": "integer"},
"metadata": {"type": "object"},
"components": {"type": "array"},
"dependencies": {"type": "array"}
}
}
96 changes: 96 additions & 0 deletions HF_files/aibom-generator/tests/fixtures/expected_aibom.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
{
"bomFormat": "CycloneDX",
"specVersion": "1.6",
"serialNumber": "urn:uuid:test-uuid-placeholder",
"version": 1,
"metadata": {
"timestamp": "2024-01-01T00:00:00Z",
"tools": {
"components": [
{
"bom-ref": "pkg:generic/owasp-genai/owasp-aibom-generator@1.0.0",
"type": "application",
"name": "OWASP AIBOM Generator",
"version": "1.0.0",
"manufacturer": {
"name": "OWASP GenAI Security Project"
}
}
]
},
"component": {
"bom-ref": "pkg:generic/test-org%2Ftest-model@1.0",
"type": "application",
"name": "test-model",
"description": "Test model for unit testing",
"version": "1.0",
"purl": "pkg:generic/test-org%2Ftest-model@1.0",
"copyright": "NOASSERTION"
}
},
"components": [
{
"bom-ref": "pkg:huggingface/test-org%2Ftest-model@1.0",
"type": "machine-learning-model",
"group": "test-org",
"name": "test-model",
"version": "1.0",
"purl": "pkg:huggingface/test-org%2Ftest-model@1.0",
"licenses": [
{
"license": {
"id": "apache-2.0",
"url": "https://www.apache.org/licenses/LICENSE-2.0"
}
}
],
"description": "Test model for unit testing",
"externalReferences": [
{
"type": "website",
"url": "https://huggingface.co/test-org/test-model"
}
],
"authors": [
{
"name": "test-org"
}
],
"publisher": "test-org",
"supplier": {
"name": "test-org",
"url": ["https://huggingface.co/test-org"]
},
"manufacturer": {
"name": "test-org",
"url": ["https://huggingface.co/test-org"]
},
"copyright": "NOASSERTION",
"modelCard": {
"modelParameters": {
"task": "text-generation",
"architectureFamily": "transformer",
"modelArchitecture": "LlamaForCausalLM",
"inputs": [{"format": "text"}],
"outputs": [{"format": "generated-text"}]
},
"properties": [
{"name": "primaryPurpose", "value": "text-generation"},
{"name": "typeOfModel", "value": "transformer"}
]
}
}
],
"dependencies": [
{
"ref": "pkg:generic/test-org%2Ftest-model@1.0",
"dependsOn": ["pkg:huggingface/test-org%2Ftest-model@1.0"]
}
],
"externalReferences": [
{
"type": "distribution",
"url": "https://huggingface.co/test-org/test-model"
}
]
}
18 changes: 18 additions & 0 deletions HF_files/aibom-generator/tests/fixtures/sample_model_card.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"model_id": "test-org/test-model",
"author": "test-org",
"pipeline_tag": "text-generation",
"library_name": "transformers",
"license": "apache-2.0",
"tags": ["pytorch", "text-generation", "llama"],
"downloads": 10000,
"likes": 500,
"model_card_content": "# Test Model\n\nThis is a test model for unit testing.\n\n## Model Details\n\n- **Model type:** Transformer\n- **Language:** English\n- **License:** Apache 2.0\n\n## Training Data\n\nTrained on test dataset.\n\n## Limitations\n\nThis model has some limitations.",
"config": {
"model_type": "llama",
"architectures": ["LlamaForCausalLM"],
"hidden_size": 4096,
"num_attention_heads": 32,
"num_hidden_layers": 32
}
}
Loading