Skip to content

Commit b20ca20

Browse files
authored
tests: refactor systests using pytest (#476)
* Move systests from monolith to new, more focused modules. * Refactor systests to use pytest fixtures. Closes #475
1 parent cb2a89b commit b20ca20

File tree

12 files changed

+3075
-2747
lines changed

12 files changed

+3075
-2747
lines changed

tests/system/__init__.py

Whitespace-only changes.

tests/system/_helpers.py

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
# Copyright 2021 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# https://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import os
16+
17+
import six
18+
19+
from google.api_core import exceptions
20+
21+
from test_utils.retry import RetryErrors
22+
from test_utils.retry import RetryInstanceState
23+
from test_utils.system import unique_resource_id
24+
25+
retry_429 = RetryErrors(exceptions.TooManyRequests)
26+
retry_429_harder = RetryErrors(exceptions.TooManyRequests, max_tries=10)
27+
retry_429_503 = RetryErrors(
28+
[exceptions.TooManyRequests, exceptions.ServiceUnavailable], max_tries=10
29+
)
30+
31+
# Work around https://github.com/googleapis/python-test-utils/issues/36
32+
if six.PY3:
33+
retry_failures = RetryErrors(AssertionError)
34+
else:
35+
36+
def retry_failures(decorated): # no-op
37+
wrapped = RetryErrors(AssertionError)(decorated)
38+
wrapped.__wrapped__ = decorated
39+
return wrapped
40+
41+
42+
user_project = os.environ.get("GOOGLE_CLOUD_TESTS_USER_PROJECT")
43+
testing_mtls = os.getenv("GOOGLE_API_USE_CLIENT_CERTIFICATE") == "true"
44+
signing_blob_content = b"This time for sure, Rocky!"
45+
46+
47+
def _bad_copy(bad_request):
48+
"""Predicate: pass only exceptions for a failed copyTo."""
49+
err_msg = bad_request.message
50+
return err_msg.startswith("No file found in request. (POST") and "copyTo" in err_msg
51+
52+
53+
def _no_event_based_hold(blob):
54+
return not blob.event_based_hold
55+
56+
57+
retry_bad_copy = RetryErrors(exceptions.BadRequest, error_predicate=_bad_copy)
58+
retry_no_event_based_hold = RetryInstanceState(_no_event_based_hold)
59+
60+
61+
def unique_name(prefix):
62+
return prefix + unique_resource_id("-")
63+
64+
65+
def empty_bucket(bucket):
66+
for blob in list(bucket.list_blobs(versions=True)):
67+
try:
68+
blob.delete()
69+
except exceptions.NotFound:
70+
pass
71+
72+
73+
def delete_blob(blob):
74+
errors = (exceptions.Conflict, exceptions.TooManyRequests)
75+
retry = RetryErrors(errors)
76+
try:
77+
retry(blob.delete)()
78+
except exceptions.NotFound: # race
79+
pass
80+
81+
82+
def delete_bucket(bucket):
83+
errors = (exceptions.Conflict, exceptions.TooManyRequests)
84+
retry = RetryErrors(errors, max_tries=15)
85+
retry(empty_bucket)(bucket)
86+
retry(bucket.delete)(force=True)

tests/system/conftest.py

Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
# Copyright 2021 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# https://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import contextlib
16+
import os
17+
18+
import pytest
19+
20+
from google.cloud.storage._helpers import _base64_md5hash
21+
from . import _helpers
22+
23+
24+
dirname = os.path.realpath(os.path.dirname(__file__))
25+
data_dirname = os.path.abspath(os.path.join(dirname, "..", "data"))
26+
_filenames = [
27+
("logo", "CloudPlatform_128px_Retina.png"),
28+
("big", "five-point-one-mb-file.zip"),
29+
("simple", "simple.txt"),
30+
]
31+
_file_data = {
32+
key: {"path": os.path.join(data_dirname, file_name)}
33+
for key, file_name in _filenames
34+
}
35+
36+
_listable_filenames = ["CloudLogo1", "CloudLogo2", "CloudLogo3", "CloudLogo4"]
37+
_hierarchy_filenames = [
38+
"file01.txt",
39+
"parent/",
40+
"parent/file11.txt",
41+
"parent/child/file21.txt",
42+
"parent/child/file22.txt",
43+
"parent/child/grand/file31.txt",
44+
"parent/child/other/file32.txt",
45+
]
46+
47+
48+
@pytest.fixture(scope="session")
49+
def storage_client():
50+
from google.cloud.storage import Client
51+
52+
client = Client()
53+
with contextlib.closing(client):
54+
yield client
55+
56+
57+
@pytest.fixture(scope="session")
58+
def user_project():
59+
if _helpers.user_project is None:
60+
pytest.skip("USER_PROJECT not set in environment.")
61+
return _helpers.user_project
62+
63+
64+
@pytest.fixture(scope="session")
65+
def no_mtls():
66+
if _helpers.testing_mtls:
67+
pytest.skip("Test incompatible with mTLS.")
68+
69+
70+
@pytest.fixture(scope="session")
71+
def service_account(storage_client):
72+
from google.oauth2.service_account import Credentials
73+
74+
if not isinstance(storage_client._credentials, Credentials):
75+
pytest.skip("These tests require a service account credential")
76+
return storage_client._credentials
77+
78+
79+
@pytest.fixture(scope="session")
80+
def shared_bucket_name():
81+
return _helpers.unique_name("gcp-systest")
82+
83+
84+
@pytest.fixture(scope="session")
85+
def shared_bucket(storage_client, shared_bucket_name):
86+
bucket = storage_client.bucket(shared_bucket_name)
87+
bucket.versioning_enabled = True
88+
_helpers.retry_429_503(bucket.create)()
89+
90+
yield bucket
91+
92+
_helpers.delete_bucket(bucket)
93+
94+
95+
@pytest.fixture(scope="session")
96+
def listable_bucket_name():
97+
return _helpers.unique_name("gcp-systest-listable")
98+
99+
100+
@pytest.fixture(scope="session")
101+
def listable_bucket(storage_client, listable_bucket_name, file_data):
102+
bucket = storage_client.bucket(listable_bucket_name)
103+
_helpers.retry_429_503(bucket.create)()
104+
105+
info = file_data["logo"]
106+
source_blob = bucket.blob(_listable_filenames[0])
107+
source_blob.upload_from_filename(info["path"])
108+
109+
for filename in _listable_filenames[1:]:
110+
_helpers.retry_bad_copy(bucket.copy_blob)(
111+
source_blob, bucket, filename,
112+
)
113+
114+
yield bucket
115+
116+
_helpers.delete_bucket(bucket)
117+
118+
119+
@pytest.fixture(scope="session")
120+
def listable_filenames():
121+
return _listable_filenames
122+
123+
124+
@pytest.fixture(scope="session")
125+
def hierarchy_bucket_name():
126+
return _helpers.unique_name("gcp-systest-hierarchy")
127+
128+
129+
@pytest.fixture(scope="session")
130+
def hierarchy_bucket(storage_client, hierarchy_bucket_name, file_data):
131+
bucket = storage_client.bucket(hierarchy_bucket_name)
132+
_helpers.retry_429_503(bucket.create)()
133+
134+
simple_path = _file_data["simple"]["path"]
135+
for filename in _hierarchy_filenames:
136+
blob = bucket.blob(filename)
137+
blob.upload_from_filename(simple_path)
138+
139+
yield bucket
140+
141+
_helpers.delete_bucket(bucket)
142+
143+
144+
@pytest.fixture(scope="session")
145+
def hierarchy_filenames():
146+
return _hierarchy_filenames
147+
148+
149+
@pytest.fixture(scope="session")
150+
def signing_bucket_name():
151+
return _helpers.unique_name("gcp-systest-signing")
152+
153+
154+
@pytest.fixture(scope="session")
155+
def signing_bucket(storage_client, signing_bucket_name):
156+
bucket = storage_client.bucket(signing_bucket_name)
157+
_helpers.retry_429_503(bucket.create)()
158+
blob = bucket.blob("README.txt")
159+
blob.upload_from_string(_helpers.signing_blob_content)
160+
161+
yield bucket
162+
163+
_helpers.delete_bucket(bucket)
164+
165+
166+
@pytest.fixture(scope="function")
167+
def buckets_to_delete():
168+
buckets_to_delete = []
169+
170+
yield buckets_to_delete
171+
172+
for bucket in buckets_to_delete:
173+
_helpers.delete_bucket(bucket)
174+
175+
176+
@pytest.fixture(scope="function")
177+
def blobs_to_delete():
178+
blobs_to_delete = []
179+
180+
yield blobs_to_delete
181+
182+
for blob in blobs_to_delete:
183+
_helpers.delete_blob(blob)
184+
185+
186+
@pytest.fixture(scope="session")
187+
def file_data():
188+
for file_data in _file_data.values():
189+
with open(file_data["path"], "rb") as file_obj:
190+
file_data["hash"] = _base64_md5hash(file_obj)
191+
192+
return _file_data

0 commit comments

Comments
 (0)