Skip to content

Commit 40dfe37

Browse files
jaydgossclaude
andcommitted
test(integrations): Add FRAMEWORKS integrity and multi-stack detection tests
Add TestFrameworksIntegrity to catch structural errors in framework definitions: duplicate platform IDs, invalid base_platforms, dangling supersedes targets, missing rules, and match_content without path. Add TestDetectPlatformsMultiStack to exercise the full pipeline against a realistic repo with Python (Django + Celery), JavaScript (Next.js), Go (Gin), TypeScript, and ignored languages — validating framework detection, supersession, priority ordering, and language filtering all work together. Co-Authored-By: Claude <noreply@anthropic.com>
1 parent f47ed9c commit 40dfe37

File tree

1 file changed

+170
-0
lines changed

1 file changed

+170
-0
lines changed

tests/sentry/integrations/github/test_platform_detection.py

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import pytest
77

88
from sentry.integrations.github.platform_detection import (
9+
FRAMEWORKS,
910
GITHUB_LANGUAGE_TO_SENTRY_PLATFORM,
1011
DetectedPlatform,
1112
DetectorRule,
@@ -890,3 +891,172 @@ def get_side_effect(path, params=None):
890891

891892
platforms = [r["platform"] for r in result]
892893
assert "go-gin" in platforms
894+
895+
896+
class TestFrameworksIntegrity:
897+
"""Validate the FRAMEWORKS list is internally consistent.
898+
899+
Catches typos and structural errors that would silently cause
900+
framework definitions to never match at runtime.
901+
"""
902+
903+
def test_no_duplicate_platform_ids(self) -> None:
904+
platform_ids = [fw["platform"] for fw in FRAMEWORKS]
905+
duplicates = [p for p in platform_ids if platform_ids.count(p) > 1]
906+
assert duplicates == [], f"Duplicate platform IDs: {set(duplicates)}"
907+
908+
def test_all_base_platforms_are_valid(self) -> None:
909+
valid_base_platforms = set(GITHUB_LANGUAGE_TO_SENTRY_PLATFORM.values())
910+
for fw in FRAMEWORKS:
911+
assert fw["base_platform"] in valid_base_platforms, (
912+
f"{fw['platform']} has base_platform={fw['base_platform']!r} "
913+
f"which is not a value in GITHUB_LANGUAGE_TO_SENTRY_PLATFORM"
914+
)
915+
916+
def test_all_supersedes_targets_exist(self) -> None:
917+
all_platform_ids = {fw["platform"] for fw in FRAMEWORKS}
918+
all_base_platforms = set(GITHUB_LANGUAGE_TO_SENTRY_PLATFORM.values())
919+
valid_targets = all_platform_ids | all_base_platforms
920+
921+
for fw in FRAMEWORKS:
922+
for target in fw.get("supersedes", []):
923+
assert target in valid_targets, (
924+
f"{fw['platform']} supersedes {target!r} "
925+
f"which does not exist as a framework or base platform"
926+
)
927+
928+
def test_every_framework_has_at_least_one_rule(self) -> None:
929+
for fw in FRAMEWORKS:
930+
has_rules = fw.get("every") or fw.get("some")
931+
assert has_rules, f"{fw['platform']} has no detection rules (no every or some)"
932+
933+
def test_sort_values_are_positive_integers(self) -> None:
934+
for fw in FRAMEWORKS:
935+
assert isinstance(fw["sort"], int), (
936+
f"{fw['platform']} sort={fw['sort']!r} is not an int"
937+
)
938+
assert 1 <= fw["sort"] <= 99, (
939+
f"{fw['platform']} sort={fw['sort']} is outside valid range 1-99"
940+
)
941+
942+
def test_no_rule_has_match_content_without_path(self) -> None:
943+
for fw in FRAMEWORKS:
944+
for rule in [*fw.get("every", []), *fw.get("some", [])]:
945+
if "match_content" in rule:
946+
assert "path" in rule, (
947+
f"{fw['platform']} has match_content without path — "
948+
f"content matching requires a file to read"
949+
)
950+
951+
952+
class TestDetectPlatformsMultiStack:
953+
"""Test detection against a realistic multi-language, multi-framework repo.
954+
955+
Simulates a repo like a typical full-stack app with:
956+
- Python backend (Django + Celery)
957+
- JavaScript frontend (Next.js with React)
958+
- Go microservice (Gin)
959+
- Plus build/infra languages that should be ignored
960+
"""
961+
962+
def test_full_stack_repo(self) -> None:
963+
client = mock.MagicMock()
964+
client.get_languages.return_value = {
965+
"Python": 120000,
966+
"JavaScript": 80000,
967+
"TypeScript": 60000,
968+
"Go": 40000,
969+
"HTML": 15000,
970+
"CSS": 10000,
971+
"Shell": 5000,
972+
"Makefile": 2000,
973+
"Dockerfile": 1000,
974+
}
975+
976+
def get_side_effect(path, params=None):
977+
if path.endswith("/contents"):
978+
return [
979+
{"name": "manage.py", "type": "file"},
980+
{"name": "requirements.txt", "type": "file"},
981+
{"name": "package.json", "type": "file"},
982+
{"name": "go.mod", "type": "file"},
983+
{"name": "next.config.js", "type": "file"},
984+
{"name": "Dockerfile", "type": "file"},
985+
{"name": "Makefile", "type": "file"},
986+
{"name": "src", "type": "dir"},
987+
{"name": "frontend", "type": "dir"},
988+
{"name": "services", "type": "dir"},
989+
]
990+
if "requirements.txt" in path:
991+
return _make_b64_response(
992+
"Django==4.2\ncelery>=5.3\ngunicorn\npsycopg2-binary\nredis\n"
993+
)
994+
if "package.json" in path:
995+
return _make_b64_response(
996+
json.dumps(
997+
{
998+
"dependencies": {
999+
"next": "^14.0.0",
1000+
"react": "^18.2.0",
1001+
"react-dom": "^18.2.0",
1002+
},
1003+
"devDependencies": {
1004+
"typescript": "^5.0.0",
1005+
"eslint": "^8.0.0",
1006+
},
1007+
}
1008+
)
1009+
)
1010+
if "go.mod" in path:
1011+
return _make_b64_response(
1012+
"module github.com/myorg/myapp\n\n"
1013+
"go 1.21\n\n"
1014+
"require (\n"
1015+
"\tgitlite.zycloud.tk/gin-gonic/gin v1.9.1\n"
1016+
"\tgitlite.zycloud.tk/lib/pq v1.10.9\n"
1017+
")\n"
1018+
)
1019+
raise ApiError("Not Found", code=404)
1020+
1021+
client.get.side_effect = get_side_effect
1022+
1023+
result = detect_platforms(client, "owner/repo")
1024+
platforms = [r["platform"] for r in result]
1025+
platform_set = set(platforms)
1026+
1027+
# Frameworks detected with high confidence
1028+
assert "python-django" in platform_set
1029+
assert "python-celery" in platform_set
1030+
assert "javascript-nextjs" in platform_set
1031+
assert "go-gin" in platform_set
1032+
1033+
# Supersession: React removed because Next.js is present
1034+
assert "javascript-react" not in platform_set
1035+
1036+
# Base platforms as fallback
1037+
assert "python" in platform_set
1038+
assert "javascript" in platform_set
1039+
assert "go" in platform_set
1040+
1041+
# Ignored languages excluded entirely
1042+
for r in result:
1043+
assert r["language"] not in ("HTML", "CSS", "Shell", "Makefile", "Dockerfile")
1044+
1045+
# TypeScript deduplicates into javascript base platform
1046+
ts_results = [r for r in result if r["language"] == "TypeScript"]
1047+
assert ts_results == []
1048+
1049+
# Priority ordering: meta-frameworks first, then primary, then utilities, then base
1050+
nextjs = next(r for r in result if r["platform"] == "javascript-nextjs")
1051+
django = next(r for r in result if r["platform"] == "python-django")
1052+
celery = next(r for r in result if r["platform"] == "python-celery")
1053+
python_base = next(r for r in result if r["platform"] == "python")
1054+
1055+
assert nextjs["priority"] > django["priority"] > celery["priority"]
1056+
assert celery["priority"] > python_base["priority"]
1057+
assert python_base["priority"] == 1
1058+
1059+
# Results are sorted by (priority, bytes) descending
1060+
for i in range(len(result) - 1):
1061+
a, b = result[i], result[i + 1]
1062+
assert (a["priority"], a["bytes"]) >= (b["priority"], b["bytes"])

0 commit comments

Comments
 (0)