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 6 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=https://[email protected]/6627632" >> $GITHUB_ENV
echo "PYTEST_SENTRY_TRACES_SAMPLE_RATE=0" >> $GITHUB_ENV
hubertdeng123 marked this conversation as resolved.
Show resolved Hide resolved

# 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
7 changes: 5 additions & 2 deletions _integration-test/custom-ca-roots/setup.sh
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,11 @@ openssl req -new -nodes -newkey rsa:2048 -keyout $TEST_NGINX_CONF_PATH/self.test

# openssl req -in nginx/self.test.req -text -noout

# Store extension details in a temporary file since python subprocess can't use process substitution
printf "subjectAltName=DNS:self.test" >/tmp/extension_details
hubertdeng123 marked this conversation as resolved.
Show resolved Hide resolved

openssl x509 -req -in $TEST_NGINX_CONF_PATH/self.test.req -CA $TEST_NGINX_CONF_PATH/ca.crt -CAkey $TEST_NGINX_CONF_PATH/ca.key \
-extfile <(printf "subjectAltName=DNS:self.test") \
-extfile /tmp/extension_details \
-CAcreateserial -out $TEST_NGINX_CONF_PATH/self.test.crt -days 1 -sha256

# openssl x509 -in nginx/self.test.crt -text -noout
Expand All @@ -42,4 +45,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
159 changes: 159 additions & 0 deletions _integration-test/run.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
import subprocess
import os
from functools import lru_cache
import requests
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 not recommend using requests in new code

if you can get away with it, stdlib urllib.request does not require additional dependencies. httpx is a modern replacement if you find urllib.request too cumbersome

Copy link
Member Author

Choose a reason for hiding this comment

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

switched to using httpx, since I wanted a good way to save cookies

Copy link
Member Author

Choose a reason for hiding this comment

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

What's the reason why requests is not the preferred choice?

import unittest
import sentry_sdk
import time
import json

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 run_shell_command(cmd, **kwargs) -> subprocess.CompletedProcess:
return subprocess.run(cmd, shell=True, check=True, **kwargs)
hubertdeng123 marked this conversation as resolved.
Show resolved Hide resolved


class IntegrationTests(unittest.TestCase):
hubertdeng123 marked this conversation as resolved.
Show resolved Hide resolved
@classmethod
def setUpClass(cls):
hubertdeng123 marked this conversation as resolved.
Show resolved Hide resolved
cls.session = requests.Session()
response = None
run_shell_command("docker compose --ansi never up -d")
for i in range(TIMEOUT_SECONDS):
Copy link
Member Author

Choose a reason for hiding this comment

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

this is pretty ugly, open to suggestions on how to make polling for a response cleaner

try:
response = requests.get(SENTRY_TEST_HOST)
except requests.ConnectionError:
pass
if response is not None and response.status_code == 200:
break
time.sleep(1)
assert response.status_code == 200

# Create test user
run_shell_command(
f"echo y | docker compose exec web sentry createuser --force-update --superuser --email {TEST_USER} --password {TEST_PASS}"
)

def poll_for_response(self, request: str) -> requests.Response:
for i in range(TIMEOUT_SECONDS):
response = self.session.get(request, headers={"Referer": SENTRY_TEST_HOST})
if response.status_code == 200:
break
time.sleep(1)
assert response.status_code == 200
return response

@lru_cache
def get_sentry_dsn(self) -> str:
for i in range(TIMEOUT_SECONDS):
response = self.session.get(
f"{SENTRY_TEST_HOST}/api/0/projects/sentry/internal/keys/",
headers={"Referer": SENTRY_TEST_HOST},
)
if response.status_code == 200:
sentry_dsn = json.loads(response.text)[0]["dsn"]["public"]
if len(sentry_dsn) > 0:
break
time.sleep(1)
sentry_dsn = json.loads(response.text)[0]["dsn"]["public"]
assert len(sentry_dsn) > 0
return sentry_dsn

def test_initial_redirect(self):
initial_auth_redirect = self.session.get(SENTRY_TEST_HOST)
assert initial_auth_redirect.url == f"{SENTRY_TEST_HOST}/auth/login/sentry/"

def test_login(self):
login_csrf_token = (
self.session.get(SENTRY_TEST_HOST)
.text.split('"csrfmiddlewaretoken" value="')[1]
.split('"')[0]
)
login_response = self.session.post(
f"{SENTRY_TEST_HOST}/auth/login/sentry/",
data={
"op": "login",
"username": TEST_USER,
"password": TEST_PASS,
"csrfmiddlewaretoken": login_csrf_token,
},
headers={"Referer": f"{SENTRY_TEST_HOST}/auth/login/sentry/"},
)
assert '"isAuthenticated":true' in login_response.text
assert '"username":"[email protected]"' in login_response.text
assert '"isSuperuser":true' in login_response.text
assert login_response.cookies["sc"] is not None
# Set up initial/required settings (InstallWizard request)
self.session.headers.update({"X-CSRFToken": login_response.cookies["sc"]})
response = self.session.put(
f"{SENTRY_TEST_HOST}/api/0/internal/options/?query=is:required",
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(self):
event_id = None
with sentry_sdk.init(dsn=self.get_sentry_dsn()):
event_id = sentry_sdk.capture_exception(Exception("a failure"))
assert event_id is not None
response = self.poll_for_response(
f"{SENTRY_TEST_HOST}/api/0/projects/sentry/internal/events/{event_id}/"
)
response_json = json.loads(response.text)
assert response_json["eventID"] == event_id
assert response_json["metadata"]["value"] == "a failure"

def test_cleanup_crons_running(self):
cleanup_crons = run_shell_command(
"docker compose --ansi never ps -a | tee debug.log | grep -E -e '\\-cleanup\\s+running\\s+' -e '\\-cleanup[_-].+\\s+Up\\s+'",
capture_output=True,
).stdout
assert len(cleanup_crons) > 0

def test_custom_cas(self):
run_shell_command(". _integration-test/custom-ca-roots/setup.sh")
run_shell_command(
"docker compose --ansi never run --no-deps web python3 /etc/sentry/test-custom-ca-roots.py"
)
run_shell_command(". _integration-test/custom-ca-roots/teardown.sh")

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

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

with sentry_sdk.start_transaction(op="task", name="Test Transactions"):
dummy_func()
profiles_response = self.poll_for_response(
f"{SENTRY_TEST_HOST}/api/0/organizations/sentry/events/?dataset=profiles&field=profile.id&project=1&statsPeriod=1h"
)
assert profiles_response.status_code == 200
profiles_response_json = json.loads(profiles_response.text)
assert len(profiles_response_json) > 0
spans_response = self.poll_for_response(
f"{SENTRY_TEST_HOST}/api/0/organizations/sentry/events/?dataset=spansIndexed&field=id&project=1&statsPeriod=1h"
)
assert spans_response.status_code == 200
spans_response_json = json.loads(spans_response.text)
assert len(spans_response_json) > 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
5 changes: 5 additions & 0 deletions requirements-dev.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
sentry-sdk>=1.39.2
requests>=2.31.0
pytest>=8.0.0
pytest-rerunfailures>=11.0
pytest-sentry>=0.1.11
Loading