tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

test_files.py (46053B)


      1 # This Source Code Form is subject to the terms of the Mozilla Public
      2 # License, v. 2.0. If a copy of the MPL was not distributed with this
      3 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
      4 
      5 import buildconfig
      6 
      7 from mozbuild.dirutils import ensureParentDir
      8 from mozbuild.nodeutil import find_node_executable
      9 from mozbuild.util import ensure_bytes
     10 from mozpack.errors import ErrorMessage
     11 from mozpack.files import (
     12    AbsoluteSymlinkFile,
     13    ComposedFinder,
     14    DeflatedFile,
     15    Dest,
     16    ExistingFile,
     17    ExtractedTarFile,
     18    File,
     19    FileFinder,
     20    GeneratedFile,
     21    HardlinkFile,
     22    JarFinder,
     23    ManifestFile,
     24    MercurialFile,
     25    MercurialRevisionFinder,
     26    MinifiedCommentStripped,
     27    MinifiedJavaScript,
     28    PreprocessedFile,
     29    TarFinder,
     30 )
     31 
     32 # We don't have hglib installed everywhere.
     33 try:
     34    import hglib
     35 except ImportError:
     36    hglib = None
     37 
     38 import os
     39 import platform
     40 import random
     41 import sys
     42 import tarfile
     43 import unittest
     44 from io import BytesIO
     45 from tempfile import mkdtemp
     46 
     47 import mozfile
     48 import mozunit
     49 
     50 import mozpack.path as mozpath
     51 from mozpack.chrome.manifest import (
     52    ManifestContent,
     53    ManifestLocale,
     54    ManifestOverride,
     55    ManifestResource,
     56 )
     57 from mozpack.mozjar import JarReader, JarWriter
     58 
     59 
     60 class TestWithTmpDir(unittest.TestCase):
     61    def setUp(self):
     62        self.tmpdir = mkdtemp()
     63 
     64        self.symlink_supported = False
     65        self.hardlink_supported = False
     66 
     67        # See comment in mozpack.files.AbsoluteSymlinkFile
     68        if hasattr(os, "symlink") and platform.system() != "Windows":
     69            dummy_path = self.tmppath("dummy_file")
     70            with open(dummy_path, "a"):
     71                pass
     72 
     73            try:
     74                os.symlink(dummy_path, self.tmppath("dummy_symlink"))
     75                os.remove(self.tmppath("dummy_symlink"))
     76            except OSError:
     77                pass
     78            finally:
     79                os.remove(dummy_path)
     80 
     81            self.symlink_supported = True
     82 
     83        if hasattr(os, "link"):
     84            dummy_path = self.tmppath("dummy_file")
     85            with open(dummy_path, "a"):
     86                pass
     87 
     88            try:
     89                os.link(dummy_path, self.tmppath("dummy_hardlink"))
     90                os.remove(self.tmppath("dummy_hardlink"))
     91            except OSError:
     92                pass
     93            finally:
     94                os.remove(dummy_path)
     95 
     96            self.hardlink_supported = True
     97 
     98    def tearDown(self):
     99        mozfile.rmtree(self.tmpdir)
    100 
    101    def tmppath(self, relpath):
    102        return os.path.normpath(os.path.join(self.tmpdir, relpath))
    103 
    104 
    105 class MockDest(BytesIO, Dest):
    106    def __init__(self):
    107        BytesIO.__init__(self)
    108        self.mode = None
    109 
    110    def read(self, length=-1):
    111        if self.mode != "r":
    112            self.seek(0)
    113            self.mode = "r"
    114        return BytesIO.read(self, length)
    115 
    116    def write(self, data):
    117        if self.mode != "w":
    118            self.seek(0)
    119            self.truncate(0)
    120            self.mode = "w"
    121        return BytesIO.write(self, data)
    122 
    123    def exists(self):
    124        return True
    125 
    126    def close(self):
    127        if self.mode:
    128            self.mode = None
    129 
    130 
    131 class DestNoWrite(Dest):
    132    def write(self, data):
    133        raise RuntimeError
    134 
    135 
    136 class TestDest(TestWithTmpDir):
    137    def test_dest(self):
    138        dest = Dest(self.tmppath("dest"))
    139        self.assertFalse(dest.exists())
    140        dest.write(b"foo")
    141        self.assertTrue(dest.exists())
    142        dest.write(b"foo")
    143        self.assertEqual(dest.read(4), b"foof")
    144        self.assertEqual(dest.read(), b"oo")
    145        self.assertEqual(dest.read(), b"")
    146        dest.write(b"bar")
    147        self.assertEqual(dest.read(4), b"bar")
    148        dest.close()
    149        self.assertEqual(dest.read(), b"bar")
    150        dest.write(b"foo")
    151        dest.close()
    152        dest.write(b"qux")
    153        self.assertEqual(dest.read(), b"qux")
    154 
    155 
    156 rand = bytes(
    157    random.choice(b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
    158    for i in range(131597)
    159 )
    160 samples = [
    161    b"",
    162    b"test",
    163    b"fooo",
    164    b"same",
    165    b"same",
    166    b"Different and longer",
    167    rand,
    168    rand,
    169    rand[:-1] + b"_",
    170    b"test",
    171 ]
    172 
    173 
    174 class TestFile(TestWithTmpDir):
    175    def test_file(self):
    176        """
    177        Check that File.copy yields the proper content in the destination file
    178        in all situations that trigger different code paths:
    179        - different content
    180        - different content of the same size
    181        - same content
    182        - long content
    183        """
    184        src = self.tmppath("src")
    185        dest = self.tmppath("dest")
    186 
    187        for content in samples:
    188            with open(src, "wb") as tmp:
    189                tmp.write(content)
    190            # Ensure the destination file, when it exists, is older than the
    191            # source
    192            if os.path.exists(dest):
    193                time = os.path.getmtime(src) - 1
    194                os.utime(dest, (time, time))
    195            f = File(src)
    196            f.copy(dest)
    197            self.assertEqual(content, open(dest, "rb").read())
    198            self.assertEqual(content, f.open().read())
    199            self.assertEqual(content, f.open().read())
    200 
    201    def test_file_dest(self):
    202        """
    203        Similar to test_file, but for a destination object instead of
    204        a destination file. This ensures the destination object is being
    205        used properly by File.copy, ensuring that other subclasses of Dest
    206        will work.
    207        """
    208        src = self.tmppath("src")
    209        dest = MockDest()
    210 
    211        for content in samples:
    212            with open(src, "wb") as tmp:
    213                tmp.write(content)
    214            f = File(src)
    215            f.copy(dest)
    216            self.assertEqual(content, dest.getvalue())
    217 
    218    def test_file_open(self):
    219        """
    220        Test whether File.open returns an appropriately reset file object.
    221        """
    222        src = self.tmppath("src")
    223        content = b"".join(samples)
    224        with open(src, "wb") as tmp:
    225            tmp.write(content)
    226 
    227        f = File(src)
    228        self.assertEqual(content[:42], f.open().read(42))
    229        self.assertEqual(content, f.open().read())
    230 
    231    def test_file_no_write(self):
    232        """
    233        Test various conditions where File.copy is expected not to write
    234        in the destination file.
    235        """
    236        src = self.tmppath("src")
    237        dest = self.tmppath("dest")
    238 
    239        with open(src, "wb") as tmp:
    240            tmp.write(b"test")
    241 
    242        # Initial copy
    243        f = File(src)
    244        f.copy(dest)
    245 
    246        # Ensure subsequent copies won't trigger writes
    247        f.copy(DestNoWrite(dest))
    248        self.assertEqual(b"test", open(dest, "rb").read())
    249 
    250        # When the source file is newer, but with the same content, no copy
    251        # should occur
    252        time = os.path.getmtime(src) - 1
    253        os.utime(dest, (time, time))
    254        f.copy(DestNoWrite(dest))
    255        self.assertEqual(b"test", open(dest, "rb").read())
    256 
    257        # When the source file is older than the destination file, even with
    258        # different content, no copy should occur.
    259        with open(src, "wb") as tmp:
    260            tmp.write(b"fooo")
    261        time = os.path.getmtime(dest) - 1
    262        os.utime(src, (time, time))
    263        f.copy(DestNoWrite(dest))
    264        self.assertEqual(b"test", open(dest, "rb").read())
    265 
    266        # Double check that under conditions where a copy occurs, we would get
    267        # an exception.
    268        time = os.path.getmtime(src) - 1
    269        os.utime(dest, (time, time))
    270        self.assertRaises(RuntimeError, f.copy, DestNoWrite(dest))
    271 
    272        # skip_if_older=False is expected to force a copy in this situation.
    273        f.copy(dest, skip_if_older=False)
    274        self.assertEqual(b"fooo", open(dest, "rb").read())
    275 
    276 
    277 class TestAbsoluteSymlinkFile(TestWithTmpDir):
    278    def test_absolute_relative(self):
    279        AbsoluteSymlinkFile("/foo")
    280 
    281        with self.assertRaisesRegex(ValueError, "Symlink target not absolute"):
    282            AbsoluteSymlinkFile("./foo")
    283 
    284    def test_symlink_file(self):
    285        source = self.tmppath("test_path")
    286        with open(source, "w") as fh:
    287            fh.write("Hello world")
    288 
    289        s = AbsoluteSymlinkFile(source)
    290        dest = self.tmppath("symlink")
    291        self.assertTrue(s.copy(dest))
    292 
    293        if self.symlink_supported:
    294            self.assertTrue(os.path.islink(dest))
    295            link = os.readlink(dest)
    296            self.assertEqual(link, source)
    297        else:
    298            self.assertTrue(os.path.isfile(dest))
    299            content = open(dest).read()
    300            self.assertEqual(content, "Hello world")
    301 
    302    def test_replace_file_with_symlink(self):
    303        # If symlinks are supported, an existing file should be replaced by a
    304        # symlink.
    305        source = self.tmppath("test_path")
    306        with open(source, "w") as fh:
    307            fh.write("source")
    308 
    309        dest = self.tmppath("dest")
    310        with open(dest, "a"):
    311            pass
    312 
    313        s = AbsoluteSymlinkFile(source)
    314        s.copy(dest, skip_if_older=False)
    315 
    316        if self.symlink_supported:
    317            self.assertTrue(os.path.islink(dest))
    318            link = os.readlink(dest)
    319            self.assertEqual(link, source)
    320        else:
    321            self.assertTrue(os.path.isfile(dest))
    322            content = open(dest).read()
    323            self.assertEqual(content, "source")
    324 
    325    def test_replace_symlink(self):
    326        if not self.symlink_supported:
    327            return
    328 
    329        source = self.tmppath("source")
    330        with open(source, "a"):
    331            pass
    332 
    333        dest = self.tmppath("dest")
    334 
    335        os.symlink(self.tmppath("bad"), dest)
    336        self.assertTrue(os.path.islink(dest))
    337 
    338        s = AbsoluteSymlinkFile(source)
    339        self.assertTrue(s.copy(dest))
    340 
    341        self.assertTrue(os.path.islink(dest))
    342        link = os.readlink(dest)
    343        self.assertEqual(link, source)
    344 
    345    def test_noop(self):
    346        if not hasattr(os, "symlink") or sys.platform == "win32":
    347            return
    348 
    349        source = self.tmppath("source")
    350        dest = self.tmppath("dest")
    351 
    352        with open(source, "a"):
    353            pass
    354 
    355        os.symlink(source, dest)
    356        link = os.readlink(dest)
    357        self.assertEqual(link, source)
    358 
    359        s = AbsoluteSymlinkFile(source)
    360        self.assertFalse(s.copy(dest))
    361 
    362        link = os.readlink(dest)
    363        self.assertEqual(link, source)
    364 
    365 
    366 class TestHardlinkFile(TestWithTmpDir):
    367    def test_absolute_relative(self):
    368        HardlinkFile("/foo")
    369        HardlinkFile("./foo")
    370 
    371    def test_hardlink_file(self):
    372        source = self.tmppath("test_path")
    373        with open(source, "w") as fh:
    374            fh.write("Hello world")
    375 
    376        s = HardlinkFile(source)
    377        dest = self.tmppath("hardlink")
    378        self.assertTrue(s.copy(dest))
    379 
    380        if self.hardlink_supported:
    381            source_stat = os.stat(source)
    382            dest_stat = os.stat(dest)
    383            self.assertEqual(source_stat.st_dev, dest_stat.st_dev)
    384            self.assertEqual(source_stat.st_ino, dest_stat.st_ino)
    385        else:
    386            self.assertTrue(os.path.isfile(dest))
    387            with open(dest) as f:
    388                content = f.read()
    389            self.assertEqual(content, "Hello world")
    390 
    391    def test_replace_file_with_hardlink(self):
    392        # If hardlink are supported, an existing file should be replaced by a
    393        # symlink.
    394        source = self.tmppath("test_path")
    395        with open(source, "w") as fh:
    396            fh.write("source")
    397 
    398        dest = self.tmppath("dest")
    399        with open(dest, "a"):
    400            pass
    401 
    402        s = HardlinkFile(source)
    403        s.copy(dest, skip_if_older=False)
    404 
    405        if self.hardlink_supported:
    406            source_stat = os.stat(source)
    407            dest_stat = os.stat(dest)
    408            self.assertEqual(source_stat.st_dev, dest_stat.st_dev)
    409            self.assertEqual(source_stat.st_ino, dest_stat.st_ino)
    410        else:
    411            self.assertTrue(os.path.isfile(dest))
    412            with open(dest) as f:
    413                content = f.read()
    414            self.assertEqual(content, "source")
    415 
    416    def test_replace_hardlink(self):
    417        if not self.hardlink_supported:
    418            raise unittest.SkipTest("hardlink not supported")
    419 
    420        source = self.tmppath("source")
    421        with open(source, "a"):
    422            pass
    423 
    424        dest = self.tmppath("dest")
    425 
    426        os.link(source, dest)
    427 
    428        s = HardlinkFile(source)
    429        self.assertFalse(s.copy(dest))
    430 
    431        source_stat = os.lstat(source)
    432        dest_stat = os.lstat(dest)
    433        self.assertEqual(source_stat.st_dev, dest_stat.st_dev)
    434        self.assertEqual(source_stat.st_ino, dest_stat.st_ino)
    435 
    436    def test_noop(self):
    437        if not self.hardlink_supported:
    438            raise unittest.SkipTest("hardlink not supported")
    439 
    440        source = self.tmppath("source")
    441        dest = self.tmppath("dest")
    442 
    443        with open(source, "a"):
    444            pass
    445 
    446        os.link(source, dest)
    447 
    448        s = HardlinkFile(source)
    449        self.assertFalse(s.copy(dest))
    450 
    451        source_stat = os.lstat(source)
    452        dest_stat = os.lstat(dest)
    453        self.assertEqual(source_stat.st_dev, dest_stat.st_dev)
    454        self.assertEqual(source_stat.st_ino, dest_stat.st_ino)
    455 
    456 
    457 class TestPreprocessedFile(TestWithTmpDir):
    458    def test_preprocess(self):
    459        """
    460        Test that copying the file invokes the preprocessor
    461        """
    462        src = self.tmppath("src")
    463        dest = self.tmppath("dest")
    464 
    465        with open(src, "wb") as tmp:
    466            tmp.write(b"#ifdef FOO\ntest\n#endif")
    467 
    468        f = PreprocessedFile(src, depfile_path=None, marker="#", defines={"FOO": True})
    469        self.assertTrue(f.copy(dest))
    470 
    471        self.assertEqual(b"test\n", open(dest, "rb").read())
    472 
    473    def test_preprocess_file_no_write(self):
    474        """
    475        Test various conditions where PreprocessedFile.copy is expected not to
    476        write in the destination file.
    477        """
    478        src = self.tmppath("src")
    479        dest = self.tmppath("dest")
    480        depfile = self.tmppath("depfile")
    481 
    482        with open(src, "wb") as tmp:
    483            tmp.write(b"#ifdef FOO\ntest\n#endif")
    484 
    485        # Initial copy
    486        f = PreprocessedFile(
    487            src, depfile_path=depfile, marker="#", defines={"FOO": True}
    488        )
    489        self.assertTrue(f.copy(dest))
    490 
    491        # Ensure subsequent copies won't trigger writes
    492        self.assertFalse(f.copy(DestNoWrite(dest)))
    493        self.assertEqual(b"test\n", open(dest, "rb").read())
    494 
    495        # When the source file is older than the destination file, even with
    496        # different content, no copy should occur.
    497        with open(src, "wb") as tmp:
    498            tmp.write(b"#ifdef FOO\nfooo\n#endif")
    499        time = os.path.getmtime(dest) - 1
    500        os.utime(src, (time, time))
    501        self.assertFalse(f.copy(DestNoWrite(dest)))
    502        self.assertEqual(b"test\n", open(dest, "rb").read())
    503 
    504        # skip_if_older=False is expected to force a copy in this situation.
    505        self.assertTrue(f.copy(dest, skip_if_older=False))
    506        self.assertEqual(b"fooo\n", open(dest, "rb").read())
    507 
    508    def test_preprocess_file_dependencies(self):
    509        """
    510        Test that the preprocess runs if the dependencies of the source change
    511        """
    512        src = self.tmppath("src")
    513        dest = self.tmppath("dest")
    514        incl = self.tmppath("incl")
    515        deps = self.tmppath("src.pp")
    516 
    517        with open(src, "wb") as tmp:
    518            tmp.write(b"#ifdef FOO\ntest\n#endif")
    519 
    520        with open(incl, "wb") as tmp:
    521            tmp.write(b"foo bar")
    522 
    523        # Initial copy
    524        f = PreprocessedFile(src, depfile_path=deps, marker="#", defines={"FOO": True})
    525        self.assertTrue(f.copy(dest))
    526 
    527        # Update the source so it #includes the include file.
    528        with open(src, "wb") as tmp:
    529            tmp.write(b"#include incl\n")
    530        time = os.path.getmtime(dest) + 1
    531        os.utime(src, (time, time))
    532        self.assertTrue(f.copy(dest))
    533        self.assertEqual(b"foo bar", open(dest, "rb").read())
    534 
    535        # If one of the dependencies changes, the file should be updated. The
    536        # mtime of the dependency is set after the destination file, to avoid
    537        # both files having the same time.
    538        with open(incl, "wb") as tmp:
    539            tmp.write(b"quux")
    540        time = os.path.getmtime(dest) + 1
    541        os.utime(incl, (time, time))
    542        self.assertTrue(f.copy(dest))
    543        self.assertEqual(b"quux", open(dest, "rb").read())
    544 
    545        # Perform one final copy to confirm that we don't run the preprocessor
    546        # again. We update the mtime of the destination so it's newer than the
    547        # input files. This would "just work" if we weren't changing
    548        time = os.path.getmtime(incl) + 1
    549        os.utime(dest, (time, time))
    550        self.assertFalse(f.copy(DestNoWrite(dest)))
    551 
    552    def test_replace_symlink(self):
    553        """
    554        Test that if the destination exists, and is a symlink, the target of
    555        the symlink is not overwritten by the preprocessor output.
    556        """
    557        if not self.symlink_supported:
    558            return
    559 
    560        source = self.tmppath("source")
    561        dest = self.tmppath("dest")
    562        pp_source = self.tmppath("pp_in")
    563        deps = self.tmppath("deps")
    564 
    565        with open(source, "a"):
    566            pass
    567 
    568        os.symlink(source, dest)
    569        self.assertTrue(os.path.islink(dest))
    570 
    571        with open(pp_source, "wb") as tmp:
    572            tmp.write(b"#define FOO\nPREPROCESSED")
    573 
    574        f = PreprocessedFile(
    575            pp_source, depfile_path=deps, marker="#", defines={"FOO": True}
    576        )
    577        self.assertTrue(f.copy(dest))
    578 
    579        self.assertEqual(b"PREPROCESSED", open(dest, "rb").read())
    580        self.assertFalse(os.path.islink(dest))
    581        self.assertEqual(b"", open(source, "rb").read())
    582 
    583 
    584 class TestExistingFile(TestWithTmpDir):
    585    def test_required_missing_dest(self):
    586        with self.assertRaisesRegex(ErrorMessage, "Required existing file"):
    587            f = ExistingFile(required=True)
    588            f.copy(self.tmppath("dest"))
    589 
    590    def test_required_existing_dest(self):
    591        p = self.tmppath("dest")
    592        with open(p, "a"):
    593            pass
    594 
    595        f = ExistingFile(required=True)
    596        f.copy(p)
    597 
    598    def test_optional_missing_dest(self):
    599        f = ExistingFile(required=False)
    600        f.copy(self.tmppath("dest"))
    601 
    602    def test_optional_existing_dest(self):
    603        p = self.tmppath("dest")
    604        with open(p, "a"):
    605            pass
    606 
    607        f = ExistingFile(required=False)
    608        f.copy(p)
    609 
    610 
    611 class TestGeneratedFile(TestWithTmpDir):
    612    def test_generated_file(self):
    613        """
    614        Check that GeneratedFile.copy yields the proper content in the
    615        destination file in all situations that trigger different code paths
    616        (see TestFile.test_file)
    617        """
    618        dest = self.tmppath("dest")
    619 
    620        for content in samples:
    621            f = GeneratedFile(content)
    622            f.copy(dest)
    623            self.assertEqual(content, open(dest, "rb").read())
    624 
    625    def test_generated_file_open(self):
    626        """
    627        Test whether GeneratedFile.open returns an appropriately reset file
    628        object.
    629        """
    630        content = b"".join(samples)
    631        f = GeneratedFile(content)
    632        self.assertEqual(content[:42], f.open().read(42))
    633        self.assertEqual(content, f.open().read())
    634 
    635    def test_generated_file_no_write(self):
    636        """
    637        Test various conditions where GeneratedFile.copy is expected not to
    638        write in the destination file.
    639        """
    640        dest = self.tmppath("dest")
    641 
    642        # Initial copy
    643        f = GeneratedFile(b"test")
    644        f.copy(dest)
    645 
    646        # Ensure subsequent copies won't trigger writes
    647        f.copy(DestNoWrite(dest))
    648        self.assertEqual(b"test", open(dest, "rb").read())
    649 
    650        # When using a new instance with the same content, no copy should occur
    651        f = GeneratedFile(b"test")
    652        f.copy(DestNoWrite(dest))
    653        self.assertEqual(b"test", open(dest, "rb").read())
    654 
    655        # Double check that under conditions where a copy occurs, we would get
    656        # an exception.
    657        f = GeneratedFile(b"fooo")
    658        self.assertRaises(RuntimeError, f.copy, DestNoWrite(dest))
    659 
    660    def test_generated_file_function(self):
    661        """
    662        Test GeneratedFile behavior with functions.
    663        """
    664        dest = self.tmppath("dest")
    665        data = {
    666            "num_calls": 0,
    667        }
    668 
    669        def content():
    670            data["num_calls"] += 1
    671            return b"content"
    672 
    673        f = GeneratedFile(content)
    674        self.assertEqual(data["num_calls"], 0)
    675        f.copy(dest)
    676        self.assertEqual(data["num_calls"], 1)
    677        self.assertEqual(b"content", open(dest, "rb").read())
    678        self.assertEqual(b"content", f.open().read())
    679        self.assertEqual(b"content", f.read())
    680        self.assertEqual(len(b"content"), f.size())
    681        self.assertEqual(data["num_calls"], 1)
    682 
    683        f.content = b"modified"
    684        f.copy(dest)
    685        self.assertEqual(data["num_calls"], 1)
    686        self.assertEqual(b"modified", open(dest, "rb").read())
    687        self.assertEqual(b"modified", f.open().read())
    688        self.assertEqual(b"modified", f.read())
    689        self.assertEqual(len(b"modified"), f.size())
    690 
    691        f.content = content
    692        self.assertEqual(data["num_calls"], 1)
    693        self.assertEqual(b"content", f.read())
    694        self.assertEqual(data["num_calls"], 2)
    695 
    696 
    697 class TestDeflatedFile(TestWithTmpDir):
    698    def test_deflated_file(self):
    699        """
    700        Check that DeflatedFile.copy yields the proper content in the
    701        destination file in all situations that trigger different code paths
    702        (see TestFile.test_file)
    703        """
    704        src = self.tmppath("src.jar")
    705        dest = self.tmppath("dest")
    706 
    707        contents = {}
    708        with JarWriter(src) as jar:
    709            for content in samples:
    710                name = "".join(
    711                    random.choice(
    712                        "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
    713                    )
    714                    for i in range(8)
    715                )
    716                jar.add(name, content, compress=True)
    717                contents[name] = content
    718 
    719        for j in JarReader(src):
    720            f = DeflatedFile(j)
    721            f.copy(dest)
    722            self.assertEqual(contents[j.filename], open(dest, "rb").read())
    723 
    724    def test_deflated_file_open(self):
    725        """
    726        Test whether DeflatedFile.open returns an appropriately reset file
    727        object.
    728        """
    729        src = self.tmppath("src.jar")
    730        content = b"".join(samples)
    731        with JarWriter(src) as jar:
    732            jar.add("content", content)
    733 
    734        f = DeflatedFile(JarReader(src)["content"])
    735        self.assertEqual(content[:42], f.open().read(42))
    736        self.assertEqual(content, f.open().read())
    737 
    738    def test_deflated_file_no_write(self):
    739        """
    740        Test various conditions where DeflatedFile.copy is expected not to
    741        write in the destination file.
    742        """
    743        src = self.tmppath("src.jar")
    744        dest = self.tmppath("dest")
    745 
    746        with JarWriter(src) as jar:
    747            jar.add("test", b"test")
    748            jar.add("test2", b"test")
    749            jar.add("fooo", b"fooo")
    750 
    751        jar = JarReader(src)
    752        # Initial copy
    753        f = DeflatedFile(jar["test"])
    754        f.copy(dest)
    755 
    756        # Ensure subsequent copies won't trigger writes
    757        f.copy(DestNoWrite(dest))
    758        self.assertEqual(b"test", open(dest, "rb").read())
    759 
    760        # When using a different file with the same content, no copy should
    761        # occur
    762        f = DeflatedFile(jar["test2"])
    763        f.copy(DestNoWrite(dest))
    764        self.assertEqual(b"test", open(dest, "rb").read())
    765 
    766        # Double check that under conditions where a copy occurs, we would get
    767        # an exception.
    768        f = DeflatedFile(jar["fooo"])
    769        self.assertRaises(RuntimeError, f.copy, DestNoWrite(dest))
    770 
    771 
    772 class TestManifestFile(TestWithTmpDir):
    773    def test_manifest_file(self):
    774        f = ManifestFile("chrome")
    775        f.add(ManifestContent("chrome", "global", "toolkit/content/global/"))
    776        f.add(ManifestResource("chrome", "gre-resources", "toolkit/res/"))
    777        f.add(ManifestResource("chrome/pdfjs", "pdfjs", "./"))
    778        f.add(ManifestContent("chrome/pdfjs", "pdfjs", "pdfjs"))
    779        f.add(ManifestLocale("chrome", "browser", "en-US", "en-US/locale/browser/"))
    780 
    781        f.copy(self.tmppath("chrome.manifest"))
    782        self.assertEqual(
    783            open(self.tmppath("chrome.manifest")).readlines(),
    784            [
    785                "content global toolkit/content/global/\n",
    786                "resource gre-resources toolkit/res/\n",
    787                "resource pdfjs pdfjs/\n",
    788                "content pdfjs pdfjs/pdfjs\n",
    789                "locale browser en-US en-US/locale/browser/\n",
    790            ],
    791        )
    792 
    793        self.assertRaises(
    794            ValueError,
    795            f.remove,
    796            ManifestContent("", "global", "toolkit/content/global/"),
    797        )
    798        self.assertRaises(
    799            ValueError,
    800            f.remove,
    801            ManifestOverride(
    802                "chrome",
    803                "chrome://global/locale/netError.dtd",
    804                "chrome://browser/locale/netError.dtd",
    805            ),
    806        )
    807 
    808        f.remove(ManifestContent("chrome", "global", "toolkit/content/global/"))
    809        self.assertRaises(
    810            ValueError,
    811            f.remove,
    812            ManifestContent("chrome", "global", "toolkit/content/global/"),
    813        )
    814 
    815        f.copy(self.tmppath("chrome.manifest"))
    816        content = open(self.tmppath("chrome.manifest"), "rb").read()
    817        self.assertEqual(content[:42], f.open().read(42))
    818        self.assertEqual(content, f.open().read())
    819 
    820 
    821 # Compiled typelib for the following IDL:
    822 #     interface foo;
    823 #     [scriptable, uuid(5f70da76-519c-4858-b71e-e3c92333e2d6)]
    824 #     interface bar {
    825 #         void bar(in foo f);
    826 #     };
    827 # We need to make this [scriptable] so it doesn't get deleted from the
    828 # typelib.  We don't need to make the foo interfaces below [scriptable],
    829 # because they will be automatically included by virtue of being an
    830 # argument to a method of |bar|.
    831 bar_xpt = GeneratedFile(
    832    b"\x58\x50\x43\x4f\x4d\x0a\x54\x79\x70\x65\x4c\x69\x62\x0d\x0a\x1a"
    833    + b"\x01\x02\x00\x02\x00\x00\x00\x7b\x00\x00\x00\x24\x00\x00\x00\x5c"
    834    + b"\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
    835    + b"\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x5f"
    836    + b"\x70\xda\x76\x51\x9c\x48\x58\xb7\x1e\xe3\xc9\x23\x33\xe2\xd6\x00"
    837    + b"\x00\x00\x05\x00\x00\x00\x00\x00\x00\x00\x0d\x00\x66\x6f\x6f\x00"
    838    + b"\x62\x61\x72\x00\x62\x61\x72\x00\x00\x00\x00\x01\x00\x00\x00\x00"
    839    + b"\x09\x01\x80\x92\x00\x01\x80\x06\x00\x00\x80"
    840 )
    841 
    842 # Compiled typelib for the following IDL:
    843 #     [uuid(3271bebc-927e-4bef-935e-44e0aaf3c1e5)]
    844 #     interface foo {
    845 #         void foo();
    846 #     };
    847 foo_xpt = GeneratedFile(
    848    b"\x58\x50\x43\x4f\x4d\x0a\x54\x79\x70\x65\x4c\x69\x62\x0d\x0a\x1a"
    849    + b"\x01\x02\x00\x01\x00\x00\x00\x57\x00\x00\x00\x24\x00\x00\x00\x40"
    850    + b"\x80\x00\x00\x32\x71\xbe\xbc\x92\x7e\x4b\xef\x93\x5e\x44\xe0\xaa"
    851    + b"\xf3\xc1\xe5\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x09\x00"
    852    + b"\x66\x6f\x6f\x00\x66\x6f\x6f\x00\x00\x00\x00\x01\x00\x00\x00\x00"
    853    + b"\x05\x00\x80\x06\x00\x00\x00"
    854 )
    855 
    856 # Compiled typelib for the following IDL:
    857 #     [uuid(7057f2aa-fdc2-4559-abde-08d939f7e80d)]
    858 #     interface foo {
    859 #         void foo();
    860 #     };
    861 foo2_xpt = GeneratedFile(
    862    b"\x58\x50\x43\x4f\x4d\x0a\x54\x79\x70\x65\x4c\x69\x62\x0d\x0a\x1a"
    863    + b"\x01\x02\x00\x01\x00\x00\x00\x57\x00\x00\x00\x24\x00\x00\x00\x40"
    864    + b"\x80\x00\x00\x70\x57\xf2\xaa\xfd\xc2\x45\x59\xab\xde\x08\xd9\x39"
    865    + b"\xf7\xe8\x0d\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x09\x00"
    866    + b"\x66\x6f\x6f\x00\x66\x6f\x6f\x00\x00\x00\x00\x01\x00\x00\x00\x00"
    867    + b"\x05\x00\x80\x06\x00\x00\x00"
    868 )
    869 
    870 
    871 class TestMinifiedCommentStripped(TestWithTmpDir):
    872    def test_minified_comment_stripped(self):
    873        propLines = [
    874            "# Comments are removed",
    875            "foo = bar",
    876            "",
    877            "# Another comment",
    878        ]
    879        prop = GeneratedFile("\n".join(propLines))
    880        self.assertEqual(
    881            MinifiedCommentStripped(prop).open().readlines(), [b"foo = bar\n", b"\n"]
    882        )
    883        open(self.tmppath("prop"), "w").write("\n".join(propLines))
    884        MinifiedCommentStripped(File(self.tmppath("prop"))).copy(self.tmppath("prop2"))
    885        self.assertEqual(open(self.tmppath("prop2")).readlines(), ["foo = bar\n", "\n"])
    886 
    887 
    888 class TestMinifiedJavaScript(TestWithTmpDir):
    889    orig_lines = [
    890        "// Comment line",
    891        'let foo = "bar";',
    892        "var bar = true;",
    893        "",
    894        "// Another comment",
    895    ]
    896 
    897    def setUp(self):
    898        super().setUp()
    899        if not buildconfig.substs.get("NODEJS"):
    900            node_exe, _ = find_node_executable()
    901            if node_exe:
    902                buildconfig.substs["NODEJS"] = node_exe
    903 
    904    def test_minified_javascript(self):
    905        """Test that MinifiedJavaScript minifies JavaScript content."""
    906        orig_f = GeneratedFile("\n".join(self.orig_lines).encode())
    907        min_f = MinifiedJavaScript(orig_f, "test.js")
    908 
    909        mini_content = min_f.open().read()
    910        orig_content = orig_f.open().read()
    911 
    912        # Verify minification occurred (content should be smaller)
    913        self.assertTrue(len(mini_content) < len(orig_content))
    914        # Verify content is not empty
    915        self.assertTrue(len(mini_content) > 0)
    916 
    917    def test_minified_javascript_open(self):
    918        """Test that MinifiedJavaScript.open returns appropriately reset file object."""
    919        orig_f = GeneratedFile("\n".join(self.orig_lines).encode())
    920        min_f = MinifiedJavaScript(orig_f, "test.js")
    921 
    922        # Test reading partial content
    923        first_read = min_f.open().read(10)
    924        self.assertTrue(len(first_read) <= 10)
    925 
    926        # Test reading full content multiple times
    927        full_content = min_f.open().read()
    928        second_read = min_f.open().read()
    929        self.assertEqual(full_content, second_read)
    930 
    931    def test_preserves_functionality(self):
    932        """Test that Terser preserves JavaScript functionality."""
    933        # More complex JavaScript with functions and objects
    934        complex_js = """
    935        // This is a test function
    936        function testFunction(param) {
    937            let result = {
    938                value: param * 2,
    939                toString: function() {
    940                    return "Result: " + this.value;
    941                }
    942            };
    943            return result;
    944        }
    945 
    946        // Export for testing
    947        var exported = testFunction;
    948        """
    949 
    950        orig_f = GeneratedFile(complex_js.encode())
    951        min_f = MinifiedJavaScript(orig_f, "complex.js")
    952 
    953        minified_content = min_f.open().read().decode()
    954 
    955        # Verify it's minified
    956        self.assertTrue(len(minified_content) < len(complex_js))
    957        # Verify functions are still present)
    958        self.assertIn("function", minified_content)
    959 
    960    def test_handles_empty_file(self):
    961        """Test that MinifiedJavaScript handles empty files gracefully."""
    962        empty_f = GeneratedFile(b"")
    963        min_f = MinifiedJavaScript(empty_f, "empty.js")
    964 
    965        # Should handle empty content gracefully
    966        result = min_f.open().read()
    967        self.assertEqual(result, b"")
    968 
    969    def test_handles_syntax_errors(self):
    970        """Test that MinifiedJavaScript raises an error for syntax errors."""
    971        # JavaScript with syntax error
    972        broken_js = b"function broken( { return 'missing parenthesis'; }"
    973 
    974        orig_f = GeneratedFile(broken_js)
    975        min_f = MinifiedJavaScript(orig_f, "broken.js")
    976 
    977        # Should raise an ErrorMessage when minification fails
    978        from mozpack.errors import ErrorMessage
    979 
    980        with self.assertRaises(ErrorMessage):
    981            min_f.open().read()
    982 
    983 
    984 class MatchTestTemplate:
    985    def prepare_match_test(self, with_dotfiles=False):
    986        self.add("bar")
    987        self.add("foo/bar")
    988        self.add("foo/baz")
    989        self.add("foo/qux/1")
    990        self.add("foo/qux/bar")
    991        self.add("foo/qux/2/test")
    992        self.add("foo/qux/2/test2")
    993        if with_dotfiles:
    994            self.add("foo/.foo")
    995            self.add("foo/.bar/foo")
    996 
    997    def do_match_test(self):
    998        self.do_check(
    999            "",
   1000            [
   1001                "bar",
   1002                "foo/bar",
   1003                "foo/baz",
   1004                "foo/qux/1",
   1005                "foo/qux/bar",
   1006                "foo/qux/2/test",
   1007                "foo/qux/2/test2",
   1008            ],
   1009        )
   1010        self.do_check(
   1011            "*",
   1012            [
   1013                "bar",
   1014                "foo/bar",
   1015                "foo/baz",
   1016                "foo/qux/1",
   1017                "foo/qux/bar",
   1018                "foo/qux/2/test",
   1019                "foo/qux/2/test2",
   1020            ],
   1021        )
   1022        self.do_check(
   1023            "foo/qux", ["foo/qux/1", "foo/qux/bar", "foo/qux/2/test", "foo/qux/2/test2"]
   1024        )
   1025        self.do_check("foo/b*", ["foo/bar", "foo/baz"])
   1026        self.do_check("baz", [])
   1027        self.do_check("foo/foo", [])
   1028        self.do_check("foo/*ar", ["foo/bar"])
   1029        self.do_check("*ar", ["bar"])
   1030        self.do_check("*/bar", ["foo/bar"])
   1031        self.do_check(
   1032            "foo/*ux", ["foo/qux/1", "foo/qux/bar", "foo/qux/2/test", "foo/qux/2/test2"]
   1033        )
   1034        self.do_check(
   1035            "foo/q*ux",
   1036            ["foo/qux/1", "foo/qux/bar", "foo/qux/2/test", "foo/qux/2/test2"],
   1037        )
   1038        self.do_check("foo/*/2/test*", ["foo/qux/2/test", "foo/qux/2/test2"])
   1039        self.do_check("**/bar", ["bar", "foo/bar", "foo/qux/bar"])
   1040        self.do_check("foo/**/test", ["foo/qux/2/test"])
   1041        self.do_check(
   1042            "foo",
   1043            [
   1044                "foo/bar",
   1045                "foo/baz",
   1046                "foo/qux/1",
   1047                "foo/qux/bar",
   1048                "foo/qux/2/test",
   1049                "foo/qux/2/test2",
   1050            ],
   1051        )
   1052        self.do_check(
   1053            "foo/**",
   1054            [
   1055                "foo/bar",
   1056                "foo/baz",
   1057                "foo/qux/1",
   1058                "foo/qux/bar",
   1059                "foo/qux/2/test",
   1060                "foo/qux/2/test2",
   1061            ],
   1062        )
   1063        self.do_check("**/2/test*", ["foo/qux/2/test", "foo/qux/2/test2"])
   1064        self.do_check(
   1065            "**/foo",
   1066            [
   1067                "foo/bar",
   1068                "foo/baz",
   1069                "foo/qux/1",
   1070                "foo/qux/bar",
   1071                "foo/qux/2/test",
   1072                "foo/qux/2/test2",
   1073            ],
   1074        )
   1075        self.do_check("**/barbaz", [])
   1076        self.do_check("f**/bar", ["foo/bar"])
   1077 
   1078    def do_finder_test(self, finder):
   1079        self.assertTrue(finder.contains("foo/.foo"))
   1080        self.assertTrue(finder.contains("foo/.bar"))
   1081        self.assertTrue("foo/.foo" in [f for f, c in finder.find("foo/.foo")])
   1082        self.assertTrue("foo/.bar/foo" in [f for f, c in finder.find("foo/.bar")])
   1083        self.assertEqual(
   1084            sorted([f for f, c in finder.find("foo/.*")]), ["foo/.bar/foo", "foo/.foo"]
   1085        )
   1086        for pattern in ["foo", "**", "**/*", "**/foo", "foo/*"]:
   1087            self.assertFalse("foo/.foo" in [f for f, c in finder.find(pattern)])
   1088            self.assertFalse("foo/.bar/foo" in [f for f, c in finder.find(pattern)])
   1089            self.assertEqual(
   1090                sorted([f for f, c in finder.find(pattern)]),
   1091                sorted([f for f, c in finder if mozpath.match(f, pattern)]),
   1092            )
   1093 
   1094 
   1095 def do_check(test, finder, pattern, result):
   1096    if result:
   1097        test.assertTrue(finder.contains(pattern))
   1098    else:
   1099        test.assertFalse(finder.contains(pattern))
   1100    test.assertEqual(sorted(list(f for f, c in finder.find(pattern))), sorted(result))
   1101 
   1102 
   1103 class TestFileFinder(MatchTestTemplate, TestWithTmpDir):
   1104    def add(self, path):
   1105        ensureParentDir(self.tmppath(path))
   1106        open(self.tmppath(path), "wb").write(path.encode())
   1107 
   1108    def do_check(self, pattern, result):
   1109        do_check(self, self.finder, pattern, result)
   1110 
   1111    def test_file_finder(self):
   1112        self.prepare_match_test(with_dotfiles=True)
   1113        self.finder = FileFinder(self.tmpdir)
   1114        self.do_match_test()
   1115        self.do_finder_test(self.finder)
   1116 
   1117    def test_get(self):
   1118        self.prepare_match_test()
   1119        finder = FileFinder(self.tmpdir)
   1120 
   1121        self.assertIsNone(finder.get("does-not-exist"))
   1122        res = finder.get("bar")
   1123        self.assertIsInstance(res, File)
   1124        self.assertEqual(mozpath.normpath(res.path), mozpath.join(self.tmpdir, "bar"))
   1125 
   1126    def test_ignored_dirs(self):
   1127        """Ignored directories should not have results returned."""
   1128        self.prepare_match_test()
   1129        self.add("fooz")
   1130 
   1131        # Present to ensure prefix matching doesn't exclude.
   1132        self.add("foo/quxz")
   1133 
   1134        self.finder = FileFinder(self.tmpdir, ignore=["foo/qux"])
   1135 
   1136        self.do_check("**", ["bar", "foo/bar", "foo/baz", "foo/quxz", "fooz"])
   1137        self.do_check("foo/*", ["foo/bar", "foo/baz", "foo/quxz"])
   1138        self.do_check("foo/**", ["foo/bar", "foo/baz", "foo/quxz"])
   1139        self.do_check("foo/qux/**", [])
   1140        self.do_check("foo/qux/*", [])
   1141        self.do_check("foo/qux/bar", [])
   1142        self.do_check("foo/quxz", ["foo/quxz"])
   1143        self.do_check("fooz", ["fooz"])
   1144 
   1145    def test_ignored_files(self):
   1146        """Ignored files should not have results returned."""
   1147        self.prepare_match_test()
   1148 
   1149        # Be sure prefix match doesn't get ignored.
   1150        self.add("barz")
   1151 
   1152        self.finder = FileFinder(self.tmpdir, ignore=["foo/bar", "bar"])
   1153        self.do_check(
   1154            "**",
   1155            [
   1156                "barz",
   1157                "foo/baz",
   1158                "foo/qux/1",
   1159                "foo/qux/2/test",
   1160                "foo/qux/2/test2",
   1161                "foo/qux/bar",
   1162            ],
   1163        )
   1164        self.do_check(
   1165            "foo/**",
   1166            [
   1167                "foo/baz",
   1168                "foo/qux/1",
   1169                "foo/qux/2/test",
   1170                "foo/qux/2/test2",
   1171                "foo/qux/bar",
   1172            ],
   1173        )
   1174 
   1175    def test_ignored_patterns(self):
   1176        """Ignore entries with patterns should be honored."""
   1177        self.prepare_match_test()
   1178 
   1179        self.add("foo/quxz")
   1180 
   1181        self.finder = FileFinder(self.tmpdir, ignore=["foo/qux/*"])
   1182        self.do_check("**", ["foo/bar", "foo/baz", "foo/quxz", "bar"])
   1183        self.do_check("foo/**", ["foo/bar", "foo/baz", "foo/quxz"])
   1184 
   1185    def test_dotfiles(self):
   1186        """Finder can find files beginning with . is configured."""
   1187        self.prepare_match_test(with_dotfiles=True)
   1188        self.finder = FileFinder(self.tmpdir, find_dotfiles=True)
   1189        self.do_check(
   1190            "**",
   1191            [
   1192                "bar",
   1193                "foo/.foo",
   1194                "foo/.bar/foo",
   1195                "foo/bar",
   1196                "foo/baz",
   1197                "foo/qux/1",
   1198                "foo/qux/bar",
   1199                "foo/qux/2/test",
   1200                "foo/qux/2/test2",
   1201            ],
   1202        )
   1203 
   1204    def test_dotfiles_plus_ignore(self):
   1205        self.prepare_match_test(with_dotfiles=True)
   1206        self.finder = FileFinder(
   1207            self.tmpdir, find_dotfiles=True, ignore=["foo/.bar/**"]
   1208        )
   1209        self.do_check(
   1210            "foo/**",
   1211            [
   1212                "foo/.foo",
   1213                "foo/bar",
   1214                "foo/baz",
   1215                "foo/qux/1",
   1216                "foo/qux/bar",
   1217                "foo/qux/2/test",
   1218                "foo/qux/2/test2",
   1219            ],
   1220        )
   1221 
   1222 
   1223 class TestJarFinder(MatchTestTemplate, TestWithTmpDir):
   1224    def add(self, path):
   1225        self.jar.add(path, ensure_bytes(path), compress=True)
   1226 
   1227    def do_check(self, pattern, result):
   1228        do_check(self, self.finder, pattern, result)
   1229 
   1230    def test_jar_finder(self):
   1231        self.jar = JarWriter(file=self.tmppath("test.jar"))
   1232        self.prepare_match_test()
   1233        self.jar.finish()
   1234        reader = JarReader(file=self.tmppath("test.jar"))
   1235        self.finder = JarFinder(self.tmppath("test.jar"), reader)
   1236        self.do_match_test()
   1237 
   1238        self.assertIsNone(self.finder.get("does-not-exist"))
   1239        self.assertIsInstance(self.finder.get("bar"), DeflatedFile)
   1240 
   1241 
   1242 class TestTarFinder(MatchTestTemplate, TestWithTmpDir):
   1243    def add(self, path):
   1244        self.tar.addfile(tarfile.TarInfo(name=path))
   1245 
   1246    def do_check(self, pattern, result):
   1247        do_check(self, self.finder, pattern, result)
   1248 
   1249    def test_tar_finder(self):
   1250        self.tar = tarfile.open(name=self.tmppath("test.tar.bz2"), mode="w:bz2")
   1251        self.prepare_match_test()
   1252        self.tar.close()
   1253        with tarfile.open(name=self.tmppath("test.tar.bz2"), mode="r:bz2") as tarreader:
   1254            self.finder = TarFinder(self.tmppath("test.tar.bz2"), tarreader)
   1255            self.do_match_test()
   1256 
   1257            self.assertIsNone(self.finder.get("does-not-exist"))
   1258            self.assertIsInstance(self.finder.get("bar"), ExtractedTarFile)
   1259 
   1260 
   1261 class TestComposedFinder(MatchTestTemplate, TestWithTmpDir):
   1262    def add(self, path, content=None):
   1263        # Put foo/qux files under $tmp/b.
   1264        if path.startswith("foo/qux/"):
   1265            real_path = mozpath.join("b", path[8:])
   1266        else:
   1267            real_path = mozpath.join("a", path)
   1268        ensureParentDir(self.tmppath(real_path))
   1269        if not content:
   1270            content = path.encode()
   1271        open(self.tmppath(real_path), "wb").write(content)
   1272 
   1273    def do_check(self, pattern, result):
   1274        if "*" in pattern:
   1275            return
   1276        do_check(self, self.finder, pattern, result)
   1277 
   1278    def test_composed_finder(self):
   1279        self.prepare_match_test()
   1280        # Also add files in $tmp/a/foo/qux because ComposedFinder is
   1281        # expected to mask foo/qux entirely with content from $tmp/b.
   1282        ensureParentDir(self.tmppath("a/foo/qux/hoge"))
   1283        open(self.tmppath("a/foo/qux/hoge"), "wb").write(b"hoge")
   1284        open(self.tmppath("a/foo/qux/bar"), "wb").write(b"not the right content")
   1285        self.finder = ComposedFinder({
   1286            "": FileFinder(self.tmppath("a")),
   1287            "foo/qux": FileFinder(self.tmppath("b")),
   1288        })
   1289        self.do_match_test()
   1290 
   1291        self.assertIsNone(self.finder.get("does-not-exist"))
   1292        self.assertIsInstance(self.finder.get("bar"), File)
   1293 
   1294 
   1295 @unittest.skipUnless(hglib, "hglib not available")
   1296 @unittest.skipIf(os.name == "nt", "Does not currently work in Python3 on Windows")
   1297 class TestMercurialRevisionFinder(MatchTestTemplate, TestWithTmpDir):
   1298    def setUp(self):
   1299        super().setUp()
   1300        hglib.init(self.tmpdir)
   1301        self._clients = []
   1302 
   1303    def tearDown(self):
   1304        # Ensure the hg client process is closed. Otherwise, Windows
   1305        # may have trouble removing the repo directory because the process
   1306        # has an open handle on it.
   1307        for client in getattr(self, "_clients", []):
   1308            if client.server:
   1309                client.close()
   1310 
   1311        self._clients[:] = []
   1312 
   1313        super().tearDown()
   1314 
   1315    def _client(self):
   1316        configs = (
   1317            # b'' because py2 needs !unicode
   1318            b'ui.username="Dummy User <dummy@example.com>"',
   1319        )
   1320        client = hglib.open(
   1321            self.tmpdir.encode(),
   1322            encoding=b"UTF-8",  # b'' because py2 needs !unicode
   1323            configs=configs,
   1324        )
   1325        self._clients.append(client)
   1326        return client
   1327 
   1328    def add(self, path):
   1329        with self._client() as c:
   1330            ensureParentDir(self.tmppath(path))
   1331            with open(self.tmppath(path), "wb") as fh:
   1332                fh.write(path.encode())
   1333            c.add(self.tmppath(path).encode())
   1334 
   1335    def do_check(self, pattern, result):
   1336        do_check(self, self.finder, pattern, result)
   1337 
   1338    def _get_finder(self, *args, **kwargs):
   1339        f = MercurialRevisionFinder(*args, **kwargs)
   1340        self._clients.append(f._client)
   1341        return f
   1342 
   1343    def test_default_revision(self):
   1344        self.prepare_match_test()
   1345        with self._client() as c:
   1346            c.commit("initial commit")
   1347 
   1348        self.finder = self._get_finder(self.tmpdir)
   1349        self.do_match_test()
   1350 
   1351        self.assertIsNone(self.finder.get("does-not-exist"))
   1352        self.assertIsInstance(self.finder.get("bar"), MercurialFile)
   1353 
   1354    def test_old_revision(self):
   1355        with self._client() as c:
   1356            with open(self.tmppath("foo"), "wb") as fh:
   1357                fh.write(b"foo initial")
   1358            c.add(self.tmppath("foo").encode())
   1359            c.commit("initial")
   1360 
   1361            with open(self.tmppath("foo"), "wb") as fh:
   1362                fh.write(b"foo second")
   1363            with open(self.tmppath("bar"), "wb") as fh:
   1364                fh.write(b"bar second")
   1365            c.add(self.tmppath("bar").encode())
   1366            c.commit("second")
   1367            # This wipes out the working directory, ensuring the finder isn't
   1368            # finding anything from the filesystem.
   1369            c.rawcommand([b"update", b"null"])
   1370 
   1371        finder = self._get_finder(self.tmpdir, "0")
   1372        f = finder.get("foo")
   1373        self.assertEqual(f.read(), b"foo initial")
   1374        self.assertEqual(f.read(), b"foo initial", "read again for good measure")
   1375        self.assertIsNone(finder.get("bar"))
   1376 
   1377        finder = self._get_finder(self.tmpdir, rev="1")
   1378        f = finder.get("foo")
   1379        self.assertEqual(f.read(), b"foo second")
   1380        f = finder.get("bar")
   1381        self.assertEqual(f.read(), b"bar second")
   1382        f = None
   1383 
   1384    def test_recognize_repo_paths(self):
   1385        with self._client() as c:
   1386            with open(self.tmppath("foo"), "wb") as fh:
   1387                fh.write(b"initial")
   1388            c.add(self.tmppath("foo").encode())
   1389            c.commit("initial")
   1390            c.rawcommand([b"update", b"null"])
   1391 
   1392        finder = self._get_finder(self.tmpdir, "0", recognize_repo_paths=True)
   1393        with self.assertRaises(NotImplementedError):
   1394            list(finder.find(""))
   1395 
   1396        with self.assertRaises(ValueError):
   1397            finder.get("foo")
   1398        with self.assertRaises(ValueError):
   1399            finder.get("")
   1400 
   1401        f = finder.get(self.tmppath("foo"))
   1402        self.assertIsInstance(f, MercurialFile)
   1403        self.assertEqual(f.read(), b"initial")
   1404        f = None
   1405 
   1406 
   1407 if __name__ == "__main__":
   1408    mozunit.main()