Skip to content

Commit 53ed01f

Browse files
authored
Parallelize integration tests (#411)
* start making int tests independent" * int tests should calc their own expected result * make dependency testing work * correctly teardown files * ouput file always created * handle django files and unique dirs * requirements.txt should be within tmp code dir * initial batch of fixed integration tests * allow for providing expected_new_code instead of replacement_lines * second batch of converted int tests * next fixed batch of integration tests * sonar integration tests can still use samples file * make test dependency manager run in parallel * update makefile * sonar int tests should use 1-idx replacements * cleanup utils * remove unnecessary samples files * fix pre-commit * fix unit tests to use tmpdir * lower coverage * move to cls setup
1 parent e1b8b9e commit 53ed01f

File tree

124 files changed

+1152
-1488
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

124 files changed

+1152
-1488
lines changed

Makefile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
PYTEST = pytest -v
2-
COV_FLAGS = --cov-fail-under=94 --cov=codemodder --cov=core_codemods
2+
COV_FLAGS = --cov-fail-under=93.5 --cov=codemodder --cov=core_codemods
33
XDIST_FLAGS = --numprocesses auto
44

55
test:
66
COVERAGE_CORE=sysmon ${PYTEST} ${COV_FLAGS} tests ${XDIST_FLAGS}
77

88
integration-test:
9-
${PYTEST} integration_tests
9+
${PYTEST} integration_tests ${XDIST_FLAGS}
1010

1111
pygoat-test:
1212
${PYTEST} -v ci_tests/test_pygoat_findings.py
Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
1-
from codemodder.codemods.test import (
2-
BaseIntegrationTest,
3-
original_and_expected_from_code_path,
4-
)
1+
from requests.exceptions import ConnectionError
2+
3+
from codemodder.codemods.test import BaseIntegrationTest
54
from core_codemods.add_requests_timeouts import (
65
AddRequestsTimeouts,
76
TransformAddRequestsTimeouts,
@@ -10,15 +9,19 @@
109

1110
class TestAddRequestsTimeouts(BaseIntegrationTest):
1211
codemod = AddRequestsTimeouts
13-
code_path = "tests/samples/requests_timeout.py"
12+
original_code = """
13+
import requests
14+
15+
requests.get("https://example.com")
16+
requests.get("https://example.com", timeout=1)
17+
requests.get("https://example.com", timeout=(1, 10), verify=False)
18+
requests.post("https://example.com", verify=False)
19+
"""
1420

15-
original_code, expected_new_code = original_and_expected_from_code_path(
16-
code_path,
17-
[
18-
(2, 'requests.get("https://example.com", timeout=60)\n'),
19-
(5, 'requests.post("https://example.com", verify=False, timeout=60)\n'),
20-
],
21-
)
21+
replacement_lines = [
22+
(3, 'requests.get("https://example.com", timeout=60)\n'),
23+
(6, 'requests.post("https://example.com", verify=False, timeout=60)\n'),
24+
]
2225

2326
expected_diff = """\
2427
---
@@ -37,3 +40,5 @@ class TestAddRequestsTimeouts(BaseIntegrationTest):
3740
num_changes = 2
3841
expected_line_change = "3"
3942
change_description = TransformAddRequestsTimeouts.change_description
43+
# expected because requests are made
44+
allowed_exceptions = (ConnectionError,)
Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
1-
from codemodder.codemods.test import (
2-
BaseIntegrationTest,
3-
original_and_expected_from_code_path,
4-
)
1+
from codemodder.codemods.test import BaseIntegrationTest
52
from core_codemods.combine_startswith_endswith import CombineStartswithEndswith
63

74

85
class TestCombineStartswithEndswith(BaseIntegrationTest):
96
codemod = CombineStartswithEndswith
10-
code_path = "tests/samples/combine_startswith_endswith.py"
11-
original_code, expected_new_code = original_and_expected_from_code_path(
12-
code_path, [(1, 'if x.startswith(("foo", "bar")):\n')]
13-
)
7+
original_code = """
8+
x = 'foo'
9+
if x.startswith("foo") or x.startswith("bar"):
10+
print("Yes")
11+
"""
12+
replacement_lines = [(2, 'if x.startswith(("foo", "bar")):\n')]
13+
1414
expected_diff = '--- \n+++ \n@@ -1,3 +1,3 @@\n x = \'foo\'\n-if x.startswith("foo") or x.startswith("bar"):\n+if x.startswith(("foo", "bar")):\n print("Yes")\n'
1515
expected_line_change = "2"
1616
change_description = CombineStartswithEndswith.change_description

integration_tests/test_dependency_manager.py

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,27 @@
11
import os
2+
import pathlib
23
import shutil
34
import subprocess
5+
import tempfile
46
from textwrap import dedent
57

68
import pytest
79

8-
from codemodder.codemods.test.integration_utils import SAMPLES_DIR, CleanRepoMixin
10+
from codemodder.codemods.test.integration_utils import SAMPLES_DIR
911

1012

11-
class TestDependencyManager(CleanRepoMixin):
12-
output_path = "test-codetf.txt"
13+
class TestDependencyManager:
14+
@classmethod
15+
def setup_class(cls):
16+
cls.output_path = tempfile.mkstemp()[1]
17+
cls.toml_file = "pyproject.toml"
18+
cls.requirements_file = "requirements.txt"
19+
cls.setup_file = "setup.py"
1320

14-
toml_file = "pyproject.toml"
15-
requirements_file = "requirements.txt"
16-
setup_file = "setup.py"
21+
@classmethod
22+
def teardown_class(cls):
23+
"""Ensure any re-written file is undone after integration test class"""
24+
pathlib.Path(cls.output_path).unlink(missing_ok=True)
1725

1826
@pytest.fixture
1927
def tmp_repo(self, tmp_path):
Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,24 @@
1-
from codemodder.codemods.test import (
2-
BaseIntegrationTest,
3-
original_and_expected_from_code_path,
4-
)
1+
from codemodder.codemods.test import BaseIntegrationTest
52
from core_codemods.django_debug_flag_on import DjangoDebugFlagOn
63

74

85
class TestDjangoDebugFlagFlip(BaseIntegrationTest):
96
codemod = DjangoDebugFlagOn
10-
code_path = "tests/samples/django-project/mysite/mysite/settings.py"
11-
12-
original_code, expected_new_code = original_and_expected_from_code_path(
13-
code_path, [(25, "DEBUG = False\n")]
7+
code_filename = "settings.py"
8+
original_code = """
9+
# SECURITY WARNING: don't run with debug turned on in production!
10+
DEBUG = True
11+
"""
12+
replacement_lines = [(2, "DEBUG = False\n")]
13+
# fmt: off
14+
expected_diff = (
15+
"""--- \n"""
16+
"""+++ \n"""
17+
"""@@ -1,2 +1,2 @@\n"""
18+
""" # SECURITY WARNING: don't run with debug turned on in production!\n"""
19+
"""-DEBUG = True\n"""
20+
"""+DEBUG = False\n"""
1421
)
15-
expected_diff = '--- \n+++ \n@@ -23,7 +23,7 @@\n SECRET_KEY = "django-insecure-t*rrda&qd4^#q+50^%q^rrsp-t$##&u5_#=9)&@ei^ppl6$*c*"\n \n # SECURITY WARNING: don\'t run with debug turned on in production!\n-DEBUG = True\n+DEBUG = False\n \n ALLOWED_HOSTS = []\n \n'
16-
expected_line_change = "26"
22+
# fmt: on
23+
expected_line_change = "2"
1724
change_description = DjangoDebugFlagOn.change_description

integration_tests/test_django_json_response_type.py

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,4 @@
1-
from codemodder.codemods.test import (
2-
BaseIntegrationTest,
3-
original_and_expected_from_code_path,
4-
)
1+
from codemodder.codemods.test import BaseIntegrationTest
52
from core_codemods.django_json_response_type import (
63
DjangoJsonResponseType,
74
DjangoJsonResponseTypeTransformer,
@@ -10,19 +7,23 @@
107

118
class TestDjangoJsonResponseType(BaseIntegrationTest):
129
codemod = DjangoJsonResponseType
13-
code_path = "tests/samples/django_json_response_type.py"
14-
original_code, expected_new_code = original_and_expected_from_code_path(
15-
code_path,
16-
[
17-
(
18-
5,
19-
""" return HttpResponse(json_response, content_type="application/json")\n""",
20-
),
21-
],
22-
)
10+
original_code = """
11+
from django.http import HttpResponse
12+
import json
13+
14+
def foo(request):
15+
json_response = json.dumps({ "user_input": request.GET.get("input") })
16+
return HttpResponse(json_response)
17+
"""
18+
replacement_lines = [
19+
(
20+
6,
21+
""" return HttpResponse(json_response, content_type="application/json")\n""",
22+
),
23+
]
2324

2425
# fmt: off
25-
expected_diff =(
26+
expected_diff = (
2627
"""--- \n"""
2728
"""+++ \n"""
2829
"""@@ -3,4 +3,4 @@\n"""

integration_tests/test_django_model_without_dunder_str.py

Lines changed: 28 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,4 @@
1-
from codemodder.codemods.test import (
2-
BaseIntegrationTest,
3-
original_and_expected_from_code_path,
4-
)
1+
from codemodder.codemods.test import BaseIntegrationTest
52
from core_codemods.django_model_without_dunder_str import (
63
DjangoModelWithoutDunderStr,
74
DjangoModelWithoutDunderStrTransformer,
@@ -10,23 +7,35 @@
107

118
class TestDjangoModelWithoutDunderStr(BaseIntegrationTest):
129
codemod = DjangoModelWithoutDunderStr
13-
code_path = "tests/samples/django-project/mysite/mysite/models.py"
14-
original_code, expected_new_code = original_and_expected_from_code_path(
15-
code_path,
16-
[
17-
(15, """\n"""),
18-
(16, """ def __str__(self):\n"""),
19-
(17, """ model_name = self.__class__.__name__\n"""),
20-
(
21-
18,
22-
""" fields_str = ", ".join((f"{field.name}={getattr(self, field.name)}" for field in self._meta.fields))\n""",
23-
),
24-
(19, """ return f"{model_name}({fields_str})"\n"""),
25-
],
26-
)
10+
original_code = """
11+
import django
12+
from django.conf import settings
13+
from django.db import models
14+
# required to run this module standalone for testing
15+
settings.configure()
16+
django.setup()
17+
18+
19+
class Message(models.Model):
20+
author = models.CharField(max_length=100)
21+
content = models.CharField(max_length=200)
22+
class Meta:
23+
app_label = 'myapp'
24+
25+
"""
26+
replacement_lines = [
27+
(15, """\n"""),
28+
(16, """ def __str__(self):\n"""),
29+
(17, """ model_name = self.__class__.__name__\n"""),
30+
(
31+
18,
32+
""" fields_str = ", ".join((f"{field.name}={getattr(self, field.name)}" for field in self._meta.fields))\n""",
33+
),
34+
(19, """ return f"{model_name}({fields_str})"\n"""),
35+
]
2736

2837
# fmt: off
29-
expected_diff =(
38+
expected_diff = (
3039
"""--- \n"""
3140
"""+++ \n"""
3241
"""@@ -11,3 +11,8 @@\n"""

integration_tests/test_django_receiver_on_top.py

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,4 @@
1-
from codemodder.codemods.test import (
2-
BaseIntegrationTest,
3-
original_and_expected_from_code_path,
4-
)
1+
from codemodder.codemods.test import BaseIntegrationTest
52
from core_codemods.django_receiver_on_top import (
63
DjangoReceiverOnTop,
74
DjangoReceiverOnTopTransformer,
@@ -10,17 +7,23 @@
107

118
class TestDjangoReceiverOnTop(BaseIntegrationTest):
129
codemod = DjangoReceiverOnTop
13-
code_path = "tests/samples/django_receiver_on_top.py"
14-
original_code, expected_new_code = original_and_expected_from_code_path(
15-
code_path,
16-
[
17-
(4, """@receiver(request_finished)\n"""),
18-
(5, """@csrf_exempt\n"""),
19-
],
20-
)
10+
original_code = """
11+
from django.dispatch import receiver
12+
from django.views.decorators.csrf import csrf_exempt
13+
from django.core.signals import request_finished
14+
15+
@csrf_exempt
16+
@receiver(request_finished)
17+
def foo():
18+
pass
19+
"""
20+
replacement_lines = [
21+
(5, """@receiver(request_finished)\n"""),
22+
(6, """@csrf_exempt\n"""),
23+
]
2124

2225
# fmt: off
23-
expected_diff =(
26+
expected_diff = (
2427
"""--- \n"""
2528
"""+++ \n"""
2629
"""@@ -2,7 +2,7 @@\n"""
Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,26 @@
1-
from codemodder.codemods.test import (
2-
BaseIntegrationTest,
3-
original_and_expected_from_code_path,
4-
)
1+
from codemodder.codemods.test import BaseIntegrationTest
52
from core_codemods.django_session_cookie_secure_off import DjangoSessionCookieSecureOff
63

74

85
class TestDjangoSessionCookieSecureOff(BaseIntegrationTest):
96
codemod = DjangoSessionCookieSecureOff
10-
code_path = "tests/samples/django-project/mysite/mysite/settings.py"
7+
code_filename = "settings.py"
118

12-
original_code, expected_new_code = original_and_expected_from_code_path(
13-
code_path, [(124, "SESSION_COOKIE_SECURE = True\n")]
9+
original_code = """
10+
# django settings
11+
# SESSION_COOKIE_SECURE is not defined
12+
"""
13+
replacement_lines = [(3, "SESSION_COOKIE_SECURE = True\n")]
14+
15+
# fmt: off
16+
expected_diff = (
17+
"""--- \n"""
18+
"""+++ \n"""
19+
"""@@ -1,2 +1,3 @@\n"""
20+
""" # django settings\n"""
21+
""" # SESSION_COOKIE_SECURE is not defined\n"""
22+
"""+SESSION_COOKIE_SECURE = True\n"""
1423
)
15-
expected_diff = '--- \n+++ \n@@ -121,3 +121,4 @@\n # https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field\n \n DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"\n+SESSION_COOKIE_SECURE = True\n'
16-
expected_line_change = "124"
24+
# fmt: on
25+
expected_line_change = "3"
1726
change_description = DjangoSessionCookieSecureOff.change_description

integration_tests/test_exception_without_raise.py

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,4 @@
1-
from codemodder.codemods.test import (
2-
BaseIntegrationTest,
3-
original_and_expected_from_code_path,
4-
)
1+
from codemodder.codemods.test import BaseIntegrationTest
52
from core_codemods.exception_without_raise import (
63
ExceptionWithoutRaise,
74
ExceptionWithoutRaiseTransformer,
@@ -10,16 +7,15 @@
107

118
class TestExceptionWithoutRaise(BaseIntegrationTest):
129
codemod = ExceptionWithoutRaise
13-
code_path = "tests/samples/exception_without_raise.py"
14-
original_code, expected_new_code = original_and_expected_from_code_path(
15-
code_path,
16-
[
17-
(1, """ raise ValueError\n"""),
18-
],
19-
)
20-
10+
original_code = """
11+
try:
12+
ValueError
13+
except:
14+
pass
15+
"""
16+
replacement_lines = [(2, """ raise ValueError\n""")]
2117
# fmt: off
22-
expected_diff =(
18+
expected_diff = (
2319
"""--- \n"""
2420
"""+++ \n"""
2521
"""@@ -1,4 +1,4 @@\n"""

0 commit comments

Comments
 (0)