Skip to content

Commit

Permalink
ci: add system tests (#623)
Browse files Browse the repository at this point in the history
* 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>
  • Loading branch information
vi3k6i5 and c24t authored May 20, 2021
1 parent 3de1a81 commit db0ab70
Show file tree
Hide file tree
Showing 7 changed files with 275 additions and 14 deletions.
55 changes: 54 additions & 1 deletion noxfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from __future__ import absolute_import

import os
import pathlib
import shutil

import nox
Expand All @@ -27,6 +28,8 @@
SYSTEM_TEST_PYTHON_VERSIONS = ["3.8"]
UNIT_TEST_PYTHON_VERSIONS = ["3.6", "3.7", "3.8", "3.9"]

CURRENT_DIRECTORY = pathlib.Path(__file__).parent.absolute()


@nox.session(python=DEFAULT_PYTHON_VERSION)
def lint(session):
Expand Down Expand Up @@ -86,7 +89,7 @@ def default(session):
"--cov-report=",
"--cov-fail-under=65",
os.path.join("tests", "unit"),
*session.posargs
*session.posargs,
)


Expand All @@ -96,6 +99,56 @@ def unit(session):
default(session)


@nox.session(python=SYSTEM_TEST_PYTHON_VERSIONS)
def system(session):
"""Run the system test suite."""
constraints_path = str(
CURRENT_DIRECTORY / "testing" / f"constraints-{session.python}.txt"
)
system_test_path = os.path.join("tests", "system.py")
system_test_folder_path = os.path.join("tests", "system")

# Check the value of `RUN_SYSTEM_TESTS` env var. It defaults to true.
if os.environ.get("RUN_SYSTEM_TESTS", "true") == "false":
session.skip("RUN_SYSTEM_TESTS is set to false, skipping")
# Sanity check: Only run tests if the environment variable is set.
if not os.environ.get(
"GOOGLE_APPLICATION_CREDENTIALS", ""
) and not os.environ.get("SPANNER_EMULATOR_HOST", ""):
session.skip(
"Credentials or emulator host must be set via environment variable"
)

system_test_exists = os.path.exists(system_test_path)
system_test_folder_exists = os.path.exists(system_test_folder_path)
# Sanity check: only run tests if found.
if not system_test_exists and not system_test_folder_exists:
session.skip("System tests were not found")

# Use pre-release gRPC for system tests.
session.install("--pre", "grpcio")

# Install all test dependencies, then install this package into the
# virtualenv's dist-packages.
session.install(
"django~=2.2",
"mock",
"pytest",
"google-cloud-testutils",
"-c",
constraints_path,
)
session.install("-e", ".[tracing]", "-c", constraints_path)

# Run py.test against the system tests.
if system_test_exists:
session.run("py.test", "--quiet", system_test_path, *session.posargs)
if system_test_folder_exists:
session.run(
"py.test", "--quiet", system_test_folder_path, *session.posargs
)


@nox.session(python=DEFAULT_PYTHON_VERSION)
def cover(session):
"""Run the final coverage report.
Expand Down
8 changes: 7 additions & 1 deletion tests/unit/conftest.py → tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
# Copyright 2021 Google LLC
#
# Use of this source code is governed by a BSD-style
# license that can be found in the LICENSE file or at
# https://developers.google.com/open-source/licenses/bsd

import os
import django
from django.conf import settings

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


# `pytest` automatically calls this function once when tests are run.
Expand Down
26 changes: 14 additions & 12 deletions tests/unit/settings.py → tests/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
# license that can be found in the LICENSE file or at
# https://developers.google.com/open-source/licenses/bsd

import time
import os

DEBUG = True
Expand All @@ -23,23 +22,26 @@

TIME_ZONE = "UTC"

ENGINE = "django_spanner"
PROJECT = os.getenv(
"GOOGLE_CLOUD_PROJECT", os.getenv("PROJECT_ID", "emulator-test-project"),
INSTANCE_ID = os.environ.get(
"GOOGLE_CLOUD_TESTS_SPANNER_INSTANCE", "spanner-django-python-systest"
)

INSTANCE = "django-test-instance"
NAME = "spanner-django-test-{}".format(str(int(time.time())))
PROJECT_ID = os.getenv("GOOGLE_CLOUD_PROJECT", "emulator-test-project")

# _get_test_db_name method in creation.py addes prefix of 'test_' to db name.
DATABASE_NAME = os.getenv("DJANGO_SPANNER_DB", "django_test_db")

DATABASES = {
"default": {
"ENGINE": ENGINE,
"PROJECT": PROJECT,
"INSTANCE": INSTANCE,
"NAME": NAME,
"ENGINE": "django_spanner",
"PROJECT": PROJECT_ID,
"INSTANCE": INSTANCE_ID,
"NAME": DATABASE_NAME,
"TEST": {"NAME": DATABASE_NAME},
}
}
SECRET_KEY = "spanner emulator secret key"

SECRET_KEY = "spanner env secret key"

PASSWORD_HASHERS = [
"django.contrib.auth.hashers.MD5PasswordHasher",
Expand All @@ -52,6 +54,6 @@
ENGINE = "django_spanner"
PROJECT = "emulator-local"
INSTANCE = "django-test-instance"
NAME = "django-test-db"
NAME = "django_test_db"
OPTIONS = {}
AUTOCOMMIT = True
Empty file.
16 changes: 16 additions & 0 deletions tests/system/django_spanner/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Copyright 2021 Google LLC
#
# Use of this source code is governed by a BSD-style
# license that can be found in the LICENSE file or at
# https://developers.google.com/open-source/licenses/bsd

"""
Different models used by system tests in django-spanner code.
"""
from django.db import models


class Author(models.Model):
first_name = models.CharField(max_length=20)
last_name = models.CharField(max_length=20)
rating = models.DecimalField()
49 changes: 49 additions & 0 deletions tests/system/django_spanner/test_queries.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Copyright 2021 Google LLC
#
# Use of this source code is governed by a BSD-style
# license that can be found in the LICENSE file or at
# https://developers.google.com/open-source/licenses/bsd

from .models import Author
from django.test import TransactionTestCase
from django.db import connection
from decimal import Decimal
from .utils import (
setup_instance,
teardown_instance,
setup_database,
teardown_database,
)


class TestQueries(TransactionTestCase):
@classmethod
def setUpClass(cls):
setup_instance()
setup_database()
with connection.schema_editor() as editor:
# Create the tables
editor.create_model(Author)

@classmethod
def tearDownClass(cls):
with connection.schema_editor() as editor:
# delete the table
editor.delete_model(Author)
teardown_database()
teardown_instance()

def test_insert_and_fetch_value(self):
"""
Tests model object creation with Author model.
Inserting data into the model and retrieving it.
"""
author_kent = Author(
first_name="Arthur", last_name="Kent", rating=Decimal("4.1"),
)
author_kent.save()
qs1 = Author.objects.all().values("first_name", "last_name")
self.assertEqual(qs1[0]["first_name"], "Arthur")
self.assertEqual(qs1[0]["last_name"], "Kent")
# Delete data from Author table.
Author.objects.all().delete()
135 changes: 135 additions & 0 deletions tests/system/django_spanner/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
# Copyright 2021 Google LLC
#
# Use of this source code is governed by a BSD-style
# license that can be found in the LICENSE file or at
# https://developers.google.com/open-source/licenses/bsd

import os
import time

from django.core.management import call_command
from django.db import connection
from google.api_core import exceptions
from google.cloud.spanner_v1 import Client
from google.cloud.spanner_v1.instance import Instance, Backup
from test_utils.retry import RetryErrors

from django_spanner.creation import DatabaseCreation

CREATE_INSTANCE = (
os.getenv("GOOGLE_CLOUD_TESTS_CREATE_SPANNER_INSTANCE") is not None
)
USE_EMULATOR = os.getenv("SPANNER_EMULATOR_HOST") is not None
SPANNER_OPERATION_TIMEOUT_IN_SECONDS = int(
os.getenv("SPANNER_OPERATION_TIMEOUT_IN_SECONDS", 60)
)
EXISTING_INSTANCES = []
INSTANCE_ID = os.environ.get(
"GOOGLE_CLOUD_TESTS_SPANNER_INSTANCE", "spanner-django-python-systest"
)
PROJECT_ID = os.getenv("GOOGLE_CLOUD_PROJECT", "emulator-test-project")
DATABASE_NAME = os.getenv("DJANGO_SPANNER_DB", "django_test_db")


class Config(object):
"""Run-time configuration to be modified at set-up.
This is a mutable stand-in to allow test set-up to modify
global state.
"""

CLIENT = None
INSTANCE_CONFIG = None
INSTANCE = None
DATABASE = None


def _list_instances():
return list(Config.CLIENT.list_instances())


def setup_instance():
if USE_EMULATOR:
from google.auth.credentials import AnonymousCredentials

Config.CLIENT = Client(
project=PROJECT_ID, credentials=AnonymousCredentials()
)
else:
Config.CLIENT = Client()

retry = RetryErrors(exceptions.ServiceUnavailable)

configs = list(retry(Config.CLIENT.list_instance_configs)())

instances = retry(_list_instances)()
EXISTING_INSTANCES[:] = instances

# Delete test instances that are older than an hour.
cutoff = int(time.time()) - 1 * 60 * 60
instance_pbs = Config.CLIENT.list_instances(
"labels.python-spanner-systests:true"
)
for instance_pb in instance_pbs:
instance = Instance.from_pb(instance_pb, Config.CLIENT)
if "created" not in instance.labels:
continue
create_time = int(instance.labels["created"])
if create_time > cutoff:
continue
if not USE_EMULATOR:
# Instance cannot be deleted while backups exist.
for backup_pb in instance.list_backups():
backup = Backup.from_pb(backup_pb, instance)
backup.delete()
instance.delete()

if CREATE_INSTANCE:
if not USE_EMULATOR:
# Defend against back-end returning configs for regions we aren't
# actually allowed to use.
configs = [config for config in configs if "-us-" in config.name]

if not configs:
raise ValueError("List instance configs failed in module set up.")

Config.INSTANCE_CONFIG = configs[0]
config_name = configs[0].name
create_time = str(int(time.time()))
labels = {"django-spanner-systests": "true", "created": create_time}

Config.INSTANCE = Config.CLIENT.instance(
INSTANCE_ID, config_name, labels=labels
)
if not Config.INSTANCE.exists():
created_op = Config.INSTANCE.create()
created_op.result(
SPANNER_OPERATION_TIMEOUT_IN_SECONDS
) # block until completion
else:
Config.INSTANCE.reload()

else:
Config.INSTANCE = Config.CLIENT.instance(INSTANCE_ID)
Config.INSTANCE.reload()


def teardown_instance():
if CREATE_INSTANCE:
Config.INSTANCE.delete()


def setup_database():
Config.DATABASE = Config.INSTANCE.database(DATABASE_NAME)
if not Config.DATABASE.exists():
creation = DatabaseCreation(connection)
creation._create_test_db(verbosity=0, autoclobber=False, keepdb=True)

# Running migrations on the db.
call_command("migrate", interactive=False)


def teardown_database():
Config.DATABASE = Config.INSTANCE.database(DATABASE_NAME)
if Config.DATABASE.exists():
Config.DATABASE.drop()

0 comments on commit db0ab70

Please sign in to comment.