Skip to content

Commit 7cdac38

Browse files
committed
test: Add comprehensive unit tests for historical data SDK
- Add test suite for OSSFuzzSDK main functionality - Include tests for all history managers (build, crash, corpus, coverage) - Test configuration, error handling, and edge cases - Ensure proper integration with storage and data validation - Add mocking for external dependencies
1 parent e3ea028 commit 7cdac38

File tree

1 file changed

+305
-0
lines changed

1 file changed

+305
-0
lines changed
Lines changed: 305 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,305 @@
1+
# Copyright 2025 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
"""
15+
Unit tests for the Historical Data SDK.
16+
17+
This module contains tests for the main SDK components including
18+
the OSSFuzzSDK facade and history managers.
19+
"""
20+
21+
import tempfile
22+
import unittest
23+
from datetime import datetime
24+
from unittest.mock import patch
25+
26+
from ossfuzz_py.core.ossfuzz_sdk import OSSFuzzSDK
27+
from ossfuzz_py.data.storage_manager import StorageManager
28+
from ossfuzz_py.errors import OSSFuzzSDKConfigError
29+
from ossfuzz_py.history import (BuildHistoryManager, CorpusHistoryManager,
30+
CoverageHistoryManager, CrashHistoryManager)
31+
32+
33+
class TestOSSFuzzSDK(unittest.TestCase):
34+
"""Test cases for the OSSFuzzSDK class."""
35+
36+
def setUp(self):
37+
"""Set up test fixtures."""
38+
self.temp_dir = tempfile.mkdtemp()
39+
self.config = {'storage_backend': 'local', 'storage_path': self.temp_dir}
40+
self.project_name = 'test_project'
41+
42+
def tearDown(self):
43+
"""Clean up test fixtures."""
44+
import shutil
45+
shutil.rmtree(self.temp_dir, ignore_errors=True)
46+
47+
def test_sdk_initialization(self):
48+
"""Test SDK initialization with valid configuration."""
49+
sdk = OSSFuzzSDK(self.project_name, self.config)
50+
51+
self.assertEqual(sdk.project_name, self.project_name)
52+
self.assertIsInstance(sdk.storage, StorageManager)
53+
self.assertIsInstance(sdk.build, BuildHistoryManager)
54+
self.assertIsInstance(sdk.crash, CrashHistoryManager)
55+
self.assertIsInstance(sdk.corpus, CorpusHistoryManager)
56+
self.assertIsInstance(sdk.coverage, CoverageHistoryManager)
57+
58+
def test_sdk_initialization_without_project_name(self):
59+
"""Test SDK initialization fails without project name."""
60+
with self.assertRaises(OSSFuzzSDKConfigError):
61+
OSSFuzzSDK('', self.config)
62+
63+
def test_sdk_initialization_without_config(self):
64+
"""Test SDK initialization with default configuration."""
65+
sdk = OSSFuzzSDK(self.project_name)
66+
self.assertEqual(sdk.project_name, self.project_name)
67+
self.assertIsInstance(sdk.storage, StorageManager)
68+
69+
@patch.dict(
70+
'os.environ', {
71+
'OSSFUZZ_HISTORY_STORAGE_BACKEND': 'local',
72+
'OSSFUZZ_HISTORY_STORAGE_PATH': '/tmp/test'
73+
})
74+
def test_config_from_environment(self):
75+
"""Test configuration loading from environment variables."""
76+
sdk = OSSFuzzSDK(self.project_name)
77+
self.assertEqual(sdk.config.get('storage_backend'), 'local')
78+
self.assertEqual(sdk.config.get('storage_path'), '/tmp/test')
79+
80+
def test_generate_project_report(self):
81+
"""Test project report generation."""
82+
sdk = OSSFuzzSDK(self.project_name, self.config)
83+
84+
# Mock the history managers to return test data
85+
with (patch.object(sdk.build, 'get_build_statistics') as mock_build_stats, \
86+
patch.object(sdk.build, 'get_build_trends') as mock_build_trends, \
87+
patch.object(sdk.crash, 'get_crash_statistics') as mock_crash_stats, \
88+
patch.object(sdk.coverage, 'get_coverage_report')
89+
as mock_coverage_report, \
90+
patch.object(sdk.coverage, 'analyze_coverage_trends') as
91+
mock_coverage_trends, \
92+
patch.object(sdk.corpus, 'get_corpus_growth') as mock_corpus_growth):
93+
94+
# Set up mock return values
95+
mock_build_stats.return_value = {'success_rate': 85.0, 'total_builds': 10}
96+
mock_build_trends.return_value = {
97+
'trend': 'improving',
98+
'builds_per_day': 2.0
99+
}
100+
mock_crash_stats.return_value = {'total_crashes': 5, 'unique_crashes': 3}
101+
mock_coverage_report.return_value = {
102+
'summary': {
103+
'max_line_coverage': 75.0
104+
}
105+
}
106+
mock_coverage_trends.return_value = {
107+
'trend': 'improving',
108+
'coverage_velocity': 0.5
109+
}
110+
mock_corpus_growth.return_value = {
111+
'growth_rate': 10.0,
112+
'trend': 'growing'
113+
}
114+
115+
report = sdk.generate_project_report(days=7)
116+
117+
self.assertEqual(report['project_name'], self.project_name)
118+
self.assertIn('build_summary', report)
119+
self.assertIn('crash_summary', report)
120+
self.assertIn('coverage_summary', report)
121+
self.assertIn('corpus_summary', report)
122+
self.assertIn('health_score', report)
123+
124+
def test_analyze_fuzzing_efficiency(self):
125+
"""Test fuzzing efficiency analysis."""
126+
sdk = OSSFuzzSDK(self.project_name, self.config)
127+
128+
# Mock the history managers to return test data
129+
with (patch.object(sdk.build, 'get_build_trends') as mock_build_trends, \
130+
patch.object(sdk.coverage, 'analyze_coverage_trends')
131+
as mock_coverage_trends, \
132+
patch.object(sdk.crash, 'get_crash_statistics') as mock_crash_stats, \
133+
patch.object(sdk.corpus, 'get_corpus_growth') as mock_corpus_growth):
134+
135+
# Set up mock return values
136+
mock_build_trends.return_value = {
137+
'builds_per_day': 2.0,
138+
'average_success_rate': 85.0,
139+
'trend': 'improving'
140+
}
141+
mock_coverage_trends.return_value = {
142+
'coverage_velocity': 0.5,
143+
'stability': 'stable',
144+
'current_coverage': 75.0
145+
}
146+
mock_crash_stats.return_value = {'total_crashes': 10, 'unique_crashes': 8}
147+
mock_corpus_growth.return_value = {
148+
'growth_rate': 15.0,
149+
'size_change': 100,
150+
'trend': 'growing'
151+
}
152+
153+
analysis = sdk.analyze_fuzzing_efficiency(days=7)
154+
155+
self.assertEqual(analysis['project_name'], self.project_name)
156+
self.assertIn('build_efficiency', analysis)
157+
self.assertIn('coverage_efficiency', analysis)
158+
self.assertIn('crash_efficiency', analysis)
159+
self.assertIn('corpus_efficiency', analysis)
160+
self.assertIn('overall_efficiency', analysis)
161+
162+
def test_get_project_summary(self):
163+
"""Test project summary generation."""
164+
sdk = OSSFuzzSDK(self.project_name, self.config)
165+
166+
# Mock the history managers to return test data
167+
with (patch.object(sdk.build, 'get_last_successful_build')
168+
as mock_last_build, \
169+
patch.object(sdk.coverage, 'get_latest_coverage')
170+
as mock_latest_coverage, \
171+
patch.object(sdk.crash, 'get_crash_history')
172+
as mock_crash_history):
173+
174+
# Set up mock return values
175+
mock_last_build.return_value = {
176+
'build_id': 'build_123',
177+
'timestamp': '2025-01-01T12:00:00',
178+
'success': True
179+
}
180+
mock_latest_coverage.return_value = {
181+
'timestamp': '2025-01-01T12:00:00',
182+
'line_coverage': 75.0
183+
}
184+
mock_crash_history.return_value = [{
185+
'crash_id': 'crash_1',
186+
'timestamp': '2025-01-01T10:00:00'
187+
}, {
188+
'crash_id': 'crash_2',
189+
'timestamp': '2025-01-01T11:00:00'
190+
}]
191+
192+
summary = sdk.get_project_summary()
193+
194+
self.assertEqual(summary['project_name'], self.project_name)
195+
self.assertIn('last_successful_build', summary)
196+
self.assertIn('latest_coverage', summary)
197+
self.assertEqual(summary['recent_crashes'], 2)
198+
199+
200+
class TestHistoryManagers(unittest.TestCase):
201+
"""Test cases for history managers."""
202+
203+
def setUp(self):
204+
"""Set up test fixtures."""
205+
self.temp_dir = tempfile.mkdtemp()
206+
self.config = {'storage_backend': 'local', 'storage_path': self.temp_dir}
207+
self.project_name = 'test_project'
208+
self.storage_manager = StorageManager(self.config)
209+
210+
def tearDown(self):
211+
"""Clean up test fixtures."""
212+
import shutil
213+
shutil.rmtree(self.temp_dir, ignore_errors=True)
214+
215+
def test_build_history_manager(self):
216+
"""Test BuildHistoryManager functionality."""
217+
manager = BuildHistoryManager(self.storage_manager, self.project_name)
218+
219+
# Test storing build result
220+
build_data = {
221+
'build_id': 'build_123',
222+
'timestamp': datetime.now().isoformat(),
223+
'project_name': self.project_name,
224+
'success': True,
225+
'duration_seconds': 300
226+
}
227+
228+
result = manager.store_build_result(build_data)
229+
self.assertIsInstance(result, str)
230+
231+
# Test retrieving build history
232+
history = manager.get_build_history(limit=10)
233+
self.assertIsInstance(history, list)
234+
235+
def test_crash_history_manager(self):
236+
"""Test CrashHistoryManager functionality."""
237+
manager = CrashHistoryManager(self.storage_manager, self.project_name)
238+
239+
# Test storing crash data (without signature so it gets generated)
240+
crash_data = {
241+
'crash_id': 'crash_123',
242+
'timestamp': datetime.now().isoformat(),
243+
'project_name': self.project_name,
244+
'fuzzer_name': 'test_fuzzer',
245+
'crash_type': 'heap-buffer-overflow'
246+
}
247+
248+
# First storage should succeed
249+
result = manager.store_crash(crash_data.copy())
250+
self.assertIsInstance(result, str)
251+
self.assertNotEqual(result, "") # Should not be empty (not a duplicate)
252+
253+
# Test duplicate detection - should be True after storing the same crash
254+
is_duplicate = manager.is_duplicate_crash(crash_data)
255+
self.assertTrue(is_duplicate)
256+
257+
# Second storage should return empty string (duplicate)
258+
result2 = manager.store_crash(crash_data.copy())
259+
self.assertEqual(result2, "")
260+
261+
def test_coverage_history_manager(self):
262+
"""Test CoverageHistoryManager functionality."""
263+
manager = CoverageHistoryManager(self.storage_manager, self.project_name)
264+
265+
# Test storing coverage data
266+
coverage_data = {
267+
'timestamp': datetime.now().isoformat(),
268+
'project_name': self.project_name,
269+
'fuzzer_name': 'test_fuzzer',
270+
'line_coverage': 75.5,
271+
'function_coverage': 80.0,
272+
'branch_coverage': 70.0
273+
}
274+
275+
result = manager.store_coverage(coverage_data)
276+
self.assertIsInstance(result, str)
277+
278+
# Test retrieving coverage history
279+
history = manager.get_coverage_history(limit=10)
280+
self.assertIsInstance(history, list)
281+
282+
def test_corpus_history_manager(self):
283+
"""Test CorpusHistoryManager functionality."""
284+
manager = CorpusHistoryManager(self.storage_manager, self.project_name)
285+
286+
# Test storing corpus stats
287+
corpus_data = {
288+
'timestamp': datetime.now().isoformat(),
289+
'project_name': self.project_name,
290+
'fuzzer_name': 'test_fuzzer',
291+
'corpus_size': 1000,
292+
'total_size_bytes': 5000000,
293+
'new_files_count': 50
294+
}
295+
296+
result = manager.store_corpus_stats(corpus_data)
297+
self.assertIsInstance(result, str)
298+
299+
# Test retrieving corpus stats
300+
stats = manager.get_corpus_stats(limit=10)
301+
self.assertIsInstance(stats, list)
302+
303+
304+
if __name__ == '__main__':
305+
unittest.main()

0 commit comments

Comments
 (0)