Merge lp:~gz/juju-release-tools/test_apply_patches into lp:juju-release-tools

Proposed by Martin Packman
Status: Merged
Approved by: Martin Packman
Approved revision: 324
Merged at revision: 322
Proposed branch: lp:~gz/juju-release-tools/test_apply_patches
Merge into: lp:juju-release-tools
Prerequisite: lp:~gz/juju-release-tools/utils_no_mock
Diff against target: 203 lines (+171/-5)
2 files modified
apply_patches.py (+11/-5)
tests/test_apply_patches.py (+160/-0)
To merge this branch: bzr merge lp:~gz/juju-release-tools/test_apply_patches
Reviewer Review Type Date Requested Status
Curtis Hovey (community) code Approve
Review via email: mp+300776@code.launchpad.net

This proposal supersedes a proposal from 2016-07-21.

Commit message

Add test coverage for apply_patches script

Description of the change

Somehow I forgot on Friday that we do actually have test infrastructure for juju-release-tools, so this branch adds coverage. There are some fiddly bits (`with open` is always annoying), and one relevent change with the addition of flushing for the parent process's output. This ensures the ordering of output is correct when mixed with the patch command, which currently is a confusion on the logs.

The addition of gettext for the user informational output is total vanity. Please forgive.

To post a comment you must log in.
Revision history for this message
Curtis Hovey (sinzui) wrote :

Do we need print_now? I have strived to move away from it. I introduced it because the buffered stdout was not appearing near the output of other procs. Per you own example, I log.XXX() which is unbuffered stderr. I print() to stdout for user info. Do we have a buffering problem that this solves?

Removing it might be awkward because I can see a few tests are mocking it.

review: Needs Information (code)
Revision history for this message
Martin Packman (gz) wrote :

I agree print_now is ugly and generally the wrong thing.

The buffering issue this fixes can be seen in the log output at tarball creation at present:

    patching file gopkg.in/mgo.v2/session.go
    Applying 1 patches
    Applied patch '001-mgo.v2-issue-277-fix.diff'

The first line is from `patch` when it exits, but it should really appear between the two lines from the parent script for clarity. As stdout is buffered, those are only output when the parent process exits.

So, four possible fixes:

# Write to stderr not stdout
  - This is often the right thing for scripts. Use stdout for results - things that could actually be piped into another command - and use stderr for feedback to the user.
# Use logging module
  - Uses stderr like above, needs extra configuration but can be easier to reuse.
# Use print(flush=True)
  - Python 3.3 and above only
# Use a helper that calls .flush()

So, why did I not write to stderr... well, this is the kind of script I don't expect to have output beyond the statement of activity, so I've tended to put that on stdout. This is much like the patch command itself, which uses stdout. But, lets try the other way.

323. By Martin Packman

Switch to using stderr for apply_patches output

Revision history for this message
Curtis Hovey (sinzui) wrote :

I accept print_now as a compromise for now.

> # Write to stderr not stdout
> - This is often the right thing for scripts. Use stdout for results - things that could
> actually be piped into another command - and use stderr for feedback to the user.

Your example is a little different from my recent experience where I did want my
output captured by other scripts.

> # Use logging module
> - Uses stderr like above, needs extra configuration but can be easier to reuse.

I considered this for my recent cases, We still configure a logger without timestamps,

> # Use print(flush=True)
> - Python 3.3 and above only

Been there. I think we can switch when we stop supporting precise or we stop using this
set of scripts on precise

> # Use a helper that calls .flush()

Yep. the helper is the cleanest path for now.

review: Approve (code)
324. By Martin Packman

Remove mostly redundant test for apply_patches

Revision history for this message
Curtis Hovey (sinzui) wrote :

Thank you for choosing a solution.

review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'apply_patches.py'
2--- apply_patches.py 2016-07-15 16:28:29 +0000
3+++ apply_patches.py 2016-07-21 16:44:36 +0000
4@@ -4,6 +4,7 @@
5 from __future__ import print_function
6
7 from argparse import ArgumentParser
8+import gettext
9 import os
10 import subprocess
11 import sys
12@@ -41,16 +42,21 @@
13 try:
14 patches = sorted(os.listdir(args.patchdir))
15 except OSError as e:
16- parser.error("Could not list patchdir: {}".format(e))
17+ parser.error("Could not list patch directory: {}".format(e))
18 if not os.path.isdir(args.srctree):
19- parser.error("Given srctree '{}' not a directory".format(args.srctree))
20- print("Applying {} patches".format(len(patches)))
21+ parser.error("Source tree '{}' not a directory".format(args.srctree))
22+ patch_count = len(patches)
23+ print(gettext.ngettext(
24+ u"Applying {} patch", u"Applying {} patches", patch_count).format(
25+ patch_count), file=sys.stderr)
26 for patch in patches:
27 patch_path = os.path.join(args.patchdir, patch)
28 if apply_patch(patch_path, args.srctree, args.dry_run, args.verbose):
29- print("Failed to apply patch '{}'".format(patch))
30+ print(gettext.gettext(
31+ u"Failed to apply patch '{}'").format(patch), file=sys.stderr)
32 return 1
33- print("Applied patch '{}'".format(patch))
34+ print(gettext.gettext(
35+ u"Applied patch '{}'").format(patch), file=sys.stderr)
36 return 0
37
38
39
40=== added file 'tests/test_apply_patches.py'
41--- tests/test_apply_patches.py 1970-01-01 00:00:00 +0000
42+++ tests/test_apply_patches.py 2016-07-21 16:44:36 +0000
43@@ -0,0 +1,160 @@
44+"""Tests for apply_patches script used in tarball building."""
45+
46+import contextlib
47+import mock
48+import os
49+import StringIO
50+import sys
51+import unittest
52+
53+from apply_patches import (
54+ apply_patch,
55+ get_arg_parser,
56+ main,
57+)
58+from utils import (
59+ temp_dir,
60+)
61+
62+
63+class TestArgParser(unittest.TestCase):
64+
65+ def parse_args(self, args):
66+ parser = get_arg_parser()
67+ return parser.parse_args(args)
68+
69+ def test_defaults(self):
70+ args = self.parse_args(["patches/", "."])
71+ self.assertEqual("patches/", args.patchdir)
72+ self.assertEqual(".", args.srctree)
73+ self.assertEqual(False, args.dry_run)
74+ self.assertEqual(False, args.verbose)
75+
76+ def test_dry_run(self):
77+ args = self.parse_args(["--dry-run", "patches/", "."])
78+ self.assertEqual("patches/", args.patchdir)
79+ self.assertEqual(".", args.srctree)
80+ self.assertEqual(True, args.dry_run)
81+ self.assertEqual(False, args.verbose)
82+
83+ def test_verbose(self):
84+ args = self.parse_args(["patches/", ".", "--verbose"])
85+ self.assertEqual("patches/", args.patchdir)
86+ self.assertEqual(".", args.srctree)
87+ self.assertEqual(False, args.dry_run)
88+ self.assertEqual(True, args.verbose)
89+
90+
91+class TestApplyPatches(unittest.TestCase):
92+
93+ def assert_open_only(self, open_mock, filename):
94+ self.assertEqual(
95+ open_mock.mock_calls,
96+ [
97+ mock.call(filename),
98+ mock.call().__enter__(),
99+ mock.call().__exit__(None, None, None),
100+ ]
101+ )
102+
103+ def test_with_defaults(self):
104+ open_mock = mock.mock_open()
105+ with mock.patch("subprocess.call", autospec=True) as call_mock:
106+ with mock.patch("apply_patches.open", open_mock, create=True):
107+ apply_patch("some.patch", "a/tree")
108+ self.assert_open_only(open_mock, "some.patch")
109+ call_mock.assert_called_once_with(
110+ ["patch", "-f", "-u", "-p1", "-r-"],
111+ stdin=open_mock(),
112+ cwd="a/tree",
113+ )
114+
115+ def test_dry_run_verbose(self):
116+ open_mock = mock.mock_open()
117+ with mock.patch("subprocess.call", autospec=True) as call_mock:
118+ with mock.patch("apply_patches.open", open_mock, create=True):
119+ apply_patch("trial.diff", "a/tree", True, True)
120+ self.assert_open_only(open_mock, "trial.diff")
121+ call_mock.assert_called_once_with(
122+ ["patch", "-f", "-u", "-p1", "-r-", "--dry-run", "--verbose"],
123+ stdin=open_mock(),
124+ cwd="a/tree",
125+ )
126+
127+ def test_verbose(self):
128+ open_mock = mock.mock_open()
129+ with mock.patch("subprocess.call", autospec=True) as call_mock:
130+ with mock.patch("apply_patches.open", open_mock, create=True):
131+ apply_patch("scary.patch", "a/tree", verbose=True)
132+ self.assert_open_only(open_mock, "scary.patch")
133+ call_mock.assert_called_once_with(
134+ ["patch", "-f", "-u", "-p1", "-r-", "--verbose"],
135+ stdin=open_mock(),
136+ cwd="a/tree",
137+ )
138+
139+
140+class TestMain(unittest.TestCase):
141+
142+ @contextlib.contextmanager
143+ def patch_output(self):
144+ messages = []
145+
146+ def fake_print(message, file=None):
147+ self.assertEqual(file, sys.stderr)
148+ messages.append(message)
149+
150+ with mock.patch("apply_patches.print", fake_print, create=True):
151+ yield messages
152+
153+ def test_no_patchdir(self):
154+ stream = StringIO.StringIO()
155+ with temp_dir() as basedir:
156+ with self.assertRaises(SystemExit):
157+ with mock.patch("sys.stderr", stream):
158+ main(["test", os.path.join(basedir, "missing"), basedir])
159+ self.assertRegexpMatches(
160+ stream.getvalue(), "Could not list patch directory: .*/missing")
161+
162+ def test_no_srctree(self):
163+ stream = StringIO.StringIO()
164+ with temp_dir() as basedir:
165+ with self.assertRaises(SystemExit):
166+ with mock.patch("sys.stderr", stream):
167+ main(["test", basedir, os.path.join(basedir, "missing")])
168+ self.assertRegexpMatches(
169+ stream.getvalue(), "Source tree '.*/missing' not a directory")
170+
171+ def test_one_patch(self):
172+ with temp_dir() as basedir:
173+ patchdir = os.path.join(basedir, "patches")
174+ os.mkdir(patchdir)
175+ patchfile = os.path.join(patchdir, "sample.patch")
176+ open(patchfile, "w").close()
177+ with self.patch_output() as messages:
178+ with mock.patch(
179+ "apply_patches.apply_patch", return_value=0,
180+ autospec=True) as ap_mock:
181+ main(["test", patchdir, basedir])
182+ ap_mock.assert_called_once_with(patchfile, basedir, False, False)
183+ self.assertEqual(messages, [
184+ u"Applying 1 patch", u"Applied patch 'sample.patch'"
185+ ])
186+
187+ def test_bad_patches(self):
188+ with temp_dir() as basedir:
189+ patchdir = os.path.join(basedir, "patches")
190+ os.mkdir(patchdir)
191+ patch_a = os.path.join(patchdir, "a.patch")
192+ open(patch_a, "w").close()
193+ patch_b = os.path.join(patchdir, "b.patch")
194+ open(patch_b, "w").close()
195+ with self.patch_output() as messages:
196+ with mock.patch(
197+ "apply_patches.apply_patch", return_value=1,
198+ autospec=True) as ap_mock:
199+ main(["test", "--verbose", patchdir, basedir])
200+ ap_mock.assert_called_once_with(patch_a, basedir, False, True)
201+ self.assertEqual(messages, [
202+ u"Applying 2 patches", u"Failed to apply patch 'a.patch'"
203+ ])

Subscribers

People subscribed via source and target branches