1#!/usr/bin/python 2 3from __future__ import print_function 4 5import pprint 6import sys 7 8TEST_COUNT = 0 9PLATFORM_ARCH_32 = False 10 11CLASSES = { "new_debians": "(debian.!ubuntu_10.!debian_6)", 12 "old_debians_32_bit": "(32_bit.(ubuntu_10|debian_6))", # No multiarch support. 13 "old_debians_64_bit": "(64_bit.(ubuntu_10|debian_6))", # No multiarch support. 14 "redhat_5": "(redhat_5|centos_5)", 15 "redhat_6_or_newer": "(redhat.!redhat_5.!centos_5)", 16 } 17 18class PromiseFailureException(Exception): 19 pass 20class NotSupportedException(Exception): 21 pass 22 23states = { 24 "absent": { 25 ( "64_bit", "1" ): False, 26 ( "64_bit", "2" ): False, 27 ( "32_bit", "1" ): False, 28 ( "32_bit", "2" ): False, 29 }, 30 "64_bit_1": { 31 ( "64_bit", "1" ): True, 32 ( "64_bit", "2" ): False, 33 ( "32_bit", "1" ): False, 34 ( "32_bit", "2" ): False, 35 }, 36 "64_bit_2": { 37 ( "64_bit", "1" ): False, 38 ( "64_bit", "2" ): True, 39 ( "32_bit", "1" ): False, 40 ( "32_bit", "2" ): False, 41 }, 42 "32_bit_1": { 43 ( "64_bit", "1" ): False, 44 ( "64_bit", "2" ): False, 45 ( "32_bit", "1" ): True, 46 ( "32_bit", "2" ): False, 47 }, 48 "32_bit_2": { 49 ( "64_bit", "1" ): False, 50 ( "64_bit", "2" ): False, 51 ( "32_bit", "1" ): False, 52 ( "32_bit", "2" ): True, 53 }, 54 "64_bit_1_32_bit_1": { 55 ( "64_bit", "1" ): True, 56 ( "64_bit", "2" ): False, 57 ( "32_bit", "1" ): True, 58 ( "32_bit", "2" ): False, 59 }, 60 # These combinations are not possible, because different architecture 61 # packages must be the same version. 62 # "64_bit_1_32_bit_2": { 63 # ( "64_bit", "1" ): True, 64 # ( "64_bit", "2" ): False, 65 # ( "32_bit", "1" ): False, 66 # ( "32_bit", "2" ): True, 67 # }, 68 # "64_bit_2_32_bit_1": { 69 # ( "64_bit", "1" ): False, 70 # ( "64_bit", "2" ): True, 71 # ( "32_bit", "1" ): True, 72 # ( "32_bit", "2" ): False, 73 # }, 74 "64_bit_2_32_bit_2": { 75 ( "64_bit", "1" ): False, 76 ( "64_bit", "2" ): True, 77 ( "32_bit", "1" ): False, 78 ( "32_bit", "2" ): True, 79 }, 80} 81 82 83def header(test_count): 84 print('''# THIS IS AN AUTOGENERATED TEST! 85# DO NOT EDIT IT DIRECTLY! 86# 87# Instead, edit the_great_package_test_generator.py and use that to regenerate 88# the test. 89# 90# Number of test cases: ''' + str(test_count) + ''' 91# 92# If you want to run a specific test case, define a class with the name of that 93# test, for example "from_absent_to_absent___promise_policy_absent_arch_64_bit". 94 95body common control 96{ 97 inputs => { "../../../dcs.cf.sub", 98 "../../../../../controls/def.cf", 99 "../../../../../$(sys.local_libdir)/packages.cf", 100 "../../../../../$(sys.local_libdir)/commands.cf", 101 "../../../../../cfe_internal/update/lib.cf", 102 "../../../../../cfe_internal/update/update_policy.cf", 103 "../../packages-info.cf.sub", 104 }; 105 bundlesequence => { default($(this.promise_filename)) }; 106 debian:: 107 package_module => "apt_get"; 108 redhat:: 109 package_module => "yum"; 110} 111 112bundle agent init 113{ 114 meta: 115 # No package modules written for platforms besides RedHat and Debian. 116 "test_skip_needs_work" string => "!debian.!redhat"; 117 118 # The package module does not support RedHat 4 or Debian 4 (Etch). 119 "test_skip_unsupported" string => "centos_4|redhat_4|debian_4|debian_etch"; 120 121 # RHEL 8 has broken DNF (upgrading a 32bit package also installs a 64bit 122 # package) 123 "test_soft_fail" string => "rhel_8", 124 meta => {"CFE-rhbz"}; 125 126 # For setting up the cfengine-selected-python symlink we want to 127 # target $(sys.bindir) as that will be in the test WORKDIR. 128 vars: 129 "python_path" string => "$(sys.bindir)/cfengine-selected-python"; 130 131 132 methods: 133 debian|redhat:: 134 "setup_python_symlink" usebundle => cfe_internal_setup_python_symlink("$(python_path)"); 135 136 files: 137 "${sys.workdir}/modules/packages/." 138 create => "true"; 139 140 debian:: 141 "${sys.workdir}/modules/packages/apt_get" 142 copy_from => local_cp("${sys.workdir}/modules/packages/vendored/apt_get.mustache"); 143 redhat:: 144 "${sys.workdir}/modules/packages/yum" 145 copy_from => local_cp("${sys.workdir}/modules/packages/vendored/yum.mustache"); 146} 147 148bundle agent log_test_case(msg) 149{ 150 reports: 151 "-------------------------------------" 152 comment => "$(msg)_1"; 153 "$(msg)" 154 comment => $(msg); 155 "-------------------------------------" 156 comment => "$(msg)_2"; 157} 158''') 159 160def get_possible_promises(): 161 # All the possible promises, expressed in terms of policy, architecture, 162 # three versions (1, 2 or latest), and file to install. 163 # P = policy => "present" 164 # D = policy => "absent" 165 # F = File install 166 # R = Repo install 167 # F64_1 = File to install is 64 bit and version 1 168 # A64 = architecture = "64_bit" 169 # V1 = version => "1" 170 # 171 # This is used to generate the promise structure. It does not include 172 # impossible promises, such as installing a 64-bit file and mentioning 173 # architecture => "32_bit". 174 text_promises = [ "PF64_1 ", 175 "PF64_2 ", 176 "PF32_1 ", 177 "PF32_2 ", 178 "PF64_1A64 ", 179 "PF64_2A64 ", 180 "PF32_1A32 ", 181 "PF32_2A32 ", 182 "PF64_1 V1", 183 "PF64_2 V2", 184 "PF32_1 V1", 185 "PF32_2 V2", 186 "PF64_1A64V1", 187 "PF64_2A64V2", 188 "PF32_1A32V1", 189 "PF32_2A32V2", 190 "PR ", 191 "PR A64 ", 192 "PR A32 ", 193 "PR V1", 194 "PR V2", 195 "PR VL", 196 "PR A64V1", 197 "PR A32V1", 198 "PR A64V2", 199 "PR A32V2", 200 "PR A64VL", 201 "PR A32VL", 202 "D ", 203 "D A64 ", 204 "D A32 ", 205 "D V1", 206 "D V2", 207 "D A64V1", 208 "D A32V1", 209 "D A64V2", 210 "D A32V2", 211 ] 212 213 promises = [] 214 for text in text_promises: 215 promise = {} 216 if text[0] != "P": 217 promise["policy"] = "absent" 218 else: 219 promise["policy"] = "present" 220 221 if text[1] != "F": 222 promise["type"] = "repo" 223 else: 224 promise["type"] = "file" 225 226 if text[2:4] == "64": 227 promise["file_arch"] = "64_bit" 228 else: 229 promise["file_arch"] = "32_bit" 230 231 if text[5] == "1": 232 promise["file_version"] = "1" 233 else: 234 promise["file_version"] = "2" 235 236 if text[6:9] == "A64": 237 promise["arch"] = "64_bit" 238 elif text[6:9] == "A32": 239 promise["arch"] = "32_bit" 240 241 if text[9:11] == "VL": 242 promise["version"] = "latest" 243 elif text[9] == "V": 244 promise["version"] = text[10] 245 246 promises.append(promise) 247 248 return promises 249 250 251# Given the transitions between the two version, determine if the change of one 252# architecture package will trigger a change in the other. Only one of the two 253# inputs can be different, but the returned value may cause both to be 254# different. This is only relevant for "repo" style promises, since "file" 255# promises will never touch more than one package at a time. 256def resolve_arch_conflicts(cur_class, from_64, from_32, to_64, to_32): 257 if cur_class.startswith("old_debians"): 258 if (from_64 != "0" and from_32 != "0") or (to_64 != "0" and to_32 != "0"): 259 raise PromiseFailureException("Not possible on old Debians (no multiarch)") 260 261 if from_64 != to_64: 262 if from_32 != "0": 263 if cur_class == "debian": 264 if to_64 >= from_32: 265 to_32 = to_64 266 else: 267 to_32 = "0" 268 elif cur_class.startswith("redhat"): 269 if to_64 > from_32 and cur_class == "redhat_5": 270 to_32 = "0" 271 elif to_64 < from_32: 272 raise PromiseFailureException("Not possible on rpm") 273 274 elif from_32 != to_32: 275 if from_64 != "0": 276 if cur_class == "debian": 277 if to_32 >= from_64: 278 to_64 = to_32 279 else: 280 to_64 = "0" 281 elif cur_class.startswith("redhat"): 282 if to_32 > from_64 and cur_class == "redhat_5": 283 to_64 = "0" 284 elif to_32 < from_64: 285 raise PromiseFailureException("Not possible on rpm") 286 287 return to_64, to_32 288 289 290def check_allowed_archs(cur_class, pkg_64, pkg_32): 291 if cur_class == "old_debians_32_bit" and pkg_64 != "0": 292 raise NotSupportedException("Only native architecture supported") 293 if cur_class == "old_debians_64_bit" and pkg_32 != "0": 294 raise NotSupportedException("Only native architecture supported") 295 296 297# Simulate, with the given promise, what the state of the system would be, 298# if we started from the versions in from_64 and from_32. 299def simulate_promise(promise, cur_class, from_64, from_32): 300 to_64 = from_64 301 to_32 = from_32 302 303 check_allowed_archs(cur_class, from_64, from_32) 304 305 arch, version = promise.get('arch'), promise.get('version') 306 if promise["policy"] == "present": 307 if promise["type"] == "repo": 308 if version == "latest": 309 version = "2" 310 311 if arch == "64_bit": 312 if version: 313 to_64 = version 314 elif from_64 == "0": 315 to_64 = "2" 316 317 to_64, to_32 = resolve_arch_conflicts(cur_class, from_64, from_32, to_64, to_32) 318 319 elif arch == "32_bit": 320 if version: 321 to_32 = version 322 elif from_32 == "0": 323 to_32 = "2" 324 325 to_64, to_32 = resolve_arch_conflicts(cur_class, from_64, from_32, to_64, to_32) 326 327 elif version: 328 329 if from_64 == "0" and from_32 == "0": 330 if cur_class == "redhat_5": 331 to_32 = version 332 to_64 = version 333 334 if from_64 != "0": 335 to_64 = version 336 337 if from_32 != "0": 338 to_32 = version 339 340 elif from_64 == "0" and from_32 == "0": 341 if cur_class == "redhat_5": 342 to_32 = "2" 343 to_64 = "2" 344 345 else: 346 # Todo: Errors when file doesn't match arch/version? 347 if promise["file_arch"] == "64_bit": 348 to_64 = promise["file_version"] 349 else: 350 to_32 = promise["file_version"] 351 352 else: 353 if arch == "64_bit": 354 if not version or version == from_64: 355 to_64 = "0" 356 elif arch == "32_bit": 357 if not version or version == from_32: 358 to_32 = "0" 359 else: 360 if version: 361 if version == from_64: 362 to_64 = "0" 363 if version == from_32: 364 to_32 = "0" 365 else: 366 to_64 = "0" 367 to_32 = "0" 368 369 check_allowed_archs(cur_class, to_64, to_32) 370 371 return to_64, to_32 372 373 374# Calculate all possible transitions from from_state, to to_state. 375def calc_transitions(from_state, to_state): 376 from_64 = "0" 377 if states[from_state][("64_bit", "1")]: 378 from_64 = "1" 379 elif states[from_state][("64_bit", "2")]: 380 from_64 = "2" 381 382 from_32 = "0" 383 if states[from_state][("32_bit", "1")]: 384 from_32 = "1" 385 elif states[from_state][("32_bit", "2")]: 386 from_32 = "2" 387 388 to_64 = "0" 389 if states[to_state][("64_bit", "1")]: 390 to_64 = "1" 391 elif states[to_state][("64_bit", "2")]: 392 to_64 = "2" 393 394 to_32 = "0" 395 if states[to_state][("32_bit", "1")]: 396 to_32 = "1" 397 elif states[to_state][("32_bit", "2")]: 398 to_32 = "2" 399 400 promise_candidates = get_possible_promises() 401 valid_promises = [] 402 403 for promise in promise_candidates: 404 classes = [] 405 failing_classes = [] 406 for cur_class in CLASSES: 407 try: 408 result = simulate_promise(promise, cur_class, from_64, from_32) 409 if result[0] == to_64 and result[1] == to_32: 410 classes.append(CLASSES[cur_class]) 411 except PromiseFailureException: 412 failing_classes.append(CLASSES[cur_class]) 413 except NotSupportedException: 414 pass 415 if classes: 416 promise["classes"] = "|".join(classes) 417 valid_promises.append(promise.copy()) 418 # Only consider failing cases in case at least one other platform 419 # passes, otherwise we get way too many test cases. We don't need to 420 # test every possible failing case. 421 if failing_classes: 422 promise["failing"] = "1" 423 promise["classes"] = "|".join(failing_classes) 424 valid_promises.append(promise.copy()) 425 426 return valid_promises 427 428 429# Calculate all possible transitions from all states to all other states, 430# including the same state. 431def calc_all_transitions(): 432 transitions = {} 433 for from_state in states: 434 for to_state in states: 435 transitions[(from_state, to_state)] = calc_transitions(from_state, to_state) 436 437 return transitions 438 439 440def formatted_version(version): 441 if version == "latest": 442 return "latest" 443 else: 444 return "$(p.version[" + version + "])" 445 446 447# Write one test case, using one promise to go from one state to another (or the 448# same) state. 449# This function is quite hard to read because of all the quoting going on, it 450# might be easier to look at the output it produces. 451def make_test(current_count, total_test_count, from_state, to_state, transition, test_handle, warn_only): 452 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")]: 453 arch_prefix = "64_bit." 454 else: 455 arch_prefix = "" 456 457 print('''bundle agent ''' + test_handle + ''' 458{ 459 methods: 460 ''' + arch_prefix + "(" + transition["classes"] + '''):: 461 "''' + test_handle + '''_start_msg" 462 usebundle => log_test_case("''' + str((current_count * 100) / total_test_count) + '''%: Starting test case \\"''' + test_handle + '''\\""); 463 "''' + test_handle + '''_init" 464 usebundle => ''' + test_handle + '''_init; 465 "''' + test_handle + '''_test" 466 usebundle => ''' + test_handle + '''_test; 467 "''' + test_handle + '''_check" 468 usebundle => ''' + test_handle + '''_check; 469 "''' + test_handle + '''_finish_msg" 470 usebundle => log_test_case("''' + str((current_count * 100) / total_test_count) + '''%: Finished test case \\"''' + test_handle + '''\\""); 471 472 classes: 473 trigger:: 474 "''' + test_handle + '''_ok" 475 not => "''' + arch_prefix + "(" + transition["classes"] + ''')", 476 scope => "namespace"; 477 any:: 478 "trigger" expression => "any"; 479} 480 481bundle agent ''' + test_handle + '''_init 482{ 483 methods: 484 "clear_packages" usebundle => clear_packages("''' + test_handle + '''"); 485 "clear_package_cache" usebundle => clear_package_cache("''' + test_handle + '''");''') 486 for arch in ["64_bit", "32_bit"]: 487 for version in ["1", "2"]: 488 if states[from_state][(arch, version)]: 489 print(" \"install_package\" usebundle => install_package($(p.name[1]), " 490 + formatted_version(version) + ", $(p." + arch + "), \"" + test_handle + "\");") 491 print("}\n") 492 493 print('''bundle agent ''' + test_handle + '''_test 494{ 495 packages:''') 496 if transition["policy"] == "absent" or transition["type"] == "repo": 497 print(" \"$(p.name[1])\"") 498 else: 499 print(" \"$(p.package[1][" + transition["file_version"] + "][" + transition["file_arch"] + "])\"") 500 print(" policy => \"" + transition["policy"] + "\"") 501 if transition.get("arch"): 502 print(" , architecture => \"$(p." + transition["arch"] + ")\"") 503 if transition.get("version"): 504 print(" , version => \"" + formatted_version(transition["version"]) + "\"") 505 if warn_only: 506 print(" , action => warn_only") 507 print(" , classes => classes_generic(\"" + test_handle + "___class\")") 508 print(''' ; 509} 510 511bundle agent ''' + test_handle + '''_check 512{ 513 classes: 514 "''' + test_handle + '''___correct_classes"''') 515 if from_state == to_state and not transition.get("failing"): 516 print(" expression => \"" + test_handle + "___class_kept.!" + test_handle + "___class_repaired.!" + test_handle + "___class_failed\";") 517 elif warn_only or transition.get("failing"): 518 print(" expression => \"!" + test_handle + "___class_kept.!" + test_handle + "___class_repaired." + test_handle + "___class_failed\";") 519 else: 520 print(" expression => \"!" + test_handle + "___class_kept." + test_handle + "___class_repaired.!" + test_handle + "___class_failed\";") 521 522 if warn_only or transition.get("failing"): 523 file_state_to_check = from_state 524 else: 525 file_state_to_check = to_state 526 for arch in ["64_bit", "32_bit"]: 527 for version in ["1", "2"]: 528 print(" \"" + arch + "_" + version + "\"") 529 if states[file_state_to_check][(arch, version)]: 530 print(" expression => fileexists(\"$(p.file[1][" + version + "][" + arch + "])\");") 531 else: 532 print(" not => fileexists(\"$(p.file[1][" + version + "][" + arch + "])\");") 533 print(''' "''' + test_handle + '''_ok" 534 scope => "namespace", 535 and => {''') 536 for arch in ["64_bit", "32_bit"]: 537 for version in ["1", "2"]: 538 print(" \"" + arch + "_" + version + "\",") 539 print(''' "''' + test_handle + '''___correct_classes" 540 }; 541''') 542 543 print(''' reports: 544 !''' + test_handle + '''_ok:: 545 "FAILED: ''' + test_handle + '''"; 546 ''' + test_handle + '''_ok:: 547 "PASS: ''' + test_handle + '''"; 548 commands: 549 !''' + test_handle + '''_ok:: 550 "$(G.echo) 'Contents of \\"/$(p.name[1])*\\"' && $(G.ls) /$(p.name[1])*" 551 contain => in_shell; 552 553 reports:''') 554 555 if (from_state != to_state and warn_only) or transition.get("failing"): 556 print(''' !''' + test_handle + '''___class_failed:: 557 "Class was not set, but should be: ''' + test_handle + '''___class_failed";''') 558 else: 559 print(''' ''' + test_handle + '''___class_failed:: 560 "Class was set, but should not be: ''' + test_handle + '''___class_failed";''') 561 562 if from_state != to_state or transition.get("failing"): 563 print(''' ''' + test_handle + '''___class_kept:: 564 "Class was set, but should not be: ''' + test_handle + '''___class_kept";''') 565 else: 566 print(''' !''' + test_handle + '''___class_kept:: 567 "Class was not set, but should be: ''' + test_handle + '''___class_kept";''') 568 569 if from_state != to_state and not warn_only and not transition.get("failing"): 570 print(''' !''' + test_handle + '''___class_repaired:: 571 "Class was not set, but should be: ''' + test_handle + '''___class_repaired";''') 572 else: 573 print(''' ''' + test_handle + '''___class_repaired:: 574 "Class was set, but should not be: ''' + test_handle + '''___class_repaired";''') 575 576 print('''} 577''') 578 579 580# Print the main test bundles. 581def main_bundles(test_handles): 582 print('''bundle agent test 583{ 584 classes: 585 "specific_test_case_specified" or => {''') 586 for handle in test_handles: 587 print(" \"" + handle + "\",") 588 print(''' }; 589 "run_all_tests" 590 not => "specific_test_case_specified", 591 scope => "namespace"; 592 593 methods:''') 594 for handle in test_handles: 595 print(" run_all_tests|" + handle + "::") 596 print(" \"" + handle + "\"\n usebundle => " + handle + ";") 597 print("}") 598 599 print('''bundle agent check 600{ 601 classes: 602 "ok" and => {''') 603 # Enable test to pass even when running only a sub test. 604 for handle in test_handles: 605 print(" \"" + handle + "_ok|(!run_all_tests.!" + handle + ")\",") 606 print(''' }; 607 608 reports: 609 !ok:: 610 "$(this.promise_filename) FAIL"; 611 ok:: 612 "$(this.promise_filename) Pass"; 613}''') 614 615 616 617transitions = calc_all_transitions() 618test_count = 0 619test_handles = [] 620current_count = 0 621# One pass to count tests, one pass to actually output them. 622for op in ["count", "do"]: 623 if op == "do": 624 header(test_count) 625 for i in transitions: 626 for j in transitions[i]: 627 if op == "count": 628 test_count += 1 629 else: 630 current_count += 1 631 test_handle = "from_" + i[0] + "_to_" + i[1] + "___promise_" + "_".join([k + "_" + j[k] for k in j if k != "classes"]) 632 test_handles.append(test_handle) 633 make_test(current_count, test_count, i[0], i[1], j, test_handle, False) 634 635 # Cut down on testing time by not testing "warn_only" together with 636 # equal states. "warn_only" has no effect there, since there is no 637 # change to begin with. 638 if i[0] == i[1]: 639 continue 640 641 if op == "count": 642 test_count += 1 643 else: 644 current_count += 1 645 test_handle += "_warn_only" 646 test_handles.append(test_handle) 647 make_test(current_count, test_count, i[0], i[1], j, test_handle, True) 648 649main_bundles(test_handles) 650