chromium/ios/build/bots/scripts/xcode_log_parser_test.py

#!/usr/bin/env vpython3
# Copyright 2019 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

"""Unittests for xcode_log_parser.py."""

import json
import mock
import os
import unittest

from test_result_util import TestStatus
import test_runner
import test_runner_test
import xcode_log_parser
import constants


OUTPUT_PATH = '/tmp/attempt_0'
XCRESULT_PATH = '/tmp/attempt_0.xcresult'
XCODE11_DICT = {
    'path': '/Users/user1/Xcode.app',
    'version': '11.0',
    'build': '11M336w',
}
# A sample of json result when executing xcresulttool on .xcresult dir without
# --id. Some unused keys and values were removed.
XCRESULT_ROOT = """
{
  "_type" : {
    "_name" : "ActionsInvocationRecord"
  },
  "actions" : {
    "_values" : [
      {
        "actionResult" : {
          "_type" : {
            "_name" : "ActionResult"
          },
          "diagnosticsRef" : {
            "id" : {
              "_value" : "DIAGNOSTICS_REF_ID"
            }
          },
          "logRef" : {
            "id" : {
              "_value" : "0~6jr1GkZxoWVzWfcUNA5feff3l7g8fPHJ1rqKetCBa3QXhCGY74PnEuRwzktleMTFounMfCdDpSr1hRfhUGIUEQ=="
            }
          },
          "testsRef" : {
            "id" : {
              "_value" : "0~iRbOkDnmtKVIvHSV2jkeuNcg4RDTUaCLZV7KijyxdCqvhqtp08MKxl0MwjBAPpjmruoI7qNHzBR1RJQAlANNHA=="
            }
          },
          "metrics" : {
            "_type" : {
              "_name" : "ResultMetrics"
            },
            "testsCount" : {
              "_type" : {
                "_name" : "Int"
              },
              "_value" : "2"
            },
            "testsFailedCount" : {
              "_type" : {
                "_name" : "Int"
              },
              "_value" : "2"
            }
          }
        }
      }
    ]
  },
  "issues" : {
    "testFailureSummaries" : {
      "_values" : [
        {
          "documentLocationInCreatingWorkspace" : {
            "url" : {
              "_value" : "file:\/\/\/..\/..\/ios\/web\/shell\/test\/page_state_egtest.mm#CharacterRangeLen=0&EndingLineNumber=130&StartingLineNumber=130"
            }
          },
          "message" : {
            "_value": "Fail. Screenshots: {\\n\\"Failure\\": \\"path.png\\"\\n}"
          },
          "testCaseName" : {
            "_value": "-[PageStateTestCase testZeroContentOffsetAfterLoad]"
          }
        }
      ]
    }
  },
  "metrics" : {
    "testsCount" : {
      "_value" : "2"
    },
    "testsFailedCount" : {
      "_value" : "1"
    }
  }
}"""

XCRESULT_MISSING_ACTIONRESULT_METRICS = b"""
{
  "_type" : {
    "_name" : "ActionsInvocationRecord"
  },
  "actions" : {
    "_values" : [
      {
        "actionResult" : {
          "metrics" : {
            "_type" : {
              "_name" : "ResultMetrics"
            },
            "errorCount" : {
              "_type" : {
                "_name" : "Int"
              },
              "_value" : "1"
            }
          }
        }
      }
    ]
  },
  "metrics" : {
    "errorCount" : {
      "_type" : {
        "_name" : "Int"
      },
      "_value" : "1"
    },
    "testsCount" : {
      "_type" : {
        "_name" : "Int"
      },
      "_value" : "30"
    }
  }
}"""

REF_ID = b"""
  {
    "actions": {
      "_values": [{
        "actionResult": {
          "testsRef": {
            "id": {
              "_value": "REF_ID"
            }
          }
        }
      }]
    }
  }"""

# A sample of json result when executing xcresulttool on .xcresult dir with
# "testsRef" as --id input. Some unused keys and values were removed.
TESTS_REF = """
  {
    "summaries": {
      "_values": [{
        "testableSummaries": {
          "_type": {
            "_name": "Array"
          },
          "_values": [{
            "tests": {
              "_type": {
                "_name": "Array"
              },
              "_values": [{
                "identifier" : {
                  "_value" : "All tests"
                },
                "name" : {
                  "_value" : "All tests"
                },
                "subtests": {
                  "_values": [{
                    "identifier" : {
                      "_value" : "ios_web_shell_eg2tests_module.xctest"
                    },
                    "name" : {
                      "_value" : "ios_web_shell_eg2tests_module.xctest"
                    },
                    "subtests": {
                      "_values": [{
                        "identifier" : {
                          "_value" : "PageStateTestCase"
                        },
                        "name" : {
                          "_value" : "PageStateTestCase"
                        },
                        "subtests": {
                          "_values": [{
                            "testStatus": {
                              "_value": "Success"
                            },
                            "duration" : {
                              "_type" : {
                                 "_name" : "Double"
                              },
                              "_value" : "35.38412606716156"
                            },
                            "identifier": {
                              "_value": "PageStateTestCase/testMethod1"
                            },
                            "name": {
                              "_value": "testMethod1"
                            }
                          },
                          {
                            "summaryRef": {
                              "id": {
                                "_value": "0~7Q_uAuUSJtx9gtHM08psXFm3g_xiTTg5bpdoDO88nMXo_iMwQTXpqlrlMe5AtkYmnZ7Ux5uEgAe83kJBfoIckw=="
                              }
                            },
                            "testStatus": {
                              "_value": "Failure"
                            },
                            "identifier": {
                              "_value": "PageStateTestCase\/testZeroContentOffsetAfterLoad"
                            },
                            "name": {
                              "_value": "testZeroContentOffsetAfterLoad"
                            }
                          },
                          {
                            "testStatus": {
                              "_value": "Expected Failure"
                            },
                            "duration" : {
                              "_type" : {
                                 "_name" : "Double"
                              },
                              "_value" : "28.988606716156"
                            },
                            "identifier": {
                              "_value": "PageStateTestCase/testMethod2"
                            },
                            "name": {
                              "_value": "testMethod2"
                            }
                          },
                          {
                            "testStatus": {
                              "_value": "Skipped"
                            },
                            "duration" : {
                              "_type" : {
                                 "_name" : "Double"
                              },
                              "_value" : "0.0606716156"
                            },
                            "identifier": {
                              "_value": "PageStateTestCase/testMethod3"
                            },
                            "name": {
                              "_value": "testMethod3"
                            }
                          }]
                        }
                      }]
                    }
                  }]
                }
              }]
            }
          }]
        }
      }]
    }
  }
"""

# A sample of json result when executing xcresulttool on .xcresult dir with
# a single test summaryRef id value as --id input. Some unused keys and values
# were removed.
SINGLE_TEST_SUMMARY_REF = """
{
  "_type" : {
    "_name" : "ActionTestSummary",
    "_supertype" : {
      "_name" : "ActionTestSummaryIdentifiableObject",
      "_supertype" : {
        "_name" : "ActionAbstractTestSummary"
      }
    }
  },
  "activitySummaries" : {
    "_values" : [
      {
        "attachments" : {
          "_values" : [
            {
              "filename" : {
                "_value" : "Screenshot_25659115-F3E4-47AE-AA34-551C94333D7E.jpg"
              },
              "payloadRef" : {
                "id" : {
                  "_value" : "SCREENSHOT_REF_ID_1"
                }
              }
            }
          ]
        },
        "title" : {
          "_value" : "Start Test at 2020-10-19 14:12:58.111"
        }
      },
      {
        "subactivities" : {
          "_values" : [
            {
              "attachments" : {
                "_values" : [
                  {
                    "filename" : {
                      "_value" : "Screenshot_23D95D0E-8B97-4F99-BE3C-A46EDE5999D7.jpg"
                    },
                    "payloadRef" : {
                      "id" : {
                        "_value" : "SCREENSHOT_REF_ID_2"
                      }
                    }
                  }
                ]
              },
              "subactivities" : {
                "_values" : [
                  {
                    "subactivities" : {
                      "_values" : [
                        {
                          "attachments" : {
                            "_values" : [
                              {
                                "filename" : {
                                  "_value" : "Crash_3F0A2B1C-7ADA-436E-A54C-D4C39B8411F8.crash"
                                },
                                "payloadRef" : {
                                  "id" : {
                                    "_value" : "CRASH_REF_ID_IN_ACTIVITY_SUMMARIES"
                                  }
                                }
                              }
                            ]
                          },
                          "title" : {
                            "_value" : "Wait for org.chromium.ios-web-shell-eg2tests to idle"
                          }
                        }
                      ]
                    },
                    "title" : {
                      "_value" : "Activate org.chromium.ios-web-shell-eg2tests"
                    }
                  }
                ]
              },
              "title" : {
                "_value" : "Open org.chromium.ios-web-shell-eg2tests"
              }
            }
          ]
        },
        "title" : {
          "_value" : "Set Up"
        }
      },
      {
        "title" : {
          "_value" : "Find the Target Application 'org.chromium.ios-web-shell-eg2tests'"
        }
      },
      {
        "attachments" : {
          "_values" : [
            {
              "filename" : {
                "_value" : "Screenshot_278BA84B-2196-4CCD-9D31-2C07DDDC9DFC.jpg"
              },
              "payloadRef" : {
                "id" : {
                  "_value" : "SCREENSHOT_REF_ID_3"
                }
              }

            }
          ]
        },
        "title" : {
          "_value" : "Uncaught Exception at page_state_egtest.mm:131: \\nCannot scroll, the..."
        }
      },
      {
        "title" : {
          "_value" : "Uncaught Exception: Immediately halt execution of testcase (EarlGreyInternalTestInterruptException)"
        }
      },
      {
        "title" : {
          "_value" : "Tear Down"
        }
      }
    ]
  },
  "failureSummaries" : {
    "_values" : [
      {
        "attachments" : {
          "_values" : [
            {
              "filename" : {
                "_value" : "kXCTAttachmentLegacyScreenImageData_1_6CED1FE5-96CA-47EA-9852-6FADED687262.jpeg"
              },
              "payloadRef" : {
                "id" : {
                  "_value" : "SCREENSHOT_REF_ID_IN_FAILURE_SUMMARIES"
                }
              }
            }
          ]
        },
        "fileName" : {
          "_value" : "\/..\/..\/ios\/web\/shell\/test\/page_state_egtest.mm"
        },
        "lineNumber" : {
          "_value" : "131"
        },
        "message" : {
          "_value" : "Some logs."
        }
      },
      {
        "message" : {
          "_value" : "Immediately halt execution of testcase (EarlGreyInternalTestInterruptException)"
        }
      }
    ]
  },
  "identifier" : {
    "_value" : "PageStateTestCase\/testZeroContentOffsetAfterLoad"
  },
  "name" : {
    "_value" : "testZeroContentOffsetAfterLoad"
  },
  "testStatus" : {
    "_value" : "Failure"
  }
}"""

APP_SIDE_FAILURE_LOG = """Will attempt to recover by breaking constraint
<NSLayoutConstraint:0x1000444e0e0 H:[UIView:0x100031b0fc0]-(4)-[UIView:0x100031b1180]   (active)>

Make a symbolic breakpoint at UIViewAlertForUnsatisfiableConstraints to catch this in the debugger.
The methods in the UIConstraintBasedLayoutDebugging category on UIView listed in <UIKitCore/UIView.h> may also be helpful.
[1009/111922.254778:WARNING:base_earl_grey_test_case_app_interface.mm(21)] *********************************
Starting test: -[SmokeTestCase testOpenTab]
2023-10-09 09:25:00.318076-0700 ios_chrome_eg2tests[56690:4719583] [LayoutConstraints] Unable to simultaneously satisfy constraints.
  Probably at least one of the constraints in the following list is one you don't want.
  Try this:
    (1) look at each constraint and try to figure out which you don't expect;
    (2) find the code that added the unwanted constraint or constraints and fix it.
(
    "<NSLayoutConstraint:0x12003d1a6e0 H:|-(0)-[UIView:0x12002478a80]   (active, names: '|':UIView:0x120029d4380 )>",
    "<NSLayoutConstraint:0x12003d1a680 UIView:0x12002478a80.trailing == UIView:0x120029d4380.trailing   (active)>",
    "<NSLayoutConstraint:0x12003d1a440 H:|-(8)-[UIView:0x1200247d080]   (active, names: '|':UIView:0x12002478a80 )>",
    "<NSLayoutConstraint:0x12003d18340 H:[UIView:0x1200247d080]-(4)-[UIView:0x1200247ec80]   (active)>",
    "<NSLayoutConstraint:0x12003d182e0 UIView:0x1200247ec80.trailing == UIView:0x12002478a80.trailing - 8   (active)>",
    "<NSLayoutConstraint:0x12003d42a00 'UIView-Encapsulated-Layout-Width' UIView:0x120029d4380.width == 0   (active)>"
)

Will attempt to recover by breaking constraint
<NSLayoutConstraint:0x12003d18340 H:[UIView:0x1200247d080]-(4)-[UIView:0x1200247ec80]   (active)>

Make a symbolic breakpoint at UIViewAlertForUnsatisfiableConstraints to catch this in the debugger.
The methods in the UIConstraintBasedLayoutDebugging category on UIView listed in <UIKitCore/UIView.h> may also be helpful.
2023-10-09 09:25:01.480402-0700 ios_chrome_eg2tests[56690:4719583] [unspecified] container_create_or_lookup_app_group_path_by_app_group_identifier: client is not entitled
2023-10-09 09:25:02.383910-0700 ios_chrome_eg2tests[56690:4719754] [VoiceShortcutClient] -[VCVoiceShortcutClient unsafeSetupXPCConnection]_block_invoke Client connection to VCVoiceShortcut XPC server interrupted
2023-10-09 09:25:02.385353-0700 ios_chrome_eg2tests[56690:4719754] [Intents] -[INVoiceShortcutCenter getAllVoiceShortcutsWithCompletion:]_block_invoke Error from -getVoiceShortcutsWithCompletion: Error Domain=NSCocoaErrorDomain Code=4097 "Couldn’t communicate with a helper application."
[1009/092503.896821:ERROR:loopback_server.cc(907)] Loopback sync cannot read the persistent state file (/Users/chrome-bot/Library/Developer/CoreSimulator/Devices/82CF3734-9FF2-4C1B-920C-B3345C0CA891/data/Containers/Data/Application/F2938797-9668-4622-947A-895503B62BCD/tmp/.org.chromium.ost.chrome.unittests.dev.lxaMyc/profile.pb) with error FILE_ERROR_NOT_FOUND
2023-10-09 09:25:03.964248-0700 ios_chrome_eg2tests[56690:4719583] [unspecified] container_create_or_lookup_app_group_path_by_app_group_identifier: client is not entitled
2023-10-09 09:25:06.074170-0700 ios_chrome_eg2tests[56690:4719583] [LayoutConstraints] Unable to simultaneously satisfy constraints.
  Probably at least one of the constraints in the following list is one you don't want.
  Try this:
    (1) look at each constraint and try to figure out which you don't expect;
    (2) find the code that added the unwanted constraint or constraints and fix it.
(
    "<NSLayoutConstraint:0x12003d2a8c0 H:|-(0)-[UIView:0x1200235e300]   (active, names: '|':UIView:0x12002a10fc0 )>",
    "<NSLayoutConstraint:0x12003d20360 UIView:0x1200235e300.trailing == UIView:0x12002a10fc0.trailing   (active)>",
    "<NSLayoutConstraint:0x12003d21d40 H:|-(8)-[UIView:0x1200235f640]   (active, names: '|':UIView:0x1200235e300 )>",
    "<NSLayoutConstraint:0x12003d24b60 UIView:0x1200235f640.width == 16   (active)>",
    "<NSLayoutConstraint:0x12003d9c200 H:[UIView:0x1200235f640]-(4)-[UIView:0x1200235c380]   (active)>",
    "<NSLayoutConstraint:0x12003d98a20 UIView:0x1200235c380.trailing == UIView:0x1200235e300.trailing - 8   (active)>",
    "<NSLayoutConstraint:0x12001326f60 'UIView-Encapsulated-Layout-Width' UIView:0x12002a10fc0.width == 0   (active)>"
)

Will attempt to recover by breaking constraint
<NSLayoutConstraint:0x12003d24b60 UIView:0x1200235f640.width == 16   (active)>

Make a symbolic breakpoint at UIViewAlertForUnsatisfiableConstraints to catch this in the debugger.
The methods in the UIConstraintBasedLayoutDebugging category on UIView listed in <UIKitCore/UIView.h> may also be helpful.
2023-10-09 09:25:06.075364-0700 ios_chrome_eg2tests[56690:4719583] [LayoutConstraints] Unable to simultaneously satisfy constraints.
  Probably at least one of the constraints in the following list is one you don't want.
  Try this:
    (1) look at each constraint and try to figure out which you don't expect;
    (2) find the code that added the unwanted constraint or constraints and fix it.
(
    "<NSLayoutConstraint:0x12003d2a8c0 H:|-(0)-[UIView:0x1200235e300]   (active, names: '|':UIView:0x12002a10fc0 )>",
    "<NSLayoutConstraint:0x12003d20360 UIView:0x1200235e300.trailing == UIView:0x12002a10fc0.trailing   (active)>",
    "<NSLayoutConstraint:0x12003d21d40 H:|-(8)-[UIView:0x1200235f640]   (active, names: '|':UIView:0x1200235e300 )>",
    "<NSLayoutConstraint:0x12003d9c200 H:[UIView:0x1200235f640]-(4)-[UIView:0x1200235c380]   (active)>",
    "<NSLayoutConstraint:0x12003d98a20 UIView:0x1200235c380.trailing == UIView:0x1200235e300.trailing - 8   (active)>",
    "<NSLayoutConstraint:0x12001326f60 'UIView-Encapsulated-Layout-Width' UIView:0x12002a10fc0.width == 0   (active)>"
)

Will attempt to recover by breaking constraint
<NSLayoutConstraint:0x12003d9c200 H:[UIView:0x1200235f640]-(4)-[UIView:0x1200235c380]   (active)>

Make a symbolic breakpoint at UIViewAlertForUnsatisfiableConstraints to catch this in the debugger.
The methods in the UIConstraintBasedLayoutDebugging category on UIView listed in <UIKitCore/UIView.h> may also be helpful.
[1009/111926.491858:FATAL:chrome_earl_grey_app_interface.mm(147)] Check failed: NO.
0   ios_chrome_eg2testsMain             0x000000012a31b174 base::debug::CollectStackTrace(void const**, unsigned long) + 48
1   ios_chrome_eg2testsMain             0x000000012a2ed878 base::debug::StackTrace::StackTrace(unsigned long) + 92
2   ios_chrome_eg2testsMain             0x000000012a2ed910 base::debug::StackTrace::StackTrace(unsigned long) + 36
3   ios_chrome_eg2testsMain             0x000000012a2ed8dc base::debug::StackTrace::StackTrace() + 40
4   ios_chrome_eg2testsMain             0x000000012a03d7e0 logging::LogMessage::~LogMessage() + 204
5   ios_chrome_eg2testsMain             0x000000012a03e748 logging::LogMessage::~LogMessage() + 28
6   ios_chrome_eg2testsMain             0x000000012a03e774 logging::LogMessage::~LogMessage() + 28
7   ios_chrome_eg2testsMain             0x000000012a00bba8 logging::CheckError::~CheckError() + 112
8   ios_chrome_eg2testsMain             0x000000012a00bc08 logging::CheckError::~CheckError() + 28
9   ios_chrome_eg2testsMain             0x0000000126745008 +[ChromeEarlGreyAppInterface crashApp] + 104
more of the stack trace and crash report logs...

Standard output and standard error from com.google.chrome.unittests.dev with process ID 1358 beginning at 2023-10-09 15:19:36 +0000

2023-10-09 11:19:37.449520-0400 ios_chrome_eg2tests[1358:24891823] [User Defaults] Not updating lastKnownShmemState in CFPrefsPlistSource<0x6000030083f0> (Domain: com.apple.keyboard.preferences.plist, User: kCFPreferencesCurrentUser, ByHost: No, Container: (null), Contents Need Refresh: Yes): 0 -> 323
2023-10-09 11:19:37.449594-0400 ios_chrome_eg2tests[1358:24891823] [User Defaults] Source was stale because shmem was null: CFPrefsPlistSource<0x6000030083f0> (Domain: com.apple.keyboard.preferences.plist, User: kCFPreferencesCurrentUser, ByHost: No, Container: (null), Contents Need Refresh: Yes)

"""

APP_SIDE_FAILURE_LOG_EXPECTED = f"""App crashed and disconnected.
Showing logs from application under test. For complete logs see attempt_0_simulator#0_StandardOutputAndStandardError-com.google.chrome.unittests.dev.txt in CAS outputs, which can be found in the swarming task of the shard this test ran on.

Starting test: -[SmokeTestCase testOpenTab]
2023-10-09 09:25:00.318076-0700 ios_chrome_eg2tests[56690:4719583] [LayoutConstraints] {constants.LAYOUT_CONSTRAINT_MSG}.
2023-10-09 09:25:01.480402-0700 ios_chrome_eg2tests[56690:4719583] [unspecified] container_create_or_lookup_app_group_path_by_app_group_identifier: client is not entitled
2023-10-09 09:25:02.383910-0700 ios_chrome_eg2tests[56690:4719754] [VoiceShortcutClient] -[VCVoiceShortcutClient unsafeSetupXPCConnection]_block_invoke Client connection to VCVoiceShortcut XPC server interrupted
2023-10-09 09:25:02.385353-0700 ios_chrome_eg2tests[56690:4719754] [Intents] -[INVoiceShortcutCenter getAllVoiceShortcutsWithCompletion:]_block_invoke Error from -getVoiceShortcutsWithCompletion: Error Domain=NSCocoaErrorDomain Code=4097 "Couldn’t communicate with a helper application."
[1009/092503.896821:ERROR:loopback_server.cc(907)] Loopback sync cannot read the persistent state file (/Users/chrome-bot/Library/Developer/CoreSimulator/Devices/82CF3734-9FF2-4C1B-920C-B3345C0CA891/data/Containers/Data/Application/F2938797-9668-4622-947A-895503B62BCD/tmp/.org.chromium.ost.chrome.unittests.dev.lxaMyc/profile.pb) with error FILE_ERROR_NOT_FOUND
2023-10-09 09:25:03.964248-0700 ios_chrome_eg2tests[56690:4719583] [unspecified] container_create_or_lookup_app_group_path_by_app_group_identifier: client is not entitled
2023-10-09 09:25:06.074170-0700 ios_chrome_eg2tests[56690:4719583] [LayoutConstraints] {constants.LAYOUT_CONSTRAINT_MSG}.
2023-10-09 09:25:06.075364-0700 ios_chrome_eg2tests[56690:4719583] [LayoutConstraints] {constants.LAYOUT_CONSTRAINT_MSG}.
[1009/111926.491858:FATAL:chrome_earl_grey_app_interface.mm(147)] Check failed: NO.
0   ios_chrome_eg2testsMain             0x000000012a31b174 base::debug::CollectStackTrace(void const**, unsigned long) + 48
1   ios_chrome_eg2testsMain             0x000000012a2ed878 base::debug::StackTrace::StackTrace(unsigned long) + 92
2   ios_chrome_eg2testsMain             0x000000012a2ed910 base::debug::StackTrace::StackTrace(unsigned long) + 36
3   ios_chrome_eg2testsMain             0x000000012a2ed8dc base::debug::StackTrace::StackTrace() + 40
4   ios_chrome_eg2testsMain             0x000000012a03d7e0 logging::LogMessage::~LogMessage() + 204
5   ios_chrome_eg2testsMain             0x000000012a03e748 logging::LogMessage::~LogMessage() + 28
6   ios_chrome_eg2testsMain             0x000000012a03e774 logging::LogMessage::~LogMessage() + 28
7   ios_chrome_eg2testsMain             0x000000012a00bba8 logging::CheckError::~CheckError() + 112
8   ios_chrome_eg2testsMain             0x000000012a00bc08 logging::CheckError::~CheckError() + 28
9   ios_chrome_eg2testsMain             0x0000000126745008 +[ChromeEarlGreyAppInterface crashApp] + 104
more of the stack trace and crash report logs...


"""


def _xcresulttool_get_side_effect(xcresult_path, ref_id=None):
  """Side effect for _xcresulttool_get in XcodeLogParser tested."""
  if ref_id is None:
    return XCRESULT_ROOT
  if ref_id == 'testsRef':
    return TESTS_REF
  # Other situation in use cases of xcode_log_parser is asking for single test
  # summary ref.
  return SINGLE_TEST_SUMMARY_REF


class UtilMethodsTest(test_runner_test.TestCase):
  """Test case for utility methods not related with Parser class."""

  def setUp(self):
    self.summary_xcode16_with_parallel = {
        'tests': {
            '_values': ['TestSuite1', 'TestSuite2']
        }
    }

    # Example test summary when running xcode version lower than 16.
    # It could also be when running xcode version 16 without xcode
    # parallelization enabled.
    self.summary_pre_xcode16 = {
        'tests': {
            '_values': [{
                'subtests': {
                    '_values': [{
                        'subtests': {
                            '_values': ['TestSuite1', 'TestSuite2']
                        }
                    }]
                }
            }]
        }
    }

  def testParseTestsForInterruptedRun(self):
    test_output = """
    Test case '-[DownloadManagerTestCase testVisibleFileNameAndOpenInDownloads]' passed on 'Clone 2 of iPhone X 15.0 test simulator - ios_chrome_ui_eg2tests_module-Runner (34498)' (20.715 seconds)
    Test case '-[SyncFakeServerTestCase testSyncDownloadBookmark]' passed on 'Clone 1 of iPhone X 15.0 test simulator - ios_chrome_ui_eg2tests_module-Runner (34249)' (14.880 seconds)
    Random lines
         t =    53.90s Tear Down
    Test Case '-[LinkToTextTestCase testGenerateLinkForSimpleText]' failed (55.316 seconds).
     t =      nans Suite Tear Down
    Test Suite 'LinkToTextTestCase' failed at 2021-06-15 07:13:17.406.
      Executed 1 test, with 6 failures (6 unexpected) in 55.316 (55.338) seconds
    Test Suite 'ios_chrome_ui_eg2tests_module.xctest' failed at 2021-06-15 07:13:17.407.
      Executed 1 test, with 6 failures (6 unexpected) in 55.316 (55.340) seconds
    Test Suite 'Selected tests' failed at 2021-06-15 07:13:17.408.
      Executed 1 test, with 6 failures (6 unexpected) in 55.316 (55.342) seconds
    """
    test_output_list = test_output.split('\n')
    expected_passed = set([
        'DownloadManagerTestCase/testVisibleFileNameAndOpenInDownloads',
        'SyncFakeServerTestCase/testSyncDownloadBookmark'
    ])
    expected_failed = set(['LinkToTextTestCase/testGenerateLinkForSimpleText'])
    expected_failed_message = 'Test failed in interrupted(timedout) run.'

    results = xcode_log_parser.parse_passed_failed_tests_for_interrupted_run(
        test_output_list)
    self.assertEqual(results.expected_tests(), expected_passed)
    self.assertEqual(results.unexpected_tests(), expected_failed)
    for result in results.test_results:
      if result.name == 'LinkToTextTestCase/testGenerateLinkForSimpleText':
        self.assertEqual(result.test_log, expected_failed_message)

  @mock.patch('xcode_util.using_xcode_16_or_higher')
  def test_xcode16_parallel(self, mock_xcode_version):
    mock_xcode_version.return_value = True
    result = xcode_log_parser.get_test_suites(
        self.summary_xcode16_with_parallel, True)
    self.assertEqual(result, ['TestSuite1', 'TestSuite2'])

  @mock.patch('xcode_util.using_xcode_16_or_higher')
  def test_xcode16_not_parallel(self, mock_xcode_version):
    mock_xcode_version.return_value = True
    result = xcode_log_parser.get_test_suites(self.summary_pre_xcode16, False)
    self.assertEqual(result, ['TestSuite1', 'TestSuite2'])

  @mock.patch('xcode_util.using_xcode_16_or_higher')
  def test_pre_xcode16_parallel(self, mock_xcode_version):
    mock_xcode_version.return_value = False
    result = xcode_log_parser.get_test_suites(self.summary_pre_xcode16, True)
    self.assertEqual(result, ['TestSuite1', 'TestSuite2'])


class XcodeLogParserTest(test_runner_test.TestCase):
  """Test case to test XcodeLogParser."""

  def setUp(self):
    super(XcodeLogParserTest, self).setUp()
    self.mock(test_runner, 'get_current_xcode_info', lambda: XCODE11_DICT)

  @mock.patch('subprocess.check_output', autospec=True)
  @mock.patch('xcode_util.using_xcode_16_or_higher')
  def testXcresulttoolGetRoot(self, mock_xcode_version, mock_process):
    mock_xcode_version.return_value = False
    mock_process.return_value = b'%JSON%'
    xcode_log_parser.XcodeLogParser()._xcresulttool_get('xcresult_path')
    self.assertTrue(
        os.path.join(XCODE11_DICT['path'], 'usr', 'bin') in os.environ['PATH'])
    self.assertEqual(
        ['xcresulttool', 'get', '--format', 'json', '--path', 'xcresult_path'],
        mock_process.mock_calls[0][1][0])

  @mock.patch('subprocess.check_output', autospec=True)
  @mock.patch('xcode_util.using_xcode_16_or_higher')
  def testXcresulttoolGetRef(self, mock_xcode_version, mock_process):
    mock_xcode_version.return_value = False
    mock_process.side_effect = [REF_ID, b'JSON']
    xcode_log_parser.XcodeLogParser()._xcresulttool_get('xcresult_path',
                                                          'testsRef')
    self.assertEqual(
        ['xcresulttool', 'get', '--format', 'json', '--path', 'xcresult_path'],
        mock_process.mock_calls[0][1][0])
    self.assertEqual([
        'xcresulttool', 'get', '--format', 'json', '--path', 'xcresult_path',
        '--id', 'REF_ID'], mock_process.mock_calls[1][1][0])

  def testXcresulttoolListFailedTests(self):
    failure_message = (
        'file:///../../ios/web/shell/test/page_state_egtest.mm#'
        'CharacterRangeLen=0&EndingLineNumber=130&StartingLineNumber=130\n'
        'Fail. Screenshots: {\n\"Failure\": \"path.png\"\n}')
    expected = set(['PageStateTestCase/testZeroContentOffsetAfterLoad'])
    results = xcode_log_parser.XcodeLogParser()._list_of_failed_tests(
        json.loads(XCRESULT_ROOT))
    self.assertEqual(expected, results.failed_tests())
    log = results.test_results[0].test_log
    self.assertEqual(log, failure_message)

  def testXcresulttoolListFailedTestsExclude(self):
    excluded = set(['PageStateTestCase/testZeroContentOffsetAfterLoad'])
    results = xcode_log_parser.XcodeLogParser()._list_of_failed_tests(
        json.loads(XCRESULT_ROOT), excluded=excluded)
    self.assertEqual(set([]), results.all_test_names())

  @mock.patch('xcode_log_parser.XcodeLogParser._export_data')
  @mock.patch('xcode_log_parser.XcodeLogParser._xcresulttool_get')
  def testGetTestStatuses(self, mock_xcresult, mock_export):
    mock_xcresult.side_effect = _xcresulttool_get_side_effect
    #   self.assertEqual(test_result.test_log, lo
    expected_failure_log = (
        'Logs from "failureSummaries" in .xcresult:\n'
        'file: /../../ios/web/shell/test/page_state_egtest.mm, line: 131\n'
        'Some logs.\n'
        'file: , line: \n'
        'Immediately halt execution of testcase '
        '(EarlGreyInternalTestInterruptException)\n')
    expected_expected_tests = set([
        'PageStateTestCase/testMethod1', 'PageStateTestCase/testMethod2',
        'PageStateTestCase/testMethod3'
    ])
    results = xcode_log_parser.XcodeLogParser()._get_test_statuses(
        OUTPUT_PATH, False)
    self.assertEqual(expected_expected_tests, results.expected_tests())
    seen_failed_test = False
    for test_result in results.test_results:
      if test_result.name == 'PageStateTestCase/testZeroContentOffsetAfterLoad':
        seen_failed_test = True
        self.assertEqual(test_result.test_log, expected_failure_log)
        self.assertEqual(test_result.duration, None)
        crash_file_name = (
            'attempt_0_PageStateTestCase_testZeroContentOffsetAfterLoad_'
            'Crash_3F0A2B1C-7ADA-436E-A54C-D4C39B8411F8.crash'
        )
        jpeg_file_name = (
            'attempt_0_PageStateTestCase_testZeroContentOffsetAfterLoad'
            '_kXCTAttachmentLegacyScreenImageData_1'
            '_6CED1FE5-96CA-47EA-9852-6FADED687262.jpeg')
        self.assertDictEqual(
            {
                crash_file_name: '/tmp/%s' % crash_file_name,
                jpeg_file_name: '/tmp/%s' % jpeg_file_name,
            }, test_result.attachments)
      if test_result.name == 'PageStateTestCase/testMethod1':
        self.assertEqual(test_result.duration, 35384)
      if test_result.name == 'PageStateTestCase/testMethod2':
        self.assertEqual(test_result.duration, 28988)
      if test_result.name == 'PageStateTestCase/testMethod3':
        self.assertEqual(test_result.duration, 60)

    self.assertTrue(seen_failed_test)

  @mock.patch('file_util.zip_and_remove_folder')
  @mock.patch('xcode_log_parser.XcodeLogParser._extract_artifacts_for_test')
  @mock.patch('xcode_log_parser.XcodeLogParser.export_diagnostic_data')
  @mock.patch('os.path.exists', autospec=True)
  @mock.patch('xcode_log_parser.XcodeLogParser._xcresulttool_get')
  def testCollectTestTesults(self, mock_root, mock_exist_file, *args):
    expected_passed = set([
        'PageStateTestCase/testMethod1', 'PageStateTestCase/testMethod2',
        'PageStateTestCase/testMethod3'
    ])
    expected_failed = set(['PageStateTestCase/testZeroContentOffsetAfterLoad'])

    mock_root.side_effect = _xcresulttool_get_side_effect
    mock_exist_file.return_value = True
    results = xcode_log_parser.XcodeLogParser().collect_test_results(
        OUTPUT_PATH, [])

    # Length ensures no duplicate results from |_get_test_statuses| and
    # |_list_of_failed_tests|.
    self.assertEqual(len(results.test_results), 4)
    self.assertEqual(expected_passed, results.expected_tests())
    self.assertEqual(expected_failed, results.unexpected_tests())
    # Ensure format.
    for test in results.test_results:
      self.assertTrue(isinstance(test.name, str))
      if test.status == TestStatus.FAIL:
        self.assertTrue(isinstance(test.test_log, str))

  @mock.patch('file_util.zip_and_remove_folder')
  @mock.patch('xcode_log_parser.XcodeLogParser.copy_artifacts')
  @mock.patch('xcode_log_parser.XcodeLogParser.export_diagnostic_data')
  @mock.patch('os.path.exists', autospec=True)
  @mock.patch('xcode_log_parser.XcodeLogParser._xcresulttool_get')
  def testCollectTestsRanZeroTests(self, mock_root, mock_exist_file, *args):
    metrics_json = '{"actions": {}}'
    mock_root.return_value = metrics_json
    mock_exist_file.return_value = True
    results = xcode_log_parser.XcodeLogParser().collect_test_results(
        OUTPUT_PATH, [])
    self.assertTrue(results.crashed)
    self.assertEqual(results.crash_message, '0 tests executed!')
    self.assertEqual(len(results.all_test_names()), 0)

  @mock.patch('xcode_log_parser.XcodeLogParser._list_of_failed_tests')
  @mock.patch('xcode_log_parser.XcodeLogParser._get_test_statuses')
  @mock.patch('file_util.zip_and_remove_folder')
  @mock.patch('xcode_log_parser.XcodeLogParser.copy_artifacts')
  @mock.patch('xcode_log_parser.XcodeLogParser.export_diagnostic_data')
  @mock.patch('os.path.exists', autospec=True)
  @mock.patch('xcode_log_parser.XcodeLogParser._xcresulttool_get')
  def testFallbackOnRootMetrics(self, mock_root, mock_exist_file, *args):
    mock_root.return_value = XCRESULT_MISSING_ACTIONRESULT_METRICS
    mock_exist_file.return_value = True
    results = xcode_log_parser.XcodeLogParser().collect_test_results(
        OUTPUT_PATH, [])
    self.assertTrue(results.crashed != True)
    self.assertNotEqual(results.crash_message, '0 tests executed!')

  @mock.patch('os.path.exists', autospec=True)
  def testCollectTestsDidNotRun(self, mock_exist_file):
    mock_exist_file.return_value = False
    results = xcode_log_parser.XcodeLogParser().collect_test_results(
        OUTPUT_PATH, [])
    self.assertTrue(results.crashed)
    self.assertEqual(results.crash_message,
                     '/tmp/attempt_0 with staging data does not exist.\n')
    self.assertEqual(len(results.all_test_names()), 0)

  @mock.patch('os.path.exists', autospec=True)
  def testCollectTestsInterruptedRun(self, mock_exist_file):
    mock_exist_file.side_effect = [True, False]
    results = xcode_log_parser.XcodeLogParser().collect_test_results(
        OUTPUT_PATH, [])
    self.assertTrue(results.crashed)
    self.assertEqual(
        results.crash_message,
        '/tmp/attempt_0.xcresult with test results does not exist.\n')
    self.assertEqual(len(results.all_test_names()), 0)

  @mock.patch('subprocess.check_output', autospec=True)
  @mock.patch('os.path.exists', autospec=True)
  @mock.patch('xcode_log_parser.XcodeLogParser._xcresulttool_get')
  @mock.patch('xcode_util.using_xcode_16_or_higher')
  def testCopyScreenshots(self, mock_xcode_version, mock_xcresulttool_get,
                          mock_path_exists, mock_process):
    mock_xcode_version.return_value = False
    mock_path_exists.return_value = True
    mock_xcresulttool_get.side_effect = _xcresulttool_get_side_effect
    xcode_log_parser.XcodeLogParser().copy_artifacts(OUTPUT_PATH)
    mock_process.assert_any_call([
        'xcresulttool', 'export', '--type', 'file', '--id',
        'SCREENSHOT_REF_ID_IN_FAILURE_SUMMARIES', '--path', XCRESULT_PATH,
        '--output-path',
        '/tmp/attempt_0_PageStateTestCase_testZeroContentOffsetAfterLoad'
        '_kXCTAttachmentLegacyScreenImageData_1'
        '_6CED1FE5-96CA-47EA-9852-6FADED687262.jpeg'
    ])
    mock_process.assert_any_call([
        'xcresulttool', 'export', '--type', 'file', '--id',
        'CRASH_REF_ID_IN_ACTIVITY_SUMMARIES', '--path', XCRESULT_PATH,
        '--output-path',
        '/tmp/attempt_0_PageStateTestCase_testZeroContentOffsetAfterLoad'
        '_Crash_3F0A2B1C-7ADA-436E-A54C-D4C39B8411F8.crash'
    ])
    # Ensures screenshots in activitySummaries are not copied.
    self.assertEqual(2, mock_process.call_count)

  @mock.patch('file_util.zip_and_remove_folder')
  @mock.patch('subprocess.check_output', autospec=True)
  @mock.patch('os.path.exists', autospec=True)
  @mock.patch('xcode_log_parser.XcodeLogParser._xcresulttool_get')
  @mock.patch('xcode_util.using_xcode_16_or_higher')
  def testExportDiagnosticData(self, mock_xcode_version, mock_xcresulttool_get,
                               mock_path_exists, mock_process, _):
    mock_xcode_version.return_value = False
    mock_path_exists.return_value = True
    mock_xcresulttool_get.side_effect = _xcresulttool_get_side_effect
    xcode_log_parser.XcodeLogParser.export_diagnostic_data(OUTPUT_PATH)
    mock_process.assert_called_with([
        'xcresulttool', 'export', '--type', 'directory', '--id',
        'DIAGNOSTICS_REF_ID', '--path', XCRESULT_PATH, '--output-path',
        '/tmp/attempt_0.xcresult_diagnostic'
    ])

  @mock.patch('file_util.zip_and_remove_folder')
  @mock.patch('shutil.copy')
  @mock.patch('subprocess.check_output', autospec=True)
  @mock.patch('os.path.exists', autospec=True)
  @mock.patch('xcode_log_parser.XcodeLogParser._xcresulttool_get')
  @mock.patch('xcode_util.using_xcode_16_or_higher')
  def testStdoutCopiedInExportDiagnosticData(self, mock_xcode_version,
                                             mock_xcresulttool_get,
                                             mock_path_exists, mock_process,
                                             mock_copy, _):
    mock_xcode_version.return_value = False
    output_path_in_test = 'test_data/attempt_0'
    xcresult_path_in_test = 'test_data/attempt_0.xcresult'
    mock_path_exists.return_value = True
    mock_xcresulttool_get.side_effect = _xcresulttool_get_side_effect
    xcode_log_parser.XcodeLogParser.export_diagnostic_data(
        output_path_in_test)
    # os.walk() walks folders in unknown sequence. Use try-except blocks to
    # assert that any of the 2 assertions is true.
    try:
      mock_copy.assert_any_call(
          'test_data/attempt_0.xcresult_diagnostic/test_module-UUID/test_module-UUID1/StandardOutputAndStandardError.txt',
          'test_data/attempt_0/../attempt_0_simulator#1_StandardOutputAndStandardError.txt'
      )
    except AssertionError:
      mock_copy.assert_any_call(
          'test_data/attempt_0.xcresult_diagnostic/test_module-UUID/test_module-UUID1/StandardOutputAndStandardError.txt',
          'test_data/attempt_0/../attempt_0_simulator#0_StandardOutputAndStandardError.txt'
      )
    try:
      mock_copy.assert_any_call(
          'test_data/attempt_0.xcresult_diagnostic/test_module-UUID/test_module-UUID2/StandardOutputAndStandardError-org.chromium.gtest.ios-chrome-eg2tests.txt',
          'test_data/attempt_0/../attempt_0_simulator#1_StandardOutputAndStandardError-org.chromium.gtest.ios-chrome-eg2tests.txt'
      )
    except AssertionError:
      mock_copy.assert_any_call(
          'test_data/attempt_0.xcresult_diagnostic/test_module-UUID/test_module-UUID2/StandardOutputAndStandardError-org.chromium.gtest.ios-chrome-eg2tests.txt',
          'test_data/attempt_0/../attempt_0_simulator#0_StandardOutputAndStandardError-org.chromium.gtest.ios-chrome-eg2tests.txt'
      )

  @mock.patch('os.path.exists', autospec=True)
  def testCollectTestResults_interruptedTests(self, mock_path_exists):
    mock_path_exists.side_effect = [True, False]
    output = [
        '[09:03:42:INFO] Test case \'-[TestCase1 method1]\' passed on device.',
        '[09:06:40:INFO] Test Case \'-[TestCase2 method1]\' passed on device.',
        '[09:09:00:INFO] Test case \'-[TestCase2 method1]\' failed on device.',
        '** BUILD INTERRUPTED **',
    ]
    not_found_message = ['%s with test results does not exist.' % XCRESULT_PATH]
    res = xcode_log_parser.XcodeLogParser().collect_test_results(
        OUTPUT_PATH, output)
    self.assertTrue(res.crashed)
    self.assertEqual('\n'.join(not_found_message + output), res.crash_message)
    self.assertEqual(
        set(['TestCase1/method1', 'TestCase2/method1']), res.expected_tests())

  @mock.patch('file_util.zip_and_remove_folder')
  @mock.patch('xcode_log_parser.XcodeLogParser._extract_artifacts_for_test')
  @mock.patch('xcode_log_parser.XcodeLogParser.export_diagnostic_data')
  @mock.patch('os.path.exists', autospec=True)
  @mock.patch('xcode_log_parser.XcodeLogParser._xcresulttool_get')
  @mock.patch('xcode_log_parser.XcodeLogParser._list_of_failed_tests')
  def testArtifactsDiagnosticLogsExportedInCollectTestTesults(
      self, mock_get_failed_tests, mock_root, mock_exist_file,
      mock_export_diagnostic_data, mock_extract_artifacts, mock_zip):
    mock_root.side_effect = _xcresulttool_get_side_effect
    mock_exist_file.return_value = True
    xcode_log_parser.XcodeLogParser().collect_test_results(OUTPUT_PATH, [])
    mock_export_diagnostic_data.assert_called_with(OUTPUT_PATH)
    mock_extract_artifacts.assert_called()

  @mock.patch('os.listdir')
  @mock.patch(
      'builtins.open', new=mock.mock_open(read_data=APP_SIDE_FAILURE_LOG))
  def testLogAppSideFailureReason(self, mock_listdir):
    test_name = 'SmokeTestCase/testOpenTab'
    mock_listdir.return_value = [
        'run_1696864672.xctestrun', 'attempt_0.xcresult.zip',
        'attempt_0.xcresult_diagnostic.zip',
        'attempt_0_simulator#0_StandardOutputAndStandardError-com.google.chrome.unittests.dev.txt',
        'attempt_0_simulator#0_StandardOutputAndStandardError.txt',
    ]
    app_side_failure_message = \
      xcode_log_parser.XcodeLogParser()._get_app_side_failure(
        test_name, OUTPUT_PATH)
    self.assertEqual(app_side_failure_message, APP_SIDE_FAILURE_LOG_EXPECTED)


if __name__ == '__main__':
  unittest.main()