Skip to content

Commit 71f52c8

Browse files
authored
feat: Add (more) unit tests (#112)
* fix: upload of wheels * fix: upload of wheels * fix: upload of wheels * fix: upload of wheels * fix: upload of wheels * fix: upload of wheels * fix: upload of wheels * fix: upload of wheels * fix: upload of wheels * fix: upload of wheels * fix: upload of wheels * fix: upload of wheels * fix: upload of wheels * fix: upload of wheels * fix: Update test setup * fix: upload of wheels * fix: upload of wheels * fix: upload of wheels * fix: upload of wheels * fix: upload of wheels * fix: upload of wheels * fix: Test * fix: Add test * fix: Refactor tests * fix: Reader test * fix: Update readme * fix: Docs * fix: Refactor * fix: benchmark * fix: CLeaning up * fix: Stream tests * fix: Reader tests * fix: Builder double close test * fix: Remove debug logs * fix: Fix typo * fix: Stream exception * fix: Threaded reading * fix: Threading tests * fix: Interleaved thread test * fix: Threading * fix: More tests * fix: Test updates * fix: Test updates * fix: Add some async tests * fix: Test updates * fix: Test updates * fix: Test updates * fix: Test updates * fix: Add ingredients tests * fix: Test ingredient * fix: Test ingredient * fix: Add ingredient from stream * fix: Ingredient from stream * fix: Reorder test * fix: Thread * fix: Threading with asyncio * fix: RUn all the tests * fix: refactor tests * fix: refactor and format * fix: refactor and format 2 * fix: benchmark * fix: FOrmat * fix: Add test for contention * fix: Add test for contention 2 * fix: Threads * fix: Paths use and some timing * fix: autopep8 * fix: Format * fix: Undo test formating as json formatting seems touchy * fix: test_read_ingredient_file * fix: format * fix: format * fix: fix a bug found by tests * fix: Refactor sign_file to use new API internally, but keep the emthod for compatibility * fix: Clean up * fix: CLean up * fix: Remove debug code * fix: Ignore deprecation warnings in test logs * fix: One more clean up * fix: One more clean up * fix: Crypto up
1 parent 5c638bc commit 71f52c8

9 files changed

Lines changed: 2759 additions & 72 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ __pycache__/
55

66
/artifacts
77
scripts/artifacts
8+
tests/fixtures/temp_data
89

910

1011
# C extensions

Makefile

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,11 @@ rebuild: clean-c2pa-env install-deps download-native-artifacts build-python
3333
# Runs the unit tests
3434
test:
3535
python3 ./tests/test_unit_tests.py
36+
python3 ./tests/test_unit_tests_threaded.py
37+
38+
# Runs benchmarks in the venv
39+
benchmark:
40+
python -m pytest tests/benchmark.py -v
3641

3742
# Tests building and installing a local wheel package
3843
# Downloads required artifacts, builds the wheel, installs it, and verifies the installation
@@ -84,7 +89,7 @@ publish: release
8489

8590
# Formats Python source code using autopep8 with aggressive settings
8691
format:
87-
autopep8 --aggressive --aggressive --in-place src/c2pa/*.py
92+
autopep8 --aggressive --aggressive --in-place src/c2pa/**/*.py
8893

8994
# Downloads the required native artifacts for the specified version
9095
download-native-artifacts:

requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
# only used in the training example
2-
cryptography>=41.0.0
2+
cryptography>=45.0.3

src/c2pa/__init__.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@
1212
C2paSignerInfo,
1313
Signer,
1414
Stream,
15-
sdk_version
15+
sdk_version,
16+
read_ingredient_file
1617
) # NOQA
1718

1819
# Re-export C2paError and its subclasses
@@ -24,5 +25,6 @@
2425
'C2paSignerInfo',
2526
'Signer',
2627
'Stream',
27-
'sdk_version'
28+
'sdk_version',
29+
'read_ingredient_file'
2830
]

src/c2pa/c2pa.py

Lines changed: 96 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@
33
import json
44
import sys
55
import os
6+
import warnings
67
from pathlib import Path
78
from typing import Optional, Union, Callable, Any
89
import time
910
from .lib import dynamically_load_library
11+
import mimetypes
1012

1113
# Define required function names
1214
_REQUIRED_FUNCTIONS = [
@@ -16,7 +18,6 @@
1618
'c2pa_load_settings',
1719
'c2pa_read_file',
1820
'c2pa_read_ingredient_file',
19-
'c2pa_sign_file',
2021
'c2pa_reader_from_stream',
2122
'c2pa_reader_from_manifest_data_and_stream',
2223
'c2pa_reader_free',
@@ -247,13 +248,6 @@ def _setup_function(func, argtypes, restype=None):
247248
_setup_function(
248249
_lib.c2pa_read_ingredient_file, [
249250
ctypes.c_char_p, ctypes.c_char_p], ctypes.c_void_p)
250-
_setup_function(_lib.c2pa_sign_file,
251-
[ctypes.c_char_p,
252-
ctypes.c_char_p,
253-
ctypes.c_char_p,
254-
ctypes.POINTER(C2paSignerInfo),
255-
ctypes.c_char_p],
256-
ctypes.c_void_p)
257251

258252
# Set up Reader and Builder function prototypes
259253
_setup_function(_lib.c2pa_reader_from_stream,
@@ -496,7 +490,7 @@ def _parse_operation_result_for_error(
496490

497491
def sdk_version() -> str:
498492
"""
499-
Returns the c2pa-rs version string, e.g., "0.49.5".
493+
Returns the underlying c2pa-rs version string, e.g., "0.49.5".
500494
"""
501495
vstr = version()
502496
# Example: "c2pa-c/0.49.5 c2pa-rs/0.49.5"
@@ -536,52 +530,66 @@ def load_settings(settings: str, format: str = "json") -> None:
536530
raise C2paError(error)
537531

538532

539-
def read_file(path: Union[str, Path],
540-
data_dir: Optional[Union[str, Path]] = None) -> str:
541-
"""Read a C2PA manifest from a file.
533+
def read_ingredient_file(
534+
path: Union[str, Path], data_dir: Union[str, Path]) -> str:
535+
"""Read a C2PA ingredient from a file.
542536
543537
Args:
544538
path: Path to the file to read
545-
data_dir: Optional directory to write binary resources to
539+
data_dir: Directory to write binary resources to
546540
547541
Returns:
548-
The manifest as a JSON string
542+
The ingredient as a JSON string
549543
550544
Raises:
551545
C2paError: If there was an error reading the file
552546
"""
553547
container = _StringContainer()
554548

555549
container._path_str = str(path).encode('utf-8')
556-
container._data_dir_str = str(data_dir).encode(
557-
'utf-8') if data_dir else None
550+
container._data_dir_str = str(data_dir).encode('utf-8')
558551

559-
result = _lib.c2pa_read_file(container._path_str, container._data_dir_str)
552+
result = _lib.c2pa_read_ingredient_file(
553+
container._path_str, container._data_dir_str)
560554
return _parse_operation_result_for_error(result)
561555

562556

563-
def read_ingredient_file(
564-
path: Union[str, Path], data_dir: Optional[Union[str, Path]] = None) -> str:
565-
"""Read a C2PA ingredient from a file.
557+
def read_file(path: Union[str, Path],
558+
data_dir: Union[str, Path]) -> str:
559+
"""Read a C2PA manifest from a file.
560+
561+
.. deprecated:: 0.10.0
562+
This function is deprecated and will be removed in a future version.
563+
Please use the Reader class for reading C2PA metadata instead.
564+
Example:
565+
```python
566+
with Reader(path) as reader:
567+
manifest_json = reader.json()
568+
```
566569
567570
Args:
568571
path: Path to the file to read
569-
data_dir: Optional directory to write binary resources to
572+
data_dir: Directory to write binary resources to
570573
571574
Returns:
572-
The ingredient as a JSON string
575+
The manifest as a JSON string
573576
574577
Raises:
575578
C2paError: If there was an error reading the file
576579
"""
580+
warnings.warn(
581+
"The read_file function is deprecated and will be removed in a future version. "
582+
"Please use the Reader class for reading C2PA metadata instead.",
583+
DeprecationWarning,
584+
stacklevel=2
585+
)
586+
577587
container = _StringContainer()
578588

579589
container._path_str = str(path).encode('utf-8')
580-
container._data_dir_str = str(data_dir).encode(
581-
'utf-8') if data_dir else None
590+
container._data_dir_str = str(data_dir).encode('utf-8')
582591

583-
result = _lib.c2pa_read_ingredient_file(
584-
container._path_str, container._data_dir_str)
592+
result = _lib.c2pa_read_file(container._path_str, container._data_dir_str)
585593
return _parse_operation_result_for_error(result)
586594

587595

@@ -593,6 +601,11 @@ def sign_file(
593601
data_dir: Optional[Union[str, Path]] = None
594602
) -> str:
595603
"""Sign a file with a C2PA manifest.
604+
For now, this function is left here to provide a backwards-compatible API.
605+
606+
.. deprecated:: 0.10.0
607+
This function is deprecated and will be removed in a future version.
608+
Please use the Builder class for signing and the Reader class for reading signed data instead.
596609
597610
Args:
598611
source_path: Path to the source file
@@ -602,31 +615,68 @@ def sign_file(
602615
data_dir: Optional directory to write binary resources to
603616
604617
Returns:
605-
Result information as a JSON string
618+
The signed manifest as a JSON string
606619
607620
Raises:
608621
C2paError: If there was an error signing the file
609622
C2paError.Encoding: If any of the string inputs contain invalid UTF-8 characters
623+
C2paError.NotSupported: If the file type cannot be determined
610624
"""
611-
# Store encoded strings as attributes of signer_info to keep them alive
612-
try:
613-
signer_info._source_str = str(source_path).encode('utf-8')
614-
signer_info._dest_str = str(dest_path).encode('utf-8')
615-
signer_info._manifest_str = manifest.encode('utf-8')
616-
signer_info._data_dir_str = str(data_dir).encode(
617-
'utf-8') if data_dir else None
618-
except UnicodeError as e:
619-
raise C2paError.Encoding(
620-
f"Invalid UTF-8 characters in input strings: {str(e)}")
621-
622-
result = _lib.c2pa_sign_file(
623-
signer_info._source_str,
624-
signer_info._dest_str,
625-
signer_info._manifest_str,
626-
ctypes.byref(signer_info),
627-
signer_info._data_dir_str
625+
warnings.warn(
626+
"The sign_file function is deprecated and will be removed in a future version. "
627+
"Please use the Builder class for signing and the Reader class for reading signed data instead.",
628+
DeprecationWarning,
629+
stacklevel=2
628630
)
629-
return _parse_operation_result_for_error(result)
631+
632+
try:
633+
# Create a signer from the signer info
634+
signer = Signer.from_info(signer_info)
635+
636+
# Create a builder from the manifest
637+
builder = Builder(manifest)
638+
639+
# Open source and destination files
640+
with open(source_path, 'rb') as source_file, open(dest_path, 'wb') as dest_file:
641+
# Get the MIME type from the file extension
642+
mime_type = mimetypes.guess_type(str(source_path))[0]
643+
if not mime_type:
644+
raise C2paError.NotSupported(f"Could not determine MIME type for file: {source_path}")
645+
646+
# Sign the file using the builder
647+
manifest_bytes = builder.sign(
648+
signer=signer,
649+
format=mime_type,
650+
source=source_file,
651+
dest=dest_file
652+
)
653+
654+
# If we have manifest bytes and a data directory, write them
655+
if manifest_bytes and data_dir:
656+
manifest_path = os.path.join(str(data_dir), 'manifest.json')
657+
with open(manifest_path, 'wb') as f:
658+
f.write(manifest_bytes)
659+
660+
# Read the signed manifest from the destination file
661+
with Reader(dest_path) as reader:
662+
return reader.json()
663+
664+
except Exception as e:
665+
# Clean up destination file if it exists and there was an error
666+
if os.path.exists(dest_path):
667+
try:
668+
os.remove(dest_path)
669+
except OSError:
670+
pass # Ignore cleanup errors
671+
672+
# Re-raise the error
673+
raise C2paError(f"Error signing file: {str(e)}")
674+
finally:
675+
# Ensure resources are cleaned up
676+
if 'builder' in locals():
677+
builder.close()
678+
if 'signer' in locals():
679+
signer.close()
630680

631681

632682
class Stream:
@@ -960,7 +1010,7 @@ def __init__(self,
9601010

9611011
path = str(format_or_path)
9621012
mime_type = mimetypes.guess_type(
963-
path)[0] or 'application/octet-stream'
1013+
path)[0]
9641014

9651015
# Keep mime_type string alive
9661016
try:

0 commit comments

Comments
 (0)