docstr: Checks if the given value was called by this process runner.
  
  NOTE: Either pattern or exact must be specified, not both!
  
  Args:
      ip: IP address of the node that the given call was executed on.
      pattern: RegEx that matches one specific call.
      exact: List of strings that when joined exactly match one call.
docstr_tok: ["Checks","if","the","given","value","was","called","by","this","process","runner",".","NOTE",":","Either","pattern","or","exact","must","be","specified",",","not","both","!","Args",":","ip",":","IP","address","of","the","node","that","the","given","call","was","executed","on",".","pattern",":","RegEx","that","matches","one","specific","call",".","exact",":","List","of","strings","that","when","joined","exactly","match","one","call","."]
code: def assert_has_call(
      self,
      ip: str,
      pattern: Optional[str] = None,
      exact: Optional[List[str]] = None,
  ):
      """Checks if the given value was called by this process runner.
  
      NOTE: Either pattern or exact must be specified, not both!
  
      Args:
          ip: IP address of the node that the given call was executed on.
          pattern: RegEx that matches one specific call.
          exact: List of strings that when joined exactly match one call.
      """
      with self.lock:
          assert bool(pattern) ^ bool(
              exact
          ), "Must specify either a pattern or exact match."
          debug_output = ""
          if pattern is not None:
              for cmd in self.command_history():
                  if ip in cmd:
                      debug_output += cmd
                      debug_output += "\n"
                      if re.search(pattern, cmd):
                          return True
              else:
                  raise Exception(
                      f"Did not find [{pattern}] in [{debug_output}] for "
                      f"ip={ip}.\n\nFull output: {self.command_history()}"
                  )
          elif exact is not None:
              exact_cmd = " ".join(exact)
              for cmd in self.command_history():
                  if ip in cmd:
                      debug_output += cmd
                      debug_output += "\n"
                  if cmd == exact_cmd:
                      return True
              raise Exception(
                  f"Did not find [{exact_cmd}] in [{debug_output}] for "
                  f"ip={ip}.\n\nFull output: {self.command_history()}"
              )
  
imports: import re
  import threading
  from subprocess import CalledProcessError
  from typing import Callable, Dict, List, Optional
  from ray.autoscaler.node_provider import NodeProvider
label_desc: Use image node_id 4 for calling the MockProcessRunner obj's underlying member method code with example usage: obj.assert_has_call(ip, pattern, exact) and returns: True, True