/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar
0.147.1 by John Arbash Meinel
Improve the committer matcher tremendously.
1
# Copyright (C) 2006-2010 Canonical Ltd
0.140.26 by Jelmer Vernooij
Add copyright headers.
2
3
# This program is free software; you can redistribute it and/or modify
4
# it under the terms of the GNU General Public License as published by
5
# the Free Software Foundation; either version 2 of the License, or
6
# (at your option) any later version.
7
8
# This program is distributed in the hope that it will be useful,
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11
# GNU General Public License for more details.
12
13
# You should have received a copy of the GNU General Public License
14
# along with this program; if not, write to the Free Software
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
0.140.1 by John Arbash Meinel
A simple plugin for generating author statistics, may grow into more.
16
"""A Simple bzr plugin to generate statistics about the history."""
17
0.143.1 by John Arbash Meinel
Make a lot of imports lazy since they may not actually be used.
18
from bzrlib.lazy_import import lazy_import
19
lazy_import(globals(), """
20
from bzrlib import (
21
    branch,
22
    commands,
23
    config,
24
    errors,
0.140.18 by Jelmer Vernooij
Add credits command, test classify code by default, add comments to classify code.
25
    option,
0.147.1 by John Arbash Meinel
Improve the committer matcher tremendously.
26
    trace,
0.143.1 by John Arbash Meinel
Make a lot of imports lazy since they may not actually be used.
27
    tsort,
0.144.1 by Wesley J. Landaker
Added ui to bzrlib lazy imports.
28
    ui,
0.143.1 by John Arbash Meinel
Make a lot of imports lazy since they may not actually be used.
29
    workingtree,
30
    )
0.140.18 by Jelmer Vernooij
Add credits command, test classify code by default, add comments to classify code.
31
from bzrlib.plugins.stats.classify import classify_delta
32
from itertools import izip
0.143.1 by John Arbash Meinel
Make a lot of imports lazy since they may not actually be used.
33
""")
0.140.3 by John Arbash Meinel
Updated to combine by author name, as well as by email address, and report on multiple names/addresses
34
35
0.147.1 by John Arbash Meinel
Improve the committer matcher tremendously.
36
def collapse_by_person(revisions, canonical_committer):
0.140.16 by Jelmer Vernooij
Rename collapse_by_author -> collapse_by_person since author has an unambigous meaning
37
    """The committers list is sorted by email, fix it up by person.
0.140.3 by John Arbash Meinel
Updated to combine by author name, as well as by email address, and report on multiple names/addresses
38
39
    Some people commit with a similar username, but different email
40
    address. Which makes it hard to sort out when they have multiple
41
    entries. Email is actually more stable, though, since people
42
    frequently forget to set their name properly.
43
44
    So take the most common username for each email address, and
45
    combine them into one new list.
46
    """
0.147.1 by John Arbash Meinel
Improve the committer matcher tremendously.
47
    # Map from canonical committer to
48
    # {committer: ([rev_list], {email: count}, {fname:count})}
49
    committer_to_info = {}
50
    for rev in revisions:
51
        authors = rev.get_apparent_authors()
52
        for author in authors:
53
            username, email = config.parse_username(author)
54
            canon_author = canonical_committer[(username, email)]
55
            info = committer_to_info.setdefault(canon_author, ([], {}, {}))
56
            info[0].append(rev)
57
            info[1][email] = info[1].setdefault(email, 0) + 1
58
            info[2][username] = info[2].setdefault(username, 0) + 1
59
    res = [(len(revs), revs, emails, fnames)
60
           for revs, emails, fnames in committer_to_info.itervalues()]
61
    res.sort(reverse=True)
62
    return res
63
64
65
def collapse_email_and_users(email_users, combo_count):
66
    """Combine the mapping of User Name to email and email to User Name.
67
68
    If a given User Name is used for multiple emails, try to map it all to one
69
    entry.
70
    """
71
    id_to_combos = {}
72
    username_to_id = {}
73
    email_to_id = {}
74
    id_counter = 0
75
76
    def collapse_ids(old_id, new_id, new_combos):
77
        old_combos = id_to_combos.pop(old_id)
78
        new_combos.update(old_combos)
79
        for old_user, old_email in old_combos:
80
            if (old_user and old_user != user):
81
                old_user_id = username_to_id[old_user]
82
                assert old_user_id in (old_id, new_id)
83
                username_to_id[old_user] = new_id
84
            if (old_email and old_email != email):
85
                old_email_id = email_to_id[old_email]
86
                assert old_email_id in (old_id, new_id)
87
                email_to_id[old_email] = cur_id
88
    for email, usernames in email_users.iteritems():
89
        assert email not in email_to_id
0.147.2 by John Arbash Meinel
Handle the case where we have a user but no email.
90
        if not email:
91
            # We use a different algorithm for usernames that have no email
92
            # address, we just try to match by username, and not at all by
93
            # email
94
            for user in usernames:
95
                if not user:
96
                    continue # The mysterious ('', '') user
97
                user_id = username_to_id.get(user)
98
                if user_id is None:
99
                    id_counter += 1
100
                    user_id = id_counter
101
                    username_to_id[user] = user_id
102
                    id_to_combos[user_id] = id_combos = set()
103
                else:
104
                    id_combos = id_combos[user_id]
105
                id_combos.add((user, email))
106
            continue
107
0.147.1 by John Arbash Meinel
Improve the committer matcher tremendously.
108
        id_counter += 1
109
        cur_id = id_counter
110
        id_to_combos[cur_id] = id_combos = set()
0.147.2 by John Arbash Meinel
Handle the case where we have a user but no email.
111
        email_to_id[email] = cur_id
0.147.1 by John Arbash Meinel
Improve the committer matcher tremendously.
112
113
        for user in usernames:
114
            combo = (user, email)
115
            id_combos.add(combo)
0.147.2 by John Arbash Meinel
Handle the case where we have a user but no email.
116
            if not user:
117
                # We don't match on empty usernames
0.147.1 by John Arbash Meinel
Improve the committer matcher tremendously.
118
                continue
119
            user_id = username_to_id.get(user)
120
            if user_id is not None:
121
                # This UserName was matched to an cur_id
122
                if user_id != cur_id:
123
                    # And it is a different identity than the current email
124
                    collapse_ids(user_id, cur_id, id_combos)
125
            username_to_id[user] = cur_id
126
    combo_to_best_combo = {}
127
    for cur_id, combos in id_to_combos.iteritems():
128
        best_combo = sorted(combos,
129
                            key=lambda x:combo_count[x],
130
                            reverse=True)[0]
131
        for combo in combos:
132
            combo_to_best_combo[combo] = best_combo
133
    return combo_to_best_combo
134
135
136
def get_revisions_and_committers(a_repo, revids):
137
    """Get the Revision information, and the best-match for committer."""
138
139
    email_users = {} # user@email.com => User Name
140
    combo_count = {}
0.144.1 by Wesley J. Landaker
Added ui to bzrlib lazy imports.
141
    pb = ui.ui_factory.nested_progress_bar()
0.142.2 by Jelmer Vernooij
Split out functionality that sorts revids by commmitter.
142
    try:
0.147.1 by John Arbash Meinel
Improve the committer matcher tremendously.
143
        trace.note('getting revisions')
0.142.2 by Jelmer Vernooij
Split out functionality that sorts revids by commmitter.
144
        revisions = a_repo.get_revisions(revids)
145
        for count, rev in enumerate(revisions):
146
            pb.update('checking', count, len(revids))
0.140.29 by Jelmer Vernooij
Remove some uses of get_apparent_author.
147
            for author in rev.get_apparent_authors():
0.147.1 by John Arbash Meinel
Improve the committer matcher tremendously.
148
                # XXX: There is a chance sometimes with svn imports that the
149
                #      full name and email can BOTH be blank.
150
                username, email = config.parse_username(author)
151
                email_users.setdefault(email, set()).add(username)
152
                combo = (username, email)
153
                combo_count[combo] = combo_count.setdefault(combo, 0) + 1
0.142.2 by Jelmer Vernooij
Split out functionality that sorts revids by commmitter.
154
    finally:
155
        pb.finished()
0.147.1 by John Arbash Meinel
Improve the committer matcher tremendously.
156
    return revisions, collapse_email_and_users(email_users, combo_count)
0.142.2 by Jelmer Vernooij
Split out functionality that sorts revids by commmitter.
157
158
0.140.6 by John Arbash Meinel
refactor in preparation for supporting 2 revision specs
159
def get_info(a_repo, revision):
160
    """Get all of the information for a particular revision"""
0.144.1 by Wesley J. Landaker
Added ui to bzrlib lazy imports.
161
    pb = ui.ui_factory.nested_progress_bar()
0.140.6 by John Arbash Meinel
refactor in preparation for supporting 2 revision specs
162
    a_repo.lock_read()
163
    try:
0.147.1 by John Arbash Meinel
Improve the committer matcher tremendously.
164
        trace.note('getting ancestry')
0.140.6 by John Arbash Meinel
refactor in preparation for supporting 2 revision specs
165
        ancestry = a_repo.get_ancestry(revision)[1:]
0.147.1 by John Arbash Meinel
Improve the committer matcher tremendously.
166
        revs, canonical_committer = get_revisions_and_committers(a_repo, ancestry)
0.140.6 by John Arbash Meinel
refactor in preparation for supporting 2 revision specs
167
    finally:
168
        a_repo.unlock()
169
        pb.finished()
170
0.147.1 by John Arbash Meinel
Improve the committer matcher tremendously.
171
    return collapse_by_person(revs, canonical_committer)
0.140.6 by John Arbash Meinel
refactor in preparation for supporting 2 revision specs
172
173
0.140.7 by John Arbash Meinel
Compute the revisions using a difference check
174
def get_diff_info(a_repo, start_rev, end_rev):
175
    """Get only the info for new revisions between the two revisions
0.146.1 by Paul Hummer
Revisions with missing emails are no longer all attributed to the same person
176
0.140.7 by John Arbash Meinel
Compute the revisions using a difference check
177
    This lets us figure out what has actually changed between 2 revisions.
178
    """
0.144.1 by Wesley J. Landaker
Added ui to bzrlib lazy imports.
179
    pb = ui.ui_factory.nested_progress_bar()
0.140.7 by John Arbash Meinel
Compute the revisions using a difference check
180
    a_repo.lock_read()
181
    try:
182
        pb.note('getting ancestry 1')
183
        start_ancestry = set(a_repo.get_ancestry(start_rev))
184
        pb.note('getting ancestry 2')
185
        ancestry = a_repo.get_ancestry(end_rev)[1:]
186
        ancestry = [rev for rev in ancestry if rev not in start_ancestry]
0.147.1 by John Arbash Meinel
Improve the committer matcher tremendously.
187
        revs, canonical_committer = sort_by_committer(a_repo, ancestry)
0.140.7 by John Arbash Meinel
Compute the revisions using a difference check
188
    finally:
189
        a_repo.unlock()
190
        pb.finished()
191
0.147.1 by John Arbash Meinel
Improve the committer matcher tremendously.
192
    return collapse_by_person(revs, canonical_committer)
0.140.7 by John Arbash Meinel
Compute the revisions using a difference check
193
0.140.16 by Jelmer Vernooij
Rename collapse_by_author -> collapse_by_person since author has an unambigous meaning
194
0.140.20 by Jelmer Vernooij
Add --show-class argument to stats command.
195
def display_info(info, to_file, gather_class_stats=None):
0.140.6 by John Arbash Meinel
refactor in preparation for supporting 2 revision specs
196
    """Write out the information"""
197
198
    for count, revs, emails, fullnames in info:
199
        # Get the most common email name
200
        sorted_emails = sorted(((count, email)
201
                               for email,count in emails.iteritems()),
202
                               reverse=True)
203
        sorted_fullnames = sorted(((count, fullname)
204
                                  for fullname,count in fullnames.iteritems()),
205
                                  reverse=True)
0.147.2 by John Arbash Meinel
Handle the case where we have a user but no email.
206
        if sorted_fullnames[0][1] == '' and sorted_emails[0][1] == '':
0.146.1 by Paul Hummer
Revisions with missing emails are no longer all attributed to the same person
207
            to_file.write('%4d %s\n'
208
                          % (count, 'Unknown'))
209
        else:
210
            to_file.write('%4d %s <%s>\n'
211
                          % (count, sorted_fullnames[0][1],
212
                             sorted_emails[0][1]))
0.140.6 by John Arbash Meinel
refactor in preparation for supporting 2 revision specs
213
        if len(sorted_fullnames) > 1:
0.147.1 by John Arbash Meinel
Improve the committer matcher tremendously.
214
            to_file.write('     Other names:\n')
215
            for count, fname in sorted_fullnames:
0.140.6 by John Arbash Meinel
refactor in preparation for supporting 2 revision specs
216
                to_file.write('     %4d ' % (count,))
217
                if fname == '':
218
                    to_file.write("''\n")
219
                else:
220
                    to_file.write("%s\n" % (fname,))
221
        if len(sorted_emails) > 1:
0.147.1 by John Arbash Meinel
Improve the committer matcher tremendously.
222
            to_file.write('     Other email addresses:\n')
0.140.6 by John Arbash Meinel
refactor in preparation for supporting 2 revision specs
223
            for count, email in sorted_emails:
224
                to_file.write('     %4d ' % (count,))
225
                if email == '':
226
                    to_file.write("''\n")
227
                else:
228
                    to_file.write("%s\n" % (email,))
0.140.20 by Jelmer Vernooij
Add --show-class argument to stats command.
229
        if gather_class_stats is not None:
230
            print '     Contributions:'
231
            classes, total = gather_class_stats(revs)
232
            for name,count in sorted(classes.items(), lambda x,y: cmp((x[1], x[0]), (y[1], y[0]))):
0.140.24 by Jelmer Vernooij
Remove 2.5ism.
233
                if name is None:
234
                    name = "Unknown"
235
                to_file.write("     %4.0f%% %s\n" % ((float(count) / total) * 100.0, name))
0.140.6 by John Arbash Meinel
refactor in preparation for supporting 2 revision specs
236
237
0.140.14 by Jelmer Vernooij
Merge upstream.
238
class cmd_committer_statistics(commands.Command):
0.140.1 by John Arbash Meinel
A simple plugin for generating author statistics, may grow into more.
239
    """Generate statistics for LOCATION."""
240
0.140.12 by Jelmer Vernooij
Change name to committer-stats, to allow for other sorts of stats too.
241
    aliases = ['stats', 'committer-stats']
0.140.1 by John Arbash Meinel
A simple plugin for generating author statistics, may grow into more.
242
    takes_args = ['location?']
0.140.20 by Jelmer Vernooij
Add --show-class argument to stats command.
243
    takes_options = ['revision', 
244
            option.Option('show-class', help="Show the class of contributions")]
0.140.1 by John Arbash Meinel
A simple plugin for generating author statistics, may grow into more.
245
0.140.3 by John Arbash Meinel
Updated to combine by author name, as well as by email address, and report on multiple names/addresses
246
    encoding_type = 'replace'
247
0.140.20 by Jelmer Vernooij
Add --show-class argument to stats command.
248
    def run(self, location='.', revision=None, show_class=False):
0.140.6 by John Arbash Meinel
refactor in preparation for supporting 2 revision specs
249
        alternate_rev = None
0.140.1 by John Arbash Meinel
A simple plugin for generating author statistics, may grow into more.
250
        try:
0.143.1 by John Arbash Meinel
Make a lot of imports lazy since they may not actually be used.
251
            wt = workingtree.WorkingTree.open_containing(location)[0]
0.140.1 by John Arbash Meinel
A simple plugin for generating author statistics, may grow into more.
252
        except errors.NoWorkingTree:
0.143.1 by John Arbash Meinel
Make a lot of imports lazy since they may not actually be used.
253
            a_branch = branch.Branch.open(location)
0.140.6 by John Arbash Meinel
refactor in preparation for supporting 2 revision specs
254
            last_rev = a_branch.last_revision()
0.140.1 by John Arbash Meinel
A simple plugin for generating author statistics, may grow into more.
255
        else:
0.140.6 by John Arbash Meinel
refactor in preparation for supporting 2 revision specs
256
            a_branch = wt.branch
0.140.1 by John Arbash Meinel
A simple plugin for generating author statistics, may grow into more.
257
            last_rev = wt.last_revision()
0.140.7 by John Arbash Meinel
Compute the revisions using a difference check
258
0.140.8 by John Arbash Meinel
Allow branch: to work, which needs a write lock
259
        if revision is not None:
260
            last_rev = revision[0].in_history(a_branch).rev_id
261
            if len(revision) > 1:
262
                alternate_rev = revision[1].in_history(a_branch).rev_id
263
0.140.7 by John Arbash Meinel
Compute the revisions using a difference check
264
        a_branch.lock_read()
265
        try:
266
            if alternate_rev:
267
                info = get_diff_info(a_branch.repository, last_rev,
268
                                     alternate_rev)
269
            else:
270
                info = get_info(a_branch.repository, last_rev)
271
        finally:
272
            a_branch.unlock()
0.140.25 by Jelmer Vernooij
Merge support for Python2.4.
273
        if show_class:
274
            def fetch_class_stats(revs):
275
                return gather_class_stats(a_branch.repository, revs)
276
        else:
277
            fetch_class_stats = None
0.145.1 by Russ Brown
Made to work with python 2.4
278
        display_info(info, self.outf, fetch_class_stats)
0.140.1 by John Arbash Meinel
A simple plugin for generating author statistics, may grow into more.
279
280
0.140.14 by Jelmer Vernooij
Merge upstream.
281
commands.register_command(cmd_committer_statistics)
0.143.1 by John Arbash Meinel
Make a lot of imports lazy since they may not actually be used.
282
283
284
class cmd_ancestor_growth(commands.Command):
0.140.4 by John Arbash Meinel
added ancestry_growth to generate a csv of ancestors.
285
    """Figure out the ancestor graph for LOCATION"""
286
287
    takes_args = ['location?']
288
289
    encoding_type = 'replace'
290
291
    def run(self, location='.'):
292
        try:
0.143.1 by John Arbash Meinel
Make a lot of imports lazy since they may not actually be used.
293
            wt = workingtree.WorkingTree.open_containing(location)[0]
0.140.4 by John Arbash Meinel
added ancestry_growth to generate a csv of ancestors.
294
        except errors.NoWorkingTree:
0.143.1 by John Arbash Meinel
Make a lot of imports lazy since they may not actually be used.
295
            a_branch = branch.Branch.open(location)
0.140.6 by John Arbash Meinel
refactor in preparation for supporting 2 revision specs
296
            last_rev = a_branch.last_revision()
0.140.4 by John Arbash Meinel
added ancestry_growth to generate a csv of ancestors.
297
        else:
0.140.6 by John Arbash Meinel
refactor in preparation for supporting 2 revision specs
298
            a_branch = wt.branch
0.140.4 by John Arbash Meinel
added ancestry_growth to generate a csv of ancestors.
299
            last_rev = wt.last_revision()
300
0.140.6 by John Arbash Meinel
refactor in preparation for supporting 2 revision specs
301
        a_branch.lock_read()
0.140.4 by John Arbash Meinel
added ancestry_growth to generate a csv of ancestors.
302
        try:
0.140.6 by John Arbash Meinel
refactor in preparation for supporting 2 revision specs
303
            graph = a_branch.repository.get_revision_graph(last_rev)
0.140.4 by John Arbash Meinel
added ancestry_growth to generate a csv of ancestors.
304
        finally:
0.140.6 by John Arbash Meinel
refactor in preparation for supporting 2 revision specs
305
            a_branch.unlock()
0.140.4 by John Arbash Meinel
added ancestry_growth to generate a csv of ancestors.
306
307
        revno = 0
308
        cur_parents = 0
309
        sorted_graph = tsort.merge_sort(graph.iteritems(), last_rev)
310
        for num, node_name, depth, isend in reversed(sorted_graph):
311
            cur_parents += 1
312
            if depth == 0:
313
                revno += 1
314
                self.outf.write('%4d, %4d\n' % (revno, cur_parents))
315
316
0.143.1 by John Arbash Meinel
Make a lot of imports lazy since they may not actually be used.
317
commands.register_command(cmd_ancestor_growth)
0.141.1 by Jelmer Vernooij
Add some simple tests for extract_fullname.
318
0.140.10 by John Arbash Meinel
Minor whitespace cleanup
319
0.140.18 by Jelmer Vernooij
Add credits command, test classify code by default, add comments to classify code.
320
def gather_class_stats(repository, revs):
321
    ret = {}
322
    total = 0
323
    pb = ui.ui_factory.nested_progress_bar()
324
    try:
325
        repository.lock_read()
326
        try:
327
            i = 0
328
            for delta in repository.get_deltas_for_revisions(revs):
329
                pb.update("classifying commits", i, len(revs))
330
                for c in classify_delta(delta):
331
                    if not c in ret:
332
                        ret[c] = 0
333
                    ret[c] += 1
334
                    total += 1
335
                i += 1
336
        finally:
337
            repository.unlock()
338
    finally:
339
        pb.finished()
340
    return ret, total
341
342
343
def display_credits(credits):
344
    (coders, documenters, artists, translators) = credits
345
    def print_section(name, lst):
346
        if len(lst) == 0:
347
            return
348
        print "%s:" % name
349
        for name in lst:
350
            print "%s" % name
351
        print ""
352
    print_section("Code", coders)
353
    print_section("Documentation", documenters)
354
    print_section("Art", artists)
355
    print_section("Translations", translators)
356
357
358
def find_credits(repository, revid):
359
    """Find the credits of the contributors to a revision.
360
361
    :return: tuple with (authors, documenters, artists, translators)
362
    """
363
    ret = {"documentation": {},
364
           "code": {},
365
           "art": {},
366
           "translation": {},
367
           None: {}
368
           }
369
    repository.lock_read()
370
    try:
371
        ancestry = filter(lambda x: x is not None, repository.get_ancestry(revid))
0.140.23 by Jelmer Vernooij
Add another progress bar.
372
        revs = repository.get_revisions(ancestry)
0.140.18 by Jelmer Vernooij
Add credits command, test classify code by default, add comments to classify code.
373
        pb = ui.ui_factory.nested_progress_bar()
374
        try:
0.140.23 by Jelmer Vernooij
Add another progress bar.
375
            for i, (rev,delta) in enumerate(izip(revs, repository.get_deltas_for_revisions(revs))):
376
                pb.update("analysing revisions", i, len(revs))
0.140.18 by Jelmer Vernooij
Add credits command, test classify code by default, add comments to classify code.
377
                # Don't count merges
378
                if len(rev.parent_ids) > 1:
379
                    continue
380
                for c in set(classify_delta(delta)):
0.140.29 by Jelmer Vernooij
Remove some uses of get_apparent_author.
381
                    for author in rev.get_apparent_authors():
382
                        if not author in ret[c]:
383
                            ret[c][author] = 0
384
                        ret[c][author] += 1
0.140.18 by Jelmer Vernooij
Add credits command, test classify code by default, add comments to classify code.
385
        finally:
386
            pb.finished()
387
    finally:
388
        repository.unlock()
389
    def sort_class(name):
0.140.19 by Jelmer Vernooij
List contributors with more contributions first.
390
        return map(lambda (x,y): x, 
391
               sorted(ret[name].items(), lambda x,y: cmp((x[1], x[0]), (y[1], y[0])), reverse=True))
0.140.18 by Jelmer Vernooij
Add credits command, test classify code by default, add comments to classify code.
392
    return (sort_class("code"), sort_class("documentation"), sort_class("art"), sort_class("translation"))
393
394
395
class cmd_credits(commands.Command):
396
    """Determine credits for LOCATION."""
397
398
    takes_args = ['location?']
399
    takes_options = ['revision']
400
401
    encoding_type = 'replace'
402
403
    def run(self, location='.', revision=None):
404
        try:
405
            wt = workingtree.WorkingTree.open_containing(location)[0]
406
        except errors.NoWorkingTree:
407
            a_branch = branch.Branch.open(location)
408
            last_rev = a_branch.last_revision()
409
        else:
410
            a_branch = wt.branch
411
            last_rev = wt.last_revision()
412
413
        if revision is not None:
414
            last_rev = revision[0].in_history(a_branch).rev_id
415
416
        a_branch.lock_read()
417
        try:
418
            credits = find_credits(a_branch.repository, last_rev)
419
            display_credits(credits)
420
        finally:
421
            a_branch.unlock()
422
423
424
commands.register_command(cmd_credits)
425
426
0.141.1 by Jelmer Vernooij
Add some simple tests for extract_fullname.
427
def test_suite():
428
    from unittest import TestSuite
429
    from bzrlib.tests import TestLoader
430
    suite = TestSuite()
431
    loader = TestLoader()
0.140.22 by Jelmer Vernooij
Remove now-obsolete tests.
432
    testmod_names = [ 'test_classify']
0.141.1 by Jelmer Vernooij
Add some simple tests for extract_fullname.
433
    suite.addTest(loader.loadTestsFromModuleNames(['%s.%s' % (__name__, i) for i in testmod_names]))
434
    return suite
435