diff --git a/scheduler/checkwiki b/scheduler/checkwiki
--- a/scheduler/checkwiki
+++ b/scheduler/checkwiki
@@ -24,7 +24,9 @@
 from __future__ import unicode_literals
 from __future__ import print_function
 
+import glob
 import json
+import os
 import subprocess32 as subprocess
 import sys
 
@@ -40,6 +42,47 @@
 # cache of ValidationPage objects
 PAGES = {}
 
+## UTILITY CLASSES AND FUNCTIONS
+
+class FakeClient(object):
+    """A fake OpenQA_Client which just returns our fake job dicts. We
+    need this to handle the 'testsuites' condition in TESTSUITES
+    entries, which allows a test case to be considered passed only if
+    the parent test suite *and* a list of other test suites passed;
+    get_passed_testcases handles that by querying openQA for other
+    jobs for the same build and checking their result, but obviously
+    for our fake jobs, we need a fake openQA 'server'..."""
+    def __init__(self, jobs):
+        self.jobs = jobs
+
+    def openqa_request(self, method, endpoint, params=None):
+        """This is nowhere near a full implementation, it's highly
+        tuned to what get_passed_testcases needs.
+        """
+        gotjobs = self.jobs
+        if 'machine' in params:
+            gotjobs = [job for job in gotjobs if job['settings']['MACHINE'] == params['machine']]
+        if 'flavor' in params:
+            gotjobs = [job for job in gotjobs if job['settings']['FLAVOR'] == params['flavor']]
+        return {'jobs': gotjobs}
+
+
+def read_templates(distripath):
+    """Read in the openQA templates from distripath."""
+    # Locate template file, check it exists
+    tempfile = os.path.join(distripath, 'templates')
+    if not os.path.isfile(tempfile):
+        sys.exit("Could not find templates file at {0}".format(tempfile))
+
+    # Use a perl one-liner to convert the templates to JSON. hold onto
+    # your hats, folks, this be ugly. also note this executes the
+    # template file as perl, so make sure you trust it
+    perl = "use JSON; my $data = do '{0}'; print encode_json($data);".format(tempfile)
+    cmd = ['perl', '-e', perl]
+    tempjson = subprocess.check_output(cmd, universal_newlines=True)
+    # Read the JSON and return it
+    return json.loads(tempjson)
+
 def get_page(testtype):
     """Get the page if we didn't yet, cache it, and return from cache."""
     if not testtype in PAGES:
@@ -52,110 +95,162 @@
     casenames = []
     try:
         # complex (dict) case
-        casenames.extend(tscases.pop('PASS', []))
-        for (_, cases) in tscases.items():
-            casenames.extend(cases)
-    except TypeError:
+        casenames.extend(tscases.keys())
+    except AttributeError:
         # simple list case
         casenames = tscases
     return casenames
 
-def main():
-    """Main function. Could be split into the individual tests if I
-    get a minute.
+def fake_jobs(templates, modules):
+    """Create a set of fake passed openQA jobs from JobTemplates and
+    the module names in TestSuites (as found by conds_modules_match).
     """
-    if not len(sys.argv) == 2:
-        sys.exit("Usage: {0} templatefile".format(sys.argv[0]))
-    tempfile = sys.argv[1]
-
-    # read in the openQA templates. hold onto your hats, folks, this be ugly
-    perl = "use JSON; my $data = do '{0}'; print encode_json($data);".format(tempfile)
-    cmd = ['perl', '-e', perl]
-    tempjson = subprocess.check_output(cmd, universal_newlines=True)
-    templates = json.loads(tempjson)
+    fakejobs = []
+    for jobtemp in templates['JobTemplates']:
+        job = {
+            'settings': {
+                'ARCH': jobtemp['product']['arch'],
+                'FLAVOR': jobtemp['product']['flavor'],
+                'TEST': jobtemp['test_suite']['name'],
+                'MACHINE': jobtemp['machine']['name'],
+                # we don't need the BUILD value; we're just going to use
+                # the current validation pages
+                'BUILD': 'FAKE',
+            },
+            'result': 'passed',
+            # we use both settings['TEST'] and test, we probably shouldn't...
+            'test': jobtemp['test_suite']['name'],
+            # we just pretend every single job had all the modules that
+            # are used for individual module->result mapping; this is
+            # harmless for the cases where it's a lie, as get_passed_testcases
+            # will entirely ignore it except for the TESTSUITES that specify
+            # the modules
+            'modules': [{'name': module, 'result': 'passed'} for module in modules]
+        }
+        # find the desktop by finding the relevant Product and getting it
+        # from its settings
+        for setting in [prod for prod in templates['Products'] if
+                        prod['flavor'] == jobtemp['product']['flavor']][0]['settings']:
+            if setting['key'] == 'DESKTOP':
+                job['settings']['DESKTOP'] = setting['value']
+        fakejobs.append(job)
+    return fakejobs
 
-    # get the test case names from TESTCASES and test suite names from TESTSUITES
-    tcnames = list(TESTCASES.keys())
-    tsnames = list(TESTSUITES.keys())
+## CHECK FUNCTIONS
 
+def cases_suites_match():
+    """Check all case names referred in TESTSUITES are actually in
+    TESTCASES and vice versa.
+    """
     failed = 0
-
-    # Check all case names referred in TESTSUITES are actually in TESTCASES
+    # Check all case names referred in TESTSUITES are in TESTCASES
     # also store all case names in TESTSUITES for reverse check
     alltscases = set()
-    for tsname in TESTSUITES:
+    for tsname in TESTSUITES.keys():
         casenames = cases_from_suite(tsname)
         alltscases.update(casenames)
         for casename in casenames:
-            if casename not in tcnames:
+            if casename not in TESTCASES.keys():
                 print("Test case {0} listed in test suite {1} is not in TESTCASES!".format(
                     casename, tsname))
                 failed = 1
 
     # check all case names in TESTCASES are used in TESTSUITES
-    for casename in tcnames:
+    for casename in TESTCASES.keys():
         if casename not in alltscases:
             print("Test case {0} from TESTCASES is never used in TESTSUITES!".format(casename))
+            failed = 1
+
+    return failed
 
-    # check all test suites from templates are in TESTSUITES, and vice versa
-    # except for ones we know are OK (e.g. support_server)
+def templates_suites_match(templates):
+    """Check all test suites from templates are in TESTSUITES, and
+    vice versa, except for ones we know are OK (e.g. support_server).
+    """
+    failed = 0
     tempwhite = set(('support_server',))
     tempsuites = [suite['name'] for suite in templates['TestSuites']]
-    onlytemp = set(tempsuites) - set(tsnames) - tempwhite
+    onlytemp = set(tempsuites) - set(TESTSUITES.keys()) - tempwhite
     for suite in onlytemp:
         failed = 1
         print("Test suite {0} is in templates, but not in TESTSUITES!".format(suite))
 
-    onlysuites = set(tsnames) - set(tempsuites)
+    onlysuites = set(TESTSUITES.keys()) - set(tempsuites)
     for suite in onlysuites:
         failed = 1
         print("Test suite {0} is in TESTSUITES, but not in templates!".format(suite))
 
-    # Now try and check that all our wiki mappings are actually correct. This
-    # is kinda involved. First we're going to create a fake job for each
-    # JobTemplate from templates, as if we had a pass for every single one.
-    # To handle the complex TESTSUITE case where we map individual passed test
-    # modules to passed test cases, we need to extract all the relevant module
-    # names from TESTSUITES...
-    modules = set()
-    for cases in TESTSUITES.values():
+    return failed
+
+def conds_testsuites_match():
+    """For TESTSUITES entries which use the 'testsuites' condition,
+    check all listed suite names are in TESTSUITES, and warn if there
+    is no equivalent reverse mapping (as usually we'd expect one).
+    """
+    failed = 0
+    for (tsname, cases) in TESTSUITES.items():
         try:
-            modules.update(cases.keys())
+            for (tcname, conds) in cases.items():
+                for othertsname in conds.get('testsuites', []):
+                    othersuite = TESTSUITES.get(othertsname)
+                    if not othersuite:
+                        print("Test suite {0} listed as a cond in case {1} in suite {2} "
+                              "not found in TESTSUITES!".format(othertsname, tcname, tsname))
+                        failed = 1
+                    othercase = othersuite.get(tcname, {})
+                    if tsname not in othercase.get('testsuites', []):
+                        print("Warning: suite {0} case {1} has suite {2} as cond, but suite {2} "
+                              "case {1} does not have suite {0} as cond".format(
+                                  tsname, tcname, othertsname))
         except AttributeError:
             pass
+    return failed
 
-    fakejobs = []
-    for jobtemp in templates['JobTemplates']:
-        job = {
-            'settings': {
-                'ARCH': jobtemp['product']['arch'],
-                'FLAVOR': jobtemp['product']['flavor'],
-                'TEST': jobtemp['test_suite']['name'],
-                # we don't need the BUILD value; we're just going to use
-                # the current validation pages
-                'BUILD': 'FAKE',
-            },
-            'result': 'passed',
-            # we use both settings['TEST'] and test, we probably shouldn't...
-            'test': jobtemp['test_suite']['name'],
-            # we just pretend every single job had all the modules that
-            # are used for individual module->result mapping; this is
-            # harmless for the cases where it's a lie, as get_passed_testcases
-            # will entirely ignore it except for the TESTSUITES that specify
-            # the modules
-            'modules': [{'name': module, 'result': 'passed'} for module in modules]
-        }
-        # find the desktop by finding the relevant Product and getting it
-        # from its settings
-        for setting in [prod for prod in templates['Products'] if
-                        prod['flavor'] == jobtemp['product']['flavor']][0]['settings']:
-            if setting['key'] == 'DESKTOP':
-                job['settings']['DESKTOP'] = setting['value']
-        fakejobs.append(job)
+def conds_modules_match(distripath):
+    """Check all modules listed in TESTSUITES entries which use the
+    'modules' condition actually exist in the distri, and return the
+    module names for the wiki check to use.
+    """
+    failed = 0
+    distrimods = glob.glob(os.path.join(distripath, 'tests', '*.pm'))
+    distrimods = [os.path.basename(mod) for mod in distrimods]
+    distrimods = [mod.split('.pm')[0] for mod in distrimods]
+    modules = set()
+    for (suite, cases) in TESTSUITES.items():
+        try:
+            for (case, conds) in cases.items():
+                for module in conds.get('modules', []):
+                    modules.add(module)
+                    if module not in distrimods:
+                        print("Module {0} listed in case {1} in suite {2} not found in "
+                              "distri!".format(module, case, suite))
+                        failed = 1
+                modules.update(conds.get('modules', []))
+        except AttributeError:
+            pass
+    return (failed, modules)
 
+def check_wiki_mapping(templates, modules):
+    """Produce wikitcms ResTups for a set of fake passed jobs produced
+    by fake_jobs, then check we got a ResTup for every test case in
+    TESTCASES, and we can find a ResultRow and env for every generated
+    ResTup in the current validation event wiki pages.
+    """
+    failed = 0
+    fakejobs = fake_jobs(templates, modules)
     # Now we have a bunch of fake 'passed jobs', we can ask report.py to
     # give us ResTups for them
-    restups = report.get_passed_testcases(fakejobs)
+    # Create an instance of our fake 'openQA client' with the fake jobs
+    fakeclient = FakeClient(fakejobs)
+    # Call get_passed_testcases with the fake jobs and client
+    restups = report.get_passed_testcases(fakejobs, fakeclient)
+
+    # Catch if there are any test cases we don't produce a single
+    # result for, even with a full set of 'passed jobs'
+    restupcases = set([restup.testcase for restup in restups])
+    for case in set(TESTCASES.keys()) - restupcases:
+        print("No wiki result generated for test case {0}!".format(case))
+
     for restup in restups:
         # this will get us the current validation page for the testtype
         page = get_page(restup.testtype)
@@ -184,6 +279,31 @@
             print("Too many ResultRows found for {0}".format(restup))
             failed = 1
 
+    return failed
+
+## MAIN FUNCTIONS
+
+def main():
+    """Main function. Just reads the templates and runs each test."""
+    if not len(sys.argv) == 2:
+        sys.exit("Usage: {0} distripath".format(sys.argv[0]))
+    distripath = sys.argv[1]
+    templates = read_templates(distripath)
+    # Ongoing 'did we fail yet?' indicator
+    failed = 0
+    # Run the checks
+    if cases_suites_match():
+        failed = 1
+    if templates_suites_match(templates):
+        failed = 1
+    if conds_testsuites_match():
+        failed = 1
+    (ret, modules) = conds_modules_match(distripath)
+    if ret:
+        failed = 1
+    if check_wiki_mapping(templates, modules):
+        failed = 1
+    # Exit appropriately
     sys.exit(failed)
 
 if __name__ == '__main__':
diff --git a/scheduler/fedora_openqa_schedule/conf_test_suites.py b/scheduler/fedora_openqa_schedule/conf_test_suites.py
--- a/scheduler/fedora_openqa_schedule/conf_test_suites.py
+++ b/scheduler/fedora_openqa_schedule/conf_test_suites.py
@@ -380,6 +380,16 @@
         "type": "Desktop",
         "section": "Release-blocking desktops: <b>x86 / x86_64</b>",
     },
+    "QA:Testcase_desktop_update_notification": {
+        "env": "$SUBVARIANT$",
+        "type": "Desktop",
+        "section": "Release-blocking desktops: <b>x86 / x86_64</b>",
+    },
+    "QA:Testcase_desktop_error_checks": {
+        "env": "$SUBVARIANT$",
+        "type": "Desktop",
+        "section": "Release-blocking desktops: <b>x86 / x86_64</b>",
+    },
     "QA:Testcase_Server_firewall_default": {
         "env": "x86",
         "type": "Server",
@@ -418,17 +428,62 @@
 TESTSUITES = {
     # each entry in this dict is named for an openQA test suite, and
     # represents the Wikitcms test cases that passed if that openQA
-    # test suite passed. The entries can be simple lists of test case
-    # names, or they can be dicts. In the dict form, the special dict
-    # entry named 'PASS' is a list of test cases that passed if the
-    # overall openQA job passed, as in the simple list form. The other
-    # dict entries are named for individual openQA test modules, and
-    # are lists of test cases that passed if the overall openQA job
-    # passed *and* that specific test module passed. This handles
-    # test suites like realmd_join_cockpit, which has an optional
-    # module that covers QA:Testcase_FreeIPA_web_ui ; if that module
-    # passes we want to report a pass for that test case, if the job
-    # passes but that module fails we do *not* want to report a pass.
+    # test suite passed or soft failed (from here on, read 'passed' to
+    # mean 'passed or soft failed'). The value can either be a simple
+    # list of test case names (each must be an entry in the TESTCASES
+    # dict), or a dict.
+    #
+    # In the dict form, the dict's keys are the test case names, and
+    # and the values must all be dicts. These dicts indicate extra
+    # conditions that must be met before the test case will be counted
+    # as 'passed'. An empty dict works just like the simple list case:
+    # that test case will be considered 'passed' as long as the test
+    # suite passed.
+    #
+    # The dict may contain a 'testsuites' key, whose value must be an
+    # iterable. If so, the test case will only be considered 'passed'
+    # if both the test suite being considered passed *and* all the
+    # other test suites named in the iterable passed, for the same
+    # build, machine and flavor. e.g.:
+    #
+    # "testsuite_a": {
+    #     "testcase_a": {},
+    #     "testcase_b": {"testsuites": ["testsuite_b"],},
+    # },
+    #
+    # In this case, if testsuite_a passed, testcase_a would be counted
+    # as passed, but testcase_b would only be counted as passed if
+    # testsuite_b had also already passed. This covers cases like
+    # desktop notifications, where there are two wiki test cases that
+    # can be considered passed only if both desktop_notifications_live
+    # and desktop_notifications_postinstall passed.
+    #
+    # NOTE: unless we can be sure one test suite will always run after
+    # the other, we must include entries for *both* test suites,
+    # because we will process the entry for whichever test suite
+    # passes first and generate no 'pass' because the other one is not
+    # done yet, then when the other one passes we will process its
+    # entry and generate a pass so long as the first one also passed;
+    # if we don't have entries for both test suites and the 'wrong
+    # one' passes first, we will miss the result.
+    #
+    # The dict may contain a 'modules' key, whose value must be an
+    # iterable. If so, the test case will only be considered 'passed'
+    # if the overall openQA job passed *and* each of the individual
+    # openQA test modules named in the iterable were present in the
+    # openQA job and passed. e.g.:
+    #
+    # "testsuite_c": {
+    #     "testcase_c": {"modules": ["module_1"],},
+    # },
+    #
+    # In this case, testcase_c would only be considered 'passed' if
+    # both testsuite_c passed *and* it contained a test module named
+    # module_1 and that module passed. This handles test suites like
+    # realmd_join_cockpit, which has an optional module that covers
+    # QA:Testcase_FreeIPA_web_ui ; if that module passes we want to
+    # report a pass for that test case, if the job passes but that
+    # module fails we do *not* want to report a pass.
     "install_default": [
         "QA:Testcase_Boot_default_install",
         "QA:Testcase_install_to_VirtIO",
@@ -751,6 +806,22 @@
     "desktop_update_graphical": [
         "QA:Testcase_desktop_update_graphical",
     ],
+    "desktop_notifications_live": {
+        "QA:Testcase_desktop_update_notification": {
+            "testsuites": ["desktop_notifications_postinstall"],
+        },
+        "QA:Testcase_desktop_error_checks": {
+            "testsuites": ["desktop_notifications_postinstall"],
+        },
+    },
+    "desktop_notifications_postinstall": {
+        "QA:Testcase_desktop_update_notification": {
+            "testsuites": ["desktop_notifications_live"],
+        },
+        "QA:Testcase_desktop_error_checks": {
+            "testsuites": ["desktop_notifications_live"],
+        },
+    },
     "server_firewall_default": [
         "QA:Testcase_Server_firewall_default",
     ],
@@ -767,17 +838,15 @@
         "QA:Testcase_database_server_remote_client",
     ],
     "realmd_join_cockpit": {
-        "PASS": [
-            "QA:Testcase_realmd_join_cockpit",
-            "QA:Testcase_FreeIPA_realmd_login",
-            "QA:Testcase_domain_client_authenticate",
-        ],
-        "freeipa_webui": [
-            "QA:Testcase_FreeIPA_web_ui",
-        ],
-        "freeipa_password_change": [
-            "QA:Testcase_FreeIPA_password_change",
-        ],
+        "QA:Testcase_realmd_join_cockpit": {},
+        "QA:Testcase_FreeIPA_realmd_login": {},
+        "QA:Testcase_domain_client_authenticate": {},
+        "QA:Testcase_FreeIPA_web_ui": {
+            "modules": ["freeipa_webui"],
+        },
+        "QA:Testcase_FreeIPA_password_change": {
+            "modules": ["freeipa_password_change"],
+        },
     },
     "realmd_join_sssd": [
         "QA:Testcase_realmd_join_sssd",
diff --git a/scheduler/fedora_openqa_schedule/report.py b/scheduler/fedora_openqa_schedule/report.py
--- a/scheduler/fedora_openqa_schedule/report.py
+++ b/scheduler/fedora_openqa_schedule/report.py
@@ -48,12 +48,11 @@
     pass
 
 
-def _uniqueres_replacements(job, uniqueres):
-    """Replace some magic values in the 'uniqueres' dict with test job
+def _uniqueres_replacements(job, tcdict):
+    """Replace some magic values in the 'tcdict' dict with test job
     properties; this is to distinguish between environments for a test
     case, or tests with the same test case but different test names,
-    and so on. Returns nothing because it modifies the uniqueres dict
-    in place.
+    and so on. Returns a new dict with the modifications.
     """
     arch = job['settings']['ARCH']
     flavor = job['settings']['FLAVOR']
@@ -78,8 +77,12 @@
     if 'role_deploy_' in job['test']:
         role = job['test'].split('role_deploy_')[1]
 
+    # We effectively deep copy the `tcdict` dict here; if we just modified it directly
+    # we'd actually be changing it in TESTCASES, so the results for later jobs in this run
+    # with the same testcase (but a different environment, section or testname) would read
+    # the modified values and be messed up
     changed = {}
-    for key, value in uniqueres.iteritems():
+    for key, value in tcdict.iteritems():
         value = value.replace('$RUNARCH_OR_UEFI$', uefi)
         value = value.replace('$FS$', fs)
         value = value.replace('$RUNARCH$', arch)
@@ -94,10 +97,107 @@
     return changed
 
 
-def get_passed_testcases(jobs):
+def _get_passed_tcnames(job, composeid, client=None):
+    """Given a job dict, find the corresponding entry from TESTSUITES
+    and return the test case names that are considered to have passed.
+    This is splitting out a chunk of logic from the middle of
+    get_passed_testcases to make it shorter and more readable.
+    composeid is the compose ID, passed in from get_passed_testcases.
+    client can be an OpenQA_Client instance (to save us instantiating
+    a new one, and also so checkwiki can use a fake one); it's used
+    for entries that use the 'testsuites' condition, we have to go get
+    the appropriate results from openQA and check them, we can't
+    assume they're already in the jobs list that get_passed_testcases
+    got.
+    """
+    tsname = job['settings']['TEST']
+    # There usually ought to be an entry in TESTSUITES for all
+    # tests, but just in case someone messed up, let's be safe
+    if tsname not in conf_test_suites.TESTSUITES:
+        logger.warning("No TESTSUITES entry found for test {0}!".format(tsname))
+        return []
+
+    passed = []
+    testsuite = conf_test_suites.TESTSUITES[tsname]
+    # testsuite can be simply a list of test case names - in which case all those test
+    # cases are considered 'passed' if the openQA job overall 'passed' - or a dict whose
+    # keys are test case names and whose values are dicts of conditions indicating when
+    # that test case should be considered as 'passed'. Here, we handle the conditions for
+    # this case. See conf_test_suites for more details.
+    isdict = None
+    try:
+        # check if this is dict case
+        testsuite.items()
+        isdict = True
+    except AttributeError:
+        isdict = False
+    if isdict:
+        # dict case.
+        for (testcase, conds) in testsuite.items():
+            if not conds:
+                # life is easy!
+                passed.append(testcase)
+                continue
+
+            # otherwise, handle the conditions...
+            # modules: the test case is only 'passed' if all listed openQA job modules
+            # are present in the job and passed
+            if 'modules' in conds:
+                tcpass = True
+                for modname in conds['modules']:
+                    try:
+                        # find the matching test module in the job's list (note this
+                        # assumes we don't run the same module multiple times, which
+                        # is something upstream has recently started allowing...)
+                        module = [module for module in job['modules'] if
+                                  module['name'] == modname][0]
+                        if module.get('result', '') not in ('passed', 'softfailed'):
+                            tcpass = False
+                            break
+                    except IndexError:
+                        tcpass = False
+                        logger.warning("Did not find module %s in job data!", modname)
+                        break
+                if not tcpass:
+                    # skip to next testsuite item
+                    continue
+
+            # test suites: the test case is only 'passed' if there are jobs for all listed
+            # test suites for the same build, machine and flavor, and they all passed
+            if 'testsuites' in conds:
+                if not client:
+                    client = OpenQA_Client()
+                # Ideally we could query on multiple test names - I'll send a PR for that.
+                # As we can't, let's not do multiple single queries, let's just get all
+                # results for the same build, machine and flavor and filter ourselves...
+                params = {
+                    'build': composeid,
+                    'machine': job['settings']['MACHINE'],
+                    'flavor': job['settings']['FLAVOR'],
+                    'latest': 'true',
+                }
+                candjobs = client.openqa_request('GET', 'jobs', params=params)['jobs']
+                _jobs = [_job for _job in candjobs if _job['test'] in conds['testsuites']]
+                if len(_jobs) != len(conds['testsuites']):
+                    continue
+                if any(_job['result'] not in ('passed', 'softfailed') for _job in _jobs):
+                    continue
+
+            # we only get here if all the conditions are satisfied
+            passed.append(testcase)
+
+    else: # isdict - this is the simple list case.
+        passed = testsuite
+
+    return passed
+
+def get_passed_testcases(jobs, client=None):
     """Given an iterable of job dicts - any waiting, filtering and so
     on is assumed to have already happened - returns a list of
-    wikitcms ResTuples derived from any passed tests.
+    wikitcms ResTuples derived from any passed tests. client can be an
+    OpenQA_Client instance (to save us instantiating a new one, and
+    also so checkwiki can use a fake one); it's passed through to
+    _get_passed_tcnames, which actually uses it.
     """
     passed_testcases = set()
     for job in jobs:
@@ -111,53 +211,20 @@
                 # to report results for these
                 logger.debug("Job was run with extra params! Will not report")
                 continue
-            tsname = job['settings']['TEST']
-            # There usually ought to be an entry in TESTSUITES for all
-            # tests, but just in case someone messed up, let's be safe
-            if tsname not in conf_test_suites.TESTSUITES:
-                logger.warning("No TESTSUITES entry found for test {0}!".format(tsname))
-                continue
-            passed = []
-            testsuite = conf_test_suites.TESTSUITES[tsname]
-            # testsuite can be simply a list of test case names - in which case all those test
-            # cases are considered 'passed' if the openQA job overall 'passed' - or a dict of
-            # lists of test case names. In the dict form, there is one special key, 'PASS'; the
-            # 'PASS' list works just like the simple list form, all test cases in that list
-            # 'passed' if the openQA job overall passed. Any other keys are the names of
-            # individual openQA test modules, which should be included in the job as non-fatal
-            # modules; if that module passed within the job, the test cases in the list 'passed'.
-            try:
-                # this is the dict case. first take all test cases from the 'PASS' list...
-                passed.extend(testsuite.pop('PASS', []))
-                # now check each individual test module status
-                for (modname, cases) in testsuite.items():
-                    try:
-                        # find the matching test module in the job's list (note this
-                        # assumes we don't run the same module multiple times, which
-                        # is something upstream has recently started allowing...)
-                        module = [module for module in job['modules'] if module['name'] == modname][0]
-                    except IndexError:
-                        logger.warning("Did not find expected module %s in job data!", modname)
-                        continue
-                    if module.get('result', '') in ('passed', 'softfailed'):
-                        passed.extend(cases)
-            except TypeError:
-                # this is the simple list case.
-                passed = testsuite
+            # find the TESTSUITES entry for the job and parse it to
+            # get a list of passed test case names (TESTCASES keys)
+            passed = _get_passed_tcnames(job, composeid, client)
             for testcase in passed:
                 # skip with warning if testcase is not in TESTCASES
                 if not testcase in conf_test_suites.TESTCASES:
                     logger.warning("No TESTCASES entry found for {0}!".format(testcase))
                     continue
-                # each 'testsuite' is a list using testcase names to indicate which Wikitcms tests
-                # have passed if this job passes. Each testcase name is the name of a dict in the
+                # Each testcase name now in the `passed` list is the name of a dict in the
                 # TESTCASES dict-of-dicts which more precisely identifies the 'test instance' (when
                 # there is more than one for a testcase) and environment for which the result
-                # should be filed. We do a deepcopy here because otherwise the dict in TESTCASES
-                # itself is modified in-place and other jobs for the same testcase (but a different
-                # environment, section or testname) will read the modified values and be messed up.
+                # should be filed.
 
-                # replace $FOO$ values in uniqueres
+                # create new dict based on the testcase dict with $FOO$ values replaced
                 uniqueres = _uniqueres_replacements(job, conf_test_suites.TESTCASES[testcase])
                 result = ResTuple(
                     testtype=uniqueres['type'], testcase=testcase,
@@ -179,7 +246,7 @@
     """
     client = OpenQA_Client()
     jobs = client.get_jobs(jobs=jobs, build=build)
-    passed_testcases = get_passed_testcases(jobs)
+    passed_testcases = get_passed_testcases(jobs, client)
     logger.info("passed testcases: %s", passed_testcases)
     if do_report is None:
         do_report = CONFIG.getboolean('report', 'submit_wiki')

