launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #02305
lp:~edwin-grubbs/launchpad/bug-664828-teammembership-delete-timeout into lp:launchpad
Edwin Grubbs has proposed merging lp:~edwin-grubbs/launchpad/bug-664828-teammembership-delete-timeout into lp:launchpad.
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
Related bugs:
#664828 TeamMembership:+index timeout deleting a member
https://bugs.launchpad.net/bugs/664828
For more details, see:
https://code.launchpad.net/~edwin-grubbs/launchpad/bug-664828-teammembership-delete-timeout/+merge/45805
Summary
-------
This branch fixes the timeout when deactivating a member of a team that
was caused by the many queries necessary to traverse the TeamMembership
table to determine what entries need to be removed from the
TeamParticipation table.
Implementation details
----------------------
Used a postgresql 8.4 recursive query to traverse the TeamMembership
table to determine which entries should be kept in TeamParticipation.
lib/lp/registry/model/teammembership.py
Extended tests to verify that there are no extra deletions from the
TeamParticipation table as a side-effect. Added test to make sure that
the number of queries doesn't grow.
lib/lp/registry/tests/test_teammembership.py
lib/lp/registry/browser/tests/test_teammembership.py
Minor edits:
lib/lp/registry/doc/teammembership.txt
lib/lp/registry/interfaces/teammembership.py
Tests
-----
./bin/test -vv -t 'doc/teammembership.txt|registry.tests.test_teammembership'
Demo and Q/A
------------
* Open http://launchpad.dev/...
Lint
----
No lint.
--
https://code.launchpad.net/~edwin-grubbs/launchpad/bug-664828-teammembership-delete-timeout/+merge/45805
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~edwin-grubbs/launchpad/bug-664828-teammembership-delete-timeout into lp:launchpad.
=== added file 'lib/lp/registry/browser/tests/test_teammembership.py'
--- lib/lp/registry/browser/tests/test_teammembership.py 1970-01-01 00:00:00 +0000
+++ lib/lp/registry/browser/tests/test_teammembership.py 2011-01-11 01:44:04 +0000
@@ -0,0 +1,49 @@
+# Copyright 2011 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+__metaclass__ = type
+
+from testtools.matchers import LessThan
+from zope.component import getUtility
+
+from canonical.testing.layers import DatabaseFunctionalLayer
+from lp.testing.matchers import HasQueryCount
+from lp.registry.interfaces.teammembership import (
+ ITeamMembershipSet,
+ TeamMembershipStatus,
+ )
+from lp.testing import (
+ login_celebrity,
+ StormStatementRecorder,
+ TestCaseWithFactory,
+ )
+from lp.testing.views import create_view
+
+
+class TestTeamMenu(TestCaseWithFactory):
+
+ layer = DatabaseFunctionalLayer
+
+ def setUp(self):
+ super(TestTeamMenu, self).setUp()
+ login_celebrity('admin')
+ self.membership_set = getUtility(ITeamMembershipSet)
+ self.team = self.factory.makeTeam()
+ self.member = self.factory.makeTeam()
+
+ def test_deactivate_member_query_count(self):
+ self.team.addMember(
+ self.member, self.team.teamowner, force_team_add=True)
+ form = {
+ 'editactive': 1,
+ 'expires': 'never',
+ 'deactivate': 'Deactivate',
+ }
+ membership = self.membership_set.getByPersonAndTeam(
+ self.member, self.team)
+ with StormStatementRecorder() as recorder:
+ view = create_view(membership, "+index", method='POST', form=form)
+ view.processForm()
+ self.assertEqual('', view.errormessage)
+ self.assertEqual(TeamMembershipStatus.DEACTIVATED, membership.status)
+ self.assertThat(recorder, HasQueryCount(LessThan(12)))
=== modified file 'lib/lp/registry/doc/teammembership.txt'
--- lib/lp/registry/doc/teammembership.txt 2010-10-24 12:46:23 +0000
+++ lib/lp/registry/doc/teammembership.txt 2011-01-11 01:44:04 +0000
@@ -131,6 +131,8 @@
... ubuntu_team, TeamMembershipStatus.DEACTIVATED, t2.teamowner)
>>> ubuntu_team in t2.activemembers
False
+ >>> [m.displayname for m in t2.allmembers]
+ [u'No Privileges Person']
>>> login(ANONYMOUS)
Another API for adding members is ITeam.addMember(), which ensures the given
=== modified file 'lib/lp/registry/interfaces/teammembership.py'
--- lib/lp/registry/interfaces/teammembership.py 2010-12-10 14:58:31 +0000
+++ lib/lp/registry/interfaces/teammembership.py 2011-01-11 01:44:04 +0000
@@ -116,7 +116,11 @@
class ITeamMembership(Interface):
- """TeamMembership for Users"""
+ """TeamMembership for Users.
+
+ This table includes *direct* team members only. Indirect memberships are
+ handled by the TeamParticipation table.
+ """
export_as_webservice_entry()
id = Int(title=_('ID'), required=True, readonly=True)
=== modified file 'lib/lp/registry/model/teammembership.py'
--- lib/lp/registry/model/teammembership.py 2010-12-10 14:58:31 +0000
+++ lib/lp/registry/model/teammembership.py 2011-01-11 01:44:04 +0000
@@ -21,7 +21,7 @@
ForeignKey,
StringCol,
)
-from storm.locals import Store
+from storm.store import Store
from zope.component import getUtility
from zope.interface import implements
@@ -498,167 +498,85 @@
person = ForeignKey(dbName='person', foreignKey='Person', notNull=True)
-def _cleanTeamParticipation(person, team):
- """Remove relevant entries in TeamParticipation for <person> and <team>.
-
- Remove all tuples "person, team" from TeamParticipation for the given
- person and team (together with all its superteams), unless this person is
- an indirect member of the given team. More information on how to use the
- TeamParticipation table can be found in the TeamParticipationUsage spec or
- the teammembership.txt system doctest.
- """
- query = """
- SELECT EXISTS(
- SELECT 1 FROM TeamParticipation
- WHERE person = %(person_id)s AND team IN (
- SELECT person
- FROM TeamParticipation JOIN Person ON
- (person = Person.id)
- WHERE team = %(team_id)s
- AND person NOT IN (%(team_id)s, %(person_id)s)
- AND teamowner IS NOT NULL
- )
- )
- """ % dict(team_id=team.id, person_id=person.id)
- store = Store.of(person)
- (result, ) = store.execute(query).get_one()
- if result:
- # The person is a participant in this team by virtue of a membership
- # in another one, so don't attempt to remove anything.
- return
-
- # First of all, we remove <person> from <team> (and its superteams).
- _removeParticipantFromTeamAndSuperTeams(person, team)
- if not person.is_team:
- # Nothing else to do.
- return
-
- store = Store.of(person)
-
- # Clean the participation of all our participant subteams, that are
- # not a direct members of the target team.
- query = """
- -- All of my participant subteams...
- SELECT person
- FROM TeamParticipation JOIN Person ON (person = Person.id)
- WHERE team = %(person_id)s AND person != %(person_id)s
- AND teamowner IS NOT NULL
- EXCEPT
- -- that aren't a direct member of the team.
- SELECT person
- FROM TeamMembership
- WHERE team = %(team_id)s AND status IN %(active_states)s
- """ % dict(
- person_id=person.id, team_id=team.id,
- active_states=sqlvalues(ACTIVE_STATES)[0])
-
- # Avoid circular import.
- from lp.registry.model.person import Person
- for subteam in store.find(Person, "id IN (%s)" % query):
- _cleanTeamParticipation(subteam, team)
-
- # Then clean-up all the non-team participants. We can remove those
- # in a single query when the team graph is up to date.
- _removeAllIndividualParticipantsFromTeamAndSuperTeams(person, team)
-
-
-def _removeParticipantFromTeamAndSuperTeams(person, team):
- """Remove participation of person in team.
-
- If <person> is a participant (that is, has a TeamParticipation entry)
- of any team that is a subteam of <team>, then <person> should be kept as
- a participant of <team> and (as a consequence) all its superteams.
- Otherwise, <person> is removed from <team> and we repeat this process for
- each superteam of <team>.
- """
- # Check if the person is a member of the given team through another team.
- query = """
- SELECT EXISTS(
- SELECT 1
- FROM TeamParticipation, TeamMembership
- WHERE
- TeamMembership.team = %(team_id)s AND
- TeamMembership.person = TeamParticipation.team AND
- TeamParticipation.person = %(person_id)s AND
- TeamMembership.status IN %(active_states)s)
- """ % dict(team_id=team.id, person_id=person.id,
- active_states=sqlvalues(ACTIVE_STATES)[0])
- store = Store.of(person)
- (result, ) = store.execute(query).get_one()
- if result:
- # The person is a participant by virtue of a membership on another
- # team, so don't remove.
- return
- store.find(TeamParticipation, (
- (TeamParticipation.team == team) &
- (TeamParticipation.person == person))).remove()
-
- for superteam in _getSuperTeamsExcludingDirectMembership(person, team):
- _removeParticipantFromTeamAndSuperTeams(person, superteam)
-
-
-def _removeAllIndividualParticipantsFromTeamAndSuperTeams(team, target_team):
- """Remove all non-team participants in <team> from <target_team>.
-
- All the non-team participants of <team> are removed from <target_team>
- and its super teams, unless they participate in <target_team> also from
- one of its sub team.
- """
- query = """
+def _cleanTeamParticipation(child, parent):
+ """Remove child from team and clean up child's subteams.
+
+ A participant of child is removed from parent's TeamParticipation
+ entries if the only path from the participant to parent is via
+ child.
+ """
+ # Delete participation entries for the child and the child's
+ # direct/indirect members in other ancestor teams, unless those
+ # ancestor teams have another path the the child besides the
+ # membership that has just been deactivated.
+ store = Store.of(parent)
+ store.execute("""
DELETE FROM TeamParticipation
- WHERE team = %(target_team_id)s AND person IN (
- -- All the individual participants.
- SELECT person
- FROM TeamParticipation JOIN Person ON (person = Person.id)
- WHERE team = %(team_id)s AND teamowner IS NULL
- EXCEPT
- -- people participating through a subteam of target_team;
- SELECT person
+ USING (
+ /* Get all the participation entries that might need to be
+ * deleted, i.e. all the entries where the
+ * TeamParticipation.person is a participant of child, and
+ * where child participated in TeamParticipation.team until
+ * child was removed from parent.
+ */
+ SELECT person, team
FROM TeamParticipation
- WHERE team IN (
- -- The subteams of target_team.
- SELECT person
- FROM TeamParticipation JOIN Person ON (person = Person.id)
- WHERE team = %(target_team_id)s
- AND person NOT IN (%(target_team_id)s, %(team_id)s)
- AND teamowner IS NOT NULL
- )
- -- or people directly a member of the target team.
- EXCEPT
- SELECT person
- FROM TeamMembership
- WHERE team = %(target_team_id)s AND status IN %(active_states)s
- )
- """ % dict(
- team_id=team.id, target_team_id=target_team.id,
- active_states=sqlvalues(ACTIVE_STATES)[0])
- store = Store.of(team)
- store.execute(query)
-
- super_teams = _getSuperTeamsExcludingDirectMembership(team, target_team)
- for superteam in super_teams:
- _removeAllIndividualParticipantsFromTeamAndSuperTeams(team, superteam)
-
-
-def _getSuperTeamsExcludingDirectMembership(person, team):
- """Return all the super teams of <team> where person isn't a member."""
- query = """
- -- All the super teams...
- SELECT team
- FROM TeamParticipation
- WHERE person = %(team_id)s AND team != %(team_id)s
- EXCEPT
- -- The one where person has an active membership.
- SELECT team
- FROM TeamMembership
- WHERE person = %(person_id)s AND status IN %(active_states)s
- """ % dict(
- person_id=person.id, team_id=team.id,
- active_states=sqlvalues(ACTIVE_STATES)[0])
-
- # Avoid circular import.
- from lp.registry.model.person import Person
- return Store.of(person).find(Person, "id IN (%s)" % query)
+ WHERE person IN (
+ SELECT person
+ FROM TeamParticipation
+ WHERE team = %(child)s
+ )
+ AND team IN (
+ SELECT team
+ FROM TeamParticipation
+ WHERE person = %(child)s
+ AND team != %(child)s
+ )
+
+
+ EXCEPT (
+
+ /* Compute the TeamParticipation entries that we need to
+ * keep by walking the tree in the TeamMembership table.
+ */
+ WITH RECURSIVE parent(person, team) AS (
+ /* Start by getting all the ancestors of the child
+ * from the TeamParticipation table, then get those
+ * ancestors' direct members to recurse through the
+ * tree from the top.
+ */
+ SELECT ancestor.person, ancestor.team
+ FROM TeamMembership ancestor
+ WHERE ancestor.status IN %(active_states)s
+ AND ancestor.team IN (
+ SELECT team
+ FROM TeamParticipation
+ WHERE person = %(child)s
+ )
+
+ UNION
+
+ /* Find the next level of direct members, but hold
+ * onto the parent.team, since we want the top and
+ * bottom of the hierarchy to calculate the
+ * TeamParticipation. The query above makes sure
+ * that we do this for all the ancestors.
+ */
+ SELECT child.person, parent.team
+ FROM TeamMembership child
+ JOIN parent ON child.team = parent.person
+ WHERE child.status IN %(active_states)s
+ )
+ SELECT person, team
+ FROM parent
+ )
+ ) AS keeping
+ WHERE TeamParticipation.person = keeping.person
+ AND TeamParticipation.team = keeping.team
+ """ % sqlvalues(
+ child=child.id,
+ active_states=ACTIVE_STATES))
+ store.invalidate()
def _fillTeamParticipation(member, accepting_team):
=== modified file 'lib/lp/registry/tests/test_teammembership.py'
--- lib/lp/registry/tests/test_teammembership.py 2010-12-02 16:13:51 +0000
+++ lib/lp/registry/tests/test_teammembership.py 2011-01-11 01:44:04 +0000
@@ -9,7 +9,10 @@
)
import re
import subprocess
-import unittest
+from unittest import (
+ TestCase,
+ TestLoader,
+ )
import pytz
from zope.component import getUtility
@@ -24,6 +27,7 @@
login,
login_person,
)
+from canonical.launchpad.interfaces.lpstorm import IStore
from canonical.launchpad.testing.systemdocs import (
default_optionflags,
LayeredDocFileSuite,
@@ -40,11 +44,14 @@
ITeamMembershipSet,
TeamMembershipStatus,
)
-from lp.registry.model.teammembership import TeamMembership
-from lp.testing.factory import LaunchpadObjectFactory
-
-
-class TestTeamMembershipSet(unittest.TestCase):
+from lp.registry.model.teammembership import (
+ TeamMembership,
+ TeamParticipation,
+ )
+from lp.testing import TestCaseWithFactory
+
+
+class TestTeamMembershipSet(TestCase):
layer = DatabaseFunctionalLayer
def setUp(self):
@@ -155,11 +162,12 @@
self.failIf(sample_person.inTeam(motu))
-class TeamParticipationTestCase(unittest.TestCase):
+class TeamParticipationTestCase(TestCaseWithFactory):
"""Tests for team participation using 5 teams."""
layer = DatabaseFunctionalLayer
def setUp(self):
+ super(TeamParticipationTestCase, self).setUp()
login('foo.bar@xxxxxxxxxxxxx')
person_set = getUtility(IPersonSet)
self.foo_bar = person_set.getByEmail('foo.bar@xxxxxxxxxxxxx')
@@ -177,6 +185,9 @@
sorted(participant_names),
sorted([participant.name for participant in team.allmembers]))
+ def getTeamParticipationCount(self):
+ return IStore(TeamParticipation).find(TeamParticipation).count()
+
class TestTeamParticipationHierarchy(TeamParticipationTestCase):
"""Participation management tests using 5 nested teams.
@@ -221,6 +232,7 @@
This is similar to what was experienced in bug 261915.
"""
+ previous_count = self.getTeamParticipationCount()
self.team2.setMembershipData(
self.team3, TeamMembershipStatus.DEACTIVATED, self.foo_bar)
self.assertParticipantsEquals(['name16', 'team2'], self.team1)
@@ -230,11 +242,15 @@
self.assertParticipantsEquals(
['name16', 'no-priv', 'team5'], self.team4)
self.assertParticipantsEquals(['name16', 'no-priv'], self.team5)
+ self.assertEqual(
+ previous_count-8,
+ self.getTeamParticipationCount())
def testRemovingLeafTeam(self):
"""Make sure that participations are updated correctly when removing
the leaf team.
"""
+ previous_count = self.getTeamParticipationCount()
self.team4.setMembershipData(
self.team5, TeamMembershipStatus.DEACTIVATED, self.foo_bar)
self.assertParticipantsEquals(
@@ -244,6 +260,9 @@
self.assertParticipantsEquals(['name16', 'team4'], self.team3)
self.assertParticipantsEquals(['name16'], self.team4)
self.assertParticipantsEquals(['name16', 'no-priv'], self.team5)
+ self.assertEqual(
+ previous_count-8,
+ self.getTeamParticipationCount())
class TestTeamParticipationTree(TeamParticipationTestCase):
@@ -252,9 +271,10 @@
Create a team hierarchy looking like this:
team1
team2
- team5 team3
- team4
- team5
+ team5
+ team3
+ team4
+ team5
no-priv
"""
layer = DatabaseFunctionalLayer
@@ -269,6 +289,10 @@
self.team3.addMember(self.team4, self.foo_bar, force_team_add=True)
self.team4.addMember(self.team5, self.foo_bar, force_team_add=True)
+ def tearDown(self):
+ super(TestTeamParticipationTree, self).tearDown()
+ self.layer.force_dirty_database()
+
def testTeamParticipationSetUp(self):
"""Make sure that the TeamParticipation are sane after setUp."""
self.assertParticipantsEquals(
@@ -284,6 +308,7 @@
['name16', 'no-priv'], self.team5)
def testRemoveTeam3FromTeam2(self):
+ previous_count = self.getTeamParticipationCount()
self.team2.setMembershipData(
self.team3, TeamMembershipStatus.DEACTIVATED, self.foo_bar)
self.assertParticipantsEquals(
@@ -295,8 +320,12 @@
self.assertParticipantsEquals(
['name16', 'no-priv', 'team5'], self.team4)
self.assertParticipantsEquals(['name16', 'no-priv'], self.team5)
+ self.assertEqual(
+ previous_count-4,
+ self.getTeamParticipationCount())
def testRemoveTeam5FromTeam4(self):
+ previous_count = self.getTeamParticipationCount()
self.team4.setMembershipData(
self.team5, TeamMembershipStatus.DEACTIVATED, self.foo_bar)
self.assertParticipantsEquals(
@@ -308,6 +337,40 @@
['name16', 'team4'], self.team3)
self.assertParticipantsEquals(['name16'], self.team4)
self.assertParticipantsEquals(['name16', 'no-priv'], self.team5)
+ self.assertEqual(
+ previous_count-4,
+ self.getTeamParticipationCount())
+
+
+class TestParticipationCleanup(TeamParticipationTestCase):
+ """Test deletion of a member from a team with many superteams.
+ Create a team hierarchy looking like this:
+ team1
+ team2
+ team3
+ team4
+ team5
+ no-priv
+ """
+
+ def setUp(self):
+ """Setup the team hierarchy."""
+ super(TestParticipationCleanup, self).setUp()
+ self.team1.addMember(self.team2, self.foo_bar, force_team_add=True)
+ self.team2.addMember(self.team3, self.foo_bar, force_team_add=True)
+ self.team3.addMember(self.team4, self.foo_bar, force_team_add=True)
+ self.team4.addMember(self.team5, self.foo_bar, force_team_add=True)
+ self.team5.addMember(self.no_priv, self.foo_bar)
+
+ def testMemberRemoval(self):
+ """Remove the member from the last team.
+
+ The number of db queries should be constant not O(depth).
+ """
+ self.assertStatementCount(
+ 7,
+ self.team5.setMembershipData, self.no_priv,
+ TeamMembershipStatus.DEACTIVATED, self.team5.teamowner)
class TestTeamParticipationMesh(TeamParticipationTestCase):
@@ -315,12 +378,15 @@
branches.
Create a team hierarchy looking like this:
- team1 /--team6
- team2 \
- | team3 |
- \--- team4-/
- team5
- no-priv
+ team1 team6
+ \ / |
+ team2 |
+ / | |
+ team3 | |
+ \ | /
+ team4
+ team5
+ no-priv
"""
layer = DatabaseFunctionalLayer
@@ -338,6 +404,10 @@
self.team6.addMember(self.team2, self.foo_bar, force_team_add=True)
self.team6.addMember(self.team4, self.foo_bar, force_team_add=True)
+ def tearDown(self):
+ super(TestTeamParticipationMesh, self).tearDown()
+ self.layer.force_dirty_database()
+
def testTeamParticipationSetUp(self):
"""Make sure that the TeamParticipation are sane after setUp."""
self.assertParticipantsEquals(
@@ -355,6 +425,7 @@
self.team6)
def testRemoveTeam3FromTeam2(self):
+ previous_count = self.getTeamParticipationCount()
self.team2.setMembershipData(
self.team3, TeamMembershipStatus.DEACTIVATED, self.foo_bar)
self.assertParticipantsEquals(
@@ -368,8 +439,12 @@
self.assertParticipantsEquals(['name16', 'no-priv'], self.team5)
self.assertParticipantsEquals(
['name16', 'no-priv', 'team2', 'team4', 'team5'], self.team6)
+ self.assertEqual(
+ previous_count-3,
+ self.getTeamParticipationCount())
def testRemoveTeam5FromTeam4(self):
+ previous_count = self.getTeamParticipationCount()
self.team4.setMembershipData(
self.team5, TeamMembershipStatus.DEACTIVATED, self.foo_bar)
self.assertParticipantsEquals(
@@ -381,9 +456,12 @@
self.assertParticipantsEquals(['name16', 'no-priv'], self.team5)
self.assertParticipantsEquals(
['name16', 'team2', 'team3', 'team4'], self.team6)
-
-
-class TestTeamMembership(unittest.TestCase):
+ self.assertEqual(
+ previous_count-10,
+ self.getTeamParticipationCount())
+
+
+class TestTeamMembership(TestCaseWithFactory):
layer = DatabaseFunctionalLayer
def test_teams_not_kicked_from_themselves_bug_248498(self):
@@ -400,12 +478,11 @@
This test will make sure that doesn't happen in the future.
"""
login('test@xxxxxxxxxxxxx')
- factory = LaunchpadObjectFactory()
- person = factory.makePerson()
+ person = self.factory.makePerson()
login_person(person) # Now login with the future owner of the teams.
- teamA = factory.makeTeam(
+ teamA = self.factory.makeTeam(
person, subscription_policy=TeamSubscriptionPolicy.MODERATED)
- teamB = factory.makeTeam(
+ teamB = self.factory.makeTeam(
person, subscription_policy=TeamSubscriptionPolicy.MODERATED)
self.failUnless(
teamA.inTeam(teamA), "teamA is not a participant of itself")
@@ -444,21 +521,21 @@
self.assertEqual(new_status, TeamMembershipStatus.DEACTIVATED.value)
-class TestTeamMembershipSetStatus(unittest.TestCase):
+class TestTeamMembershipSetStatus(TestCaseWithFactory):
"""Test the behaviour of TeamMembership's setStatus()."""
layer = DatabaseFunctionalLayer
def setUp(self):
+ super(TestTeamMembershipSetStatus, self).setUp()
login('foo.bar@xxxxxxxxxxxxx')
self.foobar = getUtility(IPersonSet).getByName('name16')
self.no_priv = getUtility(IPersonSet).getByName('no-priv')
self.ubuntu_team = getUtility(IPersonSet).getByName('ubuntu-team')
self.admins = getUtility(IPersonSet).getByName('admins')
# Create a bunch of arbitrary teams to use in the tests.
- factory = LaunchpadObjectFactory()
- self.team1 = factory.makeTeam(self.foobar)
- self.team2 = factory.makeTeam(self.foobar)
- self.team3 = factory.makeTeam(self.foobar)
+ self.team1 = self.factory.makeTeam(self.foobar)
+ self.team2 = self.factory.makeTeam(self.foobar)
+ self.team3 = self.factory.makeTeam(self.foobar)
def test_proponent_is_stored(self):
for status in [TeamMembershipStatus.DEACTIVATED,
@@ -698,7 +775,7 @@
self.assertEqual(TeamMembershipStatus.DEACTIVATED, tm.status)
-class TestCheckTeamParticipationScript(unittest.TestCase):
+class TestCheckTeamParticipationScript(TestCase):
layer = DatabaseFunctionalLayer
def _runScript(self, expected_returncode=0):
@@ -803,7 +880,7 @@
def test_suite():
- suite = unittest.TestLoader().loadTestsFromName(__name__)
+ suite = TestLoader().loadTestsFromName(__name__)
bug_249185 = LayeredDocFileSuite(
'bug-249185.txt', optionflags=default_optionflags,
layer=DatabaseFunctionalLayer, setUp=setUp, tearDown=tearDown)
Follow ups