|
22 | 22 | import random |
23 | 23 |
|
24 | 24 | 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 |
26 | 26 |
|
27 | 27 | PROJECT_PATH = os.getcwd() |
28 | 28 | FIXTURES_FOLDER = os.path.join(os.path.dirname(__file__), "fixtures") |
@@ -1837,5 +1837,192 @@ def thread_work(thread_id): |
1837 | 1837 | other_manifest["active_manifest"], |
1838 | 1838 | f"Thread {thread_id} and {other_thread_id} share the same active manifest ID") |
1839 | 1839 |
|
| 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 | + |
1840 | 2027 | if __name__ == '__main__': |
1841 | 2028 | unittest.main() |
0 commit comments