Skip to content

Commit 89f4fdf

Browse files
authored
Merge pull request Backblaze#812 from Backblaze/fix-integration-tests
Fix integration tests
2 parents 28e4aba + 4a2a21b commit 89f4fdf

File tree

9 files changed

+367
-118
lines changed

9 files changed

+367
-118
lines changed

.github/workflows/ci.yml

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,31 @@ jobs:
4747
run: python -m pip install --upgrade nox pip setuptools
4848
- name: Build the distribution
4949
run: nox -vs build
50-
test:
50+
cleanup_buckets:
5151
needs: lint
52+
env:
53+
B2_TEST_APPLICATION_KEY: ${{ secrets.B2_TEST_APPLICATION_KEY }}
54+
B2_TEST_APPLICATION_KEY_ID: ${{ secrets.B2_TEST_APPLICATION_KEY_ID }}
55+
runs-on: ubuntu-latest
56+
steps:
57+
- uses: actions/checkout@v2
58+
if: ${{ env.B2_TEST_APPLICATION_KEY != '' && env.B2_TEST_APPLICATION_KEY_ID != '' }} # TODO: skip this whole job instead
59+
with:
60+
fetch-depth: 0
61+
- name: Set up Python ${{ env.PYTHON_DEFAULT_VERSION }}
62+
if: ${{ env.B2_TEST_APPLICATION_KEY != '' && env.B2_TEST_APPLICATION_KEY_ID != '' }} # TODO: skip this whole job instead
63+
uses: actions/setup-python@v3
64+
with:
65+
python-version: ${{ env.PYTHON_DEFAULT_VERSION }}
66+
cache: "pip"
67+
- name: Install dependencies
68+
if: ${{ env.B2_TEST_APPLICATION_KEY != '' && env.B2_TEST_APPLICATION_KEY_ID != '' }} # TODO: skip this whole job instead
69+
run: python -m pip install --upgrade nox pip setuptools
70+
- name: Find and remove old buckets
71+
if: ${{ env.B2_TEST_APPLICATION_KEY != '' && env.B2_TEST_APPLICATION_KEY_ID != '' }} # TODO: skip this whole job instead
72+
run: nox -vs cleanup_buckets
73+
test:
74+
needs: cleanup_buckets
5275
env:
5376
B2_TEST_APPLICATION_KEY: ${{ secrets.B2_TEST_APPLICATION_KEY }}
5477
B2_TEST_APPLICATION_KEY_ID: ${{ secrets.B2_TEST_APPLICATION_KEY_ID }}
@@ -79,7 +102,7 @@ jobs:
79102
if: ${{ env.B2_TEST_APPLICATION_KEY != '' && env.B2_TEST_APPLICATION_KEY_ID != '' }}
80103
run: nox -vs integration -- --cleanup
81104
test-linux-bundle:
82-
needs: lint
105+
needs: cleanup_buckets
83106
env:
84107
B2_TEST_APPLICATION_KEY: ${{ secrets.B2_TEST_APPLICATION_KEY }}
85108
B2_TEST_APPLICATION_KEY_ID: ${{ secrets.B2_TEST_APPLICATION_KEY_ID }}
@@ -111,7 +134,7 @@ jobs:
111134
if-no-files-found: warn
112135
retention-days: 7
113136
test-macos-and-windows-bundle:
114-
needs: lint
137+
needs: cleanup_buckets
115138
env:
116139
B2_TEST_APPLICATION_KEY: ${{ secrets.B2_TEST_APPLICATION_KEY }}
117140
B2_TEST_APPLICATION_KEY_ID: ${{ secrets.B2_TEST_APPLICATION_KEY_ID }}

CHANGELOG.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
88

99
As in 3.4.0, replication support may be unstable, however no backward-incompatible
1010
changes are currently planned.
11-
This version is pinned strictly to `b2-sdk-python==1.17.0` for the same reason.
11+
This version is pinned strictly to `b2-sdk-python==1.17.3` for the same reason.
1212

1313
### Added
1414
* Add `--write-buffer-size` parameter
@@ -17,6 +17,10 @@ This version is pinned strictly to `b2-sdk-python==1.17.0` for the same reason.
1717

1818
### Infrastructure
1919
* Try not to crash tests due to bucket name collision
20+
* Fix replication integration tests
21+
* Fix leaking buckets in integration tests
22+
* Limit number of workers for integration tests to 1 for now
23+
* Make integration tests remove buckets only based on name, not based on creation time
2024
* Add dependabot configuration
2125

2226
## [3.4.0] - 2022-05-04

b2/console_tool.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2472,7 +2472,7 @@ def _setup_parser(cls, parser):
24722472
super()._setup_parser(parser)
24732473
parser.add_argument('source', metavar='SOURCE_BUCKET_NAME')
24742474
parser.add_argument('--rule', metavar='REPLICATION_RULE_NAME', default=None)
2475-
parser.add_argument('--destination-profile', required=True)
2475+
parser.add_argument('--destination-profile')
24762476
parser.add_argument('--dont-scan-destination', action='store_true')
24772477
parser.add_argument(
24782478
'--output-format', default='console', choices=('console', 'json', 'csv')

noxfile.py

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
import subprocess
1414

1515
from glob import glob
16-
from multiprocessing import cpu_count
1716

1817
import nox
1918

@@ -36,7 +35,7 @@
3635
"pytest==6.2.5",
3736
"pytest-cov==3.0.0",
3837
'pytest-xdist==2.5.0',
39-
'filelock==3.6.0',
38+
'backoff==2.1.2',
4039
]
4140
REQUIREMENTS_BUILD = ['setuptools>=20.2']
4241
REQUIREMENTS_BUNDLE = [
@@ -73,13 +72,14 @@ def install_myself(session, extras=None):
7372
if extras:
7473
arg += '[%s]' % ','.join(extras)
7574

75+
session.install('-e', arg)
76+
7677
if INSTALL_SDK_FROM:
7778
cwd = os.getcwd()
7879
os.chdir(INSTALL_SDK_FROM)
7980
session.run('pip', 'uninstall', 'b2sdk', '-y')
8081
session.run('python', 'setup.py', 'develop')
8182
os.chdir(cwd)
82-
session.install('-e', arg)
8383

8484

8585
@nox.session(name='format', python=PYTHON_DEFAULT_VERSION)
@@ -156,9 +156,8 @@ def integration(session):
156156
"""Run integration tests."""
157157
install_myself(session)
158158
session.install(*REQUIREMENTS_TEST)
159-
session.run(
160-
'pytest', '-s', '-n', str(min(cpu_count(), 8) * 5), *session.posargs, 'test/integration'
161-
)
159+
#session.run('pytest', '-s', '-x', '-v', '-n', '4', *session.posargs, 'test/integration')
160+
session.run('pytest', '-s', '-x', '-v', *session.posargs, 'test/integration')
162161

163162

164163
@nox.session(python=PYTHON_VERSIONS)
@@ -172,6 +171,14 @@ def test(session):
172171
session.notify('integration')
173172

174173

174+
@nox.session(python=PYTHON_DEFAULT_VERSION)
175+
def cleanup_buckets(session):
176+
"""Remove buckets from previous test runs."""
177+
install_myself(session)
178+
session.install(*REQUIREMENTS_TEST)
179+
session.run('pytest', '-s', '-x', *session.posargs, 'test/integration/cleanup_buckets.py')
180+
181+
175182
@nox.session
176183
def cover(session):
177184
"""Perform coverage analysis."""

requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
arrow>=1.0.2,<2.0.0
2-
b2sdk==1.17.2
2+
b2sdk==1.17.3
33
docutils==0.16
44
idna>=2.2.0; platform_system == 'Java'
55
importlib-metadata>=3.3.0; python_version < '3.8'
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
######################################################################
2+
#
3+
# File: test/integration/cleanup_buckets.py
4+
#
5+
# Copyright 2022 Backblaze Inc. All Rights Reserved.
6+
#
7+
# License https://www.backblaze.com/using_b2_code.html
8+
#
9+
######################################################################
10+
11+
12+
def test_cleanup_buckets(b2_api):
13+
# this is not a test, but it is intended to be called
14+
# via pytest because it reuses fixtures which have everything
15+
# set up
16+
b2_api.clean_buckets()

test/integration/conftest.py

Lines changed: 30 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,22 @@
77
# License https://www.backblaze.com/using_b2_code.html
88
#
99
######################################################################
10+
11+
import contextlib
1012
import sys
1113

1214
from os import environ, path
13-
from pathlib import Path
14-
from tempfile import TemporaryDirectory, gettempdir
15+
from tempfile import TemporaryDirectory
1516

1617
import pytest
1718

1819
from b2sdk.v2 import B2_ACCOUNT_INFO_ENV_VAR, XDG_CONFIG_HOME_ENV_VAR
19-
from filelock import FileLock
20+
from b2sdk.exception import BucketIdNotFound
2021

2122
from .helpers import Api, CommandLine, bucket_name_part
2223

24+
GENERAL_BUCKET_NAME_PREFIX = 'clitst'
25+
2326

2427
@pytest.hookimpl
2528
def pytest_addoption(parser):
@@ -50,17 +53,31 @@ def realm() -> str:
5053

5154
@pytest.fixture(scope='function')
5255
def bucket_name(b2_api) -> str:
53-
yield b2_api.create_bucket().name
56+
bucket = b2_api.create_bucket()
57+
yield bucket.name
58+
with contextlib.suppress(BucketIdNotFound):
59+
b2_api.clean_bucket(bucket)
60+
61+
62+
@pytest.fixture(scope='function') # , autouse=True)
63+
def debug_print_buckets(b2_api):
64+
print('-' * 30)
65+
print('Buckets before test ' + environ['PYTEST_CURRENT_TEST'])
66+
num_buckets = b2_api.count_and_print_buckets()
67+
print('-' * 30)
68+
try:
69+
yield
70+
finally:
71+
print('-' * 30)
72+
print('Buckets after test ' + environ['PYTEST_CURRENT_TEST'])
73+
delta = b2_api.count_and_print_buckets() - num_buckets
74+
print(f'DELTA: {delta}')
75+
print('-' * 30)
5476

5577

5678
@pytest.fixture(scope='session')
57-
def general_bucket_name_prefix() -> str:
58-
yield 'clitst'
59-
60-
61-
@pytest.fixture(scope='session')
62-
def this_run_bucket_name_prefix(general_bucket_name_prefix) -> str:
63-
yield general_bucket_name_prefix + bucket_name_part(8)
79+
def this_run_bucket_name_prefix() -> str:
80+
yield GENERAL_BUCKET_NAME_PREFIX + bucket_name_part(8)
6481

6582

6683
@pytest.fixture(scope='module')
@@ -91,45 +108,13 @@ def auto_change_account_info_dir(monkey_patch) -> dir:
91108

92109

93110
@pytest.fixture(scope='module')
94-
def b2_api(
95-
application_key_id, application_key, realm, general_bucket_name_prefix,
96-
this_run_bucket_name_prefix
97-
) -> Api:
111+
def b2_api(application_key_id, application_key, realm, this_run_bucket_name_prefix) -> Api:
98112
yield Api(
99-
application_key_id, application_key, realm, general_bucket_name_prefix,
113+
application_key_id, application_key, realm, GENERAL_BUCKET_NAME_PREFIX,
100114
this_run_bucket_name_prefix
101115
)
102116

103117

104-
@pytest.fixture(scope='module', autouse=True)
105-
def auto_clean_buckets(b2_api, request, testrun_uid):
106-
""" Automatically clean buckets before and after the whole module testing """
107-
108-
lock_file = Path(gettempdir()) / f'{testrun_uid}.lock'
109-
110-
# lock file cannot be used to store info - use another file to track # of active workers
111-
worker_count_file = Path(gettempdir()) / f'{testrun_uid}.active-workers-count.txt'
112-
113-
with FileLock(str(lock_file)):
114-
if not worker_count_file.is_file():
115-
b2_api.clean_buckets()
116-
worker_count_file.write_text('1')
117-
118-
else:
119-
worker_count = int(worker_count_file.read_text())
120-
worker_count_file.write_text(str(worker_count + 1))
121-
122-
yield
123-
124-
if request.config.getoption('--cleanup'):
125-
with FileLock(str(lock_file)):
126-
worker_count = int(worker_count_file.read_text())
127-
worker_count_file.write_text(str(worker_count - 1))
128-
if worker_count == 1:
129-
b2_api.clean_buckets()
130-
worker_count_file.unlink()
131-
132-
133118
@pytest.fixture(scope='module')
134119
def b2_tool(
135120
request, application_key_id, application_key, realm, this_run_bucket_name_prefix

test/integration/helpers.py

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,16 +20,21 @@
2020
import threading
2121

2222
from dataclasses import dataclass
23+
from datetime import datetime
2324
from os import environ, linesep, path
2425
from pathlib import Path
2526
from tempfile import gettempdir, mkdtemp
2627
from typing import List, Optional, Union
2728

29+
import backoff
30+
31+
from b2sdk._v3.exception import BucketIdNotFound as v3BucketIdNotFound
2832
from b2sdk.v2 import ALL_CAPABILITIES, NO_RETENTION_FILE_SETTING, B2Api, Bucket, EncryptionAlgorithm, EncryptionKey, EncryptionMode, EncryptionSetting, InMemoryAccountInfo, InMemoryCache, LegalHold, RetentionMode, SqliteAccountInfo, fix_windows_path_limit
29-
from b2sdk.v2.exception import BucketIdNotFound, DuplicateBucketName, FileNotPresent
33+
from b2sdk.v2.exception import BucketIdNotFound, DuplicateBucketName, FileNotPresent, TooManyRequests
3034

3135
from b2.console_tool import Command, current_time_millis
3236

37+
BUCKET_CLEANUP_PERIOD_MILLIS = 0
3338
ONE_HOUR_MILLIS = 60 * 60 * 1000
3439
ONE_DAY_MILLIS = ONE_HOUR_MILLIS * 24
3540

@@ -98,7 +103,7 @@ def _should_remove_bucket(self, bucket: Bucket):
98103
OLD_PATTERN = 'test-b2-cli-'
99104
if bucket.name.startswith(self.general_bucket_name_prefix) or bucket.name.startswith(OLD_PATTERN): # yapf: disable
100105
if BUCKET_CREATED_AT_MILLIS in bucket.bucket_info:
101-
delete_older_than = current_time_millis() - ONE_HOUR_MILLIS
106+
delete_older_than = current_time_millis() - BUCKET_CLEANUP_PERIOD_MILLIS
102107
this_bucket_creation_time = bucket.bucket_info[BUCKET_CREATED_AT_MILLIS]
103108
if int(this_bucket_creation_time) < delete_older_than:
104109
return True, f"this_bucket_creation_time={this_bucket_creation_time} < delete_older_than={delete_older_than}"
@@ -116,9 +121,24 @@ def clean_buckets(self):
116121
continue
117122

118123
print('Trying to remove bucket:', bucket.name, 'because', why)
119-
self.clean_bucket(bucket)
124+
try:
125+
self.clean_bucket(bucket)
126+
except (BucketIdNotFound, v3BucketIdNotFound):
127+
print('It seems that bucket %s has already been removed' % (bucket.name,))
128+
buckets = self.api.list_buckets()
129+
print('Total bucket count after cleanup:', len(buckets))
130+
for bucket in buckets:
131+
print(bucket)
132+
133+
@backoff.on_exception(
134+
backoff.expo,
135+
TooManyRequests,
136+
max_tries=8,
137+
)
138+
def clean_bucket(self, bucket: Union[Bucket, str]):
139+
if isinstance(bucket, str):
140+
bucket = self.api.get_bucket_by_name(bucket)
120141

121-
def clean_bucket(self, bucket: Bucket):
122142
files_leftover = False
123143
file_versions = bucket.ls(latest_only=False, recursive=True)
124144

@@ -168,6 +188,14 @@ def clean_bucket(self, bucket: Bucket):
168188
print('It seems that bucket %s has already been removed' % (bucket.name,))
169189
print()
170190

191+
def count_and_print_buckets(self) -> int:
192+
buckets = self.api.list_buckets()
193+
count = len(buckets)
194+
print(f'Total bucket count at {datetime.now()}: {count}')
195+
for i, bucket in enumerate(buckets, start=1):
196+
print(f'- {i}\t{bucket.name} [{bucket.id_}]')
197+
return count
198+
171199

172200
def print_text_indented(text):
173201
"""

0 commit comments

Comments
 (0)