8000 ci: add system tests (#623) · googleapis/python-spanner-django@db0ab70 · GitHub
[go: up one dir, main page]

Skip to content

Commit db0ab70

Browse files
vi3k6i5c24t
andauthored
ci: add system tests (#623)
* fix: lint_setup_py was failing in Kokoro is not fixed * perf: removed nox run on all kokoro workers and moved it to github actions * feat: add decimal/numeric support * fix: Update links in comments to use googleapis repo (#622) * refactor: common settings for unit tests and system tests Co-authored-by: Chris Kleinknecht <libc@google.com>
1 parent 3de1a81 commit db0ab70

File tree

7 files changed

+275
-14
lines changed

7 files changed

+275
-14
lines changed

noxfile.py

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from __future__ import absolute_import
1111

1212
import os
13+
import pathlib
1314
import shutil
1415

1516
import nox
@@ -27,6 +28,8 @@
2728
SYSTEM_TEST_PYTHON_VERSIONS = ["3.8"]
2829
UNIT_TEST_PYTHON_VERSIONS = ["3.6", "3.7", "3.8", "3.9"]
2930

31+
CURRENT_DIRECTORY = pathlib.Path(__file__).parent.absolute()
32+
3033

3134
@nox.session(python=DEFAULT_PYTHON_VERSION)
3235
def lint(session):
@@ -86,7 +89,7 @@ def default(session):
8689
"--cov-report=",
8790
"--cov-fail-under=65",
8891
os.path.join("tests", "unit"),
89-
*session.posargs
92+
*session.posargs,
9093
)
9194

9295

@@ -96,6 +99,56 @@ def unit(session):
9699
default(session)
97100

98101

102+
@nox.session(python=SYSTEM_TEST_PYTHON_VERSIONS)
103+
def system(session):
104+
"""Run the system test suite."""
105+
constraints_path = str(
106+
CURRENT_DIRECTORY / "testing" / f"constraints-{session.python}.txt"
107+
)
108+
system_test_path = os.path.join("tests", "system.py")
109+
system_test_folder_path = os.path.join("tests", "system")
110+
111+
# Check the value of `RUN_SYSTEM_TESTS` env var. It defaults to true.
112+
if os.environ.get("RUN_SYSTEM_TESTS", "true") == "false":
113+
session.skip("RUN_SYSTEM_TESTS is set to false, skipping")
114+
# Sanity check: Only run tests if the environment variable is set.
115+
if not os.environ.get(
116+
"GOOGLE_APPLICATION_CREDENTIALS", ""
117+
) and not os.environ.get("SPANNER_EMULATOR_HOST", ""):
118+
session.skip(
119+
"Credentials or emulator host must be set via environment variable"
120+
)
121+
122+
system_test_exists = os.path.exists(system_test_path)
123+
system_test_folder_exists = os.path.exists(system_test_folder_path)
124+
# Sanity check: only run tests if found.
125+
if not system_test_exists and not system_test_folder_exists:
126+
session.skip("System tests were not found")
127+
128+
# Use pre-release gRPC for system tests.
129+
session.install("--pre", "grpcio")
130+
131+
# Install all test dependencies, then install this package into the
132+
# virtualenv's dist-packages.
133+
session.install(
134+
"django~=2.2",
135+
"mock",
136+
"pytest",
137+
"google-cloud-testutils",
138+
"-c",
139+
constraints_path,
140+
)
141+
session.install("-e", ".[tracing]", "-c", constraints_path)
142+
143+
# Run py.test against the system tests.
144+
if system_test_exists:
145+
session.run("py.test", "--quiet", system_test_path, *session.posargs)
146+
if system_test_folder_exists:
147+
session.run(
148+
"py.test", "--quiet", system_test_folder_path, *session.posargs
149+
)
150+
151+
99152
@nox.session(python=DEFAULT_PYTHON_VERSION)
100153
def cover(session):
101154
"""Run the final coverage report.

tests/unit/conftest.py renamed to tests/conftest.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
1+
# Copyright 2021 Google LLC
2+
#
3+
# Use of this source code is governed by a BSD-style
4+
# license that can be found in the LICENSE file or at
5+
# https://developers.google.com/open-source/licenses/bsd
6+
17
import os
28
import django
39
from django.conf import settings
410

511
# We manually designate which settings we will be using in an environment
612
# variable. This is similar to what occurs in the `manage.py` file.
7-
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "tests.unit.settings")
13+
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "tests.settings")
814

915

1016
# `pytest` automatically calls this function once when tests are run.

tests/unit/settings.py renamed to tests/settings.py

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
# license that can be found in the LICENSE file or at
55
# https://developers.google.com/open-source/licenses/bsd
66

7-
import time
87
import os
98

109
DEBUG = True
@@ -23,23 +22,26 @@
2322

2423
TIME_ZONE = "UTC"
2524

26-
ENGINE = "django_spanner"
27-
PROJECT = os.getenv(
28-
"GOOGLE_CLOUD_PROJECT", os.getenv("PROJECT_ID", "emulator-test-project"),
25+
INSTANCE_ID = os.environ.get(
26+
"GOOGLE_CLOUD_TESTS_SPANNER_INSTANCE", "spanner-django-python-systest"
2927
)
3028

31-
INSTANCE = "django-test-instance"
32-
NAME = "spanner-django-test-{}".format(str(int(time.time())))
29+
PROJECT_ID = os.getenv("GOOGLE_CLOUD_PROJECT", "emulator-test-project")
30+
31+
# _get_test_db_name method in creation.py addes prefix of 'test_' to db name.
32+
DATABASE_NAME = os.getenv("DJANGO_SPANNER_DB", "django_test_db")
3333

3434
DATABASES = {
3535
"default": {
36-
"ENGINE": ENGINE,
37-
"PROJECT": PROJECT,
38-
"INSTANCE": INSTANCE,
39-
"NAME": NAME,
36+
"ENGINE": "django_spanner",
37+
"PROJECT": PROJECT_ID,
38+
"INSTANCE": INSTANCE_ID,
39+
"NAME": DATABASE_NAME,
40+
"TEST": {"NAME": DATABASE_NAME},
4041
}
4142
}
42-
SECRET_KEY = "spanner emulator secret key"
43+
44+
SECRET_KEY = "spanner env secret key"
4345

4446
PASSWORD_HASHERS = [
4547
"django.contrib.auth.hashers.MD5PasswordHasher",
@@ -52,6 +54,6 @@
5254
ENGINE = "django_spanner"
5355
PROJECT = "emulator-local"
5456
INSTANCE = "django-test-instance"
55-
NAME = "django-test-db"
57+
NAME = "django_test_db"
5658
OPTIONS = {}
5759
AUTOCOMMIT = True

tests/system/django_spanner/__init__.py

Whitespace-only changes.

tests/system/django_spanner/models.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# Copyright 2021 Google LLC
2+
#
3+
# Use of this source code is governed by a BSD-style
4+
# license that can be found in the LICENSE file or at
5+
# https://developers.google.com/open-source/licenses/bsd
6+
7+
"""
8+
Different models used by system tests in django-spanner code.
9+
"""
10+
from django.db import models
11+
12+
13+
class Author(models.Model):
14+
first_name = models.CharField(max_length=20)
15+
last_name = models.CharField(max_length=20)
16+
rating = models.DecimalField()
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# Copyright 2021 Google LLC
2+
#
3+
# Use of this source code is governed by a BSD-style
4+
# license that can be found in the LICENSE file or at
5+
# https://developers.google.com/open-source/licenses/bsd
6+
7+
from .models import Author
8+
from django.test import TransactionTestCase
9+
from django.db import connection
10+
from decimal import Decimal
11+
from .utils import (
12+
setup_instance,
13+
teardown_instance,
14+
setup_database,
15+
teardown_database,
16+
)
17+
18+
19+
class TestQueries(TransactionTestCase):
20+
@classmethod
21+
def setUpClass(cls):
22+
setup_instance()
23+
setup_database()
24+
with connection.schema_editor() as editor:
25+
# Create the tables
26+
editor.create_model(Author)
27+
28+
@classmethod
29+
def tearDownClass(cls):
30+
with connection.schema_editor() as editor:
31+
# delete the table
32+
editor.delete_model(Author)
33+
teardown_database()
34+
teardown_instance()
35+
36+
def test_insert_and_fetch_value(self):
37+
"""
38+
Tests model object creation with Author model.
39+
Inserting data into the model and retrieving it.
40+
"""
41+
author_kent = Author(
42+
first_name="Arthur", last_name="Kent", rating=Decimal("4.1"),
43+
)
44+
author_kent.save()
45+
qs1 = Author.objects.all().values("first_name", "last_name")
46+
self.assertEqual(qs1[0]["first_name"], "Arthur")
47+
self.assertEqual(qs1[0]["last_name"], "Kent")
48+
# Delete data from Author table.
49+
Author.objects.all().delete()

tests/system/django_spanner/utils.py

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
# Copyright 2021 Google LLC
2+
#
3+
# Use of this source code is governed by a BSD-style
4+
# license that can be found in the LICENSE file or at
5+
# https://developers.google.com/open-source/licenses/bsd
6+
7+
import os
8+
import time
9+
10+
from django.core.management import call_command
11+
from django.db import connection
12+
from google.api_core import exceptions
13+
from google.cloud.spanner_v1 import Client
14+
from google.cloud.spanner_v1.instance import Instance, Backup
15+
from test_utils.retry import RetryErrors
16+
17+
from django_spanner.creation import DatabaseCreation
18+
19+
CREATE_INSTANCE = (
20+
os.getenv("GOOGLE_CLOUD_TESTS_CREATE_SPANNER_INSTANCE") is not None
21+
)
22+
USE_EMULATOR = os.getenv("SPANNER_EMULATOR_HOST") is not None
23+
SPANNER_OPERATION_TIMEOUT_IN_SECONDS = int(
24+
os.getenv("SPANNER_OPERATION_TIMEOUT_IN_SECONDS", 60)
25+
)
26+
EXISTING_INSTANCES = []
27+
INSTANCE_ID = os.environ.get(
28+
"GOOGLE_CLOUD_TESTS_SPANNER_INSTANCE", "spanner-django-python-systest"
29+
)
30+
PROJECT_ID = os.getenv("GOOGLE_CLOUD_PROJECT", "emulator-test-project")
31+
DATABASE_NAME = os.getenv("DJANGO_SPANNER_DB", "django_test_db")
32+
33+
34+
class Config(object):
35+
"""Run-time configuration to be modified at set-up.
36+
37+
This is a mutable stand-in to allow test set-up to modify
38+
global state.
39+
"""
40+
41+
CLIENT = None
42+
INSTANCE_CONFIG = None
43+
INSTANCE = None
44+
DATABASE = None
45+
46+
47+
def _list_instances():
48+
return list(Config.CLIENT.list_instances())
49+
50+
51+
def setup_instance():
52+
if USE_EMULATOR:
53+
from google.auth.credentials import AnonymousCredentials
54+
55+
Config.CLIENT = Client(
56+
project=PROJECT_ID, credentials=AnonymousCredentials()
57+
)
58+
else:
59+
Config.CLIENT = Client()
60+
61+
retry = RetryErrors(exceptions.ServiceUnavailable)
62+
63+
configs = list(retry(Config.CLIENT.list_instance_configs)())
64+
65+
instances = retry(_list_instances)()
66+
EXISTING_INSTANCES[:] = instances
67+
68+
# Delete test instances that are older than an hour.
69+
cutoff = int(time.time()) - 1 * 60 * 60
70+
instance_pbs = Config.CLIENT.list_instances(
71+
"labels.python-spanner-systests:true"
72+
)
73+
for instance_pb in instance_pbs:
74+
instance = Instance.from_pb(instance_pb, Config.CLIENT)
75+
if "created" not in instance.labels:
76+
continue
77+
create_time = int(instance.labels["created"])
78+
if create_time > cutoff:
79+
continue
80+
if not USE_EMULATOR:
81+
# Instance cannot be deleted while backups exist.
82+
for backup_pb in instance.list_backups():
83+
backup = Backup.from_pb(backup_pb, instance)
84+
backup.delete()
85+
instance.delete()
86+
87+
if CREATE_INSTANCE:
88+
if not USE_EMULATOR:
89+
# Defend against back-end returning configs for regions we aren't
90+
# actually allowed to use.
91+
configs = [config for config in configs if "-us-" in config.name]
92+
93+
if not configs:
94+
raise ValueError("List instance configs failed in module set up.")
95+
96+
Config.INSTANCE_CONFIG = configs[0]
97+
config_name = configs[0].name
98+
create_time = str(int(time.time()))
99+
labels = {"django-spanner-systests": "true", "created": create_time}
100+
101+
Config.INSTANCE = Config.CLIENT.instance(
102+
INSTANCE_ID, config_name, labels=labels
103+
)
104+
if not Config.INSTANCE.exists():
105+
created_op = Config.INSTANCE.create()
106+
created_op.result(
107+
SPANNER_OPERATION_TIMEOUT_IN_SECONDS
108+
) # block until completion
109+
else:
110+
Config.INSTANCE.reload()
111+
112+
else:
113+
Config.INSTANCE = Config.CLIENT.instance(INSTANCE_ID)
114+
Config.INSTANCE.reload()
115+
116+
117+
def teardown_instance():
118+
if CREATE_INSTANCE:
119+
Config.INSTANCE.delete()
120+
121+
122+
def setup_database():
123+
Config.DATABASE = Config.INSTANCE.database(DATABASE_NAME)
124+
if not Config.DATABASE.exists():
125+
creation = DatabaseCreation(connection)
126+
creation._create_test_db(verbosity=0, autoclobber=False, keepdb=True)
127+
128+
# Running migrations on the db.
129+
call_command("migrate", interactive=False)
130+
131+
132+
def teardown_database():
133+
Config.DATABASE = Config.INSTANCE.database(DATABASE_NAME)
134+
if Config.DATABASE.exists():
135+
Config.DATABASE.drop()

0 commit comments

Comments
 (0)
0