view bitten/tests/upgrades.py @ 762:5f0cfee44540

Add new last_activity field to build. I considered reusing stopped, but this seemed cleaner and more obvious, which seems like the right way to go. On the completion of every step, last activity is updated to the current time. All orphaning/invalidation based on slave_timeout happens based on that time, not started. Previously, builds were orphaned if it had been more than slave_timeout (config setting) seconds since they had started, now it's since the last interaction (which is actually what the documentation on the field already says.) This requires a database upgrade. Thanks to Hodgestar for comments. Closes #222. Refs #411.
author wbell
date Sat, 24 Apr 2010 15:11:23 +0000
parents 5f95a6f38490
children 330fa697bc85
line wrap: on
line source
# -*- coding: utf-8 -*-
#
# Copyright (C) 2009 Edgewall Software
# All rights reserved.
#
# This software is licensed as described in the file COPYING, which
# you should have received as part of this distribution. The terms
# are also available at http://bitten.edgewall.org/wiki/License.

import unittest
import logging

import warnings
warnings.filterwarnings('ignore', '^Unknown table')
warnings.filterwarnings('ignore', '^the sets module is deprecated')

from trac.test import EnvironmentStub
from trac.db import Table, Column, Index, DatabaseManager
from bitten.upgrades import update_sequence, drop_index
from bitten import upgrades, main, model

import os
import shutil
import tempfile


class BaseUpgradeTestCase(unittest.TestCase):

    schema = None
    other_tables = []

    def setUp(self):
        self.env = EnvironmentStub()
        if hasattr(self.env, 'dburi'):
            # Trac gained support for testing against different databases in 0.11.5
            # If this support is available, we copy the test db uri configuration
            # into the main test config so it can be picked up by
            # upgrades._parse_scheme()
            self.env.config.set('trac', 'database', self.env.dburi)
        self.env.path = tempfile.mkdtemp()
        logs_dir = self.env.config.get("bitten", "logs_dir", "log/bitten")
        if os.path.isabs(logs_dir):
            raise ValueError("Should not have absolute logs directory for temporary test")
        logs_dir = os.path.join(self.env.path, logs_dir)
        if not os.path.isdir(logs_dir):
            os.makedirs(logs_dir)
        self.logs_dir = logs_dir

        db = self.env.get_db_cnx()
        cursor = db.cursor()

        for table_name in self.other_tables:
            cursor.execute("DROP TABLE IF EXISTS %s" % (table_name,))

        connector, _ = DatabaseManager(self.env)._get_connector()
        for table in self.schema:
            cursor.execute("DROP TABLE IF EXISTS %s" % (table.name,))
            for stmt in connector.to_sql(table):
                cursor.execute(stmt)

        db.commit()

    def tearDown(self):
        shutil.rmtree(self.env.path)
        del self.logs_dir
        del self.env


class LogWatcher(logging.Handler):

    def __init__(self, level=0):
        logging.Handler.__init__(self, level=0)
        self.records = []

    def emit(self, record):
        self.records.append(record)


class UpgradeHelperTestCase(BaseUpgradeTestCase):

    schema = [
        Table('test_update_sequence', key='id')[
            Column('id', auto_increment=True), Column('name'),
        ],
        Table('test_drop_index', key='id')[
            Column('id', type='int'), Column('name', size=20),
            Index(['name'])
        ],
    ]

    def test_update_sequence(self):
        db = self.env.get_db_cnx()
        cursor = db.cursor()

        for rowid, name in [(1, 'a'), (2, 'b'), (3, 'c')]:
            cursor.execute("INSERT INTO test_update_sequence (id, name)"
                " VALUES (%s, %s)", (rowid, name))
        update_sequence(self.env, db, 'test_update_sequence', 'id')

        cursor.execute("INSERT INTO test_update_sequence (name)"
            " VALUES (%s)", ('d',))

        cursor.execute("SELECT id FROM test_update_sequence WHERE name = %s",
            ('d',))
        row = cursor.fetchone()
        self.assertEqual(row[0], 4)

    def test_drop_index(self):
        db = self.env.get_db_cnx()
        cursor = db.cursor()

        cursor.execute("INSERT INTO test_drop_index (id, name)"
            " VALUES (%s, %s)", (1, 'a'))

        def do_drop():
            drop_index(self.env, db, 'test_drop_index', 'test_drop_index_name_idx')

        # dropping the index should succeed the first time and fail the next
        do_drop()
        self.assertRaises(Exception, do_drop)


class UpgradeScriptsTestCase(BaseUpgradeTestCase):

    schema = [
        # Sytem
        Table('system', key='name')[
            Column('name'), Column('value')
        ],
        # Config
        Table('bitten_config', key='name')[
            Column('name'), Column('path'), Column('label'),
            Column('active', type='int'), Column('description')
        ],
        # Platform
        Table('bitten_platform', key='id')[
            Column('id', auto_increment=True), Column('config'), Column('name')
        ],
        Table('bitten_rule', key=('id', 'propname'))[
            Column('id'), Column('propname'), Column('pattern'),
            Column('orderno', type='int')
        ],
        # Build
        Table('bitten_build', key='id')[
            Column('id', auto_increment=True), Column('config'), Column('rev'),
            Column('rev_time', type='int'), Column('platform', type='int'),
            Column('slave'), Column('started', type='int'),
            Column('stopped', type='int'), Column('status', size=1),
            Index(['config', 'rev', 'slave'])
        ],
        Table('bitten_slave', key=('build', 'propname'))[
            Column('build', type='int'), Column('propname'), Column('propvalue')
        ],
        # Build Step
        Table('bitten_step', key=('build', 'name'))[
            Column('build', type='int'), Column('name'), Column('description'),
            Column('status', size=1), Column('log'),
            Column('started', type='int'), Column('stopped', type='int')
        ],
    ]

    other_tables = [
        'bitten_log',
        'bitten_log_message',
        'bitten_report',
        'bitten_report_item',
        'bitten_error',
        'old_step',
        'old_config',
        'old_log_v5',
        'old_log_v8',
        'old_rule',
        'old_build_v11',
    ]

    basic_data = [
        ['system',
            ('name', 'value'), [
                ('bitten_version', '1'),
            ]
        ],
        ['bitten_config',
            ('name',), [
                ('test_config',),
            ]
        ],
        ['bitten_platform',
            ('config', 'name'), [
                ('test_config', 'test_plat'),
            ]
        ],
        ['bitten_build',
            ('config', 'rev', 'platform', 'rev_time'), [
                ('test_config', '123', 1, 456),
            ]
        ],
        ['bitten_step',
            ('build', 'name', 'log'), [
                (1, 'step1', None),
                (1, 'step2', "line1\nline2"),
            ]
        ],
    ]

    def _do_upgrade(self):
        """Do an full upgrade."""
        import inspect
        db = self.env.get_db_cnx()

        versions = sorted(upgrades.map.keys())
        for version in versions:
            for function in upgrades.map.get(version):
                self.assertTrue(inspect.getdoc(function))
                function(self.env, db)

        db.commit()

    def _insert_data(self, data):
        """Insert data for upgrading."""
        db = self.env.get_db_cnx()
        cursor = db.cursor()

        for table, cols, vals in data:
            cursor.executemany("INSERT INTO %s (%s) VALUES (%s)"
                % (table, ','.join(cols),
                ','.join(['%s' for c in cols])),
                vals)

        db.commit()

    def _check_basic_upgrade(self):
        """Check the results of an upgrade of basic data."""
        db = self.env.get_db_cnx()

        configs = list(model.BuildConfig.select(self.env,
            include_inactive=True))
        platforms = list(model.TargetPlatform.select(self.env))
        builds = list(model.Build.select(self.env))
        steps = list(model.BuildStep.select(self.env))
        logs = list(model.BuildLog.select(self.env))

        self.assertEqual(len(configs), 1)
        self.assertEqual(configs[0].name, 'test_config')

        self.assertEqual(len(platforms), 1)
        self.assertEqual(platforms[0].config, 'test_config')
        self.assertEqual(platforms[0].name, 'test_plat')

        self.assertEqual(len(builds), 1)
        self.assertEqual(builds[0].config, 'test_config')
        self.assertEqual(builds[0].rev, '123')
        self.assertEqual(builds[0].platform, 1)
        self.assertEqual(builds[0].rev_time, 456)

        self.assertEqual(len(steps), 2)
        self.assertEqual(steps[0].build, 1)
        self.assertEqual(steps[0].name, 'step1')
        self.assertEqual(steps[1].build, 1)
        self.assertEqual(steps[1].name, 'step2')

        self.assertEqual(len(logs), 1)
        self.assertEqual(logs[0].build, 1)
        self.assertEqual(logs[0].step, 'step2')
        log_file = logs[0].get_log_file(logs[0].filename)
        self.assertEqual(file(log_file, "rU").read(), "line1\nline2\n")

    def test_null_upgrade(self):
        self._do_upgrade()

    def test_basic_upgrade(self):
        self._insert_data(self.basic_data)
        self._do_upgrade()
        self._check_basic_upgrade()

    def test_upgrade_via_buildsetup(self):
        self._insert_data(self.basic_data)
        db = self.env.get_db_cnx()
        build_setup = main.BuildSetup(self.env)
        self.assertTrue(build_setup.environment_needs_upgrade(db))
        build_setup.upgrade_environment(db)
        self._check_basic_upgrade()

        # check bitten table version
        cursor = db.cursor()
        cursor.execute("SELECT value FROM system WHERE name='bitten_version'")
        rows = cursor.fetchall()
        self.assertEqual(rows, [(str(model.schema_version),)])

    def test_fix_log_levels_misnaming(self):
        logfiles = {
            "1.log": "",
            "2.log": "",
            "3.log": "",
            "1.log.level": "info\n",
            "2.log.levels": "info\ninfo\n",
            "3.log.level": "warn\n",
            "3.log.levels": "warn\nwarn\n",
            "4.log.level": "error\n",
        }
        expected_deletions = [
            "4.log.level",
        ]

        for filename, data in logfiles.items():
            path = os.path.join(self.logs_dir, filename)
            logfile = open(path, "w")
            logfile.write(data)
            logfile.close()

        logwatch = LogWatcher(logging.INFO)
        self.env.log.setLevel(logging.INFO)
        self.env.log.addHandler(logwatch)

        upgrades.fix_log_levels_misnaming(self.env, None)

        filenames = sorted(os.listdir(self.logs_dir))
        for filename in filenames:
            path = os.path.join(self.logs_dir, filename)
            origfile = filename in logfiles and filename or filename.replace("levels", "level")
            self.assertEqual(logfiles[origfile], open(path).read())
            self.assertTrue(filename not in expected_deletions)

        self.assertEqual(len(filenames), len(logfiles) - len(expected_deletions))

        logs = sorted(logwatch.records, key=lambda rec: rec.getMessage())
        self.assertEqual(len(logs), 5)
        self.assertTrue(logs[0].getMessage().startswith(
            "Deleted 1 stray log level (0 errors)"))
        self.assertTrue(logs[1].getMessage().startswith(
            "Deleted stray log level file 4.log.level"))
        self.assertTrue(logs[2].getMessage().startswith(
            "Error renaming"))
        self.assertTrue(logs[3].getMessage().startswith(
            "Renamed 1 incorrectly named log level files from previous migrate (1 errors)"))
        self.assertTrue(logs[4].getMessage().startswith(
            "Renamed incorrectly named log level file"))

    def test_remove_stray_log_levels_files(self):
        logfiles = {
            "1.log": "",
            "1.log.levels": "info\n",
            "2.log.levels": "info\ninfo\n",
        }
        expected_deletions = [
            "2.log.levels",
        ]

        for filename, data in logfiles.items():
            path = os.path.join(self.logs_dir, filename)
            logfile = open(path, "w")
            logfile.write(data)
            logfile.close()

        logwatch = LogWatcher(logging.INFO)
        self.env.log.setLevel(logging.INFO)
        self.env.log.addHandler(logwatch)

        upgrades.remove_stray_log_levels_files(self.env, None)

        filenames = sorted(os.listdir(self.logs_dir))
        for filename in filenames:
            path = os.path.join(self.logs_dir, filename)
            self.assertEqual(logfiles[filename], open(path).read())
            self.assertTrue(filename not in expected_deletions)

        self.assertEqual(len(filenames), len(logfiles) - len(expected_deletions))

        logs = sorted(logwatch.records, key=lambda rec: rec.getMessage())
        self.assertEqual(len(logs), 2)
        self.assertTrue(logs[0].getMessage().startswith(
            "Deleted 1 stray log levels (0 errors)"))
        self.assertTrue(logs[1].getMessage().startswith(
            "Deleted stray log levels file 2.log.levels"))


def suite():
    suite = unittest.TestSuite()
    suite.addTest(unittest.makeSuite(UpgradeHelperTestCase, 'test'))
    suite.addTest(unittest.makeSuite(UpgradeScriptsTestCase, 'test'))
    return suite

if __name__ == '__main__':
    unittest.main(defaultTest='suite')
Copyright (C) 2012-2017 Edgewall Software