Esempio n. 1
0
 def test_03_minimal(self):
     """ MINIMAL_METADATA """
     gnats.metadata_level = gnats.MINIMAL_METADATA
     db = Database(self.server, 'testdb', self.conn)
     self.assertEqual(db.name, 'testdb')
     self.assertEqual(db.description, '')
     self.assertEqual(db.builtin('number'), 'number')
     self.assertEqual(len(db.list_fields()), 8)
     self.assertEqual(len(db.initial_entry_fields), 0)
     self.assertEqual(db.fields['synopsis'].lcname, 'synopsis')
     self.assertEqual(db.fields['synopsis'].description, '')
     self.assertEqual(len(db.fields['enum-fld'].values), 0)
Esempio n. 2
0
 def test_03_minimal(self):
     """ MINIMAL_METADATA """
     gnats.metadata_level = gnats.MINIMAL_METADATA
     db = Database(self.server, 'testdb', self.conn)
     self.assertEqual(db.name, 'testdb')
     self.assertEqual(db.description, '')
     self.assertEqual(db.builtin('number'), 'number')
     self.assertEqual(len(db.list_fields()), 8)
     self.assertEqual(len(db.initial_entry_fields), 0)
     self.assertEqual(db.fields['synopsis'].lcname, 'synopsis')
     self.assertEqual(db.fields['synopsis'].description, '')
     self.assertEqual(len(db.fields['enum-fld'].values), 0)
Esempio n. 3
0
 def test_02_no_enum(self):
     """ NO_ENUM_METADATA """
     gnats.metadata_level = gnats.NO_ENUM_METADATA
     db = Database(self.server, 'testdb', self.conn)
     self.assertEqual(db.name, 'testdb')
     self.assertEqual(db.description, 'Fake Database')
     self.assertEqual(len(db.list_fields()), 8)
     self.assertEqual(db.builtin('number'), 'number')
     self.assertRaises(GnatsException, db._validate, 1, 1)
     self.assertRaises(GnatsException, db.validate_field, 1, 1)
     self.assertEqual(len(db.initial_entry_fields), 4)
     self.assertEqual(db.fields['synopsis'].lcname, 'synopsis')
     self.assertEqual(db.fields['synopsis'].name, 'Synopsis')
     self.assertEqual(db.fields['synopsis'].description, 'Synopsis field')
     self.assertEqual(len(db.fields['enum-fld'].values), 0)
     self.assertRaises(GnatsException, db.fields['enum-fld'].list_values)
Esempio n. 4
0
 def test_02_no_enum(self):
     """ NO_ENUM_METADATA """
     gnats.metadata_level = gnats.NO_ENUM_METADATA
     db = Database(self.server, 'testdb', self.conn)
     self.assertEqual(db.name, 'testdb')
     self.assertEqual(db.description, 'Fake Database')
     self.assertEqual(len(db.list_fields()), 8)
     self.assertEqual(db.builtin('number'), 'number')
     self.assertRaises(GnatsException, db._validate, 1, 1)
     self.assertRaises(GnatsException, db.validate_field, 1, 1)
     self.assertEqual(len(db.initial_entry_fields), 4)
     self.assertEqual(db.fields['synopsis'].lcname, 'synopsis')
     self.assertEqual(db.fields['synopsis'].name, 'Synopsis')
     self.assertEqual(db.fields['synopsis'].description,
                      'Synopsis field')
     self.assertEqual(len(db.fields['enum-fld'].values), 0)
     self.assertRaises(GnatsException, db.fields['enum-fld'].list_values)
Esempio n. 5
0
class T04_DatabaseMethods(unittest.TestCase):
    """ Test Database utility methods """
    def setUp(self):
        self.server = gnats.Server('somehost')
        self.conn = FakeServerConnectionForDB(self.server)
        self.db = Database(self.server, 'testdb', self.conn)

    def test_01_get_handle(self):
        """ get_handle returns DatabaseHandle """
        self.assertTrue(
            isinstance(self.db.get_handle('user', 'pass', self.conn),
                       DatabaseHandle))

    def test_02_get_handle_doesnt_refresh(self):
        """ get_handle uses cached metadata when not expired """
        # Change the dbdesc method to return something we can check
        self.conn.dbdesc = lambda __: 'changed database'
        self.db.get_handle('user', 'pass', self.conn)
        self.assertNotEquals(self.db.description, 'changed database')

    def test_03_get_handle_refreshes(self):
        """ get_handle refreshes metadata when expired """
        # "Refresh" the conn, so that metadata can run again
        conn = FakeServerConnectionForDB(self.server)
        # change the config time returned by conn, which should trigger refresh
        conn.cfgt = lambda: u'10000'
        # Change the dbdesc method to return something we can check
        conn.dbdesc = lambda __: 'changed database'
        self.assertEquals(self.db.last_config_time, u'1000')
        self.db.get_handle('user', passwd='pass', conn=conn)
        self.assertEquals(self.db.description, 'changed database')
        self.assertEquals(self.db.last_config_time, u'10000')

    def test_03a_no_cfgt(self):
        """ get_handle refresh works when CFGT not implemented """
        # "Refresh" the conn, so that metadata can run again
        conn = FakeServerConnectionForDB(self.server)

        # change the config time returned by conn, which should trigger refresh
        def fake_cfgt():
            raise gnats.GnatsException('boo')

        conn.cfgt = fake_cfgt
        # Change the dbdesc method to return something we can check
        conn.dbdesc = lambda __: 'changed database'
        self.db.get_handle('user', passwd='pass', conn=conn)
        self.assertEquals(self.db.description, 'changed database')

    def test_04_list_fields(self):
        """ list_fields """
        flist = self.db.list_fields()
        self.assertEqual(len(flist), 8)
        self.assertEqual(flist[1].name, 'Synopsis')
        self.assertEqual(flist[2].ftype, 'enum')

    def test_05_build_format(self):
        """ build_format """
        self.assertEqual(self.db.build_format(['synopsis', 'enum-fld']),
                         '"%s\037%s\036" synopsis enum-fld')

    def test_06_build_format_builtin(self):
        """ build_format with builtinfield:number """
        self.assertEqual(
            self.db.build_format(['builtinfield:number', 'enum-fld']),
            '"%s\037%s\036" builtinfield:number enum-fld')

    def test_07_build_format_date(self):
        """ build_format with date """
        self.assertEqual(
            self.db.build_format(['synopsis', 'last-modified']),
            '"%s\037%{%Y-%m-%d %H:%M:%S %Z}D\036" synopsis last-modified')

    def test_08_build_format_unknown(self):
        """ build_format with unknown field """
        self.assertEqual(
            self.db.build_format(['synopsis', 'enum-fld', 'fred']),
            '"%s\037%s\037%s\036" synopsis enum-fld fred')

    def test_09_build_format_table_field(self):
        """ build_format with bare table field colnames"""
        self.assertEqual(
            self.db.build_format(['username', 'datetime'],
                                 table_field='change-log'),
            '"%s\034%{%Y-%m-%d %H:%M:%S %Z}D\035" username datetime')

    def test_09a_build_format_table_columns(self):
        """ build_format with qualified table field colnames """
        self.assertEqual(
            self.db.build_format(
                ['change-log.username', 'change-log.datetime'],
                table_field='change-log'),
            '"%s\034%{%Y-%m-%d %H:%M:%S %Z}D\035" '
            'change-log.username change-log.datetime')

    def test_10_unparse_pr_no_from(self):
        """ unparse_pr raises if from: header missing """
        #text = {'state':'open', 'synopsis':'This is a test',}
        #expected = ">Synopsis: This is a test\n>State: open"
        #self.assertEquals(self.conn.unparse_pr(text), expected)
        pr = {"foo": "bar"}
        self.assertRaises(gnats.GnatsException, self.db.unparse_pr, pr)

    def test_11_unparse_pr_no_number(self):
        """ unparse_pr without Number field """
        # Number Synopsis Enum-fld Scoped-Enum-fld MultiEnum-fld Multitext-fld Last-Modified
        pr = {
            'enum-fld': 'gnats',
            'synopsis': 'This is a test',
            'multienum-fld': '',
            'multitext-fld': 'some long\ntext.',
            'last-modified': '2008-01-01',
            'identifier': [
                ('1', {
                    'scoped-enum-fld': 'open'
                }),
            ],
            'from:': 'me',
        }
        expected = """From: me

>Synopsis:       This is a test
>Enum-fld:       gnats
>MultiEnum-fld:  \n>Multitext-fld:
some long
text.
>Scoped-Enum-fld{1}: open"""
        self.assertEquals(self.db.unparse_pr(pr), expected)

    def test_12_unparse_pr_number(self):
        """ unparse_pr with Number field """
        # Number Synopsis Enum-fld Scoped-Enum-fld MultiEnum-fld Multitext-fld Last-Modified
        pr = {
            'number': '100',
            'enum-fld': 'gnats',
            'synopsis': 'This is a test',
            'multitext-fld': 'some long\ntext.',
            'last-modified': '2008-01-01',
            'identifier': [
                ('1', {
                    'scoped-enum-fld': 'open'
                }),
            ],
            'from:': 'me',
        }
        expected = """From: me

>Number:         100
>Synopsis:       This is a test
>Enum-fld:       gnats
>Multitext-fld:
some long
text.
>Scoped-Enum-fld{1}: open"""
        self.assertEquals(self.db.unparse_pr(pr), expected)

    def test_13_unparse_pr_empty_multi(self):
        """ unparse_pr with empty multitext field """
        # Number Synopsis Enum-fld Scoped-Enum-fld MultiEnum-fld Multitext-fld Last-Modified
        pr = {
            'number': '100',
            'enum-fld': 'gnats',
            'synopsis': 'This is a test',
            'last-modified': '2008-01-01',
            'multitext-fld': '',
            'identifier': [
                ('1', {
                    'scoped-enum-fld': 'open'
                }),
            ],
            'from:': 'me',
        }
        expected = """From: me

>Number:         100
>Synopsis:       This is a test
>Enum-fld:       gnats
>Multitext-fld:
>Scoped-Enum-fld{1}: open"""
        self.assertEquals(self.db.unparse_pr(pr), expected)

    def test_14_unparse_pr_change_reason(self):
        """ unparse_pr with change-reason """
        # Number Synopsis Enum-fld Scoped-Enum-fld MultiEnum-fld Multitext-fld Last-Modified
        pr = {
            'number': '100',
            'enum-fld': 'gnats',
            'enum-fld-changed-why': 'boo',
            'synopsis': 'This is a test',
            'last-modified': '2008-01-01',
            'identifier': [
                ('1', {
                    'scoped-enum-fld': 'open'
                }),
            ],
            'from:': 'me',
        }
        expected = """From: me

>Number:         100
>Synopsis:       This is a test
>Enum-fld:       gnats
>Enum-fld-Changed-Why:
boo
>Scoped-Enum-fld{1}: open"""
        self.assertEquals(self.db.unparse_pr(pr), expected)

    def test_15_unparse_pr_scoped_change_reason(self):
        """ unparse_pr with scoped change-reason """
        # Number Synopsis Enum-fld Scoped-Enum-fld MultiEnum-fld Multitext-fld Last-Modified
        pr = {
            'number':
            '100',
            'enum-fld':
            'gnats',
            'multienum-fld':
            '',
            'synopsis':
            'This is a test',
            'last-modified':
            '2008-01-01',
            'identifier': [
                ('1', {
                    'scoped-enum-fld': 'open',
                    'scoped-enum-fld-changed-why': 'boo',
                }),
            ],
            'from:':
            'me',
        }
        expected = """From: me

>Number:         100
>Synopsis:       This is a test
>Enum-fld:       gnats
>MultiEnum-fld:  \n>Scoped-Enum-fld{1}: open
>Scoped-Enum-fld-Changed-Why{1}:
boo"""
        self.assertEquals(self.db.unparse_pr(pr), expected)

    def test_16_unparse_pr_bogus_scope(self):
        """ unparse_pr with bogus scope """
        # Number Synopsis Enum-fld Scoped-Enum-fld MultiEnum-fld Multitext-fld Last-Modified
        pr = {
            'number': '100',
            'enum-fld': 'gnats',
            'multienum-fld': 'foo:bar',
            'synopsis': 'This is a test',
            'last-modified': '2008-01-01',
            'identifier': [
                ('', {
                    'scoped-enum-fld': 'open',
                }),
            ],
            'from:': 'me',
        }
        expected = """From: me

>Number:         100
>Synopsis:       This is a test
>Enum-fld:       gnats
>MultiEnum-fld:  foo:bar"""
        self.assertEquals(self.db.unparse_pr(pr), expected)

    def test_17_unparse_pr_fake_read_only(self):
        """ unparse_pr with fake read-only field """
        # Number Synopsis Enum-fld Scoped-Enum-fld MultiEnum-fld Multitext-fld Last-Modified
        pr = {
            'number': '100',
            'enum-fld': 'gnats',
            'synopsis': 'This is a test',
            'multitext-fld': 'some long\ntext.',
            'last-modified': '2008-01-01',
            'identifier': [
                ('1', {
                    'scoped-enum-fld': 'open'
                }),
            ],
            'from:': 'me',
        }
        expected = """From: me

>Number:         100
>Synopsis:       This is a test
>Enum-fld:       gnats
>Multitext-fld:
some long
text.
>Scoped-Enum-fld{1}: open"""
        self.db.fields['synopsis'].read_only = True
        self.assertEquals(self.db.unparse_pr(pr), expected)

    def test_18_unparse_pr_table_field(self):
        """ unparse_pr skips table field even if not read_only """
        # Number Synopsis Enum-fld Scoped-Enum-fld MultiEnum-fld Multitext-fld Last-Modified

        pr = {
            'number': '100',
            'enum-fld': 'gnats',
            'synopsis': 'This is a test',
            'multitext-fld': 'some long\ntext.',
            'last-modified': '2008-01-01',
            'identifier': [
                ('1', {
                    'scoped-enum-fld': 'open'
                }),
            ],
            'from:': 'me',
            'change-log': 'foo',
        }
        expected = """From: me

>Number:         100
>Synopsis:       This is a test
>Enum-fld:       gnats
>Multitext-fld:
some long
text.
>Scoped-Enum-fld{1}: open"""
        self.db.fields['change-log'].read_only = False
        self.assertEquals(self.db.unparse_pr(pr), expected)

    def test_19_unparse_pr_multienum_as_list(self):
        """ unparse_pr handles multienums given as lists """
        pr = {
            'number': '100',
            'enum-fld': 'gnats',
            'synopsis': 'This is a test',
            'multienum-fld': ['foo', 'bar'],
            'last-modified': '2008-01-01',
            'identifier': [
                ('1', {
                    'scoped-enum-fld': 'open'
                }),
            ],
            'from:': 'me',
            'change-log': 'foo',
        }
        expected = """From: me

>Number:         100
>Synopsis:       This is a test
>Enum-fld:       gnats
>MultiEnum-fld:  foo:bar
>Scoped-Enum-fld{1}: open"""
        self.assertEquals(self.db.unparse_pr(pr), expected)

    def test_20_unparse_pr_trailing_whitespace_multi(self):
        """ unparse_pr, multitext field with trailing whitespace """
        # Number Synopsis Enum-fld Scoped-Enum-fld MultiEnum-fld Multitext-fld Last-Modified
        pr = {
            'number': '100',
            'enum-fld': 'gnats',
            'synopsis': 'This is a test',
            'last-modified': '2008-01-01',
            'multitext-fld': 'a\ntest\n\n',
            'identifier': [
                ('1', {
                    'scoped-enum-fld': 'open'
                }),
            ],
            'from:': 'me',
        }
        expected = """From: me

>Number:         100
>Synopsis:       This is a test
>Enum-fld:       gnats
>Multitext-fld:
a
test

>Scoped-Enum-fld{1}: open"""
        self.assertEquals(self.db.unparse_pr(pr), expected)

    def test_21_builtin(self):
        """ Check that builtin() works for last-modified """
        self.assertEqual(self.db.builtin('last-modified'), 'last-modified')

    def test_22_builtin_non_existent(self):
        """ Check that builtin() returns empty string for non-existent field """
        self.assertEqual(self.db.builtin('fred'), '')

    def test_23_add_space_beggining_multiline(self):
        """ unparse_pr, add a space in the beginning of multitext field, if it
        contains data in the format '>fld: fld-value'. """
        # Number Synopsis Enum-fld Scoped-Enum-fld MultiEnum-fld Multitext-fld Last-Modified
        pr = {
            'number': '100',
            'enum-fld': 'gnats',
            'synopsis': 'This is a test',
            'last-modified': '2008-01-01',
            'multitext-fld': 'some text\n>from: fld-value\n>fld: fld-value',
            'identifier': [
                ('1', {
                    'scoped-enum-fld': 'open'
                }),
            ],
            'from:': 'me',
        }
        expected = """From: me

>Number:         100
>Synopsis:       This is a test
>Enum-fld:       gnats
>Multitext-fld:
some text
 >from: fld-value
 >fld: fld-value
>Scoped-Enum-fld{1}: open"""
        self.assertEquals(self.db.unparse_pr(pr), expected)
Esempio n. 6
0
class T04_DatabaseMethods(unittest.TestCase):
    """ Test Database utility methods """

    def setUp(self):
        self.server = gnats.Server('somehost')
        self.conn = FakeServerConnectionForDB(self.server)
        self.db = Database(self.server, 'testdb', self.conn)

    def test_01_get_handle(self):
        """ get_handle returns DatabaseHandle """
        self.assertTrue(isinstance(self.db.get_handle('user', 'pass', self.conn),
                        DatabaseHandle))

    def test_02_get_handle_doesnt_refresh(self):
        """ get_handle uses cached metadata when not expired """
        # Change the dbdesc method to return something we can check
        self.conn.dbdesc = lambda __: 'changed database'
        self.db.get_handle('user', 'pass', self.conn)
        self.assertNotEquals(self.db.description, 'changed database')

    def test_03_get_handle_refreshes(self):
        """ get_handle refreshes metadata when expired """
        # "Refresh" the conn, so that metadata can run again
        conn = FakeServerConnectionForDB(self.server)
        # change the config time returned by conn, which should trigger refresh
        conn.cfgt = lambda: u'10000'
        # Change the dbdesc method to return something we can check
        conn.dbdesc = lambda __: 'changed database'
        self.assertEquals(self.db.last_config_time, u'1000')
        self.db.get_handle('user', passwd='pass', conn=conn)
        self.assertEquals(self.db.description, 'changed database')
        self.assertEquals(self.db.last_config_time, u'10000')

    def test_03a_no_cfgt(self):
        """ get_handle refresh works when CFGT not implemented """
        # "Refresh" the conn, so that metadata can run again
        conn = FakeServerConnectionForDB(self.server)
        # change the config time returned by conn, which should trigger refresh
        def fake_cfgt():
            raise gnats.GnatsException('boo')
        conn.cfgt = fake_cfgt
        # Change the dbdesc method to return something we can check
        conn.dbdesc = lambda __: 'changed database'
        self.db.get_handle('user', passwd='pass', conn=conn)
        self.assertEquals(self.db.description, 'changed database')

    def test_04_list_fields(self):
        """ list_fields """
        flist = self.db.list_fields()
        self.assertEqual(len(flist), 8)
        self.assertEqual(flist[1].name, 'Synopsis')
        self.assertEqual(flist[2].ftype, 'enum')

    def test_05_build_format(self):
        """ build_format """
        self.assertEqual(self.db.build_format(['synopsis', 'enum-fld']),
                         '"%s\037%s\036" synopsis enum-fld')

    def test_06_build_format_builtin(self):
        """ build_format with builtinfield:number """
        self.assertEqual(self.db.build_format(['builtinfield:number',
                                               'enum-fld']),
                 '"%s\037%s\036" builtinfield:number enum-fld')

    def test_07_build_format_date(self):
        """ build_format with date """
        self.assertEqual(self.db.build_format(['synopsis', 'last-modified']),
            '"%s\037%{%Y-%m-%d %H:%M:%S %Z}D\036" synopsis last-modified')

    def test_08_build_format_unknown(self):
        """ build_format with unknown field """
        self.assertEqual(self.db.build_format(['synopsis', 'enum-fld', 'fred']),
            '"%s\037%s\037%s\036" synopsis enum-fld fred')

    def test_09_build_format_table_field(self):
        """ build_format with bare table field colnames"""
        self.assertEqual(self.db.build_format(['username', 'datetime'],
                                              table_field='change-log'),
            '"%s\034%{%Y-%m-%d %H:%M:%S %Z}D\035" username datetime')

    def test_09a_build_format_table_columns(self):
        """ build_format with qualified table field colnames """
        self.assertEqual(
            self.db.build_format(['change-log.username', 'change-log.datetime'],
                                 table_field='change-log'),
            '"%s\034%{%Y-%m-%d %H:%M:%S %Z}D\035" '
            'change-log.username change-log.datetime')

    def test_10_unparse_pr_no_from(self):
        """ unparse_pr raises if from: header missing """
        #text = {'state':'open', 'synopsis':'This is a test',}
        #expected = ">Synopsis: This is a test\n>State: open"
        #self.assertEquals(self.conn.unparse_pr(text), expected)
        pr = {"foo":"bar"}
        self.assertRaises(gnats.GnatsException, self.db.unparse_pr, pr)

    def test_11_unparse_pr_no_number(self):
        """ unparse_pr without Number field """
        # Number Synopsis Enum-fld Scoped-Enum-fld MultiEnum-fld Multitext-fld Last-Modified
        pr = {'enum-fld':'gnats', 'synopsis':'This is a test', 'multienum-fld':'',
              'multitext-fld':'some long\ntext.', 'last-modified':'2008-01-01',
              'identifier':[('1', {'scoped-enum-fld':'open'}),], 'from:':'me',}
        expected = """From: me

>Synopsis:       This is a test
>Enum-fld:       gnats
>MultiEnum-fld:  \n>Multitext-fld:
some long
text.
>Scoped-Enum-fld{1}: open"""
        self.assertEquals(self.db.unparse_pr(pr), expected)

    def test_12_unparse_pr_number(self):
        """ unparse_pr with Number field """
        # Number Synopsis Enum-fld Scoped-Enum-fld MultiEnum-fld Multitext-fld Last-Modified
        pr = {'number':'100', 'enum-fld':'gnats', 'synopsis':'This is a test',
              'multitext-fld':'some long\ntext.', 'last-modified':'2008-01-01',
              'identifier':[('1', {'scoped-enum-fld':'open'}),], 'from:':'me',}
        expected = """From: me

>Number:         100
>Synopsis:       This is a test
>Enum-fld:       gnats
>Multitext-fld:
some long
text.
>Scoped-Enum-fld{1}: open"""
        self.assertEquals(self.db.unparse_pr(pr), expected)

    def test_13_unparse_pr_empty_multi(self):
        """ unparse_pr with empty multitext field """
        # Number Synopsis Enum-fld Scoped-Enum-fld MultiEnum-fld Multitext-fld Last-Modified
        pr = {'number':'100', 'enum-fld':'gnats', 'synopsis':'This is a test',
              'last-modified':'2008-01-01', 'multitext-fld':'',
              'identifier':[('1', {'scoped-enum-fld':'open'}),], 'from:':'me',}
        expected = """From: me

>Number:         100
>Synopsis:       This is a test
>Enum-fld:       gnats
>Multitext-fld:
>Scoped-Enum-fld{1}: open"""
        self.assertEquals(self.db.unparse_pr(pr), expected)

    def test_14_unparse_pr_change_reason(self):
        """ unparse_pr with change-reason """
        # Number Synopsis Enum-fld Scoped-Enum-fld MultiEnum-fld Multitext-fld Last-Modified
        pr = {'number':'100', 'enum-fld':'gnats', 'enum-fld-changed-why':'boo',
              'synopsis':'This is a test', 'last-modified':'2008-01-01',
              'identifier':[('1', {'scoped-enum-fld':'open'}),], 'from:':'me',}
        expected = """From: me

>Number:         100
>Synopsis:       This is a test
>Enum-fld:       gnats
>Enum-fld-Changed-Why:
boo
>Scoped-Enum-fld{1}: open"""
        self.assertEquals(self.db.unparse_pr(pr), expected)

    def test_15_unparse_pr_scoped_change_reason(self):
        """ unparse_pr with scoped change-reason """
        # Number Synopsis Enum-fld Scoped-Enum-fld MultiEnum-fld Multitext-fld Last-Modified
        pr = {'number':'100', 'enum-fld':'gnats', 'multienum-fld':'',
              'synopsis':'This is a test', 'last-modified':'2008-01-01',
              'identifier':[('1', {'scoped-enum-fld':'open',
                   'scoped-enum-fld-changed-why':'boo',}),], 'from:':'me',}
        expected = """From: me

>Number:         100
>Synopsis:       This is a test
>Enum-fld:       gnats
>MultiEnum-fld:  \n>Scoped-Enum-fld{1}: open
>Scoped-Enum-fld-Changed-Why{1}:
boo"""
        self.assertEquals(self.db.unparse_pr(pr), expected)

    def test_16_unparse_pr_bogus_scope(self):
        """ unparse_pr with bogus scope """
        # Number Synopsis Enum-fld Scoped-Enum-fld MultiEnum-fld Multitext-fld Last-Modified
        pr = {'number':'100', 'enum-fld':'gnats', 'multienum-fld':'foo:bar',
              'synopsis':'This is a test', 'last-modified':'2008-01-01',
              'identifier':[('', {'scoped-enum-fld':'open',}),], 'from:':'me',}
        expected = """From: me

>Number:         100
>Synopsis:       This is a test
>Enum-fld:       gnats
>MultiEnum-fld:  foo:bar"""
        self.assertEquals(self.db.unparse_pr(pr), expected)

    def test_17_unparse_pr_fake_read_only(self):
        """ unparse_pr with fake read-only field """
        # Number Synopsis Enum-fld Scoped-Enum-fld MultiEnum-fld Multitext-fld Last-Modified
        pr = {'number':'100', 'enum-fld':'gnats', 'synopsis':'This is a test',
              'multitext-fld':'some long\ntext.', 'last-modified':'2008-01-01',
              'identifier':[('1', {'scoped-enum-fld':'open'}),], 'from:':'me',}
        expected = """From: me

>Number:         100
>Synopsis:       This is a test
>Enum-fld:       gnats
>Multitext-fld:
some long
text.
>Scoped-Enum-fld{1}: open"""
        self.db.fields['synopsis'].read_only = True
        self.assertEquals(self.db.unparse_pr(pr), expected)

    def test_18_unparse_pr_table_field(self):
        """ unparse_pr skips table field even if not read_only """
        # Number Synopsis Enum-fld Scoped-Enum-fld MultiEnum-fld Multitext-fld Last-Modified

        pr = {'number':'100', 'enum-fld':'gnats', 'synopsis':'This is a test',
              'multitext-fld':'some long\ntext.', 'last-modified':'2008-01-01',
              'identifier':[('1', {'scoped-enum-fld':'open'}),], 'from:':'me',
              'change-log':'foo',}
        expected = """From: me

>Number:         100
>Synopsis:       This is a test
>Enum-fld:       gnats
>Multitext-fld:
some long
text.
>Scoped-Enum-fld{1}: open"""
        self.db.fields['change-log'].read_only = False
        self.assertEquals(self.db.unparse_pr(pr), expected)

    def test_19_unparse_pr_multienum_as_list(self):
        """ unparse_pr handles multienums given as lists """
        pr = {'number':'100', 'enum-fld':'gnats', 'synopsis':'This is a test',
              'multienum-fld':['foo', 'bar'], 'last-modified':'2008-01-01',
              'identifier':[('1', {'scoped-enum-fld':'open'}),], 'from:':'me',
              'change-log':'foo',}
        expected = """From: me

>Number:         100
>Synopsis:       This is a test
>Enum-fld:       gnats
>MultiEnum-fld:  foo:bar
>Scoped-Enum-fld{1}: open"""
        self.assertEquals(self.db.unparse_pr(pr), expected)

    def test_20_unparse_pr_trailing_whitespace_multi(self):
        """ unparse_pr, multitext field with trailing whitespace """
        # Number Synopsis Enum-fld Scoped-Enum-fld MultiEnum-fld Multitext-fld Last-Modified
        pr = {'number':'100', 'enum-fld':'gnats', 'synopsis':'This is a test',
              'last-modified':'2008-01-01', 'multitext-fld':'a\ntest\n\n',
              'identifier':[('1', {'scoped-enum-fld':'open'}),], 'from:':'me',}
        expected = """From: me

>Number:         100
>Synopsis:       This is a test
>Enum-fld:       gnats
>Multitext-fld:
a
test

>Scoped-Enum-fld{1}: open"""
        self.assertEquals(self.db.unparse_pr(pr), expected)

    def test_21_builtin(self):
        """ Check that builtin() works for last-modified """
        self.assertEqual(self.db.builtin('last-modified'), 'last-modified')

    def test_22_builtin_non_existent(self):
        """ Check that builtin() returns empty string for non-existent field """
        self.assertEqual(self.db.builtin('fred'), '')

    def test_23_add_space_beggining_multiline(self):
        """ unparse_pr, add a space in the beginning of multitext field, if it
        contains data in the format '>fld: fld-value'. """
        # Number Synopsis Enum-fld Scoped-Enum-fld MultiEnum-fld Multitext-fld Last-Modified
        pr = {'number':'100', 'enum-fld':'gnats', 'synopsis':'This is a test',
              'last-modified':'2008-01-01',
              'multitext-fld':'some text\n>from: fld-value\n>fld: fld-value',
              'identifier':[('1', {'scoped-enum-fld':'open'}),], 'from:':'me',}
        expected = """From: me

>Number:         100
>Synopsis:       This is a test
>Enum-fld:       gnats
>Multitext-fld:
some text
 >from: fld-value
 >fld: fld-value
>Scoped-Enum-fld{1}: open"""
        self.assertEquals(self.db.unparse_pr(pr), expected)