|
14 | 14 | # limitations under the License. |
15 | 15 | # |
16 | 16 | import pytest |
17 | | -import uuid |
18 | 17 | import os |
| 18 | +import uuid |
19 | 19 | import datetime |
20 | 20 |
|
21 | 21 | TEST_FAMILY = "test-family" |
22 | 22 | TEST_FAMILY_2 = "test-family-2" |
23 | 23 | TEST_AGGREGATE_FAMILY = "test-aggregate-family" |
24 | 24 |
|
| 25 | +# authorized view subset to allow all qualifiers |
| 26 | +ALLOW_ALL = "" |
| 27 | +ALL_QUALIFIERS = {"qualifier_prefixes": [ALLOW_ALL]} |
| 28 | + |
25 | 29 |
|
26 | 30 | class SystemTestRunner: |
27 | 31 | """ |
@@ -80,3 +84,182 @@ def init_table_id(self, start_timestamp): |
80 | 84 | start_timestamp: accessed when building table to ensure timestamp data is loaded before tests are run |
81 | 85 | """ |
82 | 86 | return f"test-table-{uuid.uuid4().hex}" |
| 87 | + |
| 88 | + @pytest.fixture(scope="session") |
| 89 | + def admin_client(self): |
| 90 | + """ |
| 91 | + Client for interacting with Table and Instance admin APIs |
| 92 | + """ |
| 93 | + from google.cloud.bigtable.client import Client |
| 94 | + |
| 95 | + client = Client(admin=True) |
| 96 | + yield client |
| 97 | + |
| 98 | + @pytest.fixture(scope="session") |
| 99 | + def instance_id(self, admin_client, project_id, cluster_config): |
| 100 | + """ |
| 101 | + Returns BIGTABLE_TEST_INSTANCE if set, otherwise creates a new temporary instance for the test session |
| 102 | + """ |
| 103 | + from google.cloud.bigtable_admin_v2 import types |
| 104 | + from google.api_core import exceptions |
| 105 | + from google.cloud.environment_vars import BIGTABLE_EMULATOR |
| 106 | + |
| 107 | + # use user-specified instance if available |
| 108 | + user_specified_instance = os.getenv("BIGTABLE_TEST_INSTANCE") |
| 109 | + if user_specified_instance: |
| 110 | + print("Using user-specified instance: {}".format(user_specified_instance)) |
| 111 | + yield user_specified_instance |
| 112 | + return |
| 113 | + |
| 114 | + # create a new temporary test instance |
| 115 | + instance_id = f"python-bigtable-tests-{uuid.uuid4().hex[:6]}" |
| 116 | + if os.getenv(BIGTABLE_EMULATOR): |
| 117 | + # don't create instance if in emulator mode |
| 118 | + yield instance_id |
| 119 | + else: |
| 120 | + try: |
| 121 | + operation = admin_client.instance_admin_client.create_instance( |
| 122 | + parent=f"projects/{project_id}", |
| 123 | + instance_id=instance_id, |
| 124 | + instance=types.Instance( |
| 125 | + display_name="Test Instance", |
| 126 | + # labels={"python-system-test": "true"}, |
| 127 | + ), |
| 128 | + clusters=cluster_config, |
| 129 | + ) |
| 130 | + operation.result(timeout=240) |
| 131 | + except exceptions.AlreadyExists: |
| 132 | + pass |
| 133 | + yield instance_id |
| 134 | + admin_client.instance_admin_client.delete_instance( |
| 135 | + name=f"projects/{project_id}/instances/{instance_id}" |
| 136 | + ) |
| 137 | + |
| 138 | + @pytest.fixture(scope="session") |
| 139 | + def column_split_config(self): |
| 140 | + """ |
| 141 | + specify initial splits to create when creating a new test table |
| 142 | + """ |
| 143 | + return [(num * 1000).to_bytes(8, "big") for num in range(1, 10)] |
| 144 | + |
| 145 | + @pytest.fixture(scope="session") |
| 146 | + def table_id( |
| 147 | + self, |
| 148 | + admin_client, |
| 149 | + project_id, |
| 150 | + instance_id, |
| 151 | + column_family_config, |
| 152 | + init_table_id, |
| 153 | + column_split_config, |
| 154 | + ): |
| 155 | + """ |
| 156 | + Returns BIGTABLE_TEST_TABLE if set, otherwise creates a new temporary table for the test session |
| 157 | +
|
| 158 | + Args: |
| 159 | + - admin_client: Client for interacting with the Table Admin API. Supplied by the admin_client fixture. |
| 160 | + - project_id: The project ID of the GCP project to test against. Supplied by the project_id fixture. |
| 161 | + - instance_id: The ID of the Bigtable instance to test against. Supplied by the instance_id fixture. |
| 162 | + - init_column_families: A list of column families to initialize the table with, if pre-initialized table is not given with BIGTABLE_TEST_TABLE. |
| 163 | + Supplied by the init_column_families fixture. |
| 164 | + - init_table_id: The table ID to give to the test table, if pre-initialized table is not given with BIGTABLE_TEST_TABLE. |
| 165 | + Supplied by the init_table_id fixture. |
| 166 | + - column_split_config: A list of row keys to use as initial splits when creating the test table. |
| 167 | + """ |
| 168 | + from google.api_core import exceptions |
| 169 | + from google.api_core import retry |
| 170 | + |
| 171 | + # use user-specified instance if available |
| 172 | + user_specified_table = os.getenv("BIGTABLE_TEST_TABLE") |
| 173 | + if user_specified_table: |
| 174 | + print("Using user-specified table: {}".format(user_specified_table)) |
| 175 | + yield user_specified_table |
| 176 | + return |
| 177 | + |
| 178 | + retry = retry.Retry( |
| 179 | + predicate=retry.if_exception_type(exceptions.FailedPrecondition) |
| 180 | + ) |
| 181 | + try: |
| 182 | + parent_path = f"projects/{project_id}/instances/{instance_id}" |
| 183 | + print(f"Creating table: {parent_path}/tables/{init_table_id}") |
| 184 | + admin_client.table_admin_client.create_table( |
| 185 | + request={ |
| 186 | + "parent": parent_path, |
| 187 | + "table_id": init_table_id, |
| 188 | + "table": {"column_families": column_family_config}, |
| 189 | + "initial_splits": [{"key": key} for key in column_split_config], |
| 190 | + }, |
| 191 | + retry=retry, |
| 192 | + ) |
| 193 | + except exceptions.AlreadyExists: |
| 194 | + pass |
| 195 | + yield init_table_id |
| 196 | + print(f"Deleting table: {parent_path}/tables/{init_table_id}") |
| 197 | + try: |
| 198 | + admin_client.table_admin_client.delete_table( |
| 199 | + name=f"{parent_path}/tables/{init_table_id}" |
| 200 | + ) |
| 201 | + except exceptions.NotFound: |
| 202 | + print(f"Table {init_table_id} not found, skipping deletion") |
| 203 | + |
| 204 | + @pytest.fixture(scope="session") |
| 205 | + def authorized_view_id( |
| 206 | + self, |
| 207 | + admin_client, |
| 208 | + project_id, |
| 209 | + instance_id, |
| 210 | + table_id, |
| 211 | + ): |
| 212 | + """ |
| 213 | + Creates and returns a new temporary authorized view for the test session |
| 214 | +
|
| 215 | + Args: |
| 216 | + - admin_client: Client for interacting with the Table Admin API. Supplied by the admin_client fixture. |
| 217 | + - project_id: The project ID of the GCP project to test against. Supplied by the project_id fixture. |
| 218 | + - instance_id: The ID of the Bigtable instance to test against. Supplied by the instance_id fixture. |
| 219 | + - table_id: The ID of the table to create the authorized view for. Supplied by the table_id fixture. |
| 220 | + """ |
| 221 | + from google.api_core import exceptions |
| 222 | + from google.api_core import retry |
| 223 | + |
| 224 | + retry = retry.Retry( |
| 225 | + predicate=retry.if_exception_type(exceptions.FailedPrecondition) |
| 226 | + ) |
| 227 | + new_view_id = uuid.uuid4().hex[:8] |
| 228 | + parent_path = f"projects/{project_id}/instances/{instance_id}/tables/{table_id}" |
| 229 | + new_path = f"{parent_path}/authorizedViews/{new_view_id}" |
| 230 | + try: |
| 231 | + print(f"Creating view: {new_path}") |
| 232 | + admin_client.table_admin_client.create_authorized_view( |
| 233 | + request={ |
| 234 | + "parent": parent_path, |
| 235 | + "authorized_view_id": new_view_id, |
| 236 | + "authorized_view": { |
| 237 | + "subset_view": { |
| 238 | + "row_prefixes": [ALLOW_ALL], |
| 239 | + "family_subsets": { |
| 240 | + TEST_FAMILY: ALL_QUALIFIERS, |
| 241 | + TEST_FAMILY_2: ALL_QUALIFIERS, |
| 242 | + TEST_AGGREGATE_FAMILY: ALL_QUALIFIERS, |
| 243 | + }, |
| 244 | + }, |
| 245 | + }, |
| 246 | + }, |
| 247 | + retry=retry, |
| 248 | + ) |
| 249 | + except exceptions.AlreadyExists: |
| 250 | + pass |
| 251 | + except exceptions.MethodNotImplemented: |
| 252 | + # will occur when run in emulator. Pass empty id |
| 253 | + new_view_id = None |
| 254 | + yield new_view_id |
| 255 | + if new_view_id: |
| 256 | + print(f"Deleting view: {new_path}") |
| 257 | + try: |
| 258 | + admin_client.table_admin_client.delete_authorized_view(name=new_path) |
| 259 | + except exceptions.NotFound: |
| 260 | + print(f"View {new_view_id} not found, skipping deletion") |
| 261 | + |
| 262 | + @pytest.fixture(scope="session") |
| 263 | + def project_id(self, client): |
| 264 | + """Returns the project ID from the client.""" |
| 265 | + yield client.project |
0 commit comments