Skip to content

Switch context by test #204

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

Merged
merged 2 commits into from
Jun 26, 2025
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
3 changes: 3 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,9 @@ precommit:
switch:
@ scripts/dev/switch_context.sh $(context) $(additional_override)

switcht:
@ scripts/dev/switch_context_by_test.sh $(test)

# builds the Operator binary file and docker image and pushes it to the remote registry if using a remote registry. Deploys it to
# k8s cluster
operator: configure-operator build-and-push-operator-image
Expand Down
1 change: 1 addition & 0 deletions scripts/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Makes 'scripts' a Python package.
5 changes: 5 additions & 0 deletions scripts/dev/contexts/root-context
Original file line number Diff line number Diff line change
Expand Up @@ -116,3 +116,8 @@ export MDB_SEARCH_COMMUNITY_VERSION

export MDB_SEARCH_COMMUNITY_NAME="mongodb-search-community"
export MDB_SEARCH_COMMUNITY_REPO_URL="quay.io/mongodb"


if [[ ${MDB_BASH_DEBUG:-0} == 1 ]]; then
Copy link
Collaborator

Choose a reason for hiding this comment

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

can you add a small comment? I actually have no clue what this does :D

export PS4='+(${BASH_SOURCE}:${LINENO})[^$?]: ${FUNCNAME[0]:+${FUNCNAME[0]}(): }'
fi
3 changes: 1 addition & 2 deletions scripts/dev/launch_e2e.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

set -Eeou pipefail

test "${MDB_BASH_DEBUG:-0}" -eq 1 && set -x
Copy link
Collaborator

Choose a reason for hiding this comment

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

a small comment for the non bash users?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I don't want to put comment to each that line, because that line should be present in all of our bash scripts.
It's allowing us to debug deeply nested shell scripts, e.g.
MDB_BASH_DEBUG=1 make e2e ... will just propagate set -x in all scripts that declare this


# The script launches e2e test. Note, that the Operator and necessary resources are deployed
# inside the test
Expand Down Expand Up @@ -49,5 +50,3 @@ else
fi

title "E2e test ${test} is finished"

Copy link
Collaborator

Choose a reason for hiding this comment

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

eof new line?


3 changes: 2 additions & 1 deletion scripts/dev/set_env_context.sh
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
#!/usr/bin/env bash

set -Eeou pipefail
test "${MDB_BASH_DEBUG:-0}" -eq 1 && set -x

# shellcheck disable=1091
source scripts/funcs/errors

script_name=$(readlink -f "${BASH_SOURCE[0]}")
script_dir=$(dirname "${script_name}")
context_file="${script_dir}/../../.generated/context.export.env"
context_file="$(realpath "${script_dir}/../../.generated/context.export.env")"
Copy link
Contributor Author

Choose a reason for hiding this comment

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

this resolves to the physical path and avoids huge paths with multiple /../../ when debugging with -x


if [[ ! -f ${context_file} ]]; then
fatal "File ${context_file} not found! Make sure to follow this guide to get started: https://wiki.corp.mongodb.com/display/MMS/Setting+up+local+development+and+E2E+testing#SettinguplocaldevelopmentandE2Etesting-GettingStartedGuide(VariantSwitching)"
Expand Down
81 changes: 81 additions & 0 deletions scripts/dev/switch_context_by_test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
#!/usr/bin/env bash

set -Eeou pipefail
test "${MDB_BASH_DEBUG:-0}" -eq 1 && set -x

usage() {
echo "Switch context by passing the test (evergreen task name or full evergreen task URL)."
echo "If there is more than one variant running given test, then fzf picker is used."
echo "Usage: $0 <test>"
echo " <test> is a task name from .evergreen.yml (e.g. 'e2e_search_community_basic') or a full Evergreen task URL."
}

source scripts/funcs/errors

list_pytest_marks() {
rg -g '*.py' -o --no-line-number --no-heading --replace '$1' -m 1 \
'@(?:pytest\.)?mark\.(e2e_[a-zA-Z0-9_]+)' \
docker/mongodb-kubernetes-tests
}

pick_test_by_file_mark_or_task_url() {
if ! test_list="$(list_pytest_marks | sort -u)"; then
echo "Couldn't list pytest marks."
echo "${test_list}"
return 1
fi

test=$(fzf --print-query --header-first --with-nth '{2}: {1}' -d ':' --accept-nth 2 \
--header "Select file or task to find contexts where its used. You can paste full task's evergreen url here" <<< "${test_list}") || true
if [[ -z ${test} ]]; then
echo "Aborted selecting test"
return 1
fi

# test may contain one or two lines (file:mark or just mark/url)
number_of_selected_lines=$(wc -l <<< "${test}")
if [[ ${number_of_selected_lines} -eq 2 ]]; then
test="$(tail -n 1 <<< "${test}")"
elif [[ ${number_of_selected_lines} -gt 2 ]]; then
echo "Too many lines selected: ${test}"
return 1
fi

echo "${test}"
}

main() {
test="${1:-}"

if [[ -z ${test} ]]; then
test=$(pick_test_by_file_mark_or_task_url)
echo "Selected test: ${test}"
fi

if [[ "${test}" = *spruce.mongodb.com* ]]; then
find_variant_arg="--task-url"
else
find_variant_arg="--task-name"
fi

if ! contexts=$(scripts/evergreen/run_python.sh scripts/python/find_test_variants.py "${find_variant_arg}" "${test}"); then
echo "Couldn't find any test contexts running test: ${test}"
echo "${contexts}"
exit 1
fi

echo "Found contexts that are running test: ${test}"
echo "${contexts}"

selected_context="${contexts}"
if [[ $(wc -l <<< "${contexts}") -gt 1 ]]; then
if ! selected_context=$(fzf --header "${test} runs in multiple variants/contexts. Select one to switch context into." --header-first --layout=reverse <<< "${contexts}"); then
echo "Aborted selecting context"
exit 1
fi
fi

scripts/dev/switch_context.sh "${selected_context}"
}

main "$@"
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import json
import os
import sys

import requests

from scripts.python.evergreen_api import get_evergreen_auth_headers

EVERGREEN_API = "https://evergreen.mongodb.com/api"


Expand All @@ -18,16 +18,13 @@ def print_usage():


def get_variants_with_retried_tasks() -> dict[str, list[dict]]:
evg_user = os.environ.get("EVERGREEN_USER", "")
api_key = os.environ.get("API_KEY", "")

if len(sys.argv) != 2 or evg_user == "" or api_key == "":
if len(sys.argv) != 2:
print_usage()
exit(1)
raise RuntimeError("Exactly one argument (patch version number) must be provided")

version = sys.argv[1]
headers = get_evergreen_auth_headers()

headers = {"Api-User": evg_user, "Api-Key": api_key}
print("Fetching build variants...", file=sys.stderr)
build_ids = requests.get(url=f"{EVERGREEN_API}/rest/v2/versions/{version}", headers=headers).json()
build_statuses = [build_status for build_status in build_ids["build_variants_status"]]
Expand Down
6 changes: 0 additions & 6 deletions scripts/funcs/checks
Original file line number Diff line number Diff line change
@@ -1,11 +1,5 @@
#!/usr/bin/env bash

pushd "${PWD}" > /dev/null || return
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
cd "${DIR}" || return
source errors
popd > /dev/null || return

check_env_var() {
local var_name="$1"
local msg="$2"
Expand Down
13 changes: 3 additions & 10 deletions scripts/funcs/kubernetes
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,9 @@

set -Eeou pipefail

pushd "${PWD}" > /dev/null || return
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
cd "${DIR}" || return
# shellcheck source=scripts/funcs/checks
source checks
# shellcheck source=scripts/funcs/errors
source errors
# shellcheck source=scripts/funcs/printing
source printing
popd > /dev/null || return
source scripts/funcs/checks
source scripts/funcs/errors
source scripts/funcs/printing

ensure_namespace() {
local namespace="${1}"
Expand Down
1 change: 1 addition & 0 deletions scripts/python/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Makes 'scripts.python' a Python package.
30 changes: 30 additions & 0 deletions scripts/python/evergreen_api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import os

import requests

EVERGREEN_API = "https://evergreen.mongodb.com/api"


def get_evergreen_auth_headers() -> dict:
"""
Returns Evergreen API authentication headers using EVERGREEN_USER and EVERGREEN_API_KEY environment variables.
Raises RuntimeError if either variable is missing.
"""
evg_user = os.environ.get("EVERGREEN_USER", "")
api_key = os.environ.get("EVERGREEN_API_KEY", "")
if evg_user == "" or api_key == "":
raise RuntimeError("EVERGREEN_USER and EVERGREEN_API_KEY must be set")
return {"Api-User": evg_user, "Api-Key": api_key}


def get_task_details(task_id: str) -> dict:
"""
Fetch task details from Evergreen API for a given task_id.
Returns the JSON response as a dict.
Raises requests.HTTPError if the request fails.
"""
url = f"{EVERGREEN_API}/rest/v2/tasks/{task_id}"
headers = get_evergreen_auth_headers()
response = requests.get(url, headers=headers)
response.raise_for_status()
return response.json()
81 changes: 81 additions & 0 deletions scripts/python/find_test_variants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import argparse
import re
import sys

import yaml

from scripts.python.evergreen_api import get_task_details


def find_task_variants(evergreen_yml_path: str, task_name: str) -> list[str]:
with open(evergreen_yml_path, "r") as file:
evergreen_data = yaml.safe_load(file)

task_groups = evergreen_data.get("task_groups", [])
build_variants = evergreen_data.get("buildvariants", [])

matching_task_groups = [group["name"] for group in task_groups if task_name in group.get("tasks", [])]

matching_variants: list[str] = []
for variant in build_variants:
variant_tasks = variant.get("tasks", [])
for task_entry in variant_tasks:
task_name = task_entry.get("name") if isinstance(task_entry, dict) else task_entry
if task_name in matching_task_groups:
matching_variants.append(variant["name"])
break

return matching_variants


def extract_task_name_from_url(task_url: str) -> str:
match = re.search(r"/task/([^/]+)/", task_url)
if not match:
raise Exception("Could not extract task name from URL")
return match.group(1)


def find_task_variant_by_url(task_url: str) -> list[str]:
task_name = extract_task_name_from_url(task_url)
details = get_task_details(task_name)
if "build_variant" not in details:
raise Exception(f'"build_variant" not found in task details: {details}')

return details["build_variant"]


def main() -> None:
parser = argparse.ArgumentParser(description="Find Evergreen build variants for a given task.")
parser.add_argument(
"--evergreen-file", default=".evergreen.yml", help="Path to evergreen.yml (default: evergreen.yml)"
)
parser.add_argument("--task-name", required=False, help="Task name to search for")
parser.add_argument(
"--task-url",
required=False,
help="Full evergreen url to a task, e.g. https://spruce.mongodb.com/task/mongodb_kubernetes_unit_5e913/logs?execution=0",
)
args = parser.parse_args()

# Ensure exactly one of --task-name or --task-url is provided
if bool(args.task_name) == bool(args.task_url):
parser.error("Exactly one of --task-name or --task-url must be provided.")

try:
if args.task_name:
variants = find_task_variants(args.evergreen_file, args.task_name)
else:
variants = [find_task_variant_by_url(args.task_url)]
except Exception as e:
print(f"Error: {e}", file=sys.stderr)
sys.exit(1)

if not variants:
sys.exit(1)

for variant_name in variants:
print(variant_name)


if __name__ == "__main__":
main()
87 changes: 87 additions & 0 deletions scripts/python/find_test_variants_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import io
import os
import sys

import pytest

from scripts.python.find_test_variants import (
extract_task_name_from_url,
find_task_variants,
main,
)

# This test file uses our real .evergreen.yml, so it might require adjustments if we change the test structure


def test_find_task_variants():
project_dir = os.environ.get("PROJECT_DIR", ".")
evergreen_file = os.path.join(project_dir, ".evergreen.yml")
result = find_task_variants(evergreen_file, "e2e_feature_controls_authentication")
assert sorted(result) == ["e2e_mdb_kind_ubi_cloudqa", "e2e_static_mdb_kind_ubi_cloudqa"]

result = find_task_variants(evergreen_file, "e2e_sharded_cluster")
assert sorted(result) == [
"e2e_mdb_kind_ubi_cloudqa",
"e2e_multi_cluster_kind",
"e2e_static_mdb_kind_ubi_cloudqa",
"e2e_static_multi_cluster_kind",
]

result = find_task_variants(evergreen_file, "")
assert sorted(result) == []

result = find_task_variants(evergreen_file, "invalid!")
assert sorted(result) == []


def test_main_output(monkeypatch):
project_dir = os.environ.get("PROJECT_DIR", ".")
evergreen_file = os.path.join(project_dir, ".evergreen.yml")
args = [
"find_task_variants.py",
"--evergreen-file",
evergreen_file,
"--task-name",
"e2e_feature_controls_authentication",
]
monkeypatch.setattr(sys, "argv", args)
captured = io.StringIO()
monkeypatch.setattr("sys.stdout", captured)
main()
output = captured.getvalue().strip().splitlines()
assert sorted(output) == ["e2e_mdb_kind_ubi_cloudqa", "e2e_static_mdb_kind_ubi_cloudqa"]


def test_main_output_no_matches(monkeypatch):
"""
Test that main() exits with code 1 when there are no matching variants.
"""
project_dir = os.environ.get("PROJECT_DIR", ".")
evergreen_file = os.path.join(project_dir, ".evergreen.yml")
args = ["find_task_variants.py", "--evergreen-file", evergreen_file, "--task-name", "nonexistent_task_name"]
monkeypatch.setattr(sys, "argv", args)
captured = io.StringIO()
monkeypatch.setattr("sys.stdout", captured)
with pytest.raises(SystemExit) as e:
main()
assert e.value.code == 1
assert captured.getvalue().strip() == ""


def test_extract_task_name():
url = "https://spruce.mongodb.com/task/mongodb_kubernetes_e2e_custom_domain_mdb_kind_ubi_cloudqa_e2e_replica_set_patch_ca24d93d7a931f7853a679b4576674cace37bb16_6851672289288f00073de47a_25_06_17_13_01_24/logs?execution=0"
expected = "mongodb_kubernetes_e2e_custom_domain_mdb_kind_ubi_cloudqa_e2e_replica_set_patch_ca24d93d7a931f7853a679b4576674cace37bb16_6851672289288f00073de47a_25_06_17_13_01_24"
assert extract_task_name_from_url(url) == expected


def test_extract_task_name_invalid_url():
invalid_url = (
"https://spruce.mongodb.com/version/6851672289288f00073de47a/tasks?sorts=STATUS%3AASC%3BBASE_STATUS%3ADESC"
)
with pytest.raises(Exception):
extract_task_name_from_url(invalid_url)


def test_extract_task_name_empty():
with pytest.raises(Exception):
extract_task_name_from_url("")