Repository: pymedusa/Medusa

Issue:
PostProcess: media extracted from .RAR should be moved and not hard linked
As the comment in code from process.py:
`Don't Link media when the media is extracted from a rar in the same path`

But it's not working as logs (check last line)

To reproduce just PP a RARed torrent

It could lead to left overs media file and folder not being deleted as the media file is not part of the torrent

```
2017-06-05 15:15:12 DEBUG    Thread-25 :: [b4a8a54] Processing path: C:\Users\fernando\Documents\PP\Show.S06E011.720p.HDTV.x264-FLEET
2017-06-05 15:15:12 INFO     Thread-25 :: [b4a8a54] Feature 'postpone post-processing if no subtitle available' is enabled.
2017-06-05 15:15:12 DEBUG    Thread-25 :: [b4a8a54] Processing folder: C:\Users\fernando\Documents\PP\Show.S06E011.720p.HDTV.x264-FLEET
2017-06-05 15:15:12 DEBUG    Thread-25 :: [b4a8a54] Packed files detected: [u'Show.S06E011.720p.HDTV.x264-FLEET.rar']
2017-06-05 15:15:12 DEBUG    Thread-25 :: [b4a8a54] Unpacking archive: Show.S06E011.720p.HDTV.x264-FLEET.rar
2017-06-05 15:15:12 DEBUG    Thread-25 :: [b4a8a54] Extracted content: [u'Show.S06E011.720p.HDTV.x264-FLEET.mkv']
2017-06-05 15:15:12 DEBUG    Thread-25 :: [b4a8a54] Post-processing files: [u'Show.S06E011.720p.HDTV.x264-FLEET.pt-BR.srt', u'Show.S06E011.720p.HDTV.x264-FLEET.rar', u'Show.S06E011.720p.HDTV.x264-FLEET.mkv']
2017-06-05 15:15:12 DEBUG    Thread-25 :: [b4a8a54] Post-processing video files: [u'Show.S06E011.720p.HDTV.x264-FLEET.mkv']
2017-06-05 15:15:12 DEBUG    Thread-25 :: [b4a8a54] Post-processing rar content: [u'Show.S06E011.720p.HDTV.x264-FLEET.mkv']
2017-06-05 15:15:12 DEBUG    Thread-25 :: [b4a8a54] Post-processing video in rar: [u'Show.S06E011.720p.HDTV.x264-FLEET.mkv']
2017-06-05 15:15:12 DEBUG    Thread-25 :: [b4a8a54] Found unwanted files: [u'Show.S06E011.720p.HDTV.x264-FLEET.rar']
2017-06-05 15:15:12 DEBUG    Thread-25 :: [b4a8a54] Found the following associated files for C:\Users\fernando\Documents\PP\Show.S06E011.720p.HDTV.x264-FLEET\Show.S06E011.720p.HDTV.x264-FLEET.mkv: set([u'C:\\Users\\fernando\\Documents\\PP\\Show.S06E011.720p.HDTV.x264-FLEET\\Show.S06E011.720p.HDTV.x264-FLEET.pt-BR.srt'])
2017-06-05 15:15:12 INFO     Thread-25 :: [b4a8a54] Found associated subtitles. Continuing the post-processing of this file: Show.S06E011.720p.HDTV.x264-FLEET.mkv
2017-06-05 15:15:12 INFO     Thread-25 :: [b4a8a54] Processing C:\Users\fernando\Documents\PP\Show.S06E011.720p.HDTV.x264-FLEET\Show.S06E011.720p.HDTV.x264-FLEET.mkv
2017-06-05 15:15:14 DEBUG    Thread-25 :: [b4a8a54] Moving file from C:\Users\fernando\Documents\PP\Show.S06E011.720p.HDTV.x264-FLEET\Show.S06E011.720p.HDTV.x264-FLEET.pt-BR.srt to C:\Users\fernando\Documents\Series\Show\Season 06\Show.S06.E11.Daddys.Girl.720p.HDTV.x264.pt-BR.srt 
2017-06-05 15:15:14 DEBUG    Thread-25 :: [b4a8a54] Hard linking file from C:\Users\fernando\Documents\PP\Show.S06E011.720p.HDTV.x264-FLEET\Show.S06E011.720p.HDTV.x264-FLEET.mkv to C:\Users\fernando\Documents\Series\Show\Season 06\Show.S06.E11.Daddys.Girl.720p.HDTV.x264.mkv
```
ping @duramato 


Discussion:

Resolving patch:
diff --git a/medusa/process_tv.py b/medusa/process_tv.py
--- a/medusa/process_tv.py
+++ b/medusa/process_tv.py
@@ -22,7 +22,12 @@
 import socket
 import stat
 
+from medusa import app, db, failed_processor, helpers, logger, notifiers, post_processor
 from medusa.clients import torrent
+from medusa.helper.common import is_sync_file
+from medusa.helper.exceptions import EpisodePostProcessingFailedException, FailedPostProcessingFailedException, ex
+from medusa.name_parser.parser import InvalidNameException, InvalidShowException, NameParser
+from medusa.subtitles import accept_any, accept_unknown, get_embedded_subtitles
 
 import requests
 
@@ -31,11 +36,7 @@
 from unrar2 import RarFile
 from unrar2.rar_exceptions import (ArchiveHeaderBroken, FileOpenError, IncorrectRARPassword, InvalidRARArchive,
                                    InvalidRARArchiveUsage)
-from . import app, db, failed_processor, helpers, logger, notifiers, post_processor
-from .helper.common import is_sync_file
-from .helper.exceptions import EpisodePostProcessingFailedException, FailedPostProcessingFailedException, ex
-from .name_parser.parser import InvalidNameException, InvalidShowException, NameParser
-from .subtitles import accept_any, accept_unknown, get_embedded_subtitles
+
 
 shutil.copyfile = shutil_custom.copyfile_custom
 
@@ -54,7 +55,6 @@ def __init__(self, path, process_method=None):
         self.succeeded = True
         self.missedfiles = []
         self.allowed_extensions = app.ALLOWED_EXTENSIONS.split(',')
-        self.postponed_no_subs = False
 
     @property
     def directory(self):
@@ -172,7 +172,7 @@ def process(self, resource_name=None, force=False, is_priority=None, delete_on=F
                     if all([not app.NO_DELETE or proc_type == 'manual', self.process_method == 'move',
                             os.path.normpath(dir_path) != os.path.normpath(app.TV_DOWNLOAD_DIR)]):
 
-                        if self.delete_folder(dir_path, check_empty=True):
+                        if self.delete_folder(dir_path):
                             self._log('Deleted folder: {0}'.format(dir_path), logger.DEBUG)
 
                 else:
@@ -199,7 +199,7 @@ def process(self, resource_name=None, force=False, is_priority=None, delete_on=F
         if app.USE_TORRENTS and app.PROCESS_METHOD in ('hardlink', 'symlink') and app.TORRENT_SEED_LOCATION:
             to_remove_hashes = app.RECENTLY_POSTPROCESSED.items()
             for info_hash, release_names in to_remove_hashes:
-                if self.move_torrent_seeding_folder(info_hash, release_names):
+                if self.move_torrent(info_hash, release_names):
                     app.RECENTLY_POSTPROCESSED.pop(info_hash)
 
         return self.output
@@ -309,32 +309,29 @@ def process_files(self, path, force=False, is_priority=None, ignore_subs=False):
         if self.resource_name and len(self.video_files) > 1:
             self.resource_name = None
 
-        # Don't Link media when the media is extracted from a rar in the same path
-        if self.process_method in ('hardlink', 'symlink') and self.video_in_rar:
-            self.process_media(path, self.video_in_rar, force, is_priority, ignore_subs)
-
-            self.process_media(path, set(self.video_files) - set(self.video_in_rar), force,
-                               is_priority, ignore_subs)
-
-            if not self.postponed_no_subs:
-                self.delete_files(path, self.rar_content)
+        if self.video_in_rar:
+            video_files = set(self.video_files + self.video_in_rar)
+
+            if self.process_method in ('hardlink', 'symlink'):
+                process_method = self.process_method
+                # Move extracted video files instead of hard/softlinking them
+                self.process_method = 'move'
+                self.process_media(path, self.video_in_rar, force, is_priority, ignore_subs)
+                if not self.postpone_processing:
+                    self.delete_files(path, self.rar_content)
+                # Reset process method to initial value
+                self.process_method = process_method
+
+                self.process_media(path, video_files - set(self.video_in_rar), force,
+                                   is_priority, ignore_subs)
             else:
-                self.postponed_no_subs = False
+                self.process_media(path, video_files, force, is_priority, ignore_subs)
 
-        elif app.DELRARCONTENTS and self.video_in_rar:
-            self.process_media(path, self.video_in_rar, force, is_priority, ignore_subs)
-
-            self.process_media(path, set(self.video_files) - set(self.video_in_rar),
-                               force, is_priority, ignore_subs)
-
-            if not self.postponed_no_subs:
-                self.delete_files(path, self.rar_content, force=True)
-            else:
-                self.postponed_no_subs = False
+                if app.DELRARCONTENTS and not self.postpone_processing:
+                    self.delete_files(path, self.rar_content)
 
         else:
             self.process_media(path, self.video_files, force, is_priority, ignore_subs)
-            self.postponed_no_subs = False
 
     @staticmethod
     def delete_folder(folder, check_empty=True):
@@ -384,7 +381,6 @@ def delete_files(self, path, files, force=False):
 
         :param path: path to process
         :param files: files we want to delete
-        :param result: Processor results
         :param force: Boolean, force deletion, defaults to false
         """
         if not files:
@@ -397,7 +393,6 @@ def delete_files(self, path, files, force=False):
 
         # Delete all file not needed
         for cur_file in files:
-
             cur_file_path = os.path.join(path, cur_file)
 
             if not os.path.isfile(cur_file_path):
@@ -424,19 +419,16 @@ def unrar(self, path, rar_files, force=False):
         Extract RAR files.
 
         :param path: Path to look for files in
-        :param rarFiles: Names of RAR files
+        :param rar_files: Names of RAR files
         :param force: process currently processing items
-        :param result: Previous results
         :return: List of unpacked file names
         """
         unpacked_files = []
 
         if app.UNPACK and rar_files:
-
             self._log('Packed files detected: {0}'.format(rar_files), logger.DEBUG)
 
             for archive in rar_files:
-
                 self._log('Unpacking archive: {0}'.format(archive), logger.DEBUG)
 
                 failure = None
@@ -444,34 +436,30 @@ def unrar(self, path, rar_files, force=False):
                     rar_handle = RarFile(os.path.join(path, archive))
 
                     # Skip extraction if any file in archive has previously been extracted
-                    skip_file = False
+                    skip_extraction = False
                     for file_in_archive in [os.path.basename(each.filename)
                                             for each in rar_handle.infolist()
                                             if not each.isdir]:
                         if not force and self.already_postprocessed(file_in_archive):
                             self._log('Archive file already post-processed, extraction skipped: {0}'.format
                                       (file_in_archive), logger.DEBUG)
-                            skip_file = True
+                            skip_extraction = True
                             break
 
                         if app.POSTPONE_IF_NO_SUBS and os.path.isfile(os.path.join(path, file_in_archive)):
                             self._log('Archive file already extracted, extraction skipped: {0}'.format
                                       (file_in_archive), logger.DEBUG)
-                            skip_file = True
-                            # We need to return the media file inside the rar so we can
-                            # move it when the method is hardlink/symlink
-                            unpacked_files.append(file_in_archive)
+                            skip_extraction = True
                             break
 
-                    if skip_file:
-                        continue
+                    if not skip_extraction:
+                        rar_handle.extract(path=path, withSubpath=False, overwrite=False)
 
-                    rar_handle.extract(path=path, withSubpath=False, overwrite=False)
                     for each in rar_handle.infolist():
                         if not each.isdir:
                             basename = os.path.basename(each.filename)
-                            if basename not in unpacked_files:
-                                unpacked_files.append(basename)
+                            unpacked_files.append(basename)
+
                     del rar_handle
 
                 except ArchiveHeaderBroken:
@@ -489,12 +477,12 @@ def unrar(self, path, rar_files, force=False):
                     failure = (ex(error), 'Unpacking failed for an unknown reason')
 
                 if failure is not None:
-                    self._log('Failed Unrar archive {0}: {1}'.format(archive, failure[0]), logger.WARNING)
+                    self._log('Failed unpacking archive {0}: {1}'.format(archive, failure[0]), logger.WARNING)
                     self.missedfiles.append('{0}: Unpacking failed: {1}'.format(archive, failure[1]))
                     self.result = False
                     continue
 
-            self._log('Unrar content: {0}'.format(unpacked_files), logger.DEBUG)
+            self._log('Extracted content: {0}'.format(unpacked_files), logger.DEBUG)
 
         return unpacked_files
 
@@ -503,7 +491,6 @@ def already_postprocessed(self, video_file):
         Check if we already post processed a file.
 
         :param video_file: File name
-        :param result: True if file is already postprocessed
         :return:
         """
         main_db_con = db.DBConnection()
@@ -522,20 +509,19 @@ def process_media(self, path, video_files, force=False, is_priority=None, ignore
         """
         Postprocess media files.
 
-        :param processPath: Path to postprocess in
-        :param videoFiles: Filenames to look for and postprocess
+        :param path: Path to postprocess in
+        :param video_files: Filenames to look for and postprocess
         :param force: Postprocess currently postprocessing file
         :param is_priority: Boolean, is this a priority download
-        :param result: Previous results
         :param ignore_subs: True to ignore setting 'postpone if no subs'
         """
-        processor = None
-        for video_file in video_files:
-            file_path = os.path.join(path, video_file)
+        self.postpone_processing = False
 
-            if not force and self.already_postprocessed(video_file):
-                self._log('Skipping already processed file: {0}'.format(video_file), logger.DEBUG)
-                self._log('Skipping already processed directory: {0}'.format(path), logger.DEBUG)
+        for video in video_files:
+            file_path = os.path.join(path, video)
+
+            if not force and self.already_postprocessed(video):
+                self._log('Skipping already processed file: {0}'.format(video), logger.DEBUG)
                 continue
 
             try:
@@ -543,37 +529,13 @@ def process_media(self, path, video_files, force=False, is_priority=None, ignore
                                                          self.process_method, is_priority)
 
                 if app.POSTPONE_IF_NO_SUBS:
-                    if not ignore_subs:
-                        if self.subtitles_enabled(file_path, self.resource_name):
-                            embedded_subs = set() if app.IGNORE_EMBEDDED_SUBS else get_embedded_subtitles(file_path)
-
-                            # We want to ignore embedded subtitles and video has at least one
-                            if accept_unknown(embedded_subs):
-                                self._log("Found embedded unknown subtitles and we don't want to ignore them. "
-                                          "Continuing the post-processing of this file: {0}".format(video_file))
-                            elif accept_any(embedded_subs):
-                                self._log('Found wanted embedded subtitles. '
-                                          'Continuing the post-processing of this file: {0}'.format(video_file))
-                            else:
-                                associated_subs = processor.list_associated_files(file_path, subtitles_only=True)
-                                if not associated_subs:
-                                    self._log('No subtitles associated. Postponing the post-process of this file: '
-                                              '{0}'.format(video_file), logger.DEBUG)
-                                    self.postponed_no_subs = True
-                                    continue
-                                else:
-                                    self._log('Found subtitles associated. '
-                                              'Continuing the post-process of this file: {0}'.format(video_file))
-                        else:
-                            self._log('Subtitles disabled for this show. '
-                                      'Continuing the post-process of this file: {0}'.format(video_file))
-                    else:
-                        self._log('Subtitles check was disabled for this episode in manual post-processing. '
-                                  'Continuing the post-process of this file: {0}'.format(video_file))
+                    if not self._process_postponed(processor, file_path, video, ignore_subs):
+                        continue
 
                 self.result = processor.process()
                 process_fail_message = ''
             except EpisodePostProcessingFailedException as error:
+                processor = None
                 self.result = False
                 process_fail_message = ex(error)
 
@@ -587,16 +549,45 @@ def process_media(self, path, video_files, force=False, is_priority=None, ignore
                 self.missedfiles.append('{0}: Processing failed: {1}'.format(file_path, process_fail_message))
                 self.succeeded = False
 
+    def _process_postponed(self, processor, path, video, ignore_subs):
+        if not ignore_subs:
+            if self.subtitles_enabled(path, self.resource_name):
+                embedded_subs = set() if app.IGNORE_EMBEDDED_SUBS else get_embedded_subtitles(path)
+
+                # We want to ignore embedded subtitles and video has at least one
+                if accept_unknown(embedded_subs):
+                    self._log("Found embedded unknown subtitles and we don't want to ignore them. "
+                              "Continuing the post-processing of this file: {0}".format(video))
+                elif accept_any(embedded_subs):
+                    self._log('Found wanted embedded subtitles. '
+                              'Continuing the post-processing of this file: {0}'.format(video))
+                else:
+                    associated_subs = processor.list_associated_files(path, subtitles_only=True)
+                    if not associated_subs:
+                        self._log('No subtitles associated. Postponing the post-processing of this file: '
+                                  '{0}'.format(video), logger.DEBUG)
+                        self.postpone_processing = True
+                        return False
+                    else:
+                        self._log('Found associated subtitles. '
+                                  'Continuing the post-processing of this file: {0}'.format(video))
+            else:
+                self._log('Subtitles disabled for this show. '
+                          'Continuing the post-processing of this file: {0}'.format(video))
+        else:
+            self._log('Subtitles check was disabled for this episode in manual post-processing. '
+                      'Continuing the post-processing of this file: {0}'.format(video))
+        return True
+
     def process_failed(self, path):
         """Process a download that did not complete correctly."""
         if app.USE_FAILED_DOWNLOADS:
-            processor = None
-
             try:
                 processor = failed_processor.FailedProcessor(path, self.resource_name)
                 self.result = processor.process()
                 process_fail_message = ''
             except FailedPostProcessingFailedException as error:
+                processor = None
                 self.result = False
                 process_fail_message = ex(error)
 
@@ -627,7 +618,7 @@ def subtitles_enabled(*args):
                 continue
 
             try:
-                parse_result = NameParser().parse(name, cache_result=True)
+                parse_result = NameParser().parse(name)
                 if parse_result.show.indexerid:
                     main_db_con = db.DBConnection()
                     sql_results = main_db_con.select("SELECT subtitles FROM tv_shows WHERE indexer_id = ? LIMIT 1",
@@ -641,41 +632,42 @@ def subtitles_enabled(*args):
         return False
 
     @staticmethod
-    def move_torrent_seeding_folder(info_hash, release_names):
+    def move_torrent(info_hash, release_names):
         """Move torrent to a given seeding folder after PP."""
         if not os.path.isdir(app.TORRENT_SEED_LOCATION):
-            logger.log('Not possible to move torrent after Post-Processor because seed location is invalid',
+            logger.log('Not possible to move torrent after post-processing because seed location is invalid',
                        logger.WARNING)
             return False
+
+        if release_names:
+            # Log 'release' or 'releases'
+            s = 's' if len(release_names) > 1 else ''
+            release_names = ', '.join(release_names)
         else:
-            if release_names:
-                # Log 'release' or 'releases'
-                s = 's' if len(release_names) > 1 else ''
-                release_names = ', '.join(release_names)
-            else:
-                s = ''
-                release_names = 'N/A'
-            logger.log('Trying to move torrent after Post-Processor', logger.DEBUG)
-            torrent_moved = False
-            client = torrent.get_client_class(app.TORRENT_METHOD)()
-            try:
-                torrent_moved = client.move_torrent(info_hash)
-            except (requests.exceptions.RequestException, socket.gaierror) as e:
-                logger.log("Couldn't connect to client to move torrent for release{s} '{release}' with hash: {hash} "
-                           "to: '{path}'. Error: {error}".format
-                           (release=release_names, hash=info_hash, error=e.message, path=app.TORRENT_SEED_LOCATION, s=s),
-                           logger.WARNING)
-                return False
-            except AttributeError:
-                logger.log("Your client doesn't support moving torrents to new location", logger.WARNING)
-                return True
-            if torrent_moved:
-                logger.log("Moved torrent for release{s} '{release}' with hash: {hash} to: '{path}'".format
-                           (release=release_names, hash=info_hash, path=app.TORRENT_SEED_LOCATION, s=s),
-                           logger.DEBUG)
-                return True
-            else:
-                logger.log("Could not move torrent for release{s} '{release}' with hash: {hash} to: '{path}'. "
-                           "Please check logs.".format(release=release_names, hash=info_hash, s=s,
-                                                       path=app.TORRENT_SEED_LOCATION), logger.WARNING)
-                return False
+            s = ''
+            release_names = 'N/A'
+
+        logger.log('Trying to move torrent after post-processing', logger.DEBUG)
+        client = torrent.get_client_class(app.TORRENT_METHOD)()
+
+        try:
+            torrent_moved = client.move_torrent(info_hash)
+        except (requests.exceptions.RequestException, socket.gaierror) as error:
+            logger.log("Couldn't connect to client to move torrent for release{s} '{release}' with hash: {hash} "
+                       "to: '{path}'. Error: {error}".format(release=release_names, hash=info_hash, error=error.message,
+                                                             path=app.TORRENT_SEED_LOCATION, s=s), logger.WARNING)
+            return False
+        except AttributeError:
+            logger.log("Your client doesn't support moving torrents to new location", logger.WARNING)
+            return False
+
+        if torrent_moved:
+            logger.log("Moved torrent for release{s} '{release}' with hash: {hash} to: '{path}'".format
+                       (release=release_names, hash=info_hash, path=app.TORRENT_SEED_LOCATION, s=s),
+                       logger.DEBUG)
+            return True
+        else:
+            logger.log("Couldn't move torrent for release{s} '{release}' with hash: {hash} to: '{path}'. "
+                       "Please check logs.".format(release=release_names, hash=info_hash, s=s,
+                                                   path=app.TORRENT_SEED_LOCATION), logger.WARNING)
+            return False


Test patch:
diff --git a/tests/test_process_tv.py b/tests/test_process_tv.py
--- a/tests/test_process_tv.py
+++ b/tests/test_process_tv.py
@@ -2,6 +2,7 @@
 """Tests for medusa/process_tv.py."""
 import os
 
+from medusa.post_processor import PostProcessor
 from medusa.process_tv import ProcessResult
 
 import pytest
@@ -180,3 +181,67 @@ def test__get_files(monkeypatch, p, create_structure):
     for i, (dir_path, filelist) in enumerate(result):
         assert dir_path == os.path.join(test_path, os.path.normcase(p['expected'][i][0]))
         assert filelist == p['expected'][i][1]
+
+
+@pytest.mark.parametrize('p', [
+    {   # matching subtitle, process
+        'path': 'media/postprocess/Show.Name.S01E03.HDTV.x264-LOL',
+        'video': 'show.name.103.hdtv.x264-lol.mkv',
+        'ignore_subs': False,
+        'expected': True,
+        'structure': (
+            'show.name.103.hdtv.x264-lol.mkv',
+            'show.name.103.hdtv.x264-lol.en.srt',
+        ),
+        'subtitles_enabled': True
+    },
+    {   # no matching subtitle, postpone processing
+        'path': 'media/postprocess/Show.Name.S01E03.HDTV.x264-LOL',
+        'video': 'show.name.103.hdtv.x264-lol.mkv',
+        'ignore_subs': False,
+        'expected': False,
+        'structure': (
+            'show.name.103.hdtv.x264-lol.mkv',
+        ),
+        'subtitles_enabled': True
+    },
+    {   # matching subtitle, ignoring subtitles, process
+        'path': 'media/postprocess',
+        'video': 'show.name.103.hdtv.x264-lol.mkv',
+        'ignore_subs': True,
+        'expected': True,
+        'structure': (
+            'show.name.103.hdtv.x264-lol.mkv',
+            'show.name.103.hdtv.x264-lol.en.srt',
+        ),
+        'subtitles_enabled': True
+    },
+    {   # matching subtitle, subtitles disabled, process
+        'path': 'media/postprocess',
+        'video': 'show.name.103.hdtv.x264-lol.mkv',
+        'ignore_subs': False,
+        'expected': True,
+        'structure': (
+            'show.name.103.hdtv.x264-lol.mkv',
+            'show.name.103.hdtv.x264-lol.en.srt',
+        ),
+        'subtitles_enabled': False
+    },
+])
+def test__process_postponed(monkeypatch, p, create_structure):
+    """Run the test."""
+    # Given
+    test_path = create_structure(p['path'], structure=p['structure'])
+    path = os.path.join(test_path, os.path.normcase(p['path']))
+    video_path = os.path.join(path, p['video'])
+    processor = PostProcessor(path)
+    sut = ProcessResult(path)
+
+    # Overwrite internal method
+    sut.subtitles_enabled = lambda path, resource_name: p['subtitles_enabled']
+
+    # When
+    result = sut._process_postponed(processor, video_path, p['video'], p['ignore_subs'])
+
+    # Then
+    assert p['expected'] == result