Esempio n. 1
0
def get_device_test_summary(devicetype=None,
                            include_failed=False,
                            session=None):
    projectfolder = projects.cards[devicetype]
    bomobj = import_pcb(cardfolder=projectfolder)
    bomobj.configure_motifs(devicetype)

    logger.info("Creating dummy test suites")
    dummy_suites = get_electronics_test_suites(None,
                                               devicetype,
                                               projectfolder,
                                               offline=True,
                                               dummy=True)

    collector = ResultCollector(dummy_suites, include_failed=include_failed)

    snos = sno_controller.get_serialnos_by_efield(efield=devicetype,
                                                  session=session)

    for sno in snos:
        suites = get_test_suite_objects(serialno=sno.sno, session=session)
        if len(suites) > 0:
            collector.add_suites_set(suites)

    return collector
Esempio n. 2
0
def gen_cobom_csv(projfolder, namebase, force=False):
    """
    Generates a CSV file in the
    :mod:`tendril.boms.outputbase.CompositeOutputBom` format, including the
    BOMs of the all the defined configurations of the project. This function
    uses a :mod:`csv.writer` instead of rendering a jinja2 template.

    It also generates configdocs for all the defined configurations of the
    project, using :func:`gen_confpdf`.

    :param projfolder: The gEDA project folder.
    :type projfolder: str
    :param namebase: The project name.
    :type namebase: str
    :param force: Regenerate even if up-to-date.
    :type force: bool
    :return: The output file path.

    .. rubric:: Paths

    * Output Files :  ``<project_doc_folder>/confdocs/conf_boms.csv``
    * Also triggers : :func:`gen_confpdf` for all listed configurations.
    * Source Files : The project's schematic folder.

    """
    gpf = projfile.GedaProjectFile(projfolder)
    configfile = conffile.ConfigsFile(projfolder)
    sch_mtime = fsutils.get_folder_mtime(gpf.configsfile.schfolder)

    docfolder = get_project_doc_folder(projfolder)
    cobom_csv_path = path.join(docfolder, 'confdocs', 'conf-boms.csv')
    outf_mtime = fsutils.get_file_mtime(cobom_csv_path, fs=refdoc_fs)

    if not force and outf_mtime is not None and outf_mtime > sch_mtime:
        logger.debug('Skipping up-to-date ' + cobom_csv_path)
        return cobom_csv_path

    logger.info('Regenerating ' + cobom_csv_path + os.linesep +
                'Last modified : ' + str(sch_mtime) +
                '; Last Created : ' + str(outf_mtime))

    bomlist = []
    for cfn in configfile.configuration_names:
        gen_confpdf(projfolder, cfn, namebase, force=force)
        lbom = boms_electronics.import_pcb(projfolder)
        lobom = lbom.create_output_bom(cfn)
        bomlist.append(lobom)
    cobom = boms_outputbase.CompositeOutputBom(bomlist)

    with refdoc_fs.open(cobom_csv_path, 'wb') as f:
        writer = csv.writer(f)
        writer.writerow(['device'] +
                        [x.configname for x in cobom.descriptors])
        for line in cobom.lines:
            writer.writerow([line.ident] + line.columns)
Esempio n. 3
0
def gen_cobom_csv(projfolder, namebase, force=False):
    """
    Generates a CSV file in the
    :mod:`tendril.boms.outputbase.CompositeOutputBom` format, including the
    BOMs of the all the defined configurations of the project. This function
    uses a :mod:`csv.writer` instead of rendering a jinja2 template.

    It also generates configdocs for all the defined configurations of the
    project, using :func:`gen_confpdf`.

    :param projfolder: The gEDA project folder.
    :type projfolder: str
    :param namebase: The project name.
    :type namebase: str
    :param force: Regenerate even if up-to-date.
    :type force: bool
    :return: The output file path.

    .. rubric:: Paths

    * Output Files :  ``<project_doc_folder>/confdocs/conf_boms.csv``
    * Also triggers : :func:`gen_confpdf` for all listed configurations.
    * Source Files : The project's schematic folder.

    """
    gpf = projfile.GedaProjectFile(projfolder)
    configfile = conffile.ConfigsFile(projfolder)
    sch_mtime = fsutils.get_folder_mtime(gpf.schfolder)

    docfolder = get_project_doc_folder(projfolder)
    cobom_csv_path = path.join(docfolder, 'confdocs', 'conf-boms.csv')
    outf_mtime = fsutils.get_file_mtime(cobom_csv_path, fs=refdoc_fs)

    if not force and outf_mtime is not None and outf_mtime > sch_mtime:
        logger.debug('Skipping up-to-date ' + cobom_csv_path)
        return cobom_csv_path

    logger.info('Regenerating ' + cobom_csv_path + os.linesep +
                'Last modified : ' + str(sch_mtime) +
                '; Last Created : ' + str(outf_mtime))

    bomlist = []
    for cfn in configfile.configuration_names:
        gen_confpdf(projfolder, cfn, namebase, force=force)
        lbom = boms_electronics.import_pcb(projfolder)
        lobom = lbom.create_output_bom(cfn)
        bomlist.append(lobom)
    cobom = boms_outputbase.CompositeOutputBom(bomlist)

    with refdoc_fs.open(cobom_csv_path, 'wb') as f:
        writer = csv.writer(f)
        writer.writerow(['device'] +
                        [x.configname for x in cobom.descriptors])
        for line in cobom.lines:
            writer.writerow([line.ident] + line.columns)
Esempio n. 4
0
def get_device_test_summary(devicetype=None, include_failed=False,
                            session=None):
    projectfolder = projects.cards[devicetype]
    bomobj = import_pcb(cardfolder=projectfolder)
    bomobj.configure_motifs(devicetype)

    logger.info("Creating dummy test suites")
    dummy_suites = get_electronics_test_suites(None, devicetype,
                                               projectfolder,
                                               offline=True)
    for suite in dummy_suites:
        suite.dummy = True

    collector = ResultCollector(dummy_suites, include_failed=include_failed)

    snos = sno_controller.get_serialnos_by_efield(efield=devicetype,
                                                  session=session)

    for sno in snos:
        suites = get_test_suite_objects(serialno=sno.sno, session=session)
        if len(suites) > 0:
            collector.add_suites_set(suites)

    return collector
Esempio n. 5
0
def get_test_suite_objects(serialno=None, order_by='FILE_ORDER', session=None):
    # This reconstructs the test objects from the database. Using SQLAlchemy
    # as the ORM that it is, and letting it handle the object creation would
    # be infinitely better. It isn't done here since the models are separate
    # from the actual test objects, which in turn have other dependencies.
    # Integrating the models with the classes should be considered in the
    # future when there is time.
    # suite_names = controller.get_test_suite_names(serialno=serialno,
    #                                               session=session)
    suite_descs = controller.get_test_suite_descs(serialno=serialno,
                                                  session=session)
    devicetype = serialnos.get_serialno_efield(sno=serialno, session=session)
    projectfolder = projects.cards[devicetype]
    bomobj = import_pcb(cardfolder=projectfolder)
    # Perhaps this bomobject should not be recreated on the fly.
    bomobj.configure_motifs(devicetype)

    if order_by == 'FILE_ORDER':

        logger.info("Creating dummy test suites for file ordering")
        dummy_suites = get_electronics_test_suites(None,
                                                   devicetype,
                                                   projectfolder,
                                                   offline=True)
        ldummy_suites = []
        for suite in dummy_suites:
            suite.dummy = True
            ldummy_suites.append(suite)

        file_order = [(x.desc, [(y.desc, y.passfailonly) for y in x.tests])
                      for x in ldummy_suites]
        suite_order = [x[0] for x in file_order]
        test_order = {x[0]: x[1] for x in file_order}

    elif order_by == 'DONT_CARE':
        suite_order = []
        test_order = {}

    else:
        raise ValueError('Unknown order_by heuristic : ' + order_by)

    suites = []
    suite_descs = sort_by_order(suite_descs, suite_order)

    # for suite_name in suite_names:
    for desc, name in suite_descs:
        suite_db_obj = controller.get_latest_test_suite(serialno=serialno,
                                                        suite_class=name,
                                                        descr=desc,
                                                        session=session)
        if suite_db_obj.suite_class == \
                "<class 'tendril.testing.testbase.TestSuiteBase'>":
            suite_obj = TestSuiteBase()
        else:
            raise ValueError("Unrecognized suite_class : " +
                             suite_db_obj.suite_class)

        suite_obj.desc = suite_db_obj.desc
        suite_obj.title = suite_db_obj.title
        suite_obj.ts = suite_db_obj.created_at
        suite_obj.serialno = serialno
        if order_by == 'FILE_ORDER':
            test_display_params = {
                x[0]: x[1]
                for x in test_order[suite_obj.desc]
            }

        for test_db_obj in suite_db_obj.tests:
            class_name = rex_class.match(test_db_obj.test_class).group('cl')

            test_obj = get_test_object(class_name, offline=True)
            test_obj.desc = test_db_obj.desc
            test_obj.title = test_db_obj.title
            test_obj.ts = test_db_obj.created_at
            test_obj.use_bom(bomobj)
            test_obj.load_result_from_obj(test_db_obj.result)
            if order_by == 'FILE_ORDER':
                test_obj.passfailonly = test_display_params[test_obj.desc]
            suite_obj.add_test(test_obj)
            # Crosscheck test passed?

        # Crosscheck suite passed?

        suites.append(suite_obj)

    return suites
Esempio n. 6
0
def gen_schpdf(projfolder, namebase, configname=None, force=False):
    """
    Generates a PDF file of all the project schematics listed in the
    gEDA project file. This function does not use jinja2 and latex. It
    relies on :func:`tendril.gedaif.gschem.conv_gsch2pdf` instead.

    :param projfolder: The gEDA project folder.
    :type projfolder: str
    :param namebase: The project name.
    :type namebase: str
    :param force: Regenerate even if up-to-date.
    :type force: bool
    :return: The output file path.

    .. rubric:: Paths

    * Output File :  ``<project_doc_folder>/<namebase>-schematic.pdf``
    * Source Files : The project's schematic folder.

    """
    gpf = projfile.GedaProjectFile(projfolder)
    sch_mtime = fsutils.get_folder_mtime(gpf.schfolder)

    configfile = conffile.ConfigsFile(projfolder)
    docfolder = get_project_doc_folder(projfolder)

    # TODO Consider converting all configurations in one go instead?
    if configname is None:
        schpdfpath = path.join(docfolder, namebase + '-schematic.pdf')
    else:
        docfolder = path.join(docfolder, 'confdocs')
        pdfname = configname + '-conf-schematic.pdf'
        schpdfpath = path.join(docfolder, pdfname)
    outf_mtime = fsutils.get_file_mtime(schpdfpath, fs=refdoc_fs)

    if not force and outf_mtime is not None and outf_mtime > sch_mtime:
        logger.debug('Skipping up-to-date ' + schpdfpath)
        return schpdfpath

    logger.info('Regenerating ' + schpdfpath + os.linesep +
                'Last modified : ' + str(sch_mtime) + '; Last Created : ' +
                str(outf_mtime))

    if configfile.rawconfig is not None:
        workspace_outpath = workspace_fs.getsyspath(schpdfpath)
        workspace_folder = workspace_fs.getsyspath(docfolder)
        workspace_fs.makedir(docfolder, recursive=True, allow_recreate=True)
        pdffiles = []
        obom = None
        if configname is not None:
            tfolder = path.join(docfolder, configname)
            workspace_tfolder = workspace_fs.getsyspath(tfolder)
            workspace_fs.makedir(tfolder, recursive=False, allow_recreate=True)
            bom = boms_electronics.import_pcb(projfolder)
            bom.configure_motifs(configname)
            obom = bom.create_output_bom(configname)
        for schematic in gpf.schfiles:
            schfile = os.path.normpath(projfolder + '/schematic/' + schematic)
            if configname is not None:
                tschfile = path.join(workspace_tfolder, schematic)
                gschem.rewrite_schematic(schfile, obom, gpf, tschfile)
                pdffile = gschem.conv_gsch2pdf(tschfile, workspace_tfolder)
                os.remove(tschfile)
            else:
                pdffile = gschem.conv_gsch2pdf(schfile, workspace_folder)
            pdffiles.append(pdffile)
        pdf.merge_pdf(pdffiles, workspace_outpath)
        for pdffile in pdffiles:
            os.remove(pdffile)
        copyfile(workspace_fs,
                 schpdfpath,
                 refdoc_fs,
                 schpdfpath,
                 overwrite=True)
        return schpdfpath
Esempio n. 7
0
def gen_confdoc(projfolder, configname, force=False):
    """
    Generate a PDF documenting a single configuration of the project. The
    document should include a reasonably thorough representation of the
    contents of the configuration related sections of the
    `tendril.gedaif.conffile.ConfigsFile``.

    :param projfolder: The gEDA project folder
    :type projfolder: str
    :param configname: The configuration name for which the BOM should be
                       generated.
    :type configname: str
    :param force: Regenerate even if up-to-date.
    :type force: bool
    :return: The output file path.

    .. rubric:: Paths

    * Output File :  ``<project_doc_folder>/confdocs/<configname>-doc.pdf``
    * Source Files : The project's schematic folder.

    .. rubric:: Template Used

    ``tendril/dox/templates/projects/geda-conf-doc.tex``
    (:download:`Included version
    <../../tendril/dox/templates/projects/geda-conf-doc.tex>`)

    .. rubric:: Stage Keys Provided
    .. list-table::

        * - ``configname``
          - The name of the configuration (a card or cable name).
        * - ``desc``
          - The description of the configuration.
        * - ``pcbname``
          - The name of the base PCB.
        * - ``obom``
          - An :mod:`tendril.boms.outputbase.OutputBom` instance

    """
    gpf = projfile.GedaProjectFile(projfolder)
    sch_mtime = fsutils.get_folder_mtime(gpf.schfolder)

    docfolder = get_project_doc_folder(projfolder)
    outpath = path.join(docfolder, 'confdocs', configname + '-doc.pdf')
    outf_mtime = fsutils.get_file_mtime(outpath, fs=refdoc_fs)

    if not force and outf_mtime is not None and outf_mtime > sch_mtime:
        logger.debug('Skipping up-to-date ' + outpath)
        return outpath

    logger.info('Regenerating ' + outpath + os.linesep + 'Last modified : ' +
                str(sch_mtime) + '; Last Created : ' + str(outf_mtime))
    bom = boms_electronics.import_pcb(projfolder)
    obom = bom.create_output_bom(configname)
    group_oboms = bom.get_group_boms(configname)
    stage = {
        'configname': obom.descriptor.configname,
        'pcbname': obom.descriptor.pcbname,
        'bom': bom,
        'obom': obom,
        'group_oboms': group_oboms
    }

    config = obom.descriptor.configurations.configuration(configname)
    stage['desc'] = config['desc']

    template = 'projects/geda-conf-doc.tex'

    workspace_outpath = workspace_fs.getsyspath(outpath)
    workspace_fs.makedir(path.dirname(outpath),
                         recursive=True,
                         allow_recreate=True)
    render.render_pdf(stage, template, workspace_outpath)
    copyfile(workspace_fs, outpath, refdoc_fs, outpath, overwrite=True)

    return outpath
Esempio n. 8
0
def gen_confdoc(projfolder, configname, force=False):
    """
    Generate a PDF documenting a single configuration of the project. The
    document should include a reasonably thorough representation of the
    contents of the configuration related sections of the
    `tendril.gedaif.conffile.ConfigsFile``.

    :param projfolder: The gEDA project folder
    :type projfolder: str
    :param configname: The configuration name for which the BOM should be
                       generated.
    :type configname: str
    :param force: Regenerate even if up-to-date.
    :type force: bool
    :return: The output file path.

    .. rubric:: Paths

    * Output File :  ``<project_doc_folder>/confdocs/<configname>-doc.pdf``
    * Source Files : The project's schematic folder.

    .. rubric:: Template Used

    ``tendril/dox/templates/projects/geda-conf-doc.tex``
    (:download:`Included version
    <../../tendril/dox/templates/projects/geda-conf-doc.tex>`)

    .. rubric:: Stage Keys Provided
    .. list-table::

        * - ``configname``
          - The name of the configuration (a card or cable name).
        * - ``desc``
          - The description of the configuration.
        * - ``pcbname``
          - The name of the base PCB.
        * - ``obom``
          - An :mod:`tendril.boms.outputbase.OutputBom` instance

    """
    gpf = projfile.GedaProjectFile(projfolder)
    sch_mtime = fsutils.get_folder_mtime(gpf.schfolder)

    docfolder = get_project_doc_folder(projfolder)
    outpath = path.join(docfolder, 'confdocs', configname + '-doc.pdf')
    outf_mtime = fsutils.get_file_mtime(outpath, fs=refdoc_fs)

    if not force and outf_mtime is not None and outf_mtime > sch_mtime:
        logger.debug('Skipping up-to-date ' + outpath)
        return outpath

    logger.info('Regenerating ' + outpath + os.linesep +
                'Last modified : ' + str(sch_mtime) +
                '; Last Created : ' + str(outf_mtime))
    bom = boms_electronics.import_pcb(projfolder)
    obom = bom.create_output_bom(configname)
    group_oboms = bom.get_group_boms(configname)
    stage = {'configname': obom.descriptor.configname,
             'pcbname': obom.descriptor.pcbname,
             'bom': bom,
             'obom': obom,
             'group_oboms': group_oboms}

    config = obom.descriptor.configurations.configuration(configname)
    stage['desc'] = config['desc']

    template = 'projects/geda-conf-doc.tex'

    workspace_outpath = workspace_fs.getsyspath(outpath)
    workspace_fs.makedir(path.dirname(outpath),
                         recursive=True, allow_recreate=True)
    render.render_pdf(stage, template, workspace_outpath)
    copyfile(workspace_fs, outpath, refdoc_fs, outpath, overwrite=True)

    return outpath
Esempio n. 9
0
def get_suiteobj_from_cnf_suite(cnf_suite, gcf, devicetype,
                                offline=False):
    """

    :param cnf_suite:
    :param gcf:
    :type gcf: tendril.gedaif.conffile.ConfigsFile
    :param devicetype:
    :param offline:
    :param dummy:
    :return:
    """
    if len(cnf_suite.keys()) != 1:
        raise ValueError("Suite configurations are expected "
                         "to have exactly one key at the top level")

    cnf_suite_name = cnf_suite.keys()[0]
    testvars = gcf.testvars(devicetype)
    bomobj = import_pcb(gcf.projectfolder)
    bomobj.configure_motifs(devicetype)
    cnf_grouplist = gcf.configuration_grouplist(devicetype)

    desc = None
    title = None
    if 'desc' in cnf_suite[cnf_suite.keys()[0]].keys():
        logger.debug("Found Test Suite Description")
        desc = cnf_suite[cnf_suite.keys()[0]]['desc']
    if 'title' in cnf_suite[cnf_suite.keys()[0]].keys():
        logger.debug("Found Test Suite Title")
        title = cnf_suite[cnf_suite.keys()[0]]['title']

    logger.debug("Creating test suite : " + cnf_suite_name)
    if cnf_suite_name == "TestSuiteBase":
        suite = []
        suite_detail = cnf_suite[cnf_suite_name]

        if 'group-tests' in suite_detail.keys():
            suite.append(TestSuiteBase())

            if 'prep' in suite_detail.keys():
                add_prep_steps_from_cnf_prep(suite[0], suite_detail['prep'])

            if desc is not None:
                suite[0].desc = desc
            if title is not None:
                suite[0].title = title

            cnf_groups = suite_detail['group-tests']
            for cnf_group in cnf_groups:
                if len(cnf_suite.keys()) != 1:
                    raise ValueError("Group test configurations are "
                                     "expected to have exactly one "
                                     "key at the top level")

                logger.debug("Creating group tests : " + cnf_group.keys()[0])
                if cnf_group.keys()[0] in cnf_grouplist:
                    cnf_test_list = cnf_group[cnf_group.keys()[0]]
                    for cnf_test in cnf_test_list:
                        suite[0].add_test(
                            get_testobj_from_cnf_test(
                                cnf_test, testvars, bomobj, offline=offline
                            )
                        )

        if 'channel-tests' in suite_detail.keys():
            channel_defs = get_channel_defs_from_cnf_channels(
                suite_detail['channels'], cnf_grouplist
            )

            lsuites = []
            for channel_def in channel_defs:
                lsuite = TestSuiteBase()
                if 'prep' in suite_detail.keys():
                    add_prep_steps_from_cnf_prep(
                        lsuite,
                        replace_in_test_cnf_dict(
                            suite_detail['prep'], '<CH>', channel_def.idx
                        )
                    )
                if desc is not None:
                    lsuite.desc = replace_in_string(
                        desc, '<CH>', channel_def.idx
                    )
                if title is not None:
                    lsuite.title = replace_in_string(
                        title, '<CH>', channel_def.idx
                    )
                for test in suite_detail['channel-tests']:
                    if 'motif-map' in suite_detail.keys():
                        motifmap = suite_detail['motif-map']
                    else:
                        motifmap = None
                    cnf_test_dict = replace_in_test_cnf_dict(
                        test, '<CH>', channel_def.idx, motifmap
                    )
                    lsuite.add_test(
                        get_testobj_from_cnf_test(
                            cnf_test_dict, testvars, bomobj, offline=offline
                        )
                    )
                lsuites.append(lsuite)

            suite.extend(lsuites)
    else:
        suite = [get_test_object(cnf_suite)]

    return suite
Esempio n. 10
0
def get_test_suite_objects(serialno=None, order_by='FILE_ORDER',
                           session=None):
    # This reconstructs the test objects from the database. Using SQLAlchemy
    # as the ORM that it is, and letting it handle the object creation would
    # be infinitely better. It isn't done here since the models are separate
    # from the actual test objects, which in turn have other dependencies.
    # Integrating the models with the classes should be considered in the
    # future when there is time.
    # suite_names = controller.get_test_suite_names(serialno=serialno,
    #                                               session=session)
    suite_descs = controller.get_test_suite_descs(serialno=serialno,
                                                  session=session)
    devicetype = serialnos.get_serialno_efield(sno=serialno, session=session)
    projectfolder = projects.cards[devicetype]
    bomobj = import_pcb(cardfolder=projectfolder)
    # Perhaps this bomobject should not be recreated on the fly.
    bomobj.configure_motifs(devicetype)

    if order_by == 'FILE_ORDER':

        logger.info("Creating dummy test suites for file ordering")
        dummy_suites = get_electronics_test_suites(None, devicetype,
                                                   projectfolder,
                                                   offline=True)
        ldummy_suites = []
        for suite in dummy_suites:
            suite.dummy = True
            ldummy_suites.append(suite)

        file_order = [(x.desc, [(y.desc, y.passfailonly) for y in x.tests])
                      for x in ldummy_suites]
        suite_order = [x[0] for x in file_order]
        test_order = {x[0]: x[1] for x in file_order}

    elif order_by == 'DONT_CARE':
        suite_order = []
        test_order = {}

    else:
        raise ValueError('Unknown order_by heuristic : ' + order_by)

    suites = []
    suite_descs = sort_by_order(suite_descs, suite_order)

    # for suite_name in suite_names:
    for desc, name in suite_descs:
        suite_db_obj = controller.get_latest_test_suite(
            serialno=serialno, suite_class=name, descr=desc, session=session
        )
        if suite_db_obj.suite_class == \
                "<class 'tendril.testing.testbase.TestSuiteBase'>":
            suite_obj = TestSuiteBase()
        else:
            raise ValueError("Unrecognized suite_class : " +
                             suite_db_obj.suite_class)

        suite_obj.desc = suite_db_obj.desc
        suite_obj.title = suite_db_obj.title
        suite_obj.ts = suite_db_obj.created_at
        suite_obj.serialno = serialno
        if order_by == 'FILE_ORDER':
            test_display_params = {x[0]: x[1]
                                   for x in test_order[suite_obj.desc]}

        for test_db_obj in suite_db_obj.tests:
            class_name = rex_class.match(test_db_obj.test_class).group('cl')

            test_obj = get_test_object(class_name, offline=True)
            test_obj.desc = test_db_obj.desc
            test_obj.title = test_db_obj.title
            test_obj.ts = test_db_obj.created_at
            test_obj.use_bom(bomobj)
            test_obj.load_result_from_obj(test_db_obj.result)
            if order_by == 'FILE_ORDER':
                test_obj.passfailonly = test_display_params[test_obj.desc]
            suite_obj.add_test(test_obj)
            # Crosscheck test passed?

        # Crosscheck suite passed?

        suites.append(suite_obj)

    return suites
Esempio n. 11
0
def get_suiteobj_from_cnf_suite(cnf_suite, gcf, devicetype, offline=False):
    """

    :param cnf_suite:
    :param gcf:
    :type gcf: tendril.gedaif.conffile.ConfigsFile
    :param devicetype:
    :param offline:
    :return:
    """
    if len(cnf_suite.keys()) != 1:
        raise ValueError("Suite configurations are expected " "to have exactly one key at the top level")

    cnf_suite_name = cnf_suite.keys()[0]
    testvars = gcf.testvars(devicetype)
    bomobj = import_pcb(gcf.projectfolder)
    bomobj.configure_motifs(devicetype)
    cnf_grouplist = gcf.configuration_grouplist(devicetype)

    desc = None
    title = None
    if "desc" in cnf_suite[cnf_suite.keys()[0]].keys():
        logger.debug("Found Test Suite Description")
        desc = cnf_suite[cnf_suite.keys()[0]]["desc"]
    if "title" in cnf_suite[cnf_suite.keys()[0]].keys():
        logger.debug("Found Test Suite Title")
        title = cnf_suite[cnf_suite.keys()[0]]["title"]

    logger.debug("Creating test suite : " + cnf_suite_name)
    if cnf_suite_name == "TestSuiteBase":
        suite = []
        suite_detail = cnf_suite[cnf_suite_name]

        if "group-tests" in suite_detail.keys():
            suite.append(TestSuiteBase())

            if "prep" in suite_detail.keys():
                add_prep_steps_from_cnf_prep(suite[0], suite_detail["prep"])

            if desc is not None:
                suite[0].desc = desc
            if title is not None:
                suite[0].title = title

            cnf_groups = suite_detail["group-tests"]
            for cnf_group in cnf_groups:
                if len(cnf_suite.keys()) != 1:
                    raise ValueError(
                        "Group test configurations are " "expected to have exactly one " "key at the top level"
                    )

                logger.debug("Creating group tests : " + cnf_group.keys()[0])
                if cnf_group.keys()[0] in cnf_grouplist:
                    cnf_test_list = cnf_group[cnf_group.keys()[0]]
                    for cnf_test in cnf_test_list:
                        suite[0].add_test(get_testobj_from_cnf_test(cnf_test, testvars, bomobj, offline=offline))

        if "channel-tests" in suite_detail.keys():
            channel_defs = get_channel_defs_from_cnf_channels(suite_detail["channels"], cnf_grouplist)

            lsuites = []
            for channel_def in channel_defs:
                lsuite = TestSuiteBase()
                if "prep" in suite_detail.keys():
                    add_prep_steps_from_cnf_prep(
                        lsuite, replace_in_test_cnf_dict(suite_detail["prep"], "<CH>", channel_def.idx)
                    )
                if desc is not None:
                    lsuite.desc = replace_in_string(desc, "<CH>", channel_def.idx)
                if title is not None:
                    lsuite.title = replace_in_string(title, "<CH>", channel_def.idx)
                for test in suite_detail["channel-tests"]:
                    if "motif-map" in suite_detail.keys():
                        motifmap = suite_detail["motif-map"]
                    else:
                        motifmap = None
                    cnf_test_dict = replace_in_test_cnf_dict(test, "<CH>", channel_def.idx, motifmap)
                    lsuite.add_test(get_testobj_from_cnf_test(cnf_test_dict, testvars, bomobj, offline=offline))
                lsuites.append(lsuite)

            suite.extend(lsuites)
    else:
        suite = [get_test_object(cnf_suite)]

    return suite