From ed99e524a8cc2cec0aaec436299d3ce0540c3089 Mon Sep 17 00:00:00 2001 From: Lance Bragstad Date: Sat, 31 May 2014 02:58:39 +0000 Subject: [PATCH] Refactor Meeting init and enable unit tests Previously, there were two load_meetings() methods. One was in util.py and one was a member of the Meeting object. This commit combines the two and makes it general enough for both cases. Update tox.ini as well as .testr.conf so that we can run some basic unit tests. An initial test has been added so there is something to run. Change-Id: I44fd693f1a4c0a0efcf99d935123108f22afea36 --- .testr.conf | 7 +++ arbiter/meeting.py | 98 +++++++++++++++++++++++++---------- arbiter/tests/__init__.py | 0 arbiter/tests/test_meeting.py | 45 ++++++++++++++++ arbiter/util.py | 39 +++----------- test-requirements.txt | 8 +++ tox.ini | 2 + 7 files changed, 139 insertions(+), 60 deletions(-) create mode 100644 .testr.conf create mode 100644 arbiter/tests/__init__.py create mode 100644 arbiter/tests/test_meeting.py diff --git a/.testr.conf b/.testr.conf new file mode 100644 index 0000000..6d83b3c --- /dev/null +++ b/.testr.conf @@ -0,0 +1,7 @@ +[DEFAULT] +test_command=OS_STDOUT_CAPTURE=${OS_STDOUT_CAPTURE:-1} \ + OS_STDERR_CAPTURE=${OS_STDERR_CAPTURE:-1} \ + OS_TEST_TIMEOUT=${OS_TEST_TIMEOUT:-60} \ + ${PYTHON:-python} -m subunit.run discover -t ./ . $LISTOPT $IDOPTION +test_id_option=--load-list $IDFILE +test_list_option=--list diff --git a/arbiter/meeting.py b/arbiter/meeting.py index 0eab4a6..c51d760 100644 --- a/arbiter/meeting.py +++ b/arbiter/meeting.py @@ -16,6 +16,7 @@ # under the License. import datetime +import hashlib import logging import os @@ -30,22 +31,9 @@ from arbiter import schedule class Meeting: """An OpenStack meeting.""" - def __init__(self, yaml, filename): + def __init__(self): """Initialize meeting from yaml file name 'filename'.""" - - self.filename = filename - - # initialize using yaml - self.project = yaml['project'] - self.chair = yaml['chair'] - self.description = yaml['description'] - self.agenda = yaml['agenda'] # this is a list of list of topics - - # create schedule objects - self.schedules = [] - for sch in yaml['schedule']: - s = schedule.Schedule(sch) - self.schedules.append(s) + pass def write_ical(self, ical_dir): """Write this meeting to disk using the iCal format.""" @@ -56,7 +44,7 @@ class Meeting: cal.add('prodid', '-//OpenStack//Gerrit-Powered Meeting Agendas//EN') cal.add('version', '2.0') - for schedule in self.schedules: + for sch in self.schs: # one Event per iCal file event = icalendar.Event() @@ -64,12 +52,12 @@ class Meeting: # event in an ical file (at least, for it to work with # Google Calendar) - event.add('summary', self.project + ' (' + schedule.irc + ')') + event.add('summary', self.project + ' (' + sch.irc + ')') # add ical description project_descript = "Project: %s" % (self.project) chair_descript = "Chair: %s" % (self.chair) - irc_descript = "IRC: %s" % (schedule.irc) + irc_descript = "IRC: %s" % (sch.irc) agenda_yaml = yaml.dump(self.agenda, default_flow_style=False) agenda_descript = "Agenda:\n%s\n" % (agenda_yaml) descript_descript = "Description: %s" % (self.description) @@ -82,18 +70,18 @@ class Meeting: # get starting date d = datetime.datetime.utcnow() - next_meeting = self._next_weekday(d, const.WEEKDAYS[schedule.day]) + next_meeting = self._next_weekday(d, const.WEEKDAYS[sch.day]) next_meeting_dt = datetime.datetime(next_meeting.year, next_meeting.month, next_meeting.day, - schedule.time.hour, - schedule.time.minute, + sch.time.hour, + sch.time.minute, tzinfo=pytz.utc) event.add('dtstart', next_meeting_dt) # add recurrence rule - event.add('rrule', {'freq': schedule.freq}) + event.add('rrule', {'freq': sch.freq}) # add meeting length # TODO(jotan): determine duration to use for OpenStack meetings @@ -103,7 +91,7 @@ class Meeting: cal.add_component(event) # determine file name from source file - ical_filename = os.path.basename(self.filename).split('.')[0] + '.ics' + ical_filename = os.path.basename(self._filename).split('.')[0] + '.ics' ical_filename = os.path.join(ical_dir, ical_filename) if not os.path.exists(ical_dir): @@ -127,12 +115,12 @@ class Meeting: """ meetings = [] - for schedule in self.schedules: - schedule_time = schedule.time.hour * 100 + schedule.time.minute + for sch in self.schedules: + schedule_time = sch.time.hour * 100 + sch.time.minute meetings.append((self.filename, (schedule_time, - schedule.day, - schedule.irc))) + sch.day, + sch.irc))) return meetings def _next_weekday(self, ref_date, weekday): @@ -148,3 +136,59 @@ class Meeting: if days_ahead <= 0: # target day already happened this week days_ahead += 7 return ref_date + datetime.timedelta(days_ahead) + + +def load_meetings(yaml_source): + """Build YAML object and load meeting data + + :param yaml_source: source data to load, which can be a directory, file, + or stream. + :returns: list of meeting objects + """ + meetings_yaml = [] + # Determine what the yaml_source is + if os.path.isfile(yaml_source): + meetings_yaml.append(yaml_source) + elif os.path.isdir(yaml_source): + # TODO(lbragstad): use os.path.walk? + for f in os.listdir(yaml_source): + # Build the entire file path and append to the list of yaml + # meetings + yaml_file = os.path.join(yaml_source, f) + meetings_yaml.append(yaml_file) + elif isinstance(yaml_source, str): + return [_load_meeting(yaml_source)] + else: + # If we don't have a .yaml file, a directory of .yaml files, or any + # YAML data fail out here. + raise ValueError("YAML source isn't a .yaml file, directory " + "containing .yaml files, or YAML data.") + + meetings = [] + for yaml_file in meetings_yaml: + with open(yaml_file, 'r') as f: + meetings.append(_load_meeting(f)) + + return meetings + + +def _load_meeting(meeting_yaml): + yaml_obj = yaml.safe_load(meeting_yaml) + m = Meeting() + + # Build meeting attributes from yaml + m.agenda = yaml_obj['agenda'] + m.chair = yaml_obj['chair'] + m.description = yaml_obj['description'] + m.project = yaml_obj['project'] + m._filename = (yaml_obj['project'] + '-' + + hashlib.md5(str(yaml_obj).encode('utf-8')).hexdigest()[:8]) + + # TODO(lbragstad): See if there is another way we can do this instead + # of having every Meeting object build a list of Schedule objects. + m.schedules = [] + for sch in yaml_obj['schedule']: + s = schedule.Schedule(sch) + m.schedules.append(s) + + return m diff --git a/arbiter/tests/__init__.py b/arbiter/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/arbiter/tests/test_meeting.py b/arbiter/tests/test_meeting.py new file mode 100644 index 0000000..bea3e66 --- /dev/null +++ b/arbiter/tests/test_meeting.py @@ -0,0 +1,45 @@ +#! /usr/bin/env python +# -*- coding: utf-8 -*- +# +# Copyright 2014 OpenStack Foundation +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import unittest + +from arbiter import meeting + +_YAML = """ +project: OpenStack Subteam Meeting +schedule: + - time: '1200' + day: Wednesday + irc: openstack-meeting + frequency: weekly +chair: Joe Developer +description: > + Weekly meeting for Subteam project. +agenda: | + * Top bugs this week +""" + + +class MeetingTestCase(unittest.TestCase): + + def test_load_yaml_file(self): + m = meeting.load_meetings(_YAML)[0] + self.assertEqual('OpenStack Subteam Meeting', m.project) + self.assertEqual('Joe Developer', m.chair) + self.assertEqual('Weekly meeting for Subteam project.\n', + m.description) + self.assertEqual('* Top bugs this week\n', m.agenda) diff --git a/arbiter/util.py b/arbiter/util.py index f243be2..7d587ea 100644 --- a/arbiter/util.py +++ b/arbiter/util.py @@ -20,9 +20,8 @@ import os import yaml -import const -from meeting import Meeting - +from arbiter import const +from arbiter import meeting """Utility functions for check and gate jobs.""" @@ -33,26 +32,6 @@ def publish(meeting, ical): pass -def load_meetings(yaml_dir, meeting_list=None): - """Return a list of Meetings initialized from files in yaml_dir.""" - - meetings_yaml = [] - for file_name in os.listdir(yaml_dir): - yaml_file = os.path.join(yaml_dir, file_name) - if not os.path.isfile(yaml_file): - continue - if meeting_list and yaml_file not in meeting_list: - continue - meetings_yaml.append(yaml_file) - - meetings = [Meeting(yaml.load(open(f, 'r')), f) - for f in meetings_yaml] - - logging.info('Loaded %d meetings from YAML' % (len(meetings))) - - return meetings - - def convert_yaml_to_ical(yaml_dir, ical_dir, meeting_list_file=None): """Convert meeting YAML files to the iCal format and place in ical_dir. If meeting_list is specified, only those meetings @@ -60,13 +39,7 @@ def convert_yaml_to_ical(yaml_dir, ical_dir, meeting_list_file=None): converted; otherwise, all meeting in yaml_dir are converted. """ - - meeting_list = None - if meeting_list_file: - meeting_list = open(meeting_list_file).read().splitlines() - - meetings = load_meetings(yaml_dir, - meeting_list) + meetings = meeting.load_meetings(yaml_dir) # convert meetings to a list of ical for m in meetings: @@ -156,12 +129,12 @@ def _read_yaml_files(directory): meetings = [] for file in yaml_files: - meetings.append(Meeting(yaml.load(open(file, 'r')), file)) + meetings.append(meeting.Meeting(yaml.load(open(file, 'r')), file)) logging.info('Loaded %d meetings form YAML' % len(meetings)) schedules = [] - for meeting in meetings: - for schedule in meeting.get_schedule_tuple(): + for m in meetings: + for schedule in m.get_schedule_tuple(): schedules.append(schedule) return schedules diff --git a/test-requirements.txt b/test-requirements.txt index f0dd00b..9a94963 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1 +1,9 @@ hacking>=0.5.6,<0.8 + +coverage>=3.6 +discover +fixtures>=0.3.14 +python-subunit +testrepository>=0.0.17 +testscenarios>=0.4,<0.5 +testtools>=0.9.32 diff --git a/tox.ini b/tox.ini index 8288be3..01dee04 100644 --- a/tox.ini +++ b/tox.ini @@ -3,8 +3,10 @@ minversion = 1.6 envlist = py27,pep8 [testenv] +install_command = pip install -U {opts} {packages} deps = -r{toxinidir}/requirements.txt -r{toxinidir}/test-requirements.txt +commands = python setup.py testr --slowest --testr-args='{posargs}' [testenv:pep8] commands = flake8