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