Skip to content

Commit 133c4cf

Browse files
committed
5/5: Add testing for the new hab install and DistroFinder features
S3 testing features are only supported for python 3.8+
1 parent a968fc7 commit 133c4cf

15 files changed

+1560
-43
lines changed

.github/workflows/python-static-analysis-and-test.yml

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,21 @@ jobs:
4949
strategy:
5050
matrix:
5151
# Test if using native json or pyjson5 for json parsing
52-
json_ver: ['json', 'json5']
52+
pkg_mods: ['json', 'json5', 's3']
5353
os: ['ubuntu-latest', 'windows-latest']
54-
python: ['3.7', '3.8', '3.9', '3.10', '3.11']
54+
python: ['3.8', '3.9', '3.10', '3.11']
55+
# Works around the depreciation of python 3.6 for ubuntu
56+
# https://github.com/actions/setup-python/issues/544
57+
include:
58+
- pkg_mods: 'json'
59+
os: 'ubuntu-22.04'
60+
python: '3.7'
61+
- pkg_mods: 'json5'
62+
os: 'ubuntu-22.04'
63+
python: '3.7'
64+
- pkg_mods: 's3'
65+
os: 'ubuntu-22.04'
66+
python: '3.7'
5567

5668
runs-on: ${{ matrix.os }}
5769

@@ -71,12 +83,12 @@ jobs:
7183
7284
- name: Run Tox
7385
run: |
74-
tox -e begin,py-${{ matrix.json_ver }}
86+
tox -e begin,py-${{ matrix.pkg_mods }}
7587
7688
- name: Upload coverage
7789
uses: actions/upload-artifact@v4
7890
with:
79-
name: coverage-${{ matrix.os }}-${{ matrix.python }}-${{ matrix.json_ver }}
91+
name: coverage-${{ matrix.os }}-${{ matrix.python }}-${{ matrix.pkg_mods }}
8092
path: .coverage.*
8193
include-hidden-files: true
8294
retention-days: 1

tests/conftest.py

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
import json
22
import os
3+
import shutil
4+
from collections import namedtuple
35
from contextlib import contextmanager
46
from pathlib import Path, PurePath
7+
from zipfile import ZipFile
58

69
import pytest
10+
from jinja2 import Environment, FileSystemLoader
711
from packaging.requirements import Requirement
812

913
from hab import Resolver, Site
@@ -111,6 +115,141 @@ def resolver(request):
111115
return request.getfixturevalue(test_map[request.param])
112116

113117

118+
Distro = namedtuple("Distro", ["name", "version", "inc_version", "distros"])
119+
120+
121+
class DistroInfo(namedtuple("DistroInfo", ["root", "versions", "zip_root"])):
122+
default_versions = (
123+
("dist_a", "0.1", True, None),
124+
("dist_a", "0.2", False, ["dist_b"]),
125+
("dist_a", "1.0", False, None),
126+
("dist_b", "0.5", False, None),
127+
("dist_b", "0.6", False, None),
128+
)
129+
130+
@classmethod
131+
def dist_version(cls, distro, version):
132+
return f"{distro}_v{version}"
133+
134+
@classmethod
135+
def hab_json(cls, distro, version=None, distros=None):
136+
data = {"name": distro}
137+
if version:
138+
data["version"] = version
139+
if distros:
140+
data["distros"] = distros
141+
return json.dumps(data, indent=4)
142+
143+
@classmethod
144+
def generate(cls, root, versions=None, zip_created=None, zip_root=None):
145+
if versions is None:
146+
versions = cls.default_versions
147+
if zip_root is None:
148+
zip_root = root
149+
150+
versions = {(x[0], x[1]): Distro(*x) for x in versions}
151+
152+
for version in versions.values():
153+
name = cls.dist_version(version.name, version.version)
154+
filename = root / f"{name}.zip"
155+
ver = version.version if version.inc_version else None
156+
with ZipFile(filename, "w") as zf:
157+
# Make the .zip file larger than the remotezip initial_buffer_size
158+
# so testing of partial archive reading is forced use multiple requests
159+
zf.writestr("data.txt", "-" * 64 * 1024)
160+
zf.writestr(
161+
".hab.json",
162+
cls.hab_json(version.name, version=ver, distros=version.distros),
163+
)
164+
zf.writestr("file_a.txt", "File A inside the distro.")
165+
zf.writestr("folder/file_b.txt", "File B inside the distro.")
166+
if zip_created:
167+
zip_created(zf)
168+
169+
# Create a correctly named .zip file that doesn't have a .hab.json file
170+
# to test for .zip files that are not distros.
171+
with ZipFile(root / "not_valid_v0.1.zip", "w") as zf:
172+
zf.writestr("README.txt", "This file is not a hab distro zip.")
173+
174+
return cls(root, versions, zip_root)
175+
176+
177+
@pytest.fixture(scope="session")
178+
def distro_finder_info(tmp_path_factory):
179+
"""Returns a DistroInfo instance with extracted distros ready for hab.
180+
181+
This is useful for using an existing hab distro structure as your download server.
182+
"""
183+
root = tmp_path_factory.mktemp("_distro_finder")
184+
185+
def zip_created(zf):
186+
"""Extract all contents zip into a distro folder structure."""
187+
filename = Path(zf.filename).stem
188+
distro, version = filename.split("_v")
189+
zf.extractall(root / distro / version)
190+
191+
return DistroInfo.generate(root, zip_created=zip_created)
192+
193+
194+
@pytest.fixture(scope="session")
195+
def zip_distro(tmp_path_factory):
196+
"""Returns a DistroInfo instance for a zip folder structure.
197+
198+
This is useful if the zip files are locally accessible or if your hab download
199+
server supports `HTTP range requests`_. For example if you are using Amazon S3.
200+
201+
.. _HTTP range requests:
202+
https://developer.mozilla.org/en-US/docs/Web/HTTP/Range_requests
203+
"""
204+
root = tmp_path_factory.mktemp("_zip_distro_files")
205+
return DistroInfo.generate(root)
206+
207+
208+
@pytest.fixture(scope="session")
209+
def zip_distro_sidecar(tmp_path_factory):
210+
"""Returns a DistroInfo instance for a zip folder structure with sidecar
211+
`.hab.json` files.
212+
213+
This is useful when your hab download server does not support HTTP range requests.
214+
"""
215+
root = tmp_path_factory.mktemp("_zip_distro_sidecar_files")
216+
217+
def zip_created(zf):
218+
"""Extract the .hab.json from the zip to a sidecar file."""
219+
filename = Path(zf.filename).stem
220+
sidecar = root / f"{filename}.hab.json"
221+
path = zf.extract(".hab.json", root)
222+
shutil.move(path, sidecar)
223+
224+
return DistroInfo.generate(root, zip_created=zip_created)
225+
226+
227+
@pytest.fixture(scope="session")
228+
def _zip_distro_s3(tmp_path_factory):
229+
"""The files used by `zip_distro_s3` only generated once per test."""
230+
root = tmp_path_factory.mktemp("_zip_distro_s3_files")
231+
bucket_root = root / "hab-test-bucket"
232+
bucket_root.mkdir()
233+
return DistroInfo.generate(bucket_root, zip_root=root)
234+
235+
236+
@pytest.fixture()
237+
def zip_distro_s3(_zip_distro_s3, monkeypatch):
238+
"""Returns a DistroInfo instance for a s3 zip cloud based folder structure.
239+
240+
This is used to simulate using an aws s3 cloud storage bucket to host hab
241+
distro zip files.
242+
"""
243+
from cloudpathlib import implementation_registry
244+
from cloudpathlib.local import LocalS3Client, local_s3_implementation
245+
246+
from hab.distro_finders import s3_zip
247+
248+
monkeypatch.setitem(implementation_registry, "s3", local_s3_implementation)
249+
monkeypatch.setattr(s3_zip, "S3Client", LocalS3Client)
250+
return _zip_distro_s3
251+
252+
114253
class Helpers(object):
115254
"""A collection of reusable functions that tests can use."""
116255

@@ -204,6 +343,36 @@ def compare_files(generated, check):
204343
cache[i] == check[i]
205344
), f"Difference on line: {i} between the generated cache and {generated}."
206345

346+
@staticmethod
347+
def render_template(template, dest, **kwargs):
348+
"""Render a jinja template in from the test templates directory.
349+
350+
Args:
351+
template (str): The name of the template file in the templates dir.
352+
dest (os.PathLike): The destination filename to write the output.
353+
**kwargs: All kwargs are used to render the template.
354+
"""
355+
environment = Environment(
356+
loader=FileSystemLoader(str(Path(__file__).parent / "templates")),
357+
trim_blocks=True,
358+
lstrip_blocks=True,
359+
)
360+
template = environment.get_template(template)
361+
362+
text = template.render(**kwargs).rstrip() + "\n"
363+
with dest.open("w") as fle:
364+
fle.write(text)
365+
366+
@classmethod
367+
def render_resolver(cls, site_template, dest, **kwargs):
368+
"""Calls `render_template` and constructs a Resolver instance for it."""
369+
# Build the hab site
370+
site_file = dest / "site.json"
371+
cls.render_template(site_template, site_file, **kwargs)
372+
373+
site = Site([site_file])
374+
return Resolver(site)
375+
207376

208377
@pytest.fixture
209378
def helpers():

tests/site/site_distro_finder.json

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
{
2+
"set":
3+
{
4+
"distro_paths":
5+
[
6+
[
7+
"hab.distro_finders.distro_finder:DistroFinder",
8+
"hab testable/download/path"
9+
],
10+
[
11+
"hab.distro_finders.distro_finder:DistroFinder",
12+
"hab testing/downloads",
13+
{
14+
"site": "for testing only, do not specify site"
15+
}
16+
]
17+
],
18+
"downloads":
19+
{
20+
"cache_root": "hab testable/download/path",
21+
"distros":
22+
[
23+
[
24+
"hab.distro_finders.df_zip:DistroFinderZip",
25+
"network_server/distro/source"
26+
]
27+
],
28+
"install_root": "{relative_root}/distros",
29+
"relative_path": "{{distro_name}}_v{{version}}"
30+
}
31+
}
32+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"set": {
3+
"downloads": {
4+
"cache_root": ""
5+
}
6+
}
7+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
"set": {
3+
"config_paths": [
4+
"{relative_root}/configs"
5+
],
6+
"distro_paths": [
7+
"{relative_root}/distros/*"
8+
],
9+
"downloads": {
10+
"cache_root": "{relative_root}/downloads",
11+
"distros": [
12+
[
13+
"hab.distro_finders.distro_finder:DistroFinder",
14+
"{{ zip_root }}/*"
15+
]
16+
],
17+
"install_root": "{relative_root}/distros"
18+
}
19+
}
20+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
"set": {
3+
"config_paths": [
4+
"{relative_root}/configs"
5+
],
6+
"distro_paths": [
7+
"{relative_root}/distros/*"
8+
],
9+
"downloads": {
10+
"cache_root": "{relative_root}/downloads",
11+
"distros": [
12+
[
13+
"hab.distro_finders.s3_zip:DistroFinderS3Zip",
14+
"s3://hab-test-bucket",
15+
{
16+
"no_sign_request": true,
17+
"local_storage_dir": "{{ zip_root }}"
18+
}
19+
]
20+
],
21+
"install_root": "{relative_root}/distros"
22+
}
23+
}
24+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
"set": {
3+
"config_paths": [
4+
"{relative_root}/configs"
5+
],
6+
"distro_paths": [
7+
"{relative_root}/distros/*"
8+
],
9+
"downloads": {
10+
"cache_root": "{relative_root}/downloads",
11+
"distros": [
12+
[
13+
"hab.distro_finders.df_zip:DistroFinderZip",
14+
"{{ zip_root }}"
15+
]
16+
],
17+
"install_root": "{relative_root}/distros"
18+
}
19+
}
20+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
"set": {
3+
"config_paths": [
4+
"{relative_root}/configs"
5+
],
6+
"distro_paths": [
7+
"{relative_root}/distros/*"
8+
],
9+
"downloads": {
10+
"cache_root": "{relative_root}/downloads",
11+
"distros": [
12+
[
13+
"hab.distro_finders.zip_sidecar:DistroFinderZipSidecar",
14+
"{{ zip_root }}"
15+
]
16+
],
17+
"install_root": "{relative_root}/distros"
18+
}
19+
}
20+
}

tests/templates/site_download.json

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
"set": {
3+
"config_paths": [
4+
"{relative_root}/configs"
5+
],
6+
"distro_paths": [
7+
"{relative_root}/distros/*"
8+
],
9+
"downloads": {
10+
"cache_root": "{relative_root}/downloads",
11+
"distros": [
12+
[
13+
"hab.distro_finders.df_zip:DistroFinderZip",
14+
"{{ zip_root }}"
15+
]
16+
],
17+
"install_root": "{relative_root}/distros"
18+
}
19+
}
20+
}

0 commit comments

Comments
 (0)