Repository: translate/pootle

Issue:
Failed to run `sync_stores` after a file ceased to exist
Got this error after running `sync_stores` command.

``` python
OSError: [Errno 2] No such file or directory ...
```

Steps to reproduce:
- delete any file from an existing TP;
- run `sync_stores` command for the TP (add --force if it's necessary).



Discussion:

Resolving patch:
diff --git a/pootle/apps/pootle_store/models.py b/pootle/apps/pootle_store/models.py
--- a/pootle/apps/pootle_store/models.py
+++ b/pootle/apps/pootle_store/models.py
@@ -1826,7 +1826,7 @@ def sync(self, update_structure=False, conservative=True,
                 (self.pootle_path, self.last_sync_revision))
             return
 
-        if not self.file and not skip_missing:
+        if not self.file.exists() and not skip_missing:
             # File doesn't exist let's create it
             logging.debug(u"Creating file %s", self.pootle_path)
 


Test patch:
diff --git a/tests/data/po/tutorial/fr/remove_sync_tutorial.po b/tests/data/po/tutorial/fr/remove_sync_tutorial.po
new file mode 100644
--- /dev/null
+++ b/tests/data/po/tutorial/fr/remove_sync_tutorial.po
@@ -0,0 +1,22 @@
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Generator: Pootle Tests\n"
+
+#: fish.c
+msgid "fish"
+msgstr ""
+
+#: test.c
+msgid "test"
+msgstr "test"
+
+#: fish.c
+msgid "%d fish"
+msgid_plural "%d fishes"
+msgstr[0] ""
+msgstr[1] ""
diff --git a/tests/data/po/tutorial/fr/subdir_to_remove/tutorial.po b/tests/data/po/tutorial/fr/subdir_to_remove/tutorial.po
new file mode 100644
--- /dev/null
+++ b/tests/data/po/tutorial/fr/subdir_to_remove/tutorial.po
@@ -0,0 +1,22 @@
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Generator: Pootle Tests\n"
+
+#: fish.c
+msgid "apple"
+msgstr ""
+
+#: test.c
+msgid "tomato"
+msgstr "pomodoro"
+
+#: fish.c
+msgid "%d apple"
+msgid_plural "%d apples"
+msgstr[0] ""
+msgstr[1] ""
diff --git a/tests/fixtures/models/language.py b/tests/fixtures/models/language.py
--- a/tests/fixtures/models/language.py
+++ b/tests/fixtures/models/language.py
@@ -56,6 +56,12 @@ def arabic(english):
     return _require_language('ar', 'Arabic')
 
 
+@pytest.fixture
+def french(english):
+    """Require the French language."""
+    return _require_language('fr', 'French')
+
+
 @pytest.fixture
 def fish(english):
     """Require the Fish language ><(((º>"""
diff --git a/tests/fixtures/models/store.py b/tests/fixtures/models/store.py
--- a/tests/fixtures/models/store.py
+++ b/tests/fixtures/models/store.py
@@ -76,3 +76,17 @@ def af_tutorial_subdir_po(settings, afrikaans_tutorial, system):
     """Require the /af/tutorial/subdir/tutorial.po store."""
     po_directory = settings.PODIRECTORY
     return _require_store(afrikaans_tutorial, po_directory, 'subdir/tutorial.po')
+
+
+@pytest.fixture
+def fr_tutorial_subdir_to_remove_po(settings, french_tutorial, system):
+    """Require the /fr/tutorial/subdir_to_remove/tutorial.po store."""
+    po_directory = settings.PODIRECTORY
+    return _require_store(french_tutorial, po_directory, 'subdir_to_remove/tutorial.po')
+
+
+@pytest.fixture
+def fr_tutorial_remove_sync_po(settings, french_tutorial, system):
+    """Require the /af/tutorial/remove_sync_tutorial.po store."""
+    po_directory = settings.PODIRECTORY
+    return _require_store(french_tutorial, po_directory, 'remove_sync_tutorial.po')
diff --git a/tests/fixtures/models/translation_project.py b/tests/fixtures/models/translation_project.py
--- a/tests/fixtures/models/translation_project.py
+++ b/tests/fixtures/models/translation_project.py
@@ -38,3 +38,9 @@ def afrikaans_tutorial(afrikaans, tutorial):
 def arabic_tutorial_disabled(arabic, tutorial):
     """Require Arabic Tutorial in disabled state."""
     return _require_disabled_tp(arabic, tutorial)
+
+
+@pytest.fixture
+def french_tutorial(french, tutorial):
+    """Require French Tutorial."""
+    return _require_tp(french, tutorial)
diff --git a/tests/models/directory.py b/tests/models/directory.py
new file mode 100644
--- /dev/null
+++ b/tests/models/directory.py
@@ -0,0 +1,70 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) Pootle contributors.
+#
+# This file is a part of the Pootle project. It is distributed under the GPL3
+# or later license. See the LICENSE file for a copy of the license and the
+# AUTHORS file for copyright and authorship information.
+
+import os
+
+import pytest
+
+
+@pytest.mark.django_db
+def test_delete_mark_obsolete_resurrect_sync(fr_tutorial_subdir_to_remove_po):
+    """Tests that the in-DB Directory are marked as obsolete
+    after the on-disk file ceased to exist and that the on-disk file and
+    directory are recovered after syncing.
+
+    Refs. #269.
+    """
+    from pootle_app.models.directory import Directory
+    from pootle_store.models import Store, Unit
+
+    tp = fr_tutorial_subdir_to_remove_po.translation_project
+    dir_pootle_path = fr_tutorial_subdir_to_remove_po.parent.pootle_path
+    store_pootle_path = fr_tutorial_subdir_to_remove_po.pootle_path
+
+    # Parse stores
+    for store in tp.stores.all():
+        store.update(overwrite=False, only_newer=False)
+
+    # Remove on-disk directory
+    os.remove(fr_tutorial_subdir_to_remove_po.file.path)
+    af_tutorial_subdir_to_remove = \
+        os.path.dirname(fr_tutorial_subdir_to_remove_po.file.path)
+    os.rmdir(af_tutorial_subdir_to_remove)
+
+    # Update stores by rescanning TP
+    tp.scan_files()
+
+    # Now files and directories that ceased to exist should be
+    # marked as obsolete
+    updated_directory = Directory.objects.get(pootle_path=dir_pootle_path)
+    assert updated_directory.obsolete
+
+    updated_store = Store.objects.get(pootle_path=store_pootle_path)
+    assert updated_store.obsolete
+
+    # The units they contained are obsolete too
+    store_units = Unit.objects.filter(store=updated_store)
+    for unit in store_units:
+        assert unit.isobsolete()
+
+    # Resurrect directory
+    updated_directory.obsolete = False
+    updated_directory.save()
+    # Resurrect store
+    updated_store.obsolete = False
+    updated_store.save()
+    # Resurrect units
+    for unit in store_units:
+        unit.resurrect()
+        unit.save()
+
+    # Recover store and directory by syncing
+    updated_store.sync(only_newer=False)
+    # Now file and directory for the store should exist
+    assert os.path.exists(updated_store.file.path)
diff --git a/tests/models/store.py b/tests/models/store.py
--- a/tests/models/store.py
+++ b/tests/models/store.py
@@ -43,3 +43,26 @@ def test_delete_mark_obsolete(af_tutorial_subdir_po):
     store_units = Unit.objects.filter(store=updated_store)
     for unit in store_units:
         assert unit.isobsolete()
+
+
+@pytest.mark.django_db
+def test_sync(fr_tutorial_remove_sync_po):
+    """Tests that the new on-disk file is created after sync for existing
+    in-DB Store if the corresponding on-disk file ceased to exist.
+    """
+
+    tp = fr_tutorial_remove_sync_po.translation_project
+    pootle_path = fr_tutorial_remove_sync_po.pootle_path
+
+    # Parse stores
+    for store in tp.stores.all():
+        store.update(overwrite=False, only_newer=False)
+
+    assert fr_tutorial_remove_sync_po.file.exists()
+    os.remove(fr_tutorial_remove_sync_po.file.path)
+
+    from pootle_store.models import Store
+    store = Store.objects.get(pootle_path=pootle_path)
+    assert not store.file.exists()
+    store.sync()
+    assert store.file.exists()