text: #!/usr/bin/python
  
  from __future__ import print_function
  
  import pprint
  import sys
  
  TEST_COUNT = 0
  PLATFORM_ARCH_32 = False
  
  CLASSES = { "new_debians": "(debian.!ubuntu_10.!debian_6)",
              "old_debians_32_bit": "(32_bit.(ubuntu_10|debian_6))", # No multiarch support.
              "old_debians_64_bit": "(64_bit.(ubuntu_10|debian_6))", # No multiarch support.
              "redhat_5": "(redhat_5|centos_5)",
              "redhat_6_or_newer": "(redhat.!redhat_5.!centos_5)",
            }
  
  class PromiseFailureException(Exception):
      pass
  class NotSupportedException(Exception):
      pass
  
  states = {
      "absent": {
          ( "64_bit", "1" ): False,
          ( "64_bit", "2" ): False,
          ( "32_bit", "1" ): False,
          ( "32_bit", "2" ): False,
      },
      "64_bit_1": {
          ( "64_bit", "1" ): True,
          ( "64_bit", "2" ): False,
          ( "32_bit", "1" ): False,
          ( "32_bit", "2" ): False,
      },
      "64_bit_2": {
          ( "64_bit", "1" ): False,
          ( "64_bit", "2" ): True,
          ( "32_bit", "1" ): False,
          ( "32_bit", "2" ): False,
      },
      "32_bit_1": {
          ( "64_bit", "1" ): False,
          ( "64_bit", "2" ): False,
          ( "32_bit", "1" ): True,
          ( "32_bit", "2" ): False,
      },
      "32_bit_2": {
          ( "64_bit", "1" ): False,
          ( "64_bit", "2" ): False,
          ( "32_bit", "1" ): False,
          ( "32_bit", "2" ): True,
      },
      "64_bit_1_32_bit_1": {
          ( "64_bit", "1" ): True,
          ( "64_bit", "2" ): False,
          ( "32_bit", "1" ): True,
          ( "32_bit", "2" ): False,
      },
      # These combinations are not possible, because different architecture
      # packages must be the same version.
      # "64_bit_1_32_bit_2": {
      #     ( "64_bit", "1" ): True,
      #     ( "64_bit", "2" ): False,
      #     ( "32_bit", "1" ): False,
      #     ( "32_bit", "2" ): True,
      # },
      # "64_bit_2_32_bit_1": {
      #     ( "64_bit", "1" ): False,
      #     ( "64_bit", "2" ): True,
      #     ( "32_bit", "1" ): True,
      #     ( "32_bit", "2" ): False,
      # },
      "64_bit_2_32_bit_2": {
          ( "64_bit", "1" ): False,
          ( "64_bit", "2" ): True,
          ( "32_bit", "1" ): False,
          ( "32_bit", "2" ): True,
      },
  }
  
  
  def header(test_count):
      print('''# THIS IS AN AUTOGENERATED TEST!
  # DO NOT EDIT IT DIRECTLY!
  #
  # Instead, edit the_great_package_test_generator.py and use that to regenerate
  # the test.
  #
  # Number of test cases: ''' + str(test_count) + '''
  #
  # If you want to run a specific test case, define a class with the name of that
  # test, for example "from_absent_to_absent___promise_policy_absent_arch_64_bit".
  
  body common control
  {
      inputs => { "../../../dcs.cf.sub",
                  "../../../../../controls/def.cf",
                  "../../../../../$(sys.local_libdir)/packages.cf",
                  "../../../../../$(sys.local_libdir)/commands.cf",
                  "../../../../../cfe_internal/update/lib.cf",
                  "../../../../../cfe_internal/update/update_policy.cf",
                  "../../packages-info.cf.sub",
                };
      bundlesequence => { default($(this.promise_filename)) };
    debian::
      package_module => "apt_get";
    redhat::
      package_module => "yum";
  }
  
  bundle agent init
  {
    meta:
        # No package modules written for platforms besides RedHat and Debian.
        "test_skip_needs_work" string => "!debian.!redhat";
  
        # The package module does not support RedHat 4 or Debian 4 (Etch).
        "test_skip_unsupported" string => "centos_4|redhat_4|debian_4|debian_etch";
  
        # RHEL 8 has broken DNF (upgrading a 32bit package also installs a 64bit
        # package)
        "test_soft_fail" string => "rhel_8",
          meta  => {"CFE-rhbz"};
  
    vars:
        "python_path" string => execresult("/bin/sed '1s/#!//;q' $(sys.workdir)/modules/packages/yum", "noshell");
  
    methods:
      debian|redhat::
        "setup_python_symlink" usebundle => cfe_internal_setup_python_symlink("$(python_path)");
  
  }
  
  bundle agent log_test_case(msg)
  {
    reports:
        "-------------------------------------"
          comment => "$(msg)_1";
        "$(msg)"
          comment => $(msg);
        "-------------------------------------"
          comment => "$(msg)_2";
  }
  ''')
  
  def get_possible_promises():
      # All the possible promises, expressed in terms of policy, architecture,
      # three versions (1, 2 or latest), and file to install.
      # P     = policy => "present"
      # D     = policy => "absent"
      # F     = File install
      # R     = Repo install
      # F64_1 = File to install is 64 bit and version 1
      # A64   = architecture = "64_bit"
      # V1    = version => "1"
      #
      # This is used to generate the promise structure. It does not include
      # impossible promises, such as installing a 64-bit file and mentioning
      # architecture => "32_bit".
      text_promises = [ "PF64_1     ",
                        "PF64_2     ",
                        "PF32_1     ",
                        "PF32_2     ",
                        "PF64_1A64  ",
                        "PF64_2A64  ",
                        "PF32_1A32  ",
                        "PF32_2A32  ",
                        "PF64_1   V1",
                        "PF64_2   V2",
                        "PF32_1   V1",
                        "PF32_2   V2",
                        "PF64_1A64V1",
                        "PF64_2A64V2",
                        "PF32_1A32V1",
                        "PF32_2A32V2",
                        "PR         ",
                        "PR    A64  ",
                        "PR    A32  ",
                        "PR       V1",
                        "PR       V2",
                        "PR       VL",
                        "PR    A64V1",
                        "PR    A32V1",
                        "PR    A64V2",
                        "PR    A32V2",
                        "PR    A64VL",
                        "PR    A32VL",
                        "D          ",
                        "D     A64  ",
                        "D     A32  ",
                        "D        V1",
                        "D        V2",
                        "D     A64V1",
                        "D     A32V1",
                        "D     A64V2",
                        "D     A32V2",
                      ]
  
      promises = []
      for text in text_promises:
          promise = {}
          if text[0] != "P":
              promise["policy"] = "absent"
          else:
              promise["policy"] = "present"
  
              if text[1] != "F":
                  promise["type"] = "repo"
              else:
                  promise["type"] = "file"
  
                  if text[2:4] == "64":
                      promise["file_arch"] = "64_bit"
                  else:
                      promise["file_arch"] = "32_bit"
  
                  if text[5] == "1":
                      promise["file_version"] = "1"
                  else:
                      promise["file_version"] = "2"
  
          if text[6:9] == "A64":
              promise["arch"] = "64_bit"
          elif text[6:9] == "A32":
              promise["arch"] = "32_bit"
  
          if text[9:11] == "VL":
              promise["version"] = "latest"
          elif text[9] == "V":
              promise["version"] = text[10]
  
          promises.append(promise)
  
      return promises
  
  
  # Given the transitions between the two version, determine if the change of one
  # architecture package will trigger a change in the other. Only one of the two
  # inputs can be different, but the returned value may cause both to be
  # different. This is only relevant for "repo" style promises, since "file"
  # promises will never touch more than one package at a time.
  def resolve_arch_conflicts(cur_class, from_64, from_32, to_64, to_32):
      if cur_class.startswith("old_debians"):
          if (from_64 != "0" and from_32 != "0") or (to_64 != "0" and to_32 != "0"):
              raise PromiseFailureException("Not possible on old Debians (no multiarch)")
  
      if from_64 != to_64:
          if from_32 != "0":
              if cur_class == "debian":
                  if to_64 >= from_32:
                      to_32 = to_64
                  else:
                      to_32 = "0"
              elif cur_class.startswith("redhat"):
                  if to_64 > from_32 and cur_class == "redhat_5":
                      to_32 = "0"
                  elif to_64 < from_32:
                      raise PromiseFailureException("Not possible on rpm")
  
      elif from_32 != to_32:
          if from_64 != "0":
              if cur_class == "debian":
                  if to_32 >= from_64:
                      to_64 = to_32
                  else:
                      to_64 = "0"
              elif cur_class.startswith("redhat"):
                  if to_32 > from_64 and cur_class == "redhat_5":
                      to_64 = "0"
                  elif to_32 < from_64:
                      raise PromiseFailureException("Not possible on rpm")
  
      return to_64, to_32
  
  
  def check_allowed_archs(cur_class, pkg_64, pkg_32):
      if cur_class == "old_debians_32_bit" and pkg_64 != "0":
          raise NotSupportedException("Only native architecture supported")
      if cur_class == "old_debians_64_bit" and pkg_32 != "0":
          raise NotSupportedException("Only native architecture supported")
  
  
  # Simulate, with the given promise, what the state of the system would be,
  # if we started from the versions in from_64 and from_32.
  def simulate_promise(promise, cur_class, from_64, from_32):
      to_64 = from_64
      to_32 = from_32
  
      check_allowed_archs(cur_class, from_64, from_32)
  
      arch, version = promise.get('arch'), promise.get('version')
      if promise["policy"] == "present":
          if promise["type"] == "repo":
              if version == "latest":
                  version = "2"
  
              if arch == "64_bit":
                  if version:
                      to_64 = version
                  elif from_64 == "0":
                      to_64 = "2"
  
                  to_64, to_32 = resolve_arch_conflicts(cur_class, from_64, from_32, to_64, to_32)
  
              elif arch == "32_bit":
                  if version:
                      to_32 = version
                  elif from_32 == "0":
                      to_32 = "2"
  
                  to_64, to_32 = resolve_arch_conflicts(cur_class, from_64, from_32, to_64, to_32)
  
              elif version:
  
                  if from_64 == "0" and from_32 == "0":
                      if cur_class == "redhat_5":
                          to_32 = version
                      to_64 = version
  
                  if from_64 != "0":
                      to_64 = version
  
                  if from_32 != "0":
                      to_32 = version
  
              elif from_64 == "0" and from_32 == "0":
                  if cur_class == "redhat_5":
                      to_32 = "2"
                  to_64 = "2"
  
          else:
              # Todo: Errors when file doesn't match arch/version?
              if promise["file_arch"] == "64_bit":
                  to_64 = promise["file_version"]
              else:
                  to_32 = promise["file_version"]
  
      else:
          if arch == "64_bit":
              if not version or version == from_64:
                  to_64 = "0"
          elif arch == "32_bit":
              if not version or version == from_32:
                  to_32 = "0"
          else:
              if version:
                  if version == from_64:
                      to_64 = "0"
                  if version == from_32:
                      to_32 = "0"
              else:
                  to_64 = "0"
                  to_32 = "0"
  
      check_allowed_archs(cur_class, to_64, to_32)
  
      return to_64, to_32
  
  
  # Calculate all possible transitions from from_state, to to_state.
  def calc_transitions(from_state, to_state):
      from_64 = "0"
      if states[from_state][("64_bit", "1")]:
          from_64 = "1"
      elif states[from_state][("64_bit", "2")]:
          from_64 = "2"
  
      from_32 = "0"
      if states[from_state][("32_bit", "1")]:
          from_32 = "1"
      elif states[from_state][("32_bit", "2")]:
          from_32 = "2"
  
      to_64 = "0"
      if states[to_state][("64_bit", "1")]:
          to_64 = "1"
      elif states[to_state][("64_bit", "2")]:
          to_64 = "2"
  
      to_32 = "0"
      if states[to_state][("32_bit", "1")]:
          to_32 = "1"
      elif states[to_state][("32_bit", "2")]:
          to_32 = "2"
  
      promise_candidates = get_possible_promises()
      valid_promises = []
  
      for promise in promise_candidates:
          classes = []
          failing_classes = []
          for cur_class in CLASSES:
              try:
                  result = simulate_promise(promise, cur_class, from_64, from_32)
                  if result[0] == to_64 and result[1] == to_32:
                      classes.append(CLASSES[cur_class])
              except PromiseFailureException:
                  failing_classes.append(CLASSES[cur_class])
              except NotSupportedException:
                  pass
          if classes:
              promise["classes"] = "|".join(classes)
              valid_promises.append(promise.copy())
              # Only consider failing cases in case at least one other platform
              # passes, otherwise we get way too many test cases. We don't need to
              # test every possible failing case.
              if failing_classes:
                  promise["failing"] = "1"
                  promise["classes"] = "|".join(failing_classes)
                  valid_promises.append(promise.copy())
  
      return valid_promises
  
  
  # Calculate all possible transitions from all states to all other states,
  # including the same state.
  def calc_all_transitions():
      transitions = {}
      for from_state in states:
          for to_state in states:
              transitions[(from_state, to_state)] = calc_transitions(from_state, to_state)
  
      return transitions
  
  
  def formatted_version(version):
      if version == "latest":
          return "latest"
      else:
          return "$(p.version[" + version + "])"
  
  
  # Write one test case, using one promise to go from one state to another (or the
  # same) state.
  # This function is quite hard to read because of all the quoting going on, it
  # might be easier to look at the output it produces.
  def make_test(current_count, total_test_count, from_state, to_state, transition, test_handle, warn_only):
      if states[from_state][("64_bit", "1")] or states[from_state][("64_bit", "2")] or states[to_state][("64_bit", "1")] or states[to_state][("64_bit", "2")]:
          arch_prefix = "64_bit."
      else:
          arch_prefix = ""
  
      print('''bundle agent ''' + test_handle + '''
  {
    methods:
      ''' + arch_prefix + "(" + transition["classes"] + ''')::
        "''' + test_handle + '''_start_msg"
          usebundle => log_test_case("''' + str((current_count * 100) / total_test_count) + '''%: Starting test case \\"''' + test_handle + '''\\"");
        "''' + test_handle + '''_init"
          usebundle => ''' + test_handle + '''_init;
        "''' + test_handle + '''_test"
          usebundle => ''' + test_handle + '''_test;
        "''' + test_handle + '''_check"
          usebundle => ''' + test_handle + '''_check;
        "''' + test_handle + '''_finish_msg"
          usebundle => log_test_case("''' + str((current_count * 100) / total_test_count) + '''%: Finished test case \\"''' + test_handle + '''\\"");
  
    classes:
      trigger::
        "''' + test_handle + '''_ok"
          not => "''' + arch_prefix + "(" + transition["classes"] + ''')",
          scope => "namespace";
      any::
        "trigger" expression => "any";
  }
  
  bundle agent ''' + test_handle + '''_init
  {
    methods:
        "clear_packages" usebundle => clear_packages("''' + test_handle + '''");
        "clear_package_cache" usebundle => clear_package_cache("''' + test_handle + '''");''')
      for arch in ["64_bit", "32_bit"]:
          for version in ["1", "2"]:
              if states[from_state][(arch, version)]:
                  print("      \"install_package\" usebundle => install_package($(p.name[1]), "
                        + formatted_version(version) + ", $(p." + arch + "), \"" + test_handle + "\");")
      print("}\n")
  
      print('''bundle agent ''' + test_handle + '''_test
  {
    packages:''')
      if transition["policy"] == "absent" or transition["type"] == "repo":
          print("      \"$(p.name[1])\"")
      else:
          print("      \"$(p.package[1][" + transition["file_version"] + "][" + transition["file_arch"] + "])\"")
      print("        policy => \"" + transition["policy"] + "\"")
      if transition.get("arch"):
          print("      , architecture => \"$(p." + transition["arch"] + ")\"")
      if transition.get("version"):
          print("      , version => \"" + formatted_version(transition["version"]) + "\"")
      if warn_only:
          print("      , action => warn_only")
      print("      , classes => classes_generic(\"" + test_handle + "___class\")")
      print('''      ;
  }
  
  bundle agent ''' + test_handle + '''_check
  {
    classes:
        "''' + test_handle + '''___correct_classes"''')
      if from_state == to_state and not transition.get("failing"):
          print("        expression => \"" + test_handle + "___class_kept.!" + test_handle + "___class_repaired.!" + test_handle + "___class_failed\";")
      elif warn_only or transition.get("failing"):
          print("        expression => \"!" + test_handle + "___class_kept.!" + test_handle + "___class_repaired." + test_handle + "___class_failed\";")
      else:
          print("        expression => \"!" + test_handle + "___class_kept." + test_handle + "___class_repaired.!" + test_handle + "___class_failed\";")
  
      if warn_only or transition.get("failing"):
          file_state_to_check = from_state
      else:
          file_state_to_check = to_state
      for arch in ["64_bit", "32_bit"]:
          for version in ["1", "2"]:
              print("      \"" + arch + "_" + version + "\"")
              if states[file_state_to_check][(arch, version)]:
                  print("        expression => fileexists(\"$(p.file[1][" + version + "][" + arch + "])\");")
              else:
                  print("        not => fileexists(\"$(p.file[1][" + version + "][" + arch + "])\");")
      print('''      "''' + test_handle + '''_ok"
          scope => "namespace",
          and => {''')
      for arch in ["64_bit", "32_bit"]:
          for version in ["1", "2"]:
              print("          \"" + arch + "_" + version + "\",")
      print('''          "''' + test_handle + '''___correct_classes"
          };
  ''')
  
      print('''  reports:
      !''' + test_handle + '''_ok::
        "FAILED: ''' + test_handle + '''";
      ''' + test_handle + '''_ok::
        "PASS: ''' + test_handle + '''";
    commands:
      !''' + test_handle + '''_ok::
        "$(G.echo) 'Contents of \\"/$(p.name[1])*\\"' && $(G.ls) /$(p.name[1])*"
          contain => in_shell;
  
    reports:''')
  
      if (from_state != to_state and warn_only) or transition.get("failing"):
          print('''    !''' + test_handle + '''___class_failed::
        "Class was not set, but should be: ''' + test_handle + '''___class_failed";''')
      else:
          print('''    ''' + test_handle + '''___class_failed::
        "Class was set, but should not be: ''' + test_handle + '''___class_failed";''')
  
      if from_state != to_state or transition.get("failing"):
          print('''    ''' + test_handle + '''___class_kept::
        "Class was set, but should not be: ''' + test_handle + '''___class_kept";''')
      else:
          print('''    !''' + test_handle + '''___class_kept::
        "Class was not set, but should be: ''' + test_handle + '''___class_kept";''')
  
      if from_state != to_state and not warn_only and not transition.get("failing"):
          print('''    !''' + test_handle + '''___class_repaired::
        "Class was not set, but should be: ''' + test_handle + '''___class_repaired";''')
      else:
          print('''    ''' + test_handle + '''___class_repaired::
        "Class was set, but should not be: ''' + test_handle + '''___class_repaired";''')
  
      print('''}
  ''')
  
  
  # Print the main test bundles.
  def main_bundles(test_handles):
      print('''bundle agent test
  {
    classes:
        "specific_test_case_specified" or => {''')
      for handle in test_handles:
          print("          \"" + handle + "\",")
      print('''      };
        "run_all_tests"
          not => "specific_test_case_specified",
          scope => "namespace";
  
    methods:''')
      for handle in test_handles:
          print("    run_all_tests|" + handle + "::")
          print("      \"" + handle + "\"\n        usebundle => " + handle + ";")
      print("}")
  
      print('''bundle agent check
  {
    classes:
        "ok" and => {''')
      # Enable test to pass even when running only a sub test.
      for handle in test_handles:
          print("          \"" + handle + "_ok|(!run_all_tests.!" + handle + ")\",")
      print('''        };
  
    reports:
      !ok::
        "$(this.promise_filename) FAIL";
      ok::
        "$(this.promise_filename) Pass";
  }''')
  
  
  
  transitions = calc_all_transitions()
  test_count = 0
  test_handles = []
  current_count = 0
  # One pass to count tests, one pass to actually output them.
  for op in ["count", "do"]:
      if op == "do":
          header(test_count)
      for i in transitions:
          for j in transitions[i]:
              if op == "count":
                  test_count += 1
              else:
                  current_count += 1
                  test_handle = "from_" + i[0] + "_to_" + i[1] + "___promise_" + "_".join([k + "_" + j[k] for k in j if k != "classes"])
                  test_handles.append(test_handle)
                  make_test(current_count, test_count, i[0], i[1], j, test_handle, False)
  
              # Cut down on testing time by not testing "warn_only" together with
              # equal states. "warn_only" has no effect there, since there is no
              # change to begin with.
              if i[0] == i[1]:
                  continue
  
              if op == "count":
                  test_count += 1
              else:
                  current_count += 1
                  test_handle += "_warn_only"
                  test_handles.append(test_handle)
                  make_test(current_count, test_count, i[0], i[1], j, test_handle, True)
  
  main_bundles(test_handles)
  