Merge lp:~bac/launchpad/repeats into lp:launchpad

Proposed by Brad Crittenden
Status: Merged
Approved by: Gary Poster
Approved revision: no longer in the source branch.
Merged at revision: 15329
Proposed branch: lp:~bac/launchpad/repeats
Merge into: lp:launchpad
Diff against target: 159 lines (+130/-5)
2 files modified
lib/lp/services/testing/customresult.py (+10/-5)
lib/lp/services/testing/tests/test_customresult.py (+120/-0)
To merge this branch: bzr merge lp:~bac/launchpad/repeats
Reviewer Review Type Date Requested Status
Benji York (community) code Approve
Review via email: mp+107651@code.launchpad.net

Commit message

Ensure --load-list properly runs multiple instances of the same test.

Description of the change

= Summary =

The change to ensure --load-list ran tests in the same order as
presented in the test list file incorrectly assumed that testcase ids
were unique and added them to a hash. This approach broke for the
doctests that get registered manually and run multiple times. (See
the 'special' set in lp/answers/tests/test_doc.py for examples.)

== Proposed fix ==

Rather than assume a 1:1 mapping between test name and the test cases
discovered by zope.testing, assume it is 1:n.

== Pre-implementation notes ==

Productive pair programming with Benji to help find the culprit.

== Implementation details ==

As above.

== Tests ==

bin/test --vv bin/test -vv lp.services.testing.tests.test_customresult TestFilterTests

== Demo and Q/A ==

It can be demoed on the command-line. First create a file with one
repeated testcase:

% cat testlist
lib/lp/answers/doc/faqcollection.txt

Then run using --load-list:
% bin/test --load-list testlist
Running lp.testing.layers.DatabaseFunctionalLayer tests:
  Set up lp.testing.layers.BaseLayer in 0.088 seconds.
  Set up lp.testing.layers.DatabaseLayer in 1.627 seconds.
  Set up lp.testing.layers.FunctionalLayer in 5.041 seconds.
  Set up lp.testing.layers.DatabaseFunctionalLayer in 0.000 seconds.
  Ran 3 tests with 0 failures and 0 errors in 1.026 seconds.
Tearing down left over layers:
  Tear down lp.testing.layers.DatabaseFunctionalLayer in 0.000 seconds.
  Tear down lp.testing.layers.DatabaseLayer in 0.468 seconds.
  Tear down lp.testing.layers.FunctionalLayer ... not supported
  Tear down lp.testing.layers.BaseLayer in 0.001 seconds.

You'll note the test was run three times.

If you repeat the same exercise in trunk it'll just show one test,
which is wrong.

= Launchpad lint =

Checking for conflicts and issues in changed files.

Linting changed files:
  lib/lp/services/testing/customresult.py
  lib/lp/services/testing/tests/test_customresult.py

./lib/lp/services/testing/tests/test_customresult.py
      31: E301 expected 1 blank line, found 0
      33: E301 expected 1 blank line, found 0
      35: E301 expected 1 blank line, found 0
      37: E301 expected 1 blank line, found 0

To post a comment you must log in.
Revision history for this message
Benji York (benji) wrote :

I have some questions about this branch.

Why are test cases that have no layer filtered out? I would think that
we have at least a few tests that aren't in a layer (pure unit tests).

The imports for TestCase and filter_tests only need one line so they
shouldn't use multi-line imports.

FakeTestCase looks like it needs to stretch (it doesn't have any
whitespace between methods).

The TestFilterTests setUp method probably did something in an earlier
edit other than calling the super class' method of the same name, but
now that it doesn't, there's no need for it to be defined at all.

Instead of "self.assertTrue(layername in results)" you should use
assertIn, which generates slightly nicer error messages when the
assertion fails.

Revision history for this message
Brad Crittenden (bac) wrote :

I don't think there are any layer-less tests but you make a good point. I've changed the conditional to not filter tests with no layer and added a test to ensure it works.

I've incorporated your other suggestions.

Revision history for this message
Benji York (benji) wrote :

Looks great.

(We discussed the accidental change to .testr.conf on IRC and it is being removed.)

review: Approve (code)
Revision history for this message
Robert Collins (lifeless) wrote :

testr assumes that test ids are unique: we already have a hard
dependency on uniqueness, and I had totally forgotten about our test
suite containing duplicates.
https://launchpad.net/bugs/682771
and
https://launchpad.net/bugs/682772

were filed when I originally noticed that we had duplicates.

This branch would result in all tests with an identical name being
loaded when its given, but testr will already be assuming that there
is only one such test, so test counts will be permanently off.

It would be simpler, and better, to fix the root of the issue.
Testtools has a feature for renaming the test id of a test, if we have
multiple parameterised versions of a single test (testscenarios uses
that to do its parameterisation).

-Rob

Revision history for this message
Gary Poster (gary) wrote :

I'd like to have this landed now. This scenario was working and we made a change that caused this regression. Meanwhile, we haven't been testing with these duplicated tests for weeks, and I don't want to have additional regressions because of that.

I appreciate Robert's concerns about an underlying problem and will communicate with him separately about that.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/services/testing/customresult.py'
2--- lib/lp/services/testing/customresult.py 2012-05-01 20:49:04 +0000
3+++ lib/lp/services/testing/customresult.py 2012-05-30 11:46:18 +0000
4@@ -1,4 +1,4 @@
5-# Copyright 2009 Canonical Ltd. This software is licensed under the
6+# Copyright 2009-2012 Canonical Ltd. This software is licensed under the
7 # GNU Affero General Public License version 3 (see the file LICENSE).
8
9 """Support code for using a custom test result in test.py."""
10@@ -53,15 +53,20 @@
11 # Read the tests, filtering out any blank lines.
12 tests = filter(None, [line.strip() for line in open(list_name, 'rb')])
13 test_lookup = {}
14+ # Multiple unique testcases can be represented by a single id and they
15+ # must be tracked separately.
16 for layer_name, suite in tests_by_layer_name.iteritems():
17 for testcase in suite:
18- test_lookup[testcase.id()] = (testcase, layer_name)
19+ layer, testlist = test_lookup.setdefault(
20+ testcase.id(), (layer_name, []))
21+ testlist.append(testcase)
22
23 result = {}
24 for testname in tests:
25- testcase, layer = test_lookup.get(testname, (None, None))
26- if layer is not None:
27+ layer, testcases = test_lookup.get(testname, (None, None))
28+ if testcases is not None:
29 suite = result.setdefault(layer, TestSuite())
30- suite.addTest(testcase)
31+ for testcase in testcases:
32+ suite.addTest(testcase)
33 return result
34 return do_filter
35
36=== added file 'lib/lp/services/testing/tests/test_customresult.py'
37--- lib/lp/services/testing/tests/test_customresult.py 1970-01-01 00:00:00 +0000
38+++ lib/lp/services/testing/tests/test_customresult.py 2012-05-30 11:46:18 +0000
39@@ -0,0 +1,120 @@
40+# Copyright 2012 Canonical Ltd. This software is licensed under the
41+# GNU Affero General Public License version 3 (see the file LICENSE).
42+
43+"""Parallel test glue."""
44+
45+__metaclass__ = type
46+
47+import string
48+import tempfile
49+from testtools import TestCase
50+import unittest
51+
52+from lp.services.testing.customresult import filter_tests
53+from lp.testing.layers import BaseLayer
54+
55+
56+NEWLINE = '\n'
57+
58+
59+class FakeTestCase(unittest.TestCase):
60+ """A minimal TestCase that can be instantiated."""
61+ def __init__(self, name, *args, **kwargs):
62+ super(FakeTestCase, self).__init__(*args, **kwargs)
63+ self.name = name
64+
65+ def id(self):
66+ return self.name
67+
68+ def runTest(self):
69+ pass
70+
71+
72+class TestFilterTests(TestCase):
73+
74+ layer = BaseLayer
75+
76+ def writeFile(self, fd, contents):
77+ for line in contents:
78+ fd.write(line + NEWLINE)
79+ fd.flush()
80+
81+ def make_suites(self):
82+ """Make two suites.
83+
84+ The first has 'a'..'m' and the second 'n'..'z'.
85+ """
86+ suite_am = unittest.TestSuite()
87+ suite_nz = unittest.TestSuite()
88+ # Create one layer with the 'a'..'m'.
89+ for letter in string.lowercase[:13]:
90+ suite_am.addTest(FakeTestCase(letter))
91+ # And another layer with 'n'..'z'.
92+ for letter in string.lowercase[13:]:
93+ suite_nz.addTest(FakeTestCase(letter))
94+ return suite_am, suite_nz
95+
96+ def test_ordering(self):
97+ # Tests should be returned in the order seen in the testfile.
98+ layername = 'layer-1'
99+ testnames = ['d', 'c', 'a']
100+ suite = unittest.TestSuite()
101+ for letter in string.lowercase:
102+ suite.addTest(FakeTestCase(letter))
103+ with tempfile.NamedTemporaryFile() as fd:
104+ self.writeFile(fd, testnames)
105+ do_filter = filter_tests(fd.name)
106+ results = do_filter({layername: suite})
107+ self.assertEqual(1, len(results))
108+ self.assertIn(layername, results)
109+ suite = results[layername]
110+ self.assertEqual(testnames, [t.id() for t in suite])
111+
112+ def test_layer_separation(self):
113+ # Tests must be kept in their layer.
114+ suite1, suite2 = self.make_suites()
115+ testnames = ['a', 'b', 'c', 'z', 'y', 'x']
116+ with tempfile.NamedTemporaryFile() as fd:
117+ self.writeFile(fd, testnames)
118+ do_filter = filter_tests(fd.name)
119+ results = do_filter({'layer1': suite1,
120+ 'layer2': suite2})
121+ self.assertEqual(2, len(results))
122+ self.assertEqual(['layer1', 'layer2'], sorted(results.keys()))
123+ self.assertEqual(['a', 'b', 'c'], [t.id() for t in results['layer1']])
124+ self.assertEqual(['z', 'y', 'x'], [t.id() for t in results['layer2']])
125+
126+ def test_repeated_names(self):
127+ # Some doctests are run repeatedly with different scenarios. They
128+ # have the same name but different testcases. Those tests must not be
129+ # collapsed and lost.
130+ layername = 'layer-1'
131+ testnames = ['1', '2', '3']
132+ suite = unittest.TestSuite()
133+ for t in testnames:
134+ # Each test will be repeated equal to the number represented.
135+ for i in range(int(t)):
136+ suite.addTest(FakeTestCase(t))
137+ with tempfile.NamedTemporaryFile() as fd:
138+ self.writeFile(fd, testnames)
139+ do_filter = filter_tests(fd.name)
140+ results = do_filter({layername: suite})
141+ self.assertEqual(1, len(results))
142+ self.assertIn(layername, results)
143+ suite = results[layername]
144+ expected = ['1', '2', '2', '3', '3', '3']
145+ self.assertEqual(expected, [t.id() for t in suite])
146+
147+ def test_no_layer(self):
148+ # If tests have no layer (None) work.
149+ testnames = ['a', 'b', 'y', 'z']
150+ suite1, suite2 = self.make_suites()
151+ with tempfile.NamedTemporaryFile() as fd:
152+ self.writeFile(fd, testnames)
153+ do_filter = filter_tests(fd.name)
154+ results = do_filter({'layer1': suite1,
155+ None: suite2})
156+ self.assertEqual(2, len(results))
157+ self.assertEqual([None, 'layer1'], sorted(results.keys()))
158+ self.assertEqual(['a', 'b'], [t.id() for t in results['layer1']])
159+ self.assertEqual(['y', 'z'], [t.id() for t in results[None]])