diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4c668e14..94a0bb90 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -46,15 +46,15 @@ jobs: flags: python39 name: python-39 fail_ci_if_error: true - - name: Read test output - id: getoutput - run: echo "contents=$(cat err.txt)" >> $GITHUB_OUTPUT - - name: Check if all tests passed - if: contains( steps.getoutput.outputs.contents, 'FAILED (failures=' ) - shell: cmd + - name: Read test output and Check if all tests passed + shell: bash run: | - echo "${{ steps.getoutput.outputs.contents }}" - exit 1 + CONTENTS=$(cat err.txt) + if echo "$CONTENTS" | grep -q 'FAILED (failures='; then + echo "There are failed tests" + echo "Contents: $CONTENTS" + exit 1 + fi codecov-python-27: runs-on: windows-latest needs: codecov-python-39 @@ -112,12 +112,12 @@ jobs: flags: python27 name: python-27 fail_ci_if_error: true - - name: Read test output - id: getoutput - run: echo "contents=$(cat err2.txt)" >> $GITHUB_OUTPUT - - name: Check if all tests passed - if: contains( steps.getoutput.outputs.contents, 'FAILED (failures=' ) + - name: Read test output and Check if all tests passed + shell: bash run: | - echo "${{ steps.getoutput.outputs.contents }}" - echo "There are failed tests" - exit 1 + CONTENTS=$(cat err2.txt) + if echo "$CONTENTS" | grep -q 'FAILED (failures='; then + echo "There are failed tests" + echo "Contents: $CONTENTS" + exit 1 + fi diff --git a/src/core/src/bootstrap/Constants.py b/src/core/src/bootstrap/Constants.py index b65d2a6f..8f04b21f 100644 --- a/src/core/src/bootstrap/Constants.py +++ b/src/core/src/bootstrap/Constants.py @@ -181,6 +181,7 @@ class StatusTruncationConfig(EnumBackport): INTERNAL_FILE_SIZE_LIMIT_IN_BYTES = 126 * 1024 AGENT_FACING_STATUS_FILE_SIZE_LIMIT_IN_BYTES = 128 * 1024 MIN_ASSESSMENT_PATCHES_TO_RETAIN = 5 + TRUNCATION_WARNING_MESSAGE = "Package lists were truncated to limit reporting data volume. In-VM logs contain complete lists." TURN_ON_TRUNCATION = True # Wrapper-core handshake files @@ -265,7 +266,6 @@ class PatchAssessmentSummaryStartedBy(EnumBackport): # Maintenance Window PACKAGE_INSTALL_EXPECTED_MAX_TIME_IN_MINUTES = 5 - # As per telemetry data, when batch size is 3, the average time taken per package installation for different package managers is as follow: # apt: 43 seconds # yum: 71 seconds @@ -298,6 +298,7 @@ class PatchOperationErrorCodes(EnumBackport): PACKAGE_MANAGER_FAILURE = "PACKAGE_MANAGER_FAILURE" NEWER_OPERATION_SUPERSEDED = "NEWER_OPERATION_SUPERSEDED" UA_ESM_REQUIRED = "UA_ESM_REQUIRED" + TRUNCATION = "PACKAGE_LIST_TRUNCATED" ERROR_ADDED_TO_STATUS = "Error_added_to_status" diff --git a/src/core/src/service_interfaces/StatusHandler.py b/src/core/src/service_interfaces/StatusHandler.py index 3c34afec..05a8230f 100644 --- a/src/core/src/service_interfaces/StatusHandler.py +++ b/src/core/src/service_interfaces/StatusHandler.py @@ -239,7 +239,7 @@ def set_package_install_status_classification(self, package_names, package_versi package_classification_summary += "[P={0},V={1},C={2}] ".format(str(package_name), str(package_version), str(classification if classification is not None and classification_matching_package_found else "-")) - self.composite_logger.log_debug("Package install status summary (classification): " + package_classification_summary) + # self.composite_logger.log_debug("Package install status summary (classification): " + package_classification_summary) self.__installation_packages = list(self.__installation_packages_map.values()) self.__installation_packages = self.sort_packages_by_classification_and_state(self.__installation_packages) self.set_installation_substatus_json() @@ -817,17 +817,24 @@ def __try_add_error(error_list, detail): error_list.insert(0, detail) return True - def __set_errors_json(self, error_count_by_operation, errors_by_operation): + def __set_errors_json(self, error_count_by_operation, errors_by_operation, is_status_truncated=False): """ Compose the error object json to be added in 'errors' in given operation's summary """ + code = Constants.PatchOperationTopLevelErrorCode.SUCCESS if error_count_by_operation == 0 else Constants.PatchOperationTopLevelErrorCode.ERROR + if error_count_by_operation == 1 and errors_by_operation[0]['code'] == Constants.PatchOperationErrorCodes.INFORMATIONAL: # special-casing for single informational messages message = errors_by_operation[0]['message'] errors_by_operation = [] - error_count_by_operation = 0 else: + # Update msg error code to warning for truncation + if is_status_truncated: + error_count_by_operation += 1 # add 1 because of truncation creates a new error detail object + code = Constants.PatchOperationTopLevelErrorCode.WARNING if code != Constants.PatchOperationTopLevelErrorCode.ERROR else Constants.PatchOperationTopLevelErrorCode.ERROR + message = "{0} error/s reported.".format(error_count_by_operation) message += " The latest {0} error/s are shared in detail. To view all errors, review this log file on the machine: {1}".format(len(errors_by_operation), self.__log_file_path) if error_count_by_operation > 0 else "" + return { - "code": Constants.PatchOperationTopLevelErrorCode.SUCCESS if error_count_by_operation == 0 else Constants.PatchOperationTopLevelErrorCode.ERROR, + "code": code, "details": errors_by_operation, "message": message } @@ -890,13 +897,15 @@ def __create_truncated_status_file(self, status_file_size_in_bytes, complete_sta if len(self.__assessment_patches_removed) > 0: self.composite_logger.log_verbose("Recomposing truncated status payload: [Substatus={0}]".format(Constants.PATCH_ASSESSMENT_SUMMARY)) - truncated_status_file = self.__recompose_truncated_status_file(truncated_status_file=truncated_status_file, truncated_patches=patches_retained_in_assessment, substatus_message=self.__assessment_substatus_msg_copy, substatus_index=assessment_substatus_index) + truncated_status_file = self.__recompose_truncated_status_file(truncated_status_file=truncated_status_file, truncated_patches=patches_retained_in_assessment, count_total_errors=self.__assessment_total_error_count, substatus_message=self.__assessment_substatus_msg_copy, substatus_index=assessment_substatus_index) if len(self.__installation_patches_removed) > 0: self.composite_logger.log_verbose("Recomposing truncated status payload: [Substatus={0}]".format(Constants.PATCH_INSTALLATION_SUMMARY)) - truncated_status_file = self.__recompose_truncated_status_file(truncated_status_file=truncated_status_file, truncated_patches=patches_retained_in_installation, substatus_message=self.__installation_substatus_msg_copy, substatus_index=installation_substatus_index) + truncated_status_file = self.__recompose_truncated_status_file(truncated_status_file=truncated_status_file, truncated_patches=patches_retained_in_installation, count_total_errors=self.__installation_total_error_count, substatus_message=self.__installation_substatus_msg_copy, substatus_index=installation_substatus_index) status_file_size_in_bytes = self.__calc_status_size_on_disk(json.dumps(truncated_status_file)) + status_file_agent_size_diff = status_file_size_in_bytes - Constants.StatusTruncationConfig.INTERNAL_FILE_SIZE_LIMIT_IN_BYTES + max_allowed_patches_size_in_bytes -= status_file_agent_size_diff # Reduce the max packages byte size by new error and new escape chars byte size self.composite_logger.log_verbose("End patches truncation: [TruncatedStatusFileSizeInBytes={0}] [InternalFileSizeLimitInBytes={1}]".format(str(status_file_size_in_bytes), str(Constants.StatusTruncationConfig.INTERNAL_FILE_SIZE_LIMIT_IN_BYTES))) return truncated_status_file @@ -968,7 +977,7 @@ def __truncate_patches(self, patches, max_allowed_patches_size_in_bytes): else: left_index = mid_index + 1 - truncated_patches = patches[:left_index - 1] + truncated_patches = patches[:left_index-1] patches_removed = patches[left_index - 1:] truncated_patches_size_in_bytes = self.__calc_patches_payload_size_on_disk(truncated_patches) return truncated_patches, patches_removed, max_allowed_patches_size_in_bytes - truncated_patches_size_in_bytes @@ -1019,28 +1028,81 @@ def __get_substatus_index(self, substatus_list_name, substatus_list): return substatus_index return None - def __recompose_truncated_status_file(self, truncated_status_file, truncated_patches, substatus_message, substatus_index): + def __recompose_truncated_status_file(self, truncated_status_file, truncated_patches, count_total_errors, substatus_message, substatus_index): """ Recompose status file with truncated patches """ - error_code, _ = self.__get_errors_from_substatus(substatus_msg=substatus_message) + error_code, errors_details_list = self.__get_errors_from_substatus(substatus_msg=substatus_message) # Check for existing errors before recompose if error_code != Constants.PatchOperationTopLevelErrorCode.ERROR: self.composite_logger.log_verbose("Patches in substatus have been truncated hence updating status to [status={0}] [PreviousErrorCode={1}]".format(Constants.STATUS_WARNING, str(error_code))) truncated_status_file['status']['substatus'][substatus_index]['status'] = Constants.STATUS_WARNING.lower() # Update substatus status to warning + truncated_msg_errors = self.__recompose_substatus_msg_errors(errors_details_list, count_total_errors) + self.composite_logger.log_verbose("Recompose truncated substatus") - truncated_substatus_message = self.__update_patches_in_substatus(substatus_msg=substatus_message, substatus_msg_patches=truncated_patches) + truncated_substatus_message = self.__update_patches_in_substatus(substatus_msg=substatus_message, substatus_msg_patches=truncated_patches, substatus_msg_errors=truncated_msg_errors) truncated_status_file['status']['substatus'][substatus_index]['formattedMessage']['message'] = json.dumps(truncated_substatus_message) return truncated_status_file - def __update_patches_in_substatus(self, substatus_msg, substatus_msg_patches): - """ update the substatus message patches """ + def __recompose_substatus_msg_errors(self, errors_details_list, count_total_errors): + """ Recompose truncated substatus message errors json """ + truncated_error_detail = self.__set_error_detail(Constants.PatchOperationErrorCodes.TRUNCATION, Constants.StatusTruncationConfig.TRUNCATION_WARNING_MESSAGE) # Reuse the errors object set up + self.__try_add_error(errors_details_list, truncated_error_detail) # add new truncated error detail to beginning in errors details list + truncated_errors_json = self.__set_errors_json(count_total_errors, errors_details_list, is_status_truncated=True) + + return truncated_errors_json + + def __update_patches_in_substatus(self, substatus_msg, substatus_msg_patches, substatus_msg_errors=None): + """ update the substatus message patches and errors """ substatus_msg['patches'] = substatus_msg_patches + if substatus_msg_errors: + substatus_msg['errors'] = substatus_msg_errors + return substatus_msg def __get_errors_from_substatus(self, substatus_msg): """ Get errors code and errors details from substatus message json """ return substatus_msg['errors']['code'], substatus_msg['errors']['details'] + + def __create_assessment_tombstone_list(self, packages_removed_from_assessment): + """ Create a list of tombstone by classifications and classification_count , omit unclassified """ + assessment_tombstone_map = {} + tombstone_record_list = [] + + # Map['classification', classification_count] + for package in packages_removed_from_assessment: + classifications = package['classifications'][0] + assessment_tombstone_map[classifications] = assessment_tombstone_map.get(classifications, 0) + 1 + + # Add assessment tombstone record per classifications except unclassified + for classification_name, patches_count_by_classification in assessment_tombstone_map.items(): + if not classification_name == Constants.PackageClassification.UNCLASSIFIED: + tombstone_record_list.append(self.__create_assessment_tombstone(patches_count_by_classification, classification_name)) + + return tombstone_record_list + + def __create_assessment_tombstone(self, patches_count_by_classification, classification_name): + """ Tombstone record for truncated assessment patches + Patch Name: xx additional updates of classification reported. + Classification: [Critical, Security, Other] + """ + tombstone_name = str(patches_count_by_classification) + ' additional updates of classification ' + classification_name + ' reported', + return { + 'patchId': 'Truncated_patch_list_id', + 'name': tombstone_name, + 'version': '0.0.0', + 'classifications': [classification_name] + } + + def __create_installation_tombstone(self): + """ Tombstone record for truncated installation """ + return { + 'patchId': 'Truncated_patch_list_id', + 'name': 'Truncated_patch_list', + 'version': '0.0.0', + 'classifications': ['Other'], + 'patchInstallationState': 'NotSelected' + } # endregion diff --git a/src/core/tests/Test_CoreMain.py b/src/core/tests/Test_CoreMain.py index 2d542e27..ae70fd57 100644 --- a/src/core/tests/Test_CoreMain.py +++ b/src/core/tests/Test_CoreMain.py @@ -1145,4 +1145,4 @@ def __check_telemetry_events(self, runtime): if __name__ == '__main__': - unittest.main() \ No newline at end of file + unittest.main() diff --git a/src/core/tests/Test_StatusHandler.py b/src/core/tests/Test_StatusHandler.py index 09e227db..94af1fb4 100644 --- a/src/core/tests/Test_StatusHandler.py +++ b/src/core/tests/Test_StatusHandler.py @@ -580,4 +580,4 @@ def __set_up_packages_func(self, val): return test_packages, test_package_versions if __name__ == '__main__': - unittest.main() + unittest.main() \ No newline at end of file diff --git a/src/core/tests/Test_StatusHandlerTruncation.py b/src/core/tests/Test_StatusHandlerTruncation.py index dddbb606..e3d20666 100644 --- a/src/core/tests/Test_StatusHandlerTruncation.py +++ b/src/core/tests/Test_StatusHandlerTruncation.py @@ -30,13 +30,11 @@ def setUp(self): self.runtime = RuntimeCompositor(ArgumentComposer().get_composed_arguments(), True) self.container = self.runtime.container self.__test_scenario = None - self.__expected_truncated_patch_count = 0 self.__patch_count_assessment = 0 self.__patch_count_installation = 0 def tearDown(self): self.__test_scenario = None - self.__expected_truncated_patch_count = 0 self.__patch_count_assessment = 0 self.__patch_count_installation = 0 self.runtime.stop() @@ -55,7 +53,6 @@ def test_assessment_patches_under_size_limit_not_truncated(self): self.__test_scenario = 'assessment_only' self.__patch_count_assessment = 500 - self.__expected_truncated_patch_count = 500 self.__set_up_status_file(run='assessment', config_operation=Constants.ASSESSMENT, patch_count=self.__patch_count_assessment, status=Constants.STATUS_SUCCESS) @@ -74,18 +71,17 @@ def test_only_assessment_patches_over_size_limit_truncated(self): """ Perform truncation on very large assessment patches and checks for time performance concern. Before truncation: 100000 assessment patches in status complete status file byte size: 19,022kb, - Expected (After truncation): 672 assessment patches in status + Expected (After truncation): ~671 assessment patches in status operation: Assessment, assessment substatus name: PatchAssessmentSummary, assessment substatus status: warning, assessment errors code: 0 (success), assessment errors details count: 0, - count of assessment patches removed: 99328, + count of assessment patches removed: 99329, truncated status file byte size: 126kb. """ self.__test_scenario = 'assessment_only' self.__patch_count_assessment = 100000 - self.__expected_truncated_patch_count = 672 self.__set_up_status_file(run='assessment', config_operation=Constants.ASSESSMENT, patch_count=self.__patch_count_assessment, status=Constants.STATUS_SUCCESS, classification='Critical') @@ -98,24 +94,23 @@ def test_only_assessment_patches_over_size_limit_truncated(self): truncated_substatus_file_data = self.__get_substatus_file_json(self.runtime.execution_config.status_file_path) # Assert truncated status file size - self.__assert_patch_summary_from_status(truncated_substatus_file_data, Constants.ASSESSMENT, Constants.PATCH_ASSESSMENT_SUMMARY, Constants.STATUS_WARNING, self.__patch_count_assessment, complete_substatus_file_data=complete_substatus_file_data, is_under_internal_size_limit=True, is_truncated=True) + self.__assert_patch_summary_from_status(truncated_substatus_file_data, Constants.ASSESSMENT, Constants.PATCH_ASSESSMENT_SUMMARY, Constants.STATUS_WARNING, self.__patch_count_assessment, errors_count=1, errors_code=Constants.PatchOperationTopLevelErrorCode.WARNING, complete_substatus_file_data=complete_substatus_file_data, is_under_internal_size_limit=True, is_truncated=True) def test_only_assessment_patches_over_size_limit_with_status_error_truncated(self): """ Perform truncation on assessment patches and substatus status is set to Error (not warning) due to per-existing patching errors Before truncation: 1000 assessment patches in status, 6 exceptions completed status file byte size: 188kb - Expected (After truncation): 670 assessment patches in status + Expected (After truncation): ~669 assessment patches in status operation: Assessment, assessment substatus name: PatchAssessmentSummary, assessment substatus status: error, assessment errors code: 1 (error), assessment errors details count: 5, - count of assessment patches removed: 330, + count of assessment patches removed: 331, truncated status file byte size: 126kb. """ self.__test_scenario = 'assessment_only' self.__patch_count_assessment = 1000 - self.__expected_truncated_patch_count = 670 self.__set_up_status_file(run='assessment', config_operation=Constants.ASSESSMENT, patch_count=self.__patch_count_assessment, classification='Security') @@ -155,7 +150,6 @@ def test_only_installation_under_size_limit_not_truncated(self): self.__test_scenario = 'installation_only' self.__patch_count_installation = 500 - self.__expected_truncated_patch_count = 500 self.__set_up_status_file(run='installation', config_operation=Constants.INSTALLATION, patch_count=self.__patch_count_installation, status=Constants.STATUS_SUCCESS, package_status=Constants.INSTALLED) @@ -174,18 +168,17 @@ def test_only_installation_patches_over_size_limit_truncated(self): """ Perform truncation on very large installation patches and checks for time performance concern. Before truncation: 100000 installation patches in status complete status file byte size: 22,929kb, - Expected (After truncation): 555 installation patches in status + Expected (After truncation): ~554 installation patches in status operation: Installation, installation substatus name: PatchInstallationSummary, installation substatus status: warning, installation errors code: 0 (success), installation errors details count: 0, - count of installation patches removed: 99445, + count of installation patches removed: 99446, truncated status file byte size: 126kb. """ self.__test_scenario = 'installation_only' self.__patch_count_installation = 100000 - self.__expected_truncated_patch_count = 555 self.__set_up_status_file(run='installation', config_operation=Constants.INSTALLATION, patch_count=self.__patch_count_installation, status=Constants.STATUS_SUCCESS, package_status=Constants.INSTALLED) @@ -197,13 +190,13 @@ def test_only_installation_patches_over_size_limit_truncated(self): # Assert installation truncated status file truncated_substatus_file_data = self.__get_substatus_file_json(self.runtime.execution_config.status_file_path) - self.__assert_patch_summary_from_status(truncated_substatus_file_data, Constants.INSTALLATION, Constants.PATCH_INSTALLATION_SUMMARY, Constants.STATUS_WARNING, self.__patch_count_installation, complete_substatus_file_data=complete_substatus_file_data, is_under_internal_size_limit=True, is_truncated=True) + self.__assert_patch_summary_from_status(truncated_substatus_file_data, Constants.INSTALLATION, Constants.PATCH_INSTALLATION_SUMMARY, Constants.STATUS_WARNING, self.__patch_count_installation, errors_count=1, errors_code=Constants.PatchOperationTopLevelErrorCode.WARNING, complete_substatus_file_data=complete_substatus_file_data, is_under_internal_size_limit=True, is_truncated=True) def test_only_installation_low_priority_patches_over_size_limit_truncated(self): """ Perform truncation on only installation with low priority patches (Pending, Exclude, Not_Selected), truncated status files have no Not_Selected patches. Before truncation: 1040 installation patches in status complete status file byte size: 235kb, - Expected (After truncation): 559 installation patches in status + Expected (After truncation): ~558 installation patches in status operation: Installation, installation substatus name: PatchInstallationSummary, installation substatus status: warning, @@ -212,14 +205,13 @@ def test_only_installation_low_priority_patches_over_size_limit_truncated(self): last complete status file installation patch state: Not_Selected first truncated installation patch state: Pending, last truncated installation patch state: Excluded, - count of installation patches removed: 481, + count of installation patches removed: 482, truncated status file byte size: 126kb. """ self.__test_scenario = 'installation_only' patch_count_pending = 400 patch_count_exclude = 600 patch_count_not_selected = 40 - self.__expected_truncated_patch_count = 559 # random_char=random.choice(string.ascii_letters) ensure the packages are unique due to __set_up_packages_func remove duplicates self.__run_installation_package_set_up(patch_count_exclude, Constants.EXCLUDED, random_char=random.choice(string.ascii_letters)) @@ -244,13 +236,13 @@ def test_only_installation_low_priority_patches_over_size_limit_truncated(self): self.assertEqual(installation_truncated_msg['patches'][0]['patchInstallationState'], Constants.PENDING) self.assertEqual(installation_truncated_msg['patches'][-1]['patchInstallationState'], Constants.EXCLUDED) - self.__assert_patch_summary_from_status(truncated_substatus_file_data, Constants.INSTALLATION, Constants.PATCH_INSTALLATION_SUMMARY, Constants.STATUS_WARNING, self.__patch_count_installation, complete_substatus_file_data=complete_substatus_file_data, is_under_internal_size_limit=True, is_truncated=True) + self.__assert_patch_summary_from_status(truncated_substatus_file_data, Constants.INSTALLATION, Constants.PATCH_INSTALLATION_SUMMARY, Constants.STATUS_WARNING, self.__patch_count_installation, errors_count=1, errors_code=Constants.PatchOperationTopLevelErrorCode.WARNING, complete_substatus_file_data=complete_substatus_file_data, is_under_internal_size_limit=True, is_truncated=True) def test_only_installation_patches_over_size_limit_with_status_error_truncated(self): """ Perform truncation on installation patches and substatus status is set to Error (not warning) due to per-existing patching errors Before truncation: 800 installation patches in status, 6 exceptions completed status file byte size: 182kb, - Expected (After truncation): 553 installation patches in status + Expected (After truncation): ~553 installation patches in status operation: Installation, installation substatus name: PatchInstallationSummary, installation substatus status: error, @@ -260,7 +252,6 @@ def test_only_installation_patches_over_size_limit_with_status_error_truncated(s truncated status file byte size: 126kb. """ self.__test_scenario = 'installation_only' - self.__expected_truncated_patch_count = 553 self.__patch_count_installation = 800 self.__set_up_status_file(run='installation', config_operation=Constants.INSTALLATION, patch_count=self.__patch_count_installation, package_status=Constants.INSTALLED) @@ -290,13 +281,13 @@ def test_both_assessment_and_installation_over_size_limit_truncated(self): """ Perform truncation on assessment/installation patches. Before truncation: 700 assessment patches in status, 500 installation patches in status complete status file byte size: 242kb, - Expected (After truncation): 374 assessment patches in status, 250 installation patches in status + Expected (After truncation): ~370 assessment patches in status, ~250 installation patches in status operation: Installation, substatus name: [assessment=PatchAssessmentSummary][installation=PatchInstallationSummary], substatus status: [assessment=warning][installation=warning], errors code: [assessment=0 (success)][installation=0 (success)], errors details count: [assessment=0][installation=0], - count of patches removed from log: [assessment=326[installation=950], + count of patches removed from log: [assessment=330[installation=950], truncated status file byte size: 126kb. """ self.__test_scenario = 'both' @@ -322,24 +313,22 @@ def test_both_assessment_and_installation_over_size_limit_truncated(self): truncated_substatus_file_data = self.__get_substatus_file_json(self.runtime.execution_config.status_file_path) # Assert assessment truncation - self.__expected_truncated_patch_count = 374 - self.__assert_patch_summary_from_status(truncated_substatus_file_data, Constants.INSTALLATION, Constants.PATCH_ASSESSMENT_SUMMARY, Constants.STATUS_WARNING, self.__patch_count_assessment, complete_substatus_file_data=complete_substatus_file_data, is_under_internal_size_limit=True, is_truncated=True) + self.__assert_patch_summary_from_status(truncated_substatus_file_data, Constants.INSTALLATION, Constants.PATCH_ASSESSMENT_SUMMARY, Constants.STATUS_WARNING, self.__patch_count_assessment, errors_count=1, errors_code=Constants.PatchOperationTopLevelErrorCode.WARNING, complete_substatus_file_data=complete_substatus_file_data, is_under_internal_size_limit=True, is_truncated=True) # Assert installation truncation - self.__expected_truncated_patch_count = 250 - self.__assert_patch_summary_from_status(truncated_substatus_file_data, Constants.INSTALLATION, Constants.PATCH_INSTALLATION_SUMMARY, Constants.STATUS_WARNING, self.__patch_count_installation, installation_substatus_index=1, complete_substatus_file_data=complete_substatus_file_data, is_under_internal_size_limit=True, is_truncated=True) + self.__assert_patch_summary_from_status(truncated_substatus_file_data, Constants.INSTALLATION, Constants.PATCH_INSTALLATION_SUMMARY, Constants.STATUS_WARNING, self.__patch_count_installation, errors_count=1, errors_code=Constants.PatchOperationTopLevelErrorCode.WARNING, installation_substatus_index=1, complete_substatus_file_data=complete_substatus_file_data, is_under_internal_size_limit=True, is_truncated=True) def test_both_assessment_and_installation__keep_min_5_assessment_patches_truncated(self): """ Perform truncation on very large assessment / installation patches and checks for time performance concern. but keep min 5 assessment patches. Before truncation: 100000 assessment patches in status, 100000 installation patches in status complete status file byte size: 41,658kb, - Expected (After truncation): 5 assessment patches in status, 549 installation patches in status + Expected (After truncation): ~5 assessment patches in status, ~546 installation patches in status operation: Installation, substatus name: [assessment=PatchAssessmentSummary][installation=PatchInstallationSummary], substatus status: [assessment=warning][installation=warning], errors code: [assessment=0 (success)][installation=0 (success)], errors details count: [assessment=0][installation=0], - count of patches removed from log: [assessment=99995[installation=99451], + count of patches removed from log: [assessment=99995[installation=99454], truncated status file byte size: 126kb. """ self.__test_scenario = 'both' @@ -363,24 +352,22 @@ def test_both_assessment_and_installation__keep_min_5_assessment_patches_truncat truncated_substatus_file_data = self.__get_substatus_file_json(self.runtime.execution_config.status_file_path) # Assert assessment truncation - self.__expected_truncated_patch_count = 5 - self.__assert_patch_summary_from_status(truncated_substatus_file_data, Constants.INSTALLATION, Constants.PATCH_ASSESSMENT_SUMMARY, Constants.STATUS_WARNING, self.__patch_count_assessment, complete_substatus_file_data=complete_substatus_file_data, is_under_internal_size_limit=True, is_truncated=True) + self.__assert_patch_summary_from_status(truncated_substatus_file_data, Constants.INSTALLATION, Constants.PATCH_ASSESSMENT_SUMMARY, Constants.STATUS_WARNING, self.__patch_count_assessment, errors_count=1, errors_code=Constants.PatchOperationTopLevelErrorCode.WARNING, complete_substatus_file_data=complete_substatus_file_data, is_under_internal_size_limit=True, is_truncated=True) # Assert installation truncation - self.__expected_truncated_patch_count = 549 - self.__assert_patch_summary_from_status(truncated_substatus_file_data, Constants.INSTALLATION, Constants.PATCH_INSTALLATION_SUMMARY, Constants.STATUS_WARNING, self.__patch_count_installation, installation_substatus_index=1, complete_substatus_file_data=complete_substatus_file_data, is_under_internal_size_limit=True, is_truncated=True) + self.__assert_patch_summary_from_status(truncated_substatus_file_data, Constants.INSTALLATION, Constants.PATCH_INSTALLATION_SUMMARY, Constants.STATUS_WARNING, self.__patch_count_installation, errors_count=1, errors_code=Constants.PatchOperationTopLevelErrorCode.WARNING, installation_substatus_index=1, complete_substatus_file_data=complete_substatus_file_data, is_under_internal_size_limit=True, is_truncated=True) def test_both_assessment_and_installation_with_status_error_truncated(self): """ Perform truncation on assessment / installation patches but installation substatus status is set to Error (not warning) due to per-existing patching errors Before truncation: 800 assessment patches in status, 800 installation patches in status with 6 exception errors complete status file byte size: > 128kb, - Expected (After truncation): 5 assessment patches in status, 547 installation patches in status + Expected (After truncation): ~5 assessment patches in status, ~545 installation patches in status operation: Installation, substatus name: [assessment=PatchAssessmentSummary][installation=PatchInstallationSummary], substatus status: [assessment=warning][installation=error], errors code: [assessment=0 (success)][installation=1 (error)], errors details count: [assessment=0][installation=5], - count of patches removed from log: [assessment=795][installation=253], + count of patches removed from log: [assessment=795][installation=255], truncated status file byte size: < 126kb """ self.__test_scenario = 'both' @@ -414,17 +401,19 @@ def test_both_assessment_and_installation_with_status_error_truncated(self): truncated_substatus_file_data = self.__get_substatus_file_json(self.runtime.execution_config.status_file_path) # Assert assessment truncated - self.__expected_truncated_patch_count = 5 - self.__assert_patch_summary_from_status(truncated_substatus_file_data, Constants.INSTALLATION, Constants.PATCH_ASSESSMENT_SUMMARY, Constants.STATUS_WARNING, self.__patch_count_assessment, complete_substatus_file_data=complete_substatus_file_data, is_under_internal_size_limit=True, is_truncated=True) + self.__assert_patch_summary_from_status(truncated_substatus_file_data, Constants.INSTALLATION, Constants.PATCH_ASSESSMENT_SUMMARY, Constants.STATUS_WARNING, self.__patch_count_assessment, errors_count=1, errors_code=Constants.PatchOperationTopLevelErrorCode.WARNING, complete_substatus_file_data=complete_substatus_file_data, is_under_internal_size_limit=True, is_truncated=True) # Assert installation truncated status file with multi exceptions - self.__expected_truncated_patch_count = 547 self.__assert_patch_summary_from_status(truncated_substatus_file_data, Constants.INSTALLATION, Constants.PATCH_INSTALLATION_SUMMARY, Constants.STATUS_ERROR, self.__patch_count_installation, errors_count=5, errors_code=Constants.PatchOperationTopLevelErrorCode.ERROR, installation_substatus_index=1, complete_substatus_file_data=complete_substatus_file_data, is_under_internal_size_limit=True, is_truncated=True) def test_truncation_method_time_performance(self): """ Comparing truncation code performance on prior and post on 1000 packages with frequency of 300 - assert truncation code logic time performance is 30 secs more than current (prior truncation) logic""" + assert truncation code logic time performance is 30 secs more than current (prior truncation) logic """ + start_index = 0 + end_index = 300 + patch_count = 500 + expected_time_performance = 30 self.runtime.execution_config.operation = Constants.INSTALLATION self.runtime.status_handler.set_current_operation(Constants.INSTALLATION) @@ -432,8 +421,8 @@ def test_truncation_method_time_performance(self): # Start performance test prior truncation Constants.StatusTruncationConfig.TURN_ON_TRUNCATION = False start_time_no_truncation = time.time() - for i in range(0, 301): - test_packages, test_package_versions = self.__set_up_packages_func(500) + for i in range(start_index, end_index): + test_packages, test_package_versions = self.__set_up_packages_func(patch_count) self.runtime.status_handler.set_package_assessment_status(test_packages, test_package_versions) self.runtime.status_handler.set_package_install_status(test_packages, test_package_versions, Constants.INSTALLED) @@ -443,8 +432,8 @@ def test_truncation_method_time_performance(self): # Start truncation performance test Constants.StatusTruncationConfig.TURN_ON_TRUNCATION = True start_time_with_truncation = time.time() - for i in range(0, 301): - test_packages, test_package_versions = self.__set_up_packages_func(500) + for i in range(start_index, end_index): + test_packages, test_package_versions = self.__set_up_packages_func(patch_count) self.runtime.status_handler.set_package_assessment_status(test_packages, test_package_versions) self.runtime.status_handler.set_package_install_status(test_packages, test_package_versions, Constants.INSTALLED) @@ -454,8 +443,8 @@ def test_truncation_method_time_performance(self): performance_time_formatted_with_truncation = self.__convert_performance_time_to_date_time_format(performance_time_with_truncation) self.runtime.status_handler.composite_logger.log_debug('performance_time_formatted_no_truncation ' + performance_time_formatted_no_truncation ) - self.runtime.status_handler.composite_logger.log_debug('performance_time_formatted_with_truncation' + performance_time_formatted_with_truncation) - self.assertTrue((performance_time_with_truncation - performance_time_no_truncation) < 30) + self.runtime.status_handler.composite_logger.log_debug('performance_time_formatted_with_truncation ' + performance_time_formatted_with_truncation) + self.assertTrue((performance_time_with_truncation - performance_time_no_truncation) < expected_time_performance) # Setup functions for testing def __assert_patch_summary_from_status(self, substatus_file_data, operation, patch_summary, status, patch_count, errors_count=0, @@ -477,21 +466,27 @@ def __assert_patch_summary_from_status(self, substatus_file_data, operation, pat self.assertTrue(substatus_file_in_bytes <= Constants.StatusTruncationConfig.INTERNAL_FILE_SIZE_LIMIT_IN_BYTES) if is_truncated: - self.assertEqual(status_file_patch_count, self.__expected_truncated_patch_count) self.assertTrue(status_file_patch_count < patch_count) # Assert length of truncated patches < patch_count post truncation self.assertTrue(substatus_file_in_bytes < len(json.dumps(complete_substatus_file_data).encode('utf-8'))) # Assert truncated status file size < completed status file size + self.assertTrue(any(Constants.PatchOperationErrorCodes.TRUNCATION in details['code'] for details in message["errors"]["details"])) + self.assertTrue(any(Constants.StatusTruncationConfig.TRUNCATION_WARNING_MESSAGE in details['message'] for details in message["errors"]["details"])) + self.assertTrue('The latest ' + str(errors_count) + ' error/s are shared in detail. To view all errors, review this log file on the machine' in message["errors"]["message"]) if self.__test_scenario == 'assessment_only': + self.assertEqual(status_file_patch_count, patch_count - self.runtime.status_handler.get_num_assessment_patches_removed()) self.assertEqual(patch_count - status_file_patch_count, self.runtime.status_handler.get_num_assessment_patches_removed()) # Assert # assessment removed packages self.__assert_assessment_truncated_msg_fields(complete_substatus_file_data, substatus_file_data) # Assert all assessment fields in the message json are equal in both status files if self.__test_scenario == 'installation_only': + self.assertEqual(status_file_patch_count, patch_count - self.runtime.status_handler.get_num_installation_patches_removed()) self.assertEqual(patch_count - status_file_patch_count, self.runtime.status_handler.get_num_installation_patches_removed()) # Assert # installation removed packages self.__assert_installation_truncated_msg_fields(complete_substatus_file_data, substatus_file_data) # Assert all installation fields in the message json are equal in both status files if self.__test_scenario == 'both': status_file_assessment_count = len(json.loads(substatus_file_data["status"]["substatus"][0]["formattedMessage"]["message"])["patches"]) status_file_installation_count = len(json.loads(substatus_file_data["status"]["substatus"][1]["formattedMessage"]["message"])["patches"]) + self.assertEqual(status_file_assessment_count, self.__patch_count_assessment - self.runtime.status_handler.get_num_assessment_patches_removed()) + self.assertEqual(status_file_installation_count, self.__patch_count_installation - self.runtime.status_handler.get_num_installation_patches_removed()) self.assertEqual(self.__patch_count_assessment - status_file_assessment_count, self.runtime.status_handler.get_num_assessment_patches_removed()) # Assert # assessment removed packages self.assertEqual(self.__patch_count_installation - status_file_installation_count, self.runtime.status_handler.get_num_installation_patches_removed()) # Assert # installation removed packages @@ -502,9 +497,10 @@ def __assert_patch_summary_from_status(self, substatus_file_data, operation, pat # assert error self.assertEqual(message["errors"]["code"], errors_code) self.assertEqual(len(message["errors"]["details"]), errors_count) - if errors_count > 0: - self.assertEqual(message["errors"]["details"][0]["code"], Constants.PatchOperationErrorCodes.OPERATION_FAILED) - self.assertEqual(message["errors"]["details"][1]["code"], Constants.PatchOperationErrorCodes.PACKAGE_MANAGER_FAILURE) + + if errors_code == Constants.PatchOperationTopLevelErrorCode.ERROR: + self.assertTrue(any(Constants.PatchOperationErrorCodes.OPERATION_FAILED in details['code'] for details in message["errors"]["details"])) + self.assertTrue(any(Constants.PatchOperationErrorCodes.PACKAGE_MANAGER_FAILURE in details['code'] for details in message["errors"]["details"])) def __add_multiple_exception_errors(self): # Adding multiple exception errors @@ -601,5 +597,6 @@ def __set_up_packages_func(self, val, random_char=None): return test_packages, test_package_versions + if __name__ == '__main__': unittest.main()