15
15
_QUEUE_NAME : str = "queueName"
16
16
_QUEUE_URL : str = "queueUrl"
17
17
18
+ # Patch names
19
+ GET_DISTRIBUTION_PATCH : str = (
20
+ "amazon.opentelemetry.distro.patches._instrumentation_patch.pkg_resources.get_distribution"
21
+ )
18
22
19
- class TestInstrumentationPatch (TestCase ):
20
- @classmethod
21
- def setUpClass (cls ):
22
- super ().setUpClass ()
23
- cls .mock_get_distribution = patch (
24
- "amazon.opentelemetry.distro.patches._instrumentation_patch.pkg_resources.get_distribution"
25
- ).start ()
26
-
27
- @classmethod
28
- def tearDownClass (cls ):
29
- super ().tearDownClass ()
30
- cls .mock_get_distribution .stop ()
31
-
32
- def test_botocore_not_installed (self ):
33
- # Test scenario 1: Botocore package not installed
34
- self .mock_get_distribution .side_effect = pkg_resources .DistributionNotFound
35
- apply_instrumentation_patches ()
36
- with patch (
37
- "amazon.opentelemetry.distro.patches._botocore_patches._apply_botocore_instrumentation_patches"
38
- ) as mock_apply_patches :
39
- mock_apply_patches .assert_not_called ()
40
23
41
- def test_botocore_installed_wrong_version (self ):
42
- # Test scenario 2: Botocore package installed with wrong version
43
- self .mock_get_distribution .side_effect = pkg_resources .VersionConflict ("botocore==1.0.0" , "botocore==0.0.1" )
44
- apply_instrumentation_patches ()
45
- with patch (
46
- "amazon.opentelemetry.distro.patches._botocore_patches._apply_botocore_instrumentation_patches"
47
- ) as mock_apply_patches :
48
- mock_apply_patches .assert_not_called ()
24
+ class TestInstrumentationPatch (TestCase ):
25
+ """
26
+ This test class has exactly one test, test_instrumentation_patch. This is an anti-pattern, but the scenario is
27
+ fairly unusual and we feel justifies the code smell. Essentially the _instrumentation_patch module monkey-patches
28
+ upstream components, so once it's run, it's challenging to "undo" between tests. To work around this, we have a
29
+ monolith test framework that tests two major categories of test scenarios:
30
+ 1. Patch behaviour
31
+ 2. Patch mechanism
32
+
33
+ Patch behaviour tests validate upstream behaviour without patches, apply patches, and validate patched behaviour.
34
+ Patch mechanism tests validate the logic that is used to actually apply patches, and can be run regardless of the
35
+ pre- or post-patch behaviour.
36
+ """
37
+
38
+ method_patches : Dict [str , patch ] = {}
39
+ mock_metric_exporter_init : patch
40
+
41
+ def test_instrumentation_patch (self ):
42
+ # Set up method patches used by all tests
43
+ self .method_patches [GET_DISTRIBUTION_PATCH ] = patch (GET_DISTRIBUTION_PATCH ).start ()
44
+
45
+ # Run tests that validate patch behaviour before and after patching
46
+ self ._run_patch_behaviour_tests ()
47
+ # Run tests not specifically related to patch behaviour
48
+ self ._run_patch_mechanism_tests ()
49
+
50
+ # Clean up method patches
51
+ for method_patch in self .method_patches .values ():
52
+ method_patch .stop ()
53
+
54
+ def _run_patch_behaviour_tests (self ):
55
+ # Test setup
56
+ self .method_patches [GET_DISTRIBUTION_PATCH ].return_value = "CorrectDistributionObject"
49
57
50
- def test_botocore_installed_correct_version (self ):
51
- # Test scenario 3: Botocore package installed with correct version
52
58
# Validate unpatched upstream behaviour - important to detect upstream changes that may break instrumentation
53
- self ._validate_unpatched_botocore_instrumentation ()
54
-
55
- self .mock_get_distribution .return_value = "CorrectDistributionObject"
59
+ self ._test_unpatched_botocore_instrumentation ()
56
60
57
61
# Apply patches
58
62
apply_instrumentation_patches ()
59
63
60
64
# Validate patched upstream behaviour - important to detect downstream changes that may break instrumentation
61
- self ._validate_patched_botocore_instrumentation ()
62
-
63
- def _validate_unpatched_botocore_instrumentation (self ):
65
+ self ._test_patched_botocore_instrumentation ()
66
+
67
+ # Test teardown
68
+ self ._reset_mocks ()
69
+
70
+ def _run_patch_mechanism_tests (self ):
71
+ """
72
+ Each test should be invoked, resetting mocks in between each test. E.g.:
73
+ self.test_x()
74
+ self.reset_mocks()
75
+ self.test_y()
76
+ self.reset_mocks()
77
+ etc.
78
+ """
79
+ self ._test_botocore_installed_flag ()
80
+ self ._reset_mocks ()
81
+
82
+ def _test_unpatched_botocore_instrumentation (self ):
64
83
# Kinesis
65
84
self .assertFalse ("kinesis" in _KNOWN_EXTENSIONS , "Upstream has added a Kinesis extension" )
66
85
@@ -74,7 +93,7 @@ def _validate_unpatched_botocore_instrumentation(self):
74
93
self .assertFalse ("aws.sqs.queue_url" in attributes )
75
94
self .assertFalse ("aws.sqs.queue_name" in attributes )
76
95
77
- def _validate_patched_botocore_instrumentation (self ):
96
+ def _test_patched_botocore_instrumentation (self ):
78
97
# Kinesis
79
98
self .assertTrue ("kinesis" in _KNOWN_EXTENSIONS )
80
99
kinesis_attributes : Dict [str , str ] = _do_extract_kinesis_attributes ()
@@ -96,6 +115,28 @@ def _validate_patched_botocore_instrumentation(self):
96
115
self .assertTrue ("aws.sqs.queue_name" in sqs_attributes )
97
116
self .assertEqual (sqs_attributes ["aws.sqs.queue_name" ], _QUEUE_NAME )
98
117
118
+ def _test_botocore_installed_flag (self ):
119
+ with patch (
120
+ "amazon.opentelemetry.distro.patches._botocore_patches._apply_botocore_instrumentation_patches"
121
+ ) as mock_apply_patches :
122
+ get_distribution_patch : patch = self .method_patches [GET_DISTRIBUTION_PATCH ]
123
+ get_distribution_patch .side_effect = pkg_resources .DistributionNotFound
124
+ apply_instrumentation_patches ()
125
+ mock_apply_patches .assert_not_called ()
126
+
127
+ get_distribution_patch .side_effect = pkg_resources .VersionConflict ("botocore==1.0.0" , "botocore==0.0.1" )
128
+ apply_instrumentation_patches ()
129
+ mock_apply_patches .assert_not_called ()
130
+
131
+ get_distribution_patch .side_effect = None
132
+ get_distribution_patch .return_value = "CorrectDistributionObject"
133
+ apply_instrumentation_patches ()
134
+ mock_apply_patches .assert_called ()
135
+
136
+ def _reset_mocks (self ):
137
+ for method_patch in self .method_patches .values ():
138
+ method_patch .reset_mock ()
139
+
99
140
100
141
def _do_extract_kinesis_attributes () -> Dict [str , str ]:
101
142
service_name : str = "kinesis"
0 commit comments