11import glob
22import logging
33import os
4+ import sys
45from pathlib import Path
56
67import pytest
@@ -75,43 +76,6 @@ def test_glob_path(config_root, glob_str, count):
7576 assert len (result ) == count
7677
7778
78- class TestLoadPath :
79- """Test the various `DistroFinder.load_path` implementations."""
80-
81- def test_distro_finder (self , uncached_resolver ):
82- """Currently load_path for DistroFinder just returns None."""
83- finder = distro_finder .DistroFinder ("" , uncached_resolver .site )
84- assert finder .load_path (Path ("." )) is None
85-
86- def test_zip_sidecar (self , zip_distro_sidecar ):
87- """The Zip Sidecar reads a .json file next to the zip distro.
88-
89- Ensure it's able to read data from the .json file.
90- """
91- finder = zip_sidecar .DistroFinderZipSidecar (zip_distro_sidecar .root )
92-
93- # This distro hard codes the version inside the .json file
94- data = finder .load_path (zip_distro_sidecar .root / "dist_a_v0.1.hab.json" )
95- assert data ["name" ] == "dist_a"
96- assert "distros" not in data
97- assert data ["version" ] == "0.1"
98-
99- # Test a different distro that doesn't hard code the version
100- data = finder .load_path (zip_distro_sidecar .root / "dist_b_v0.5.hab.json" )
101- assert data ["name" ] == "dist_b"
102- assert "distros" not in data
103- assert data ["version" ] == "0.5"
104-
105- # This distro includes required distros
106- data = finder .load_path (zip_distro_sidecar .root / "dist_a_v0.2.hab.json" )
107- assert data ["name" ] == "dist_a"
108- assert data ["distros" ] == ["dist_b" ]
109- assert data ["version" ] == "0.2"
110-
111- def test_s3 (self ):
112- pass
113-
114-
11579class CheckDistroFinder :
11680 distro_finder_cls = distro_finder .DistroFinder
11781 site_template = "site_distro_finder.json"
@@ -180,19 +144,54 @@ def test_content(self, distro_finder_info):
180144 result = finder .content (path )
181145 assert result == distro_finder_info .root
182146
147+ def test_load_path (self , uncached_resolver ):
148+ """Currently load_path for DistroFinder just returns None."""
149+ finder = distro_finder .DistroFinder ("" , uncached_resolver .site )
150+ assert finder .load_path (Path ("." )) is None
151+
183152 def test_installed (self , distro_finder_info , helpers , tmp_path ):
184153 self .check_installed (distro_finder_info , helpers , tmp_path )
185154
186155 def test_install (self , distro_finder_info , helpers , tmp_path ):
187156 self .check_install (distro_finder_info , helpers , tmp_path )
188157
158+ def test_clear_cache (self , distro_finder_info ):
159+ """Cover the clear_cache function, which for this class does nothing."""
160+ finder = self .distro_finder_cls (distro_finder_info .root )
161+ finder .clear_cache ()
162+
189163
190164class TestZipSidecar (CheckDistroFinder ):
191165 """Tests specific to `DistroFinderZipSidecar`."""
192166
193167 distro_finder_cls = zip_sidecar .DistroFinderZipSidecar
194168 site_template = "site_distro_zip_sidecar.json"
195169
170+ def test_load_path (self , zip_distro_sidecar ):
171+ """The Zip Sidecar reads a .json file next to the zip distro.
172+
173+ Ensure it's able to read data from the .json file.
174+ """
175+ finder = self .distro_finder_cls (zip_distro_sidecar .root )
176+
177+ # This distro hard codes the version inside the .json file
178+ data = finder .load_path (zip_distro_sidecar .root / "dist_a_v0.1.hab.json" )
179+ assert data ["name" ] == "dist_a"
180+ assert "distros" not in data
181+ assert data ["version" ] == "0.1"
182+
183+ # Test a different distro that doesn't hard code the version
184+ data = finder .load_path (zip_distro_sidecar .root / "dist_b_v0.5.hab.json" )
185+ assert data ["name" ] == "dist_b"
186+ assert "distros" not in data
187+ assert data ["version" ] == "0.5"
188+
189+ # This distro includes required distros
190+ data = finder .load_path (zip_distro_sidecar .root / "dist_a_v0.2.hab.json" )
191+ assert data ["name" ] == "dist_a"
192+ assert data ["distros" ] == ["dist_b" ]
193+ assert data ["version" ] == "0.2"
194+
196195 def test_installed (self , zip_distro_sidecar , helpers , tmp_path ):
197196 self .check_installed (zip_distro_sidecar , helpers , tmp_path )
198197
@@ -238,7 +237,7 @@ def test_load_path(self, zip_distro):
238237
239238 Ensure it's able to read data from the .json file.
240239 """
241- finder = df_zip . DistroFinderZip (zip_distro .root )
240+ finder = self . distro_finder_cls (zip_distro .root )
242241
243242 # This distro hard codes the version inside the .json file
244243 data = finder .load_path (zip_distro .root / "dist_a_v0.1.zip" )
@@ -293,24 +292,132 @@ def test_installed(self, zip_distro, helpers, tmp_path):
293292 def test_install (self , zip_distro , helpers , tmp_path ):
294293 self .check_install (zip_distro , helpers , tmp_path )
295294
295+ def test_clear_cache (self , distro_finder_info ):
296+ """Test the clear_cache function for this class."""
297+ finder = self .distro_finder_cls (distro_finder_info .root )
298+ finder ._cache ["test" ] = "case"
299+ finder .clear_cache ()
300+ assert finder ._cache == {}
301+
296302
297303# These tests only work if using the `pyXX-s3` tox testing env
298304@pytest .mark .skipif (
299305 not os .getenv ("VIRTUAL_ENV" , "" ).endswith ("-s3" ),
300306 reason = "not testing optional s3 cloud" ,
301307)
302308class TestS3 (CheckDistroFinder ):
303- """Tests specific to `DistroFinderS3Zip`."""
309+ """Tests specific to `DistroFinderS3Zip`.
310+
311+ Note: All tests should use the `zip_distro_s3` fixture. This ensures that
312+ any s3 requests are local and also speeds up the test.
313+ """
304314
305315 site_template = "site_distro_s3.json"
306316
317+ class ServerSimulator :
318+ """Requests server used for testing downloading a partial zip file.
319+
320+ Based on remotezip test code:
321+ https://github.com/gtsystem/python-remotezip/blob/master/test_remotezip.py
322+ """
323+
324+ def __init__ (self , fname ):
325+ self ._fname = fname
326+ self .requested_ranges = []
327+
328+ def serve (self , request , context ):
329+ import remotezip
330+
331+ from_byte , to_byte = remotezip .RemoteFetcher .parse_range_header (
332+ request .headers ["Range" ]
333+ )
334+ self .requested_ranges .append ((from_byte , to_byte ))
335+
336+ with open (self ._fname , "rb" ) as f :
337+ if from_byte < 0 :
338+ f .seek (0 , 2 )
339+ size = f .tell ()
340+ f .seek (max (size + from_byte , 0 ), 0 )
341+ init_pos = f .tell ()
342+ content = f .read (min (size , - from_byte ))
343+ else :
344+ f .seek (from_byte , 0 )
345+ init_pos = f .tell ()
346+ content = f .read (to_byte - from_byte + 1 )
347+
348+ context .headers [
349+ "Content-Range"
350+ ] = remotezip .RemoteFetcher .build_range_header (
351+ init_pos , init_pos + len (content )
352+ )
353+ return content
354+
307355 @property
308356 def distro_finder_cls (self ):
309357 """Only import this class if the test is not skipped."""
310358 from hab .distro_finders .s3_zip import DistroFinderS3Zip
311359
312360 return DistroFinderS3Zip
313361
362+ def test_load_path (self , zip_distro_s3 , helpers , tmp_path , requests_mock ):
363+ """Simulate reading only part of a remote zip file hosted in an aws s3 bucket.
364+
365+ This doesn't actually connect to an aws s3 bucket, it uses mock libraries
366+ to simulate the process.
367+ """
368+ import boto3
369+
370+ if sys .version_info .minor <= 7 :
371+ # NOTE: boto3 has dropped python 3.7. Moto changed their context name
372+ # when they dropped support for python 3.7.
373+ from moto import mock_s3 as mock_aws
374+ else :
375+ from moto import mock_aws
376+
377+ # Make requests connect to a simulated s3 server that supports the range header
378+ server = self .ServerSimulator (
379+ zip_distro_s3 .zip_root / "hab-test-bucket" / "dist_a_v0.1.zip"
380+ )
381+ requests_mock .register_uri (
382+ "GET" ,
383+ "s3://hab-test-bucket/dist_a_v0.1.zip" ,
384+ content = server .serve ,
385+ status_code = 200 ,
386+ )
387+
388+ # Create a mock aws setup using moto to test the authorization code
389+ resolver = self .create_resolver (zip_distro_s3 .zip_root , helpers , tmp_path )
390+ dl_finder = resolver .site .downloads ["distros" ][0 ]
391+
392+ with mock_aws ():
393+ # The LocalS3Client objects don't have all of the s3 properties we
394+ # require for configuring requests auth. Add them and crate the bucket.
395+ sess = boto3 .Session (region_name = "us-east-2" )
396+ conn = boto3 .resource ("s3" , region_name = "us-east-2" )
397+ conn .create_bucket (
398+ Bucket = "hab-test-bucket" ,
399+ CreateBucketConfiguration = {"LocationConstraint" : "us-east-2" },
400+ )
401+ dl_finder .client .sess = sess
402+ dl_finder .client .client = sess .client ("s3" , region_name = "us-east-2" )
403+
404+ # Test reading .hab.json from inside a remote .zip file.
405+ dl_finder = resolver .site .downloads ["distros" ][0 ]
406+ zip_path = dl_finder .root / "dist_a_v0.1.zip"
407+ archive = dl_finder .archive (zip_path )
408+
409+ # Check that the filename property is always populated
410+ assert str (archive .filename ) == str (zip_path )
411+
412+ # Check that we were able to read the data from the archive
413+ data = dl_finder .load_path (zip_path / ".hab.json" )
414+ assert data ["name" ] == "dist_a"
415+ assert data ["version" ] == "0.1"
416+
417+ # Verify that remotezip had to make more than one request. This is because
418+ # the .zip file is larger than `initial_buffer_size`.
419+ assert len (server .requested_ranges ) == 2
420+
314421 def test_installed (self , zip_distro_s3 , helpers , tmp_path ):
315422 self .check_installed (zip_distro_s3 , helpers , tmp_path )
316423
@@ -334,6 +441,66 @@ def test_client(self, zip_distro_s3, helpers, tmp_path):
334441 finder .client = "A custom client"
335442 assert finder .client == "A custom client"
336443
444+ # Test init with a custom client
445+ from cloudpathlib .local import LocalS3Client
446+
447+ client = LocalS3Client ()
448+ finder = self .distro_finder_cls ("s3://hab-test-bucket" , client = client )
449+ assert finder .client == client
450+
451+ def test_as_posix (self , zip_distro_s3 ):
452+ """Cloudpathlib doesn't support `as_posix` a simple str is returned."""
453+ # Test that as_posix for CloudPath's returns the CloudPath as a str
454+ finder = self .distro_finder_cls ("s3://hab-test-bucket" )
455+ assert finder .as_posix () == "s3://hab-test-bucket"
456+
457+ # Otherwise it returns a standard pathlib.Path.as_posix value
458+ finder .root = zip_distro_s3 .root
459+ assert finder .as_posix () == zip_distro_s3 .root .as_posix ()
460+
461+ def test_clear_cache (self , zip_distro_s3 ):
462+ """Test the clear_cache function for this class."""
463+
464+ class Archive :
465+ """Simulated ZipFile class to test that open archives get closed."""
466+
467+ def __init__ (self ):
468+ self .is_open = True
469+
470+ def close (self ):
471+ self .is_open = False
472+
473+ class Client :
474+ """Simulated S3Client to test calling clear_cache on."""
475+
476+ def __init__ (self ):
477+ self .cleared = False
478+
479+ def clear_cache (self ):
480+ self .cleared = True
481+
482+ finder = self .distro_finder_cls ("s3://hab-test-bucket" )
483+ # Simulate use of the finder
484+ archive = Archive ()
485+ finder ._archives ["s3://hab-test-bucket/dist_a_v0.1.zip" ] = archive
486+ finder ._cache ["test" ] = "case"
487+ finder .client = Client ()
488+ assert archive .is_open
489+ assert not finder .client .cleared
490+
491+ # Check that clearing reset the cache variables
492+ finder .clear_cache ()
493+ assert finder ._archives == {}
494+ assert finder ._cache == {}
495+ # Check that any open archives were closed
496+ assert not archive .is_open
497+ # Check that the client was not cleared as persistent is False
498+ assert not finder .client .cleared
499+
500+ # Clearing of persistent caches clears the cache
501+ finder .clear_cache (persistent = True )
502+ assert finder .client .cleared
503+
337504
338505# TODO: Break this into separate smaller tests of components for each class not this
339506@pytest .mark .parametrize (
0 commit comments