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

Enable WireClient tests #1800

Merged
merged 5 commits into from
Mar 9, 2020
Merged

Enable WireClient tests #1800

merged 5 commits into from
Mar 9, 2020

Conversation

narrieta
Copy link
Member

@narrieta narrieta commented Mar 6, 2020

Re-enables the tests disabled by #1799

I'll annotate the PR with the relevant changes.


This change is Reviewable

@codecov
Copy link

codecov bot commented Mar 6, 2020

Codecov Report

Merging #1800 into develop will increase coverage by 0.05%.
The diff coverage is n/a.

Impacted file tree graph

@@             Coverage Diff             @@
##           develop    #1800      +/-   ##
===========================================
+ Coverage    68.49%   68.54%   +0.05%     
===========================================
  Files           82       82              
  Lines        11734    11729       -5     
  Branches      1644     1644              
===========================================
+ Hits          8037     8040       +3     
+ Misses        3349     3343       -6     
+ Partials       348      346       -2
Impacted Files Coverage Δ
azurelinuxagent/common/protocol/wire.py 71.4% <ø> (+0.88%) ⬆️

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update f917863...f855715. Read the comment docs.

@@ -520,11 +520,6 @@ def __init__(self, endpoint):
logger.info("Wire server endpoint:{0}", endpoint)
self._endpoint = endpoint
self._goal_state = None
self._hosting_env = None
Copy link
Member Author

Choose a reason for hiding this comment

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

I forgot to remove these on #1777

@@ -33,7 +33,7 @@
from azurelinuxagent.common.utils import restutil
from tests.protocol import mock_wire_protocol
from tests.protocol.mockwiredata import WireProtocolData, DATA_FILE, DATA_FILE_NO_EXT
from tests.protocol.test_wire import MockResponse
from tests.protocol.test_wire import MockResponse as TestWireMockResponse
Copy link
Member Author

@narrieta narrieta Mar 6, 2020

Choose a reason for hiding this comment

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

This one was so much fun to debug! This test uses 2 entities named MockResponse: the one imported here and another at the bottom of the file.

Which one gets used depends on whether patch is used as a decorator or as a statement. Ah, the tangled web we weave!

The import alias takes care of this issue.

@@ -403,6 +403,7 @@ def test_validate_http_request(self):

with mock_wire_protocol.create(DATA_FILE) as protocol:
test_goal_state = protocol.client._goal_state
plugin = protocol.client.get_host_plugin()
Copy link
Member Author

Choose a reason for hiding this comment

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

Since now get_host_plugin() always fetches the goal state, this initialization was breaking the code below under the scope of the http_request mock.

I may come back (and also ask your help) to update more tests. Similar issues are all over the code.

return_value=MockResponse(status_code=200, body=b''))
@patch("azurelinuxagent.common.protocol.healthservice.HealthService.report_host_plugin_versions")
def test_ensure_health_service_called(self, patch_http_get, patch_report_versions):
with patch("azurelinuxagent.common.utils.restutil.http_get") as patch_http_get:
Copy link
Member Author

@narrieta narrieta Mar 6, 2020

Choose a reason for hiding this comment

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

I moved these patch decorators to be statements while I was debugging the issue with the mock response. Although this change is no longer needed I'm leaving there should we need to debug more.

"""
with create_mock_protocol(status_upload_blob=testurl, status_upload_blob_type=testtype) as protocol:
protocol.client.status_blob.vm_status = VMStatus(message="Ready", status="Ready")
def test_upload_status_blob_should_use_the_host_channel_by_default(self, *_):
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 changes below are the meat of this PR. Now we mock the interaction with the host/storage in terms of http requests, instead of reaching into implementation internals.

Copy link
Contributor

@pgombar pgombar left a comment

Choose a reason for hiding this comment

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

Thanks a lot for this, the tests look so much simpler and it's easier to keep track of what's happening in them.

ret = client.fetch_manifest([VMAgentManifestUri(uri="uri1")])
self.assertEquals(ret, "OK")
with mock_wire_protocol.create(mockwiredata.DATA_FILE) as protocol:
protocol.client.get_host_plugin() # force initialization of the host plugin
Copy link
Contributor

Choose a reason for hiding this comment

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

Why is this needed here and not in the test above?

Copy link
Member Author

@narrieta narrieta Mar 7, 2020

Choose a reason for hiding this comment

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

It is not needed, but simplifies the mock below. Initialization of the host plugin does a request for the goal state. Since the mock below is tracking goal state requests I would need to add code to filter out this one request. It is not too hard, but adding that code there would make the code less readable since it would distract from the actual intention of the test.

Some of the other tests don't track goal state requests so I do it only when it simplifies the code.

Thanks, I'll add a comment.

mock_failed_response = MockResponse(body=b"", status_code=httpclient.GONE)
mock_successful_response = MockResponse(body=b"OK", status_code=200)
self.assertEquals(manifest, manifest_xml)
self.assertTrue(any(args[0][0] == 'GET' and args[0][1] == manifest_url for args in mock_request.call_args_list)) # direct channel
Copy link
Contributor

Choose a reason for hiding this comment

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

Don't we expect the first GET to succeed since we're mocking it? Why are we expecting several requests?

Copy link
Member Author

@narrieta narrieta Mar 7, 2020

Choose a reason for hiding this comment

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

Note that this test follows a different pattern than the other ones. This one was one of the first tests I started rewriting.

The underlying implementation of the protocol can trigger multiple requests while executing those fetch* and upload* methods. For example, if we get to use the host plugin we will trigger a request for the goal state, followed by puts to the health service for the plugin, etc.

The mocks in the other tests keep track of the interesting requests in http_request.urls and discard the rest. In this one test I am using mock_request.call_args_list instead. If you run the test under the debugger and set a breakpoint on the first assert you can check the call list and you will see a request for the goal state on top of the the GET for the manifest.

I think the pattern I used on the other tests allows for stronger assertions, and also makes tests a lot more readable so I'll change this one to follow that pattern as well.

Also, post-release, I'll see if I can capture that pattern in a more general tool that we can use for this kind of tests.

Copy link
Member Author

Choose a reason for hiding this comment

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

I captured the pattern I was mentioning as mock_http_request in mocks.py

Copy link
Contributor

Choose a reason for hiding this comment

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

Got it, thanks for the update.

@@ -28,7 +28,8 @@
WALAEventOperation, parse_xml_event, parse_json_event, AGENT_EVENT_FILE_EXTENSION, EVENTS_DIRECTORY
from azurelinuxagent.common.future import ustr
from azurelinuxagent.common.protocol.goal_state import GoalState
from tests.protocol import mockwiredata, mock_wire_protocol
from tests.protocol import mockwiredata
from tests.protocol.mocks import mock_wire_protocol
Copy link
Member Author

Choose a reason for hiding this comment

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

I moved mock_wire_protocol to a different file (mocks.py)

Copy link
Member Author

Choose a reason for hiding this comment

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

I also changed the usage from

with mock_wire_protocol.create(...)...

to

with mock_wire_protocol(...)...

As I'm adding more functionality to the mocks the latter feels better.

Copy link
Member

Choose a reason for hiding this comment

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

Yup - Looks cleaner. Thanks

from tests.protocol.mockwiredata import WireProtocolData

@contextlib.contextmanager
def create(mock_wire_data_file):
Copy link
Member Author

Choose a reason for hiding this comment

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

moved this to mocks.py as mock_wire_protocol



@contextlib.contextmanager
def mock_wire_protocol(mock_wire_data_file):
Copy link
Member Author

@narrieta narrieta Mar 9, 2020

Choose a reason for hiding this comment

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

Moved here from the previous mock_wire_protocol.py.

Also, I changed it to mock 1 level deeper (http_request instead of http_get) to address all the scenarios in the tests.

def http_request(method, url, data, headers=None, use_proxy=False, max_retry=restutil.DEFAULT_RETRIES, retry_codes=restutil.RETRY_CODES, retry_delay=restutil.DELAY_IN_SECONDS):
if method == 'GET' and mock_data_re.match(url) is not None:
return protocol.mock_wire_data.mock_http_get(url, headers, use_proxy, max_retry, retry_codes, retry_delay)
elif method == 'POST':
Copy link
Member Author

Choose a reason for hiding this comment

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

added support for POST

stop_mock_crypt_util.mock = None
stop_mock_crypt_util.mock = None

def stop():
Copy link
Member Author

Choose a reason for hiding this comment

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

those stop functions are mainly for debugging purposes; they come handy when there are conflicts with the mocks used by tests and the ones used here.



@contextlib.contextmanager
def mock_http_request(http_get_handler=None, http_post_handler=None, http_put_handler=None):
Copy link
Member Author

@narrieta narrieta Mar 9, 2020

Choose a reason for hiding this comment

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

New mock useful for tests that need to interact with the host/storage at the HTTP request level.

I'll probably merge it with mock_wire_protocol since most test scenarios likely will use mock_http_request nested within mock_wire_protocol. Will see first what kind of usage this mock gets in other tests.

@narrieta narrieta requested a review from pgombar March 9, 2020 16:42
Copy link
Member

@vrdmr vrdmr left a comment

Choose a reason for hiding this comment

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

minors; LGTM.

@@ -28,7 +28,8 @@
WALAEventOperation, parse_xml_event, parse_json_event, AGENT_EVENT_FILE_EXTENSION, EVENTS_DIRECTORY
from azurelinuxagent.common.future import ustr
from azurelinuxagent.common.protocol.goal_state import GoalState
from tests.protocol import mockwiredata, mock_wire_protocol
from tests.protocol import mockwiredata
from tests.protocol.mocks import mock_wire_protocol
Copy link
Member

Choose a reason for hiding this comment

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

Yup - Looks cleaner. Thanks

self.assertEquals(success, True, 'The download should have succeeded')
self.assertEquals(len(urls), 2, "Unexpected number of HTTP requests: [{0}]".format(urls))
self.assertEquals(urls[0], extension_url, "The first attempt should have been over the direct channel")
self.assertTrue(urls[1].endswith('/extensionArtifact'), "The retry attempt should have been over the host channel")
Copy link
Member

Choose a reason for hiding this comment

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

Do we need to validate the IP address to validate it went through the HGAP? I know the /extensionArtifact should be good enough to validate if the correct URI was hit.

Copy link
Member Author

Choose a reason for hiding this comment

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

yes, that is one of the changes i want to do post-release

Copy link
Contributor

@pgombar pgombar left a comment

Choose a reason for hiding this comment

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

LGTM

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants