From 70f04dc8b8283b1c0ef92ca9c1e52a86c59736b1 Mon Sep 17 00:00:00 2001 From: Josh Durgin Date: Mon, 30 Nov 2015 17:14:34 -0800 Subject: [PATCH] teuthology-describe-tests: change input format to yaml Rather than using comments, read description metadata from a yaml 'description' section. Make it a list so that teuthology-suite aggregates the descriptions in order, and allow multiple fields by making the list contain a dict. Signed-off-by: Josh Durgin --- scripts/describe_tests.py | 28 +++++++++----- teuthology/describe_tests.py | 51 ++++++++++++++++---------- teuthology/exceptions.py | 4 ++ teuthology/test/test_describe_tests.py | 39 ++++++++++++++++---- 4 files changed, 85 insertions(+), 37 deletions(-) diff --git a/scripts/describe_tests.py b/scripts/describe_tests.py index c6648abcc1..b59ff6258a 100644 --- a/scripts/describe_tests.py +++ b/scripts/describe_tests.py @@ -8,21 +8,29 @@ usage: teuthology-describe-tests -h teuthology-describe-tests [options] [--] -Describe the contents of a qa suite by extracting comments -starting with particular prefixes from files in the suite. +Describe the contents of a qa suite by reading 'description' elements +from yaml files in the suite. -By default, the remainder of a line starting with '# desc:' will -be included from each file in the specified suite directory. +The 'description' element should contain a list with a dictionary +of fields, e.g.: + +description: +- field1: value1 + field2: value2 + field3: value3 + desc: short human-friendly description + +Fields are user-defined, and are not required to be in all yaml files. positional arguments: - qa suite path to traverse and describe + path of qa suite optional arguments: - -h, --help Show this help message and exit - -p , --prefix Comma-separated list of prefixes - [default: desc] - --show-facet [yes|no] List the facet of each file - [default: yes] + -h, --help Show this help message and exit + -f , --fields Comma-separated list of fields to + include [default: desc] + --show-facet [yes|no] List the facet of each file + [default: yes] """ diff --git a/teuthology/describe_tests.py b/teuthology/describe_tests.py index 13f192eca6..340d2fce0c 100644 --- a/teuthology/describe_tests.py +++ b/teuthology/describe_tests.py @@ -1,43 +1,54 @@ # -*- coding: utf-8 -*- from prettytable import PrettyTable, FRAME, ALL import os +import yaml + +from teuthology.exceptions import ParseError def main(args): suite_dir = os.path.abspath(args[""]) - filters = args["--prefix"].split(',') + fields = args["--fields"].split(',') include_facet = args['--show-facet'] == 'yes' - print(suite_dir) - rows = tree_with_info(suite_dir, filters, include_facet, '', []) + try: + rows = tree_with_info(suite_dir, fields, include_facet, '', []) + except ParseError: + return 1 headers = ['path'] if include_facet: headers.append('facet') - table = PrettyTable(headers + filters) + table = PrettyTable(headers + fields) table.align = 'l' table.vrules = ALL table.hrules = FRAME for row in rows: table.add_row(row) + + print(suite_dir) print(table) -def extract_info(file_name, filters, _isdir=os.path.isdir, _open=open): - result = {f: '' for f in filters} - if _isdir(file_name): - return result +def extract_info(file_name, fields, _isdir=os.path.isdir, _open=open): + if _isdir(file_name) or not file_name.endswith('.yaml'): + return {f: '' for f in fields} + with _open(file_name, 'r') as f: - for line in f: - for filt in filters: - prefix = '# ' + filt + ':' - if line.startswith(prefix): - if result[filt]: - result[filt] += '\n' - result[filt] += line[len(prefix):].rstrip('\n').strip() - return result + parsed = yaml.load(f) + + description = parsed.get('description', [{}]) + if not (isinstance(description, list) and + len(description) == 1 and + isinstance(description[0], dict)): + print 'Error in description format in', file_name + print 'Description must be a list containing exactly one dict.' + print 'Description is:', description + raise ParseError() + + return {field: description[0].get(field, '') for field in fields} -def tree_with_info(cur_dir, filters, include_facet, prefix, rows, +def tree_with_info(cur_dir, fields, include_facet, prefix, rows, _listdir=os.listdir, _isdir=os.path.isdir, _open=open): files = sorted(_listdir(cur_dir)) @@ -51,15 +62,15 @@ def tree_with_info(cur_dir, filters, include_facet, prefix, rows, else: file_pad = '├── ' dir_pad = '│ ' - info = extract_info(path, filters, _isdir, _open) + info = extract_info(path, fields, _isdir, _open) tree_node = prefix + file_pad + f - meta = [info[f] for f in filters] + meta = [info[f] for f in fields] row = [tree_node] if include_facet: row.append(facet) rows.append(row + meta) if _isdir(path): - tree_with_info(path, filters, include_facet, + tree_with_info(path, fields, include_facet, prefix + dir_pad, rows, _listdir, _isdir, _open) return rows diff --git a/teuthology/exceptions.py b/teuthology/exceptions.py index 9b7c65991b..4e4e135fd8 100644 --- a/teuthology/exceptions.py +++ b/teuthology/exceptions.py @@ -28,6 +28,10 @@ class ConfigError(RuntimeError): pass +class ParseError(Exception): + pass + + class CommandFailedError(Exception): """ diff --git a/teuthology/test/test_describe_tests.py b/teuthology/test/test_describe_tests.py index 5a8ce32db8..1fa44b8846 100644 --- a/teuthology/test/test_describe_tests.py +++ b/teuthology/test/test_describe_tests.py @@ -1,27 +1,33 @@ # -*- coding: utf-8 -*- +import pytest + from fake_fs import make_fake_fstools from teuthology.describe_tests import tree_with_info, extract_info +from teuthology.exceptions import ParseError realistic_fs = { 'basic': { '%': None, 'base': { 'install.yaml': - """# desc: install ceph + """description: +- desc: install ceph install: """ }, 'clusters': { 'fixed-1.yaml': - """# desc: single node cluster + """description: +- desc: single node cluster roles: - [osd.0, osd.1, osd.2, mon.a, mon.b, mon.c, client.0] """ }, 'workloads': { 'rbd_api_tests_old_format.yaml': - """# desc: c/c++ librbd api tests with format 1 images -# rbd_features: none + """description: +- desc: c/c++ librbd api tests with format 1 images + rbd_features: none overrides: ceph: conf: @@ -36,8 +42,9 @@ tasks: - rbd/test_librbd.sh """, 'rbd_api_tests.yaml': - """# desc: c/c++ librbd api tests with default settings -# rbd_features: default + """description: +- desc: c/c++ librbd api tests with default settings + rbd_features: default tasks: - workunit: clients: @@ -182,10 +189,28 @@ class TestDescribeTests(object): def test_extract_info_dir(): - simple_fs = {'a': {'b': '# foo:'}} + simple_fs = {'a': {'b.yaml': 'description: [{foo: c}]'}} _, _, fake_isdir, fake_open = make_fake_fstools(simple_fs) info = extract_info('a', [], fake_isdir, fake_open) assert info == {} info = extract_info('a', ['foo', 'bar'], fake_isdir, fake_open) assert info == {'foo': '', 'bar': ''} + + info = extract_info('a/b.yaml', ['foo', 'bar'], fake_isdir, fake_open) + assert info == {'foo': 'c', 'bar': ''} + +def check_parse_error(fs): + _, _, fake_isdir, fake_open = make_fake_fstools(fs) + with pytest.raises(ParseError): + a = extract_info('a.yaml', ['a'], fake_isdir, fake_open) + raise Exception(str(a)) + +def test_extract_info_too_many_elements(): + check_parse_error({'a.yaml': 'description: [{a: b}, {b: c}]'}) + +def test_extract_info_not_a_list(): + check_parse_error({'a.yaml': 'description: {a: b}'}) + +def test_extract_info_not_a_dict(): + check_parse_error({'a.yaml': 'description: [[a, b]]'}) -- 2.39.5