Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Integration tests in python #2892

Merged
merged 16 commits into from
Mar 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,21 @@ jobs:
- name: Checkout
uses: actions/checkout@v4

- name: Setup dev environment
run: |
pip install -r requirements-dev.txt
### pytest-sentry configuration ###
if [ "$GITHUB_REPOSITORY" = "getsentry/self-hosted" ]; then
echo "PYTEST_SENTRY_DSN=${{ env.SELF_HOSTED_TESTING_DSN }}" >> $GITHUB_ENV
echo "PYTEST_SENTRY_TRACES_SAMPLE_RATE=0" >> $GITHUB_ENV

# This records failures on master to sentry in order to detect flakey tests, as it's
# expected that people have failing tests on their PRs
if [ "$GITHUB_REF" = "refs/heads/master" ]; then
echo "PYTEST_SENTRY_ALWAYS_REPORT=1" >> $GITHUB_ENV
fi
fi

- name: Get Compose
run: |
# Always remove `docker compose` support as that's the newer version
Expand Down
3 changes: 2 additions & 1 deletion _integration-test/custom-ca-roots/setup.sh
100644 → 100755
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
#!/usr/bin/env bash
set -e
export COMPOSE_FILE=docker-compose.yml:_integration-test/custom-ca-roots/docker-compose.test.yml

Expand Down Expand Up @@ -42,4 +43,4 @@ openssl req -x509 -newkey rsa:2048 -nodes -days 1 -keyout $TEST_NGINX_CONF_PATH/

cp _integration-test/custom-ca-roots/test.py sentry/test-custom-ca-roots.py

$dc up -d fixture-custom-ca-roots
docker compose --ansi never up -d fixture-custom-ca-roots
1 change: 1 addition & 0 deletions _integration-test/custom-ca-roots/teardown.sh
100644 → 100755
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
#!/usr/bin/env bash
$dc rm -s -f -v fixture-custom-ca-roots
rm -f certificates/test-custom-ca-roots.crt sentry/test-custom-ca-roots.py
unset COMPOSE_FILE
209 changes: 209 additions & 0 deletions _integration-test/run.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
import subprocess
import os
from functools import lru_cache
from bs4 import BeautifulSoup
import httpx
import pytest
import sentry_sdk
import time
import json
import re
from typing import Callable

SENTRY_CONFIG_PY = "sentry/sentry.conf.py"
SENTRY_TEST_HOST = os.getenv("SENTRY_TEST_HOST", "http://localhost:9000")
TEST_USER = "[email protected]"
TEST_PASS = "test123TEST"
TIMEOUT_SECONDS = 60


def poll_for_response(
request: str, client: httpx.Client, validator: Callable = None
) -> httpx.Response:
for i in range(TIMEOUT_SECONDS):
response = client.get(
request, follow_redirects=True, headers={"Referer": SENTRY_TEST_HOST}
)
if response.status_code == 200:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function won't work for spans as it's waiting for a span to be ingested to succeed, so it's receiving a 200 no matter what and looking for the data field to not be empty.

We risk flaky tests if we leave it at that. Maybe we can have a custom behavior via a function to return True/False if it's considered a success of if we need to retry?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, thanks for the context. I added a optional validator function passed into poll_for_response that checks the response json to check to ensure the data field length is greater than 0

if validator is None or validator(response.text):
break
time.sleep(1)
else:
raise AssertionError(
"timeout waiting for response status code 200 or valid data"
)
return response


@lru_cache
def get_sentry_dsn(client: httpx.Client) -> str:
response = poll_for_response(
f"{SENTRY_TEST_HOST}/api/0/projects/sentry/internal/keys/",
client,
lambda x: len(json.loads(x)[0]["dsn"]["public"]) > 0,
)
sentry_dsn = json.loads(response.text)[0]["dsn"]["public"]
return sentry_dsn


@pytest.fixture(scope="session", autouse=True)
def configure_self_hosted_environment():
subprocess.run(["docker", "compose", "--ansi", "never", "up", "-d"], check=True)
for i in range(TIMEOUT_SECONDS):
try:
response = httpx.get(SENTRY_TEST_HOST, follow_redirects=True)
except httpx.ConnectionError:
time.sleep(1)
else:
if response.status_code == 200:
break
else:
raise AssertionError("timeout waiting for self-hosted to come up")

# Create test user
subprocess.run(
[
"docker",
"compose",
"exec",
"web",
"sentry",
"createuser",
"--force-update",
"--superuser",
"--email",
TEST_USER,
"--password",
TEST_PASS,
"--no-input",
],
check=True,
text=True,
)


@pytest.fixture()
def client_login():
client = httpx.Client()
response = client.get(SENTRY_TEST_HOST, follow_redirects=True)
parser = BeautifulSoup(response.text, "html.parser")
login_csrf_token = parser.find("input", {"name": "csrfmiddlewaretoken"})["value"]
login_response = client.post(
f"{SENTRY_TEST_HOST}/auth/login/sentry/",
follow_redirects=True,
data={
"op": "login",
"username": TEST_USER,
"password": TEST_PASS,
"csrfmiddlewaretoken": login_csrf_token,
},
headers={"Referer": f"{SENTRY_TEST_HOST}/auth/login/sentry/"},
)
assert login_response.status_code == 200
yield (client, login_response)


def test_initial_redirect():
initial_auth_redirect = httpx.get(SENTRY_TEST_HOST, follow_redirects=True)
assert initial_auth_redirect.url == f"{SENTRY_TEST_HOST}/auth/login/sentry/"


def test_login(client_login):
client, login_response = client_login
parser = BeautifulSoup(login_response.text, "html.parser")
script_tag = parser.find(
"script", string=lambda x: x and "window.__initialData" in x
)
assert script_tag is not None
json_data = json.loads(script_tag.text.split("=", 1)[1].strip().rstrip(";"))
assert json_data["isAuthenticated"] is True
assert json_data["user"]["username"] == "[email protected]"
assert json_data["user"]["isSuperuser"] is True
assert login_response.cookies["sc"] is not None
# Set up initial/required settings (InstallWizard request)
client.headers.update({"X-CSRFToken": login_response.cookies["sc"]})
response = client.put(
f"{SENTRY_TEST_HOST}/api/0/internal/options/?query=is:required",
follow_redirects=True,
headers={"Referer": SENTRY_TEST_HOST},
data={
"mail.use-tls": False,
"mail.username": "",
"mail.port": 25,
"system.admin-email": "[email protected]",
"mail.password": "",
"system.url-prefix": SENTRY_TEST_HOST,
"auth.allow-registration": False,
"beacon.anonymous": True,
},
)
assert response.status_code == 200


def test_receive_event(client_login):
event_id = None
client, _ = client_login
with sentry_sdk.init(dsn=get_sentry_dsn(client)):
event_id = sentry_sdk.capture_exception(Exception("a failure"))
assert event_id is not None
response = poll_for_response(
f"{SENTRY_TEST_HOST}/api/0/projects/sentry/internal/events/{event_id}/", client
)
response_json = json.loads(response.text)
assert response_json["eventID"] == event_id
assert response_json["metadata"]["value"] == "a failure"


def test_cleanup_crons_running():
docker_services = subprocess.check_output(
[
"docker",
"compose",
"--ansi",
"never",
"ps",
"-a",
],
text=True,
)
pattern = re.compile(
r"(\-cleanup\s+running)|(\-cleanup[_-].+\s+Up\s+)", re.MULTILINE
)
cleanup_crons = pattern.findall(docker_services)
assert len(cleanup_crons) > 0


def test_custom_cas():
try:
subprocess.run(["./_integration-test/custom-ca-roots/setup.sh"], check=True)
subprocess.run(
["docker", "compose", "--ansi", "never", "run", "--no-deps", "web", "python3", "/etc/sentry/test-custom-ca-roots.py"], check=True
)
finally:
subprocess.run(["./_integration-test/custom-ca-roots/teardown.sh"], check=True)


def test_receive_transaction_events(client_login):
client, _ = client_login
with sentry_sdk.init(
dsn=get_sentry_dsn(client), profiles_sample_rate=1.0, traces_sample_rate=1.0
):

def placeholder_fn():
sum = 0
for i in range(5):
sum += i
time.sleep(0.25)

with sentry_sdk.start_transaction(op="task", name="Test Transactions"):
placeholder_fn()
poll_for_response(
f"{SENTRY_TEST_HOST}/api/0/organizations/sentry/events/?dataset=profiles&field=profile.id&project=1&statsPeriod=1h",
client,
lambda x: len(json.loads(x)["data"]) > 0,
)
poll_for_response(
f"{SENTRY_TEST_HOST}/api/0/organizations/sentry/events/?dataset=spansIndexed&field=id&project=1&statsPeriod=1h",
client,
lambda x: len(json.loads(x)["data"]) > 0,
)
4 changes: 2 additions & 2 deletions integration-test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export MINIMIZE_DOWNTIME=0

if [[ "$test_option" == "--initial-install" ]]; then
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The goal is to rewrite this file entirely in python using pytest, but baby steps!

echo "Testing initial install"
source _integration-test/run.sh
pytest --reruns 5 _integration-test/run.py
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is that even needed? If we have proper retries when polling data, we likely don't need to retry the tests up to 5 times, do we?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would recommend not having reruns -- they're a crutch that allows flakiness to creep in and ideally we wouldn't have them in sentry

Copy link
Member Author

@hubertdeng123 hubertdeng123 Mar 19, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Primary reason behind this is because docker compose itself is quite flaky in CI, and I wanted to have a way to rerun the fixture that brings self-hosted up and report the flakiness

getsentry/team-ospo#242

source _integration-test/ensure-customizations-not-present.sh
source _integration-test/ensure-backup-restore-works.sh
elif [[ "$test_option" == "--customizations" ]]; then
Expand All @@ -34,7 +34,7 @@ EOT
echo "Testing in-place upgrade and customizations"
export MINIMIZE_DOWNTIME=1
./install.sh
source _integration-test/run.sh
pytest --reruns 5 _integration-test/run.py
source _integration-test/ensure-customizations-work.sh
source _integration-test/ensure-backup-restore-works.sh
fi
6 changes: 6 additions & 0 deletions requirements-dev.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
sentry-sdk>=1.39.2
pytest>=8.0.0
pytest-rerunfailures>=11.0
pytest-sentry>=0.1.11
httpx>=0.25.2
beautifulsoup4>=4.7.1
Loading