-
Notifications
You must be signed in to change notification settings - Fork 10
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Refactor integration tests to use persistent buckets #379
Changes from 5 commits
d7ebaae
fa3158b
184580e
cdaa527
560a58a
749aa47
4193185
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
Improve internal testing infrastructure by updating integration tests to use persistent buckets. |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -10,8 +10,14 @@ | |
from __future__ import annotations | ||
|
||
import os | ||
import re | ||
import secrets | ||
import sys | ||
import time | ||
|
||
from b2sdk._internal.b2http import B2Http | ||
from b2sdk._internal.file_lock import NO_RETENTION_FILE_SETTING | ||
from b2sdk._internal.raw_api import REALM_URLS, B2RawHTTPApi | ||
from b2sdk.v2 import ( | ||
BUCKET_NAME_CHARS_UNIQ, | ||
BUCKET_NAME_LENGTH_RANGE, | ||
|
@@ -45,3 +51,87 @@ def authorize(b2_auth_data, api_config=DEFAULT_HTTP_API_CONFIG): | |
realm = os.environ.get('B2_TEST_ENVIRONMENT', 'production') | ||
b2_api.authorize_account(realm, *b2_auth_data) | ||
return b2_api, info | ||
|
||
|
||
def authorize_raw_api(raw_api): | ||
application_key_id = os.environ.get('B2_TEST_APPLICATION_KEY_ID') | ||
if application_key_id is None: | ||
print('B2_TEST_APPLICATION_KEY_ID is not set.', file=sys.stderr) | ||
sys.exit(1) | ||
|
||
application_key = os.environ.get('B2_TEST_APPLICATION_KEY') | ||
if application_key is None: | ||
print('B2_TEST_APPLICATION_KEY is not set.', file=sys.stderr) | ||
sys.exit(1) | ||
|
||
realm = os.environ.get('B2_TEST_ENVIRONMENT', 'production') | ||
realm_url = REALM_URLS.get(realm, realm) | ||
auth_dict = raw_api.authorize_account(realm_url, application_key_id, application_key) | ||
return auth_dict | ||
|
||
|
||
def cleanup_old_buckets(): | ||
raw_api = B2RawHTTPApi(B2Http()) | ||
auth_dict = authorize_raw_api(raw_api) | ||
bucket_list_dict = raw_api.list_buckets( | ||
auth_dict['apiUrl'], auth_dict['authorizationToken'], auth_dict['accountId'] | ||
) | ||
_cleanup_old_buckets(raw_api, auth_dict, bucket_list_dict) | ||
|
||
|
||
def _cleanup_old_buckets(raw_api, auth_dict, bucket_list_dict): | ||
for bucket_dict in bucket_list_dict['buckets']: | ||
bucket_id = bucket_dict['bucketId'] | ||
bucket_name = bucket_dict['bucketName'] | ||
if _should_delete_bucket(bucket_name): | ||
print('cleaning up old bucket: ' + bucket_name) | ||
_clean_and_delete_bucket( | ||
raw_api, | ||
auth_dict['apiUrl'], | ||
auth_dict['authorizationToken'], | ||
auth_dict['accountId'], | ||
bucket_id, | ||
) | ||
|
||
|
||
def _clean_and_delete_bucket(raw_api, api_url, account_auth_token, account_id, bucket_id): | ||
# Delete the files. This test never creates more than a few files, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This test? which test? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It seems like this is still tightly tied with raw-api tests - not sure why it was moved to general helpers if its only useful in raw-api tests. You seem to have created a def test_bucket(...):
...
yield bucket
_clean_and_delete_bucket(..., bucket) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Context got lost when I moved it from It's also called, albeit indirectly, by |
||
# so one call to list_file_versions should get them all. | ||
versions_dict = raw_api.list_file_versions(api_url, account_auth_token, bucket_id) | ||
for version_dict in versions_dict['files']: | ||
file_id = version_dict['fileId'] | ||
file_name = version_dict['fileName'] | ||
action = version_dict['action'] | ||
if action in ['hide', 'upload']: | ||
print('b2_delete_file', file_name, action) | ||
if action == 'upload' and version_dict[ | ||
'fileRetention'] and version_dict['fileRetention']['value']['mode'] is not None: | ||
raw_api.update_file_retention( | ||
api_url, | ||
account_auth_token, | ||
file_id, | ||
file_name, | ||
NO_RETENTION_FILE_SETTING, | ||
bypass_governance=True | ||
) | ||
raw_api.delete_file_version(api_url, account_auth_token, file_id, file_name) | ||
else: | ||
print('b2_cancel_large_file', file_name) | ||
raw_api.cancel_large_file(api_url, account_auth_token, file_id) | ||
|
||
# Delete the bucket | ||
print('b2_delete_bucket', bucket_id) | ||
raw_api.delete_bucket(api_url, account_auth_token, account_id, bucket_id) | ||
|
||
|
||
def _should_delete_bucket(bucket_name): | ||
# Bucket names for this test look like: c7b22d0b0ad7-1460060364-5670 | ||
# Other buckets should not be deleted. | ||
match = re.match(r'^test-raw-api-[a-f0-9]+-([0-9]+)-([0-9]+)', bucket_name) | ||
if match is None: | ||
return False | ||
|
||
# Is it more than an hour old? | ||
bucket_time = int(match.group(1)) | ||
now = time.time() | ||
return bucket_time + 3600 <= now |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,89 @@ | ||
###################################################################### | ||
# | ||
# File: test/integration/persistent_bucket.py | ||
# | ||
# Copyright 2024 Backblaze Inc. All Rights Reserved. | ||
# | ||
# License https://www.backblaze.com/using_b2_code.html | ||
# | ||
###################################################################### | ||
import hashlib | ||
import os | ||
import uuid | ||
from dataclasses import dataclass | ||
from functools import cached_property | ||
from test.integration.helpers import BUCKET_NAME_LENGTH | ||
|
||
from b2sdk._internal.bucket import Bucket | ||
from b2sdk.v2 import B2Api | ||
from b2sdk.v2.exception import NonExistentBucket | ||
|
||
PERSISTENT_BUCKET_NAME_PREFIX = "constst" | ||
|
||
|
||
@dataclass | ||
class PersistentBucketAggregate: | ||
bucket: Bucket | ||
|
||
def __post_init__(self): | ||
self.subfolder = self.new_subfolder() | ||
|
||
@property | ||
def bucket_name(self) -> str: | ||
return self.bucket.name | ||
|
||
def new_subfolder(self) -> str: | ||
return f"test-{uuid.uuid4().hex[:8]}" | ||
|
||
@property | ||
def bucket_id(self): | ||
return self.bucket.id_ | ||
|
||
@cached_property | ||
def b2_uri(self): | ||
return f"b2://{self.bucket_name}/{self.subfolder}" | ||
|
||
|
||
def hash_dict_sha256(d): | ||
""" | ||
Create a sha256 hash of the given dictionary. | ||
""" | ||
dict_repr = repr(sorted((k, repr(v)) for k, v in d.items())) | ||
hash_obj = hashlib.sha256() | ||
hash_obj.update(dict_repr.encode('utf-8')) | ||
return hash_obj.hexdigest() | ||
|
||
|
||
def get_persistent_bucket_name(b2_api: B2Api, create_options: dict) -> str: | ||
""" | ||
Create a hash of the `create_options` dictionary, include it in the bucket name | ||
so that we can easily reuse buckets with the same options across (parallel) test runs. | ||
""" | ||
# Exclude sensitive options from the hash | ||
unsafe_options = {"authorizationToken", "accountId", "default_server_side_encryption"} | ||
create_options_hashable = {k: v for k, v in create_options.items() if k not in unsafe_options} | ||
hashed_options = hash_dict_sha256(create_options_hashable) | ||
bucket_owner = os.environ.get("GITHUB_REPOSITORY_ID", b2_api.get_account_id()) | ||
bucket_base = f"{bucket_owner}:{hashed_options}" | ||
bucket_hash = hashlib.sha256(bucket_base.encode()).hexdigest() | ||
return f"{PERSISTENT_BUCKET_NAME_PREFIX}-{bucket_hash}" [:BUCKET_NAME_LENGTH] | ||
|
||
|
||
def get_or_create_persistent_bucket(b2_api: B2Api, **create_options) -> Bucket: | ||
bucket_name = get_persistent_bucket_name(b2_api, create_options.copy()) | ||
try: | ||
bucket = b2_api.get_bucket_by_name(bucket_name) | ||
except NonExistentBucket: | ||
bucket = b2_api.create_bucket( | ||
bucket_name, | ||
bucket_type="allPublic", | ||
lifecycle_rules=[ | ||
{ | ||
"daysFromHidingToDeleting": 1, | ||
"daysFromUploadingToHiding": 1, | ||
"fileNamePrefix": "", | ||
} | ||
], | ||
**create_options, | ||
) | ||
return bucket |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
use
b2_auth_data
fixture instead