1# Copyright (c) Twisted Matrix Laboratories.
2# See LICENSE for details.
3
4"""
5Tests for L{twisted.runner.procmon}.
6"""
7import pickle
8
9from twisted.internet.error import ProcessDone, ProcessExitedAlready, ProcessTerminated
10from twisted.internet.task import Clock
11from twisted.logger import globalLogPublisher
12from twisted.python.failure import Failure
13from twisted.runner.procmon import LoggingProtocol, ProcessMonitor
14from twisted.test.proto_helpers import MemoryReactor
15from twisted.trial import unittest
16
17
18class DummyProcess:
19    """
20    An incomplete and fake L{IProcessTransport} implementation for testing how
21    L{ProcessMonitor} behaves when its monitored processes exit.
22
23    @ivar _terminationDelay: the delay in seconds after which the DummyProcess
24        will appear to exit when it receives a TERM signal
25    """
26
27    pid = 1
28    proto = None
29
30    _terminationDelay = 1
31
32    def __init__(
33        self,
34        reactor,
35        executable,
36        args,
37        environment,
38        path,
39        proto,
40        uid=None,
41        gid=None,
42        usePTY=0,
43        childFDs=None,
44    ):
45
46        self.proto = proto
47
48        self._reactor = reactor
49        self._executable = executable
50        self._args = args
51        self._environment = environment
52        self._path = path
53        self._uid = uid
54        self._gid = gid
55        self._usePTY = usePTY
56        self._childFDs = childFDs
57
58    def signalProcess(self, signalID):
59        """
60        A partial implementation of signalProcess which can only handle TERM and
61        KILL signals.
62         - When a TERM signal is given, the dummy process will appear to exit
63           after L{DummyProcess._terminationDelay} seconds with exit code 0
64         - When a KILL signal is given, the dummy process will appear to exit
65           immediately with exit code 1.
66
67        @param signalID: The signal name or number to be issued to the process.
68        @type signalID: C{str}
69        """
70        params = {"TERM": (self._terminationDelay, 0), "KILL": (0, 1)}
71
72        if self.pid is None:
73            raise ProcessExitedAlready()
74
75        if signalID in params:
76            delay, status = params[signalID]
77            self._signalHandler = self._reactor.callLater(
78                delay, self.processEnded, status
79            )
80
81    def processEnded(self, status):
82        """
83        Deliver the process ended event to C{self.proto}.
84        """
85        self.pid = None
86        statusMap = {
87            0: ProcessDone,
88            1: ProcessTerminated,
89        }
90        self.proto.processEnded(Failure(statusMap[status](status)))
91
92
93class DummyProcessReactor(MemoryReactor, Clock):
94    """
95    @ivar spawnedProcesses: a list that keeps track of the fake process
96        instances built by C{spawnProcess}.
97    @type spawnedProcesses: C{list}
98    """
99
100    def __init__(self):
101        MemoryReactor.__init__(self)
102        Clock.__init__(self)
103
104        self.spawnedProcesses = []
105
106    def spawnProcess(
107        self,
108        processProtocol,
109        executable,
110        args=(),
111        env={},
112        path=None,
113        uid=None,
114        gid=None,
115        usePTY=0,
116        childFDs=None,
117    ):
118        """
119        Fake L{reactor.spawnProcess}, that logs all the process
120        arguments and returns a L{DummyProcess}.
121        """
122
123        proc = DummyProcess(
124            self,
125            executable,
126            args,
127            env,
128            path,
129            processProtocol,
130            uid,
131            gid,
132            usePTY,
133            childFDs,
134        )
135        processProtocol.makeConnection(proc)
136        self.spawnedProcesses.append(proc)
137        return proc
138
139
140class ProcmonTests(unittest.TestCase):
141    """
142    Tests for L{ProcessMonitor}.
143    """
144
145    def setUp(self):
146        """
147        Create an L{ProcessMonitor} wrapped around a fake reactor.
148        """
149        self.reactor = DummyProcessReactor()
150        self.pm = ProcessMonitor(reactor=self.reactor)
151        self.pm.minRestartDelay = 2
152        self.pm.maxRestartDelay = 10
153        self.pm.threshold = 10
154
155    def test_reprLooksGood(self):
156        """
157        Repr includes all details
158        """
159        self.pm.addProcess("foo", ["arg1", "arg2"], uid=1, gid=2, env={})
160        representation = repr(self.pm)
161        self.assertIn("foo", representation)
162        self.assertIn("1", representation)
163        self.assertIn("2", representation)
164
165    def test_simpleReprLooksGood(self):
166        """
167        Repr does not include unneeded details.
168
169        Values of attributes that just mean "inherit from launching
170        process" do not appear in the repr of a process.
171        """
172        self.pm.addProcess("foo", ["arg1", "arg2"], env={})
173        representation = repr(self.pm)
174        self.assertNotIn("(", representation)
175        self.assertNotIn(")", representation)
176
177    def test_getStateIncludesProcesses(self):
178        """
179        The list of monitored processes must be included in the pickle state.
180        """
181        self.pm.addProcess("foo", ["arg1", "arg2"], uid=1, gid=2, env={})
182        self.assertEqual(
183            self.pm.__getstate__()["processes"], {"foo": (["arg1", "arg2"], 1, 2, {})}
184        )
185
186    def test_getStateExcludesReactor(self):
187        """
188        The private L{ProcessMonitor._reactor} instance variable should not be
189        included in the pickle state.
190        """
191        self.assertNotIn("_reactor", self.pm.__getstate__())
192
193    def test_addProcess(self):
194        """
195        L{ProcessMonitor.addProcess} only starts the named program if
196        L{ProcessMonitor.startService} has been called.
197        """
198        self.pm.addProcess("foo", ["arg1", "arg2"], uid=1, gid=2, env={})
199        self.assertEqual(self.pm.protocols, {})
200        self.assertEqual(self.pm.processes, {"foo": (["arg1", "arg2"], 1, 2, {})})
201        self.pm.startService()
202        self.reactor.advance(0)
203        self.assertEqual(list(self.pm.protocols.keys()), ["foo"])
204
205    def test_addProcessDuplicateKeyError(self):
206        """
207        L{ProcessMonitor.addProcess} raises a C{KeyError} if a process with the
208        given name already exists.
209        """
210        self.pm.addProcess("foo", ["arg1", "arg2"], uid=1, gid=2, env={})
211        self.assertRaises(
212            KeyError, self.pm.addProcess, "foo", ["arg1", "arg2"], uid=1, gid=2, env={}
213        )
214
215    def test_addProcessEnv(self):
216        """
217        L{ProcessMonitor.addProcess} takes an C{env} parameter that is passed to
218        L{IReactorProcess.spawnProcess}.
219        """
220        fakeEnv = {"KEY": "value"}
221        self.pm.startService()
222        self.pm.addProcess("foo", ["foo"], uid=1, gid=2, env=fakeEnv)
223        self.reactor.advance(0)
224        self.assertEqual(self.reactor.spawnedProcesses[0]._environment, fakeEnv)
225
226    def test_addProcessCwd(self):
227        """
228        L{ProcessMonitor.addProcess} takes an C{cwd} parameter that is passed
229        to L{IReactorProcess.spawnProcess}.
230        """
231        self.pm.startService()
232        self.pm.addProcess("foo", ["foo"], cwd="/mnt/lala")
233        self.reactor.advance(0)
234        self.assertEqual(self.reactor.spawnedProcesses[0]._path, "/mnt/lala")
235
236    def test_removeProcess(self):
237        """
238        L{ProcessMonitor.removeProcess} removes the process from the public
239        processes list.
240        """
241        self.pm.startService()
242        self.pm.addProcess("foo", ["foo"])
243        self.assertEqual(len(self.pm.processes), 1)
244        self.pm.removeProcess("foo")
245        self.assertEqual(len(self.pm.processes), 0)
246
247    def test_removeProcessUnknownKeyError(self):
248        """
249        L{ProcessMonitor.removeProcess} raises a C{KeyError} if the given
250        process name isn't recognised.
251        """
252        self.pm.startService()
253        self.assertRaises(KeyError, self.pm.removeProcess, "foo")
254
255    def test_startProcess(self):
256        """
257        When a process has been started, an instance of L{LoggingProtocol} will
258        be added to the L{ProcessMonitor.protocols} dict and the start time of
259        the process will be recorded in the L{ProcessMonitor.timeStarted}
260        dictionary.
261        """
262        self.pm.addProcess("foo", ["foo"])
263        self.pm.startProcess("foo")
264        self.assertIsInstance(self.pm.protocols["foo"], LoggingProtocol)
265        self.assertIn("foo", self.pm.timeStarted.keys())
266
267    def test_startProcessAlreadyStarted(self):
268        """
269        L{ProcessMonitor.startProcess} silently returns if the named process is
270        already started.
271        """
272        self.pm.addProcess("foo", ["foo"])
273        self.pm.startProcess("foo")
274        self.assertIsNone(self.pm.startProcess("foo"))
275
276    def test_startProcessUnknownKeyError(self):
277        """
278        L{ProcessMonitor.startProcess} raises a C{KeyError} if the given
279        process name isn't recognised.
280        """
281        self.assertRaises(KeyError, self.pm.startProcess, "foo")
282
283    def test_stopProcessNaturalTermination(self):
284        """
285        L{ProcessMonitor.stopProcess} immediately sends a TERM signal to the
286        named process.
287        """
288        self.pm.startService()
289        self.pm.addProcess("foo", ["foo"])
290        self.assertIn("foo", self.pm.protocols)
291
292        # Configure fake process to die 1 second after receiving term signal
293        timeToDie = self.pm.protocols["foo"].transport._terminationDelay = 1
294
295        # Advance the reactor to just before the short lived process threshold
296        # and leave enough time for the process to die
297        self.reactor.advance(self.pm.threshold)
298        # Then signal the process to stop
299        self.pm.stopProcess("foo")
300
301        # Advance the reactor just enough to give the process time to die and
302        # verify that the process restarts
303        self.reactor.advance(timeToDie)
304
305        # We expect it to be restarted immediately
306        self.assertEqual(self.reactor.seconds(), self.pm.timeStarted["foo"])
307
308    def test_stopProcessForcedKill(self):
309        """
310        L{ProcessMonitor.stopProcess} kills a process which fails to terminate
311        naturally within L{ProcessMonitor.killTime} seconds.
312        """
313        self.pm.startService()
314        self.pm.addProcess("foo", ["foo"])
315        self.assertIn("foo", self.pm.protocols)
316        self.reactor.advance(self.pm.threshold)
317        proc = self.pm.protocols["foo"].transport
318        # Arrange for the fake process to live longer than the killTime
319        proc._terminationDelay = self.pm.killTime + 1
320        self.pm.stopProcess("foo")
321        # If process doesn't die before the killTime, procmon should
322        # terminate it
323        self.reactor.advance(self.pm.killTime - 1)
324        self.assertEqual(0.0, self.pm.timeStarted["foo"])
325
326        self.reactor.advance(1)
327        # We expect it to be immediately restarted
328        self.assertEqual(self.reactor.seconds(), self.pm.timeStarted["foo"])
329
330    def test_stopProcessUnknownKeyError(self):
331        """
332        L{ProcessMonitor.stopProcess} raises a C{KeyError} if the given process
333        name isn't recognised.
334        """
335        self.assertRaises(KeyError, self.pm.stopProcess, "foo")
336
337    def test_stopProcessAlreadyStopped(self):
338        """
339        L{ProcessMonitor.stopProcess} silently returns if the named process
340        is already stopped. eg Process has crashed and a restart has been
341        rescheduled, but in the meantime, the service is stopped.
342        """
343        self.pm.addProcess("foo", ["foo"])
344        self.assertIsNone(self.pm.stopProcess("foo"))
345
346    def test_outputReceivedCompleteLine(self):
347        """
348        Getting a complete output line on stdout generates a log message.
349        """
350        events = []
351        self.addCleanup(globalLogPublisher.removeObserver, events.append)
352        globalLogPublisher.addObserver(events.append)
353        self.pm.addProcess("foo", ["foo"])
354        # Schedule the process to start
355        self.pm.startService()
356        # Advance the reactor to start the process
357        self.reactor.advance(0)
358        self.assertIn("foo", self.pm.protocols)
359        # Long time passes
360        self.reactor.advance(self.pm.threshold)
361        # Process greets
362        self.pm.protocols["foo"].outReceived(b"hello world!\n")
363        self.assertEquals(len(events), 1)
364        namespace = events[0]["log_namespace"]
365        stream = events[0]["stream"]
366        tag = events[0]["tag"]
367        line = events[0]["line"]
368        self.assertEquals(namespace, "twisted.runner.procmon.ProcessMonitor")
369        self.assertEquals(stream, "stdout")
370        self.assertEquals(tag, "foo")
371        self.assertEquals(line, "hello world!")
372
373    def test_ouputReceivedCompleteErrLine(self):
374        """
375        Getting a complete output line on stderr generates a log message.
376        """
377        events = []
378        self.addCleanup(globalLogPublisher.removeObserver, events.append)
379        globalLogPublisher.addObserver(events.append)
380        self.pm.addProcess("foo", ["foo"])
381        # Schedule the process to start
382        self.pm.startService()
383        # Advance the reactor to start the process
384        self.reactor.advance(0)
385        self.assertIn("foo", self.pm.protocols)
386        # Long time passes
387        self.reactor.advance(self.pm.threshold)
388        # Process greets
389        self.pm.protocols["foo"].errReceived(b"hello world!\n")
390        self.assertEquals(len(events), 1)
391        namespace = events[0]["log_namespace"]
392        stream = events[0]["stream"]
393        tag = events[0]["tag"]
394        line = events[0]["line"]
395        self.assertEquals(namespace, "twisted.runner.procmon.ProcessMonitor")
396        self.assertEquals(stream, "stderr")
397        self.assertEquals(tag, "foo")
398        self.assertEquals(line, "hello world!")
399
400    def test_outputReceivedCompleteLineInvalidUTF8(self):
401        """
402        Getting invalid UTF-8 results in the repr of the raw message
403        """
404        events = []
405        self.addCleanup(globalLogPublisher.removeObserver, events.append)
406        globalLogPublisher.addObserver(events.append)
407        self.pm.addProcess("foo", ["foo"])
408        # Schedule the process to start
409        self.pm.startService()
410        # Advance the reactor to start the process
411        self.reactor.advance(0)
412        self.assertIn("foo", self.pm.protocols)
413        # Long time passes
414        self.reactor.advance(self.pm.threshold)
415        # Process greets
416        self.pm.protocols["foo"].outReceived(b"\xffhello world!\n")
417        self.assertEquals(len(events), 1)
418        message = events[0]
419        namespace = message["log_namespace"]
420        stream = message["stream"]
421        tag = message["tag"]
422        output = message["line"]
423        self.assertEquals(namespace, "twisted.runner.procmon.ProcessMonitor")
424        self.assertEquals(stream, "stdout")
425        self.assertEquals(tag, "foo")
426        self.assertEquals(output, repr(b"\xffhello world!"))
427
428    def test_outputReceivedPartialLine(self):
429        """
430        Getting partial line results in no events until process end
431        """
432        events = []
433        self.addCleanup(globalLogPublisher.removeObserver, events.append)
434        globalLogPublisher.addObserver(events.append)
435        self.pm.addProcess("foo", ["foo"])
436        # Schedule the process to start
437        self.pm.startService()
438        # Advance the reactor to start the process
439        self.reactor.advance(0)
440        self.assertIn("foo", self.pm.protocols)
441        # Long time passes
442        self.reactor.advance(self.pm.threshold)
443        # Process greets
444        self.pm.protocols["foo"].outReceived(b"hello world!")
445        self.assertEquals(len(events), 0)
446        self.pm.protocols["foo"].processEnded(Failure(ProcessDone(0)))
447        self.assertEquals(len(events), 1)
448        namespace = events[0]["log_namespace"]
449        stream = events[0]["stream"]
450        tag = events[0]["tag"]
451        line = events[0]["line"]
452        self.assertEquals(namespace, "twisted.runner.procmon.ProcessMonitor")
453        self.assertEquals(stream, "stdout")
454        self.assertEquals(tag, "foo")
455        self.assertEquals(line, "hello world!")
456
457    def test_connectionLostLongLivedProcess(self):
458        """
459        L{ProcessMonitor.connectionLost} should immediately restart a process
460        if it has been running longer than L{ProcessMonitor.threshold} seconds.
461        """
462        self.pm.addProcess("foo", ["foo"])
463        # Schedule the process to start
464        self.pm.startService()
465        # advance the reactor to start the process
466        self.reactor.advance(0)
467        self.assertIn("foo", self.pm.protocols)
468        # Long time passes
469        self.reactor.advance(self.pm.threshold)
470        # Process dies after threshold
471        self.pm.protocols["foo"].processEnded(Failure(ProcessDone(0)))
472        self.assertNotIn("foo", self.pm.protocols)
473        # Process should be restarted immediately
474        self.reactor.advance(0)
475        self.assertIn("foo", self.pm.protocols)
476
477    def test_connectionLostMurderCancel(self):
478        """
479        L{ProcessMonitor.connectionLost} cancels a scheduled process killer and
480        deletes the DelayedCall from the L{ProcessMonitor.murder} list.
481        """
482        self.pm.addProcess("foo", ["foo"])
483        # Schedule the process to start
484        self.pm.startService()
485        # Advance 1s to start the process then ask ProcMon to stop it
486        self.reactor.advance(1)
487        self.pm.stopProcess("foo")
488        # A process killer has been scheduled, delayedCall is active
489        self.assertIn("foo", self.pm.murder)
490        delayedCall = self.pm.murder["foo"]
491        self.assertTrue(delayedCall.active())
492        # Advance to the point at which the dummy process exits
493        self.reactor.advance(self.pm.protocols["foo"].transport._terminationDelay)
494        # Now the delayedCall has been cancelled and deleted
495        self.assertFalse(delayedCall.active())
496        self.assertNotIn("foo", self.pm.murder)
497
498    def test_connectionLostProtocolDeletion(self):
499        """
500        L{ProcessMonitor.connectionLost} removes the corresponding
501        ProcessProtocol instance from the L{ProcessMonitor.protocols} list.
502        """
503        self.pm.startService()
504        self.pm.addProcess("foo", ["foo"])
505        self.assertIn("foo", self.pm.protocols)
506        self.pm.protocols["foo"].transport.signalProcess("KILL")
507        self.reactor.advance(self.pm.protocols["foo"].transport._terminationDelay)
508        self.assertNotIn("foo", self.pm.protocols)
509
510    def test_connectionLostMinMaxRestartDelay(self):
511        """
512        L{ProcessMonitor.connectionLost} will wait at least minRestartDelay s
513        and at most maxRestartDelay s
514        """
515        self.pm.minRestartDelay = 2
516        self.pm.maxRestartDelay = 3
517
518        self.pm.startService()
519        self.pm.addProcess("foo", ["foo"])
520
521        self.assertEqual(self.pm.delay["foo"], self.pm.minRestartDelay)
522        self.reactor.advance(self.pm.threshold - 1)
523        self.pm.protocols["foo"].processEnded(Failure(ProcessDone(0)))
524        self.assertEqual(self.pm.delay["foo"], self.pm.maxRestartDelay)
525
526    def test_connectionLostBackoffDelayDoubles(self):
527        """
528        L{ProcessMonitor.connectionLost} doubles the restart delay each time
529        the process dies too quickly.
530        """
531        self.pm.startService()
532        self.pm.addProcess("foo", ["foo"])
533        self.reactor.advance(self.pm.threshold - 1)  # 9s
534        self.assertIn("foo", self.pm.protocols)
535        self.assertEqual(self.pm.delay["foo"], self.pm.minRestartDelay)
536        # process dies within the threshold and should not restart immediately
537        self.pm.protocols["foo"].processEnded(Failure(ProcessDone(0)))
538        self.assertEqual(self.pm.delay["foo"], self.pm.minRestartDelay * 2)
539
540    def test_startService(self):
541        """
542        L{ProcessMonitor.startService} starts all monitored processes.
543        """
544        self.pm.addProcess("foo", ["foo"])
545        # Schedule the process to start
546        self.pm.startService()
547        # advance the reactor to start the process
548        self.reactor.advance(0)
549        self.assertIn("foo", self.pm.protocols)
550
551    def test_stopService(self):
552        """
553        L{ProcessMonitor.stopService} should stop all monitored processes.
554        """
555        self.pm.addProcess("foo", ["foo"])
556        self.pm.addProcess("bar", ["bar"])
557        # Schedule the process to start
558        self.pm.startService()
559        # advance the reactor to start the processes
560        self.reactor.advance(self.pm.threshold)
561        self.assertIn("foo", self.pm.protocols)
562        self.assertIn("bar", self.pm.protocols)
563
564        self.reactor.advance(1)
565
566        self.pm.stopService()
567        # Advance to beyond the killTime - all monitored processes
568        # should have exited
569        self.reactor.advance(self.pm.killTime + 1)
570        # The processes shouldn't be restarted
571        self.assertEqual({}, self.pm.protocols)
572
573    def test_restartAllRestartsOneProcess(self):
574        """
575        L{ProcessMonitor.restartAll} succeeds when there is one process.
576        """
577        self.pm.addProcess("foo", ["foo"])
578        self.pm.startService()
579        self.reactor.advance(1)
580        self.pm.restartAll()
581        # Just enough time for the process to die,
582        # not enough time to start a new one.
583        self.reactor.advance(1)
584        processes = list(self.reactor.spawnedProcesses)
585        myProcess = processes.pop()
586        self.assertEquals(processes, [])
587        self.assertIsNone(myProcess.pid)
588
589    def test_stopServiceCancelRestarts(self):
590        """
591        L{ProcessMonitor.stopService} should cancel any scheduled process
592        restarts.
593        """
594        self.pm.addProcess("foo", ["foo"])
595        # Schedule the process to start
596        self.pm.startService()
597        # advance the reactor to start the processes
598        self.reactor.advance(self.pm.threshold)
599        self.assertIn("foo", self.pm.protocols)
600
601        self.reactor.advance(1)
602        # Kill the process early
603        self.pm.protocols["foo"].processEnded(Failure(ProcessDone(0)))
604        self.assertTrue(self.pm.restart["foo"].active())
605        self.pm.stopService()
606        # Scheduled restart should have been cancelled
607        self.assertFalse(self.pm.restart["foo"].active())
608
609    def test_stopServiceCleanupScheduledRestarts(self):
610        """
611        L{ProcessMonitor.stopService} should cancel all scheduled process
612        restarts.
613        """
614        self.pm.threshold = 5
615        self.pm.minRestartDelay = 5
616        # Start service and add a process (started immediately)
617        self.pm.startService()
618        self.pm.addProcess("foo", ["foo"])
619        # Stop the process after 1s
620        self.reactor.advance(1)
621        self.pm.stopProcess("foo")
622        # Wait 1s for it to exit it will be scheduled to restart 5s later
623        self.reactor.advance(1)
624        # Meanwhile stop the service
625        self.pm.stopService()
626        # Advance to beyond the process restart time
627        self.reactor.advance(6)
628        # The process shouldn't have restarted because stopService has cancelled
629        # all pending process restarts.
630        self.assertEqual(self.pm.protocols, {})
631
632
633class DeprecationTests(unittest.SynchronousTestCase):
634
635    """
636    Tests that check functionality that should be deprecated is deprecated.
637    """
638
639    def setUp(self):
640        """
641        Create reactor and process monitor.
642        """
643        self.reactor = DummyProcessReactor()
644        self.pm = ProcessMonitor(reactor=self.reactor)
645
646    def test_toTuple(self):
647        """
648        _Process.toTuple is deprecated.
649
650        When getting the deprecated processes property, the actual
651        data (kept in the class _Process) is converted to a tuple --
652        which produces a DeprecationWarning per process so converted.
653        """
654        self.pm.addProcess("foo", ["foo"])
655        myprocesses = self.pm.processes
656        self.assertEquals(len(myprocesses), 1)
657        warnings = self.flushWarnings()
658        foundToTuple = False
659        for warning in warnings:
660            self.assertIs(warning["category"], DeprecationWarning)
661            if "toTuple" in warning["message"]:
662                foundToTuple = True
663        self.assertTrue(foundToTuple, f"no tuple deprecation found:{repr(warnings)}")
664
665    def test_processes(self):
666        """
667        Accessing L{ProcessMonitor.processes} results in deprecation warning
668
669        Even when there are no processes, and thus no process is converted
670        to a tuple, accessing the L{ProcessMonitor.processes} property
671        should generate its own DeprecationWarning.
672        """
673        myProcesses = self.pm.processes
674        self.assertEquals(myProcesses, {})
675        warnings = self.flushWarnings()
676        first = warnings.pop(0)
677        self.assertIs(first["category"], DeprecationWarning)
678        self.assertEquals(warnings, [])
679
680    def test_getstate(self):
681        """
682        Pickling an L{ProcessMonitor} results in deprecation warnings
683        """
684        pickle.dumps(self.pm)
685        warnings = self.flushWarnings()
686        for warning in warnings:
687            self.assertIs(warning["category"], DeprecationWarning)
688