Skip to content

Commit 2e713fa

Browse files
committed
fix: Threaded settings tests
1 parent 324bb81 commit 2e713fa

File tree

2 files changed

+189
-2
lines changed

2 files changed

+189
-2
lines changed

tests/test_unit_tests.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1124,7 +1124,7 @@ def test_builder_sign_with_setting_no_thumbnail_and_ingredient(self):
11241124

11251125
builder.close()
11261126

1127-
# Settings are global, so we reset to the default "true" here
1127+
# Settings are thread-local, so we reset to the default "true" here
11281128
load_settings('{"builder": { "thumbnail": {"enabled": true}}}')
11291129

11301130
def test_builder_sign_with_duplicate_ingredient(self):

tests/test_unit_tests_threaded.py

Lines changed: 188 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
import random
2323

2424
from c2pa import Builder, C2paError as Error, Reader, C2paSigningAlg as SigningAlg, C2paSignerInfo, Signer, sdk_version
25-
from c2pa.c2pa import Stream
25+
from c2pa.c2pa import Stream, load_settings
2626

2727
PROJECT_PATH = os.getcwd()
2828
FIXTURES_FOLDER = os.path.join(os.path.dirname(__file__), "fixtures")
@@ -1837,5 +1837,192 @@ def thread_work(thread_id):
18371837
other_manifest["active_manifest"],
18381838
f"Thread {thread_id} and {other_thread_id} share the same active manifest ID")
18391839

1840+
def test_builder_sign_with_setting_no_thumbnail_and_ingredient(self):
1841+
"""Test Builder class operations with thumbnail disabled and ingredient added using multiple threads."""
1842+
# Thread synchronization
1843+
thread_results = {}
1844+
completed_threads = 0
1845+
thread_lock = threading.Lock()
1846+
settings_lock = threading.Lock() # Lock for settings changes
1847+
1848+
def thread_work(thread_id):
1849+
nonlocal completed_threads
1850+
try:
1851+
# Create a new builder for this thread
1852+
builder = Builder.from_json(self.manifestDefinition)
1853+
assert builder._builder is not None
1854+
1855+
# Thread-safe settings change (settings are thread-local)
1856+
with settings_lock:
1857+
# The following removes the manifest's thumbnail
1858+
load_settings('{"builder": { "thumbnail": {"enabled": false}}}')
1859+
1860+
# Test adding ingredient
1861+
ingredient_json = f'{{ "title": "Test Ingredient Thread {thread_id}" }}'
1862+
with open(self.testPath3, 'rb') as f:
1863+
builder.add_ingredient(ingredient_json, "image/jpeg", f)
1864+
1865+
with open(self.testPath2, "rb") as file:
1866+
output = io.BytesIO(bytearray())
1867+
builder.sign(self.signer, "image/jpeg", file, output)
1868+
output.seek(0)
1869+
reader = Reader("image/jpeg", output)
1870+
json_data = reader.json()
1871+
manifest_data = json.loads(json_data)
1872+
1873+
# Store results for verification
1874+
with thread_lock:
1875+
thread_results[thread_id] = {
1876+
'manifest': manifest_data,
1877+
'thread_id': thread_id
1878+
}
1879+
1880+
builder.close()
1881+
1882+
except Exception as e:
1883+
with thread_lock:
1884+
thread_results[thread_id] = {
1885+
'error': str(e),
1886+
'thread_id': thread_id
1887+
}
1888+
finally:
1889+
with thread_lock:
1890+
completed_threads += 1
1891+
1892+
# Create and start multiple threads
1893+
threads = []
1894+
num_threads = 3
1895+
for i in range(1, num_threads + 1):
1896+
thread = threading.Thread(target=thread_work, args=(i,))
1897+
threads.append(thread)
1898+
thread.start()
1899+
1900+
# Wait for all threads to complete
1901+
for thread in threads:
1902+
thread.join()
1903+
1904+
# Verify all threads completed
1905+
self.assertEqual(completed_threads, num_threads, f"All {num_threads} threads should have completed")
1906+
self.assertEqual(len(thread_results), num_threads, f"Should have results from all {num_threads} threads")
1907+
1908+
# Verify results for each thread
1909+
for thread_id in range(1, num_threads + 1):
1910+
result = thread_results[thread_id]
1911+
1912+
# Check if thread encountered an error
1913+
if 'error' in result:
1914+
self.fail(f"Thread {thread_id} failed with error: {result['error']}")
1915+
1916+
manifest_data = result['manifest']
1917+
1918+
# Verify active manifest exists
1919+
self.assertIn("active_manifest", manifest_data)
1920+
active_manifest_id = manifest_data["active_manifest"]
1921+
1922+
# Verify active manifest object exists
1923+
self.assertIn("manifests", manifest_data)
1924+
self.assertIn(active_manifest_id, manifest_data["manifests"])
1925+
active_manifest = manifest_data["manifests"][active_manifest_id]
1926+
1927+
# There should be no thumbnail anymore here
1928+
self.assertNotIn("thumbnail", active_manifest)
1929+
1930+
# Verify ingredients array exists in active manifest
1931+
self.assertIn("ingredients", active_manifest)
1932+
self.assertIsInstance(active_manifest["ingredients"], list)
1933+
self.assertTrue(len(active_manifest["ingredients"]) > 0)
1934+
1935+
# Verify the first ingredient's title matches what we set for this thread
1936+
first_ingredient = active_manifest["ingredients"][0]
1937+
expected_title = f"Test Ingredient Thread {thread_id}"
1938+
self.assertEqual(first_ingredient["title"], expected_title)
1939+
self.assertNotIn("thumbnail", first_ingredient)
1940+
1941+
# Settings are thread-local, so we reset to the default "true" here
1942+
with settings_lock:
1943+
load_settings('{"builder": { "thumbnail": {"enabled": true}}}')
1944+
1945+
def test_builder_sign_with_setting_no_thumbnail_and_ingredient_async(self):
1946+
"""Test Builder class operations with thumbnail disabled and ingredient added using async tasks."""
1947+
async def async_thread_work(task_id):
1948+
try:
1949+
# Create a new builder for this task
1950+
builder = Builder.from_json(self.manifestDefinition)
1951+
assert builder._builder is not None
1952+
1953+
# The following removes the manifest's thumbnail
1954+
load_settings('{"builder": { "thumbnail": {"enabled": false}}}')
1955+
1956+
# Test adding ingredient
1957+
ingredient_json = f'{{ "title": "Test Ingredient Async Task {task_id}" }}'
1958+
with open(self.testPath3, 'rb') as f:
1959+
builder.add_ingredient(ingredient_json, "image/jpeg", f)
1960+
1961+
with open(self.testPath2, "rb") as file:
1962+
output = io.BytesIO(bytearray())
1963+
builder.sign(self.signer, "image/jpeg", file, output)
1964+
output.seek(0)
1965+
reader = Reader("image/jpeg", output)
1966+
json_data = reader.json()
1967+
manifest_data = json.loads(json_data)
1968+
1969+
# Verify active manifest exists
1970+
self.assertIn("active_manifest", manifest_data)
1971+
active_manifest_id = manifest_data["active_manifest"]
1972+
1973+
# Verify active manifest object exists
1974+
self.assertIn("manifests", manifest_data)
1975+
self.assertIn(active_manifest_id, manifest_data["manifests"])
1976+
active_manifest = manifest_data["manifests"][active_manifest_id]
1977+
1978+
# There should be no thumbnail anymore here
1979+
self.assertNotIn("thumbnail", active_manifest)
1980+
1981+
# Verify ingredients array exists in active manifest
1982+
self.assertIn("ingredients", active_manifest)
1983+
self.assertIsInstance(active_manifest["ingredients"], list)
1984+
self.assertTrue(len(active_manifest["ingredients"]) > 0)
1985+
1986+
# Verify the first ingredient's title matches what we set for this task
1987+
first_ingredient = active_manifest["ingredients"][0]
1988+
expected_title = f"Test Ingredient Async Task {task_id}"
1989+
self.assertEqual(first_ingredient["title"], expected_title)
1990+
self.assertNotIn("thumbnail", first_ingredient)
1991+
1992+
builder.close()
1993+
return None # Success case
1994+
1995+
except Exception as e:
1996+
return f"Async task {task_id} error: {str(e)}"
1997+
1998+
async def run_async_tests():
1999+
# Create multiple async tasks
2000+
tasks = []
2001+
num_tasks = 3
2002+
for i in range(1, num_tasks + 1):
2003+
task = asyncio.create_task(async_thread_work(i))
2004+
tasks.append(task)
2005+
2006+
# Wait for all tasks to complete and collect results
2007+
results = await asyncio.gather(*tasks, return_exceptions=True)
2008+
2009+
# Process results
2010+
errors = []
2011+
for i, result in enumerate(results, 1):
2012+
if isinstance(result, Exception):
2013+
errors.append(f"Async task {i} failed with exception: {str(result)}")
2014+
elif result: # Non-None result indicates an error
2015+
errors.append(result)
2016+
2017+
# If any errors occurred, fail the test with all error messages
2018+
if errors:
2019+
self.fail("\n".join(errors))
2020+
2021+
# Run the async tests
2022+
asyncio.run(run_async_tests())
2023+
2024+
# Settings are thread-local, so we reset to the default "true" here
2025+
load_settings('{"builder": { "thumbnail": {"enabled": true}}}')
2026+
18402027
if __name__ == '__main__':
18412028
unittest.main()

0 commit comments

Comments
 (0)