Skip to content

Commit 4d7e6f3

Browse files
committed
Split download code from install_arduino_cli() to general-purpose function
This code is useful for downloading and installing dependencies in addition to Arduino CLI.
1 parent 1979ae5 commit 4d7e6f3

File tree

8 files changed

+146
-40
lines changed

8 files changed

+146
-40
lines changed

compilesketches/compilesketches.py

Lines changed: 83 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
import shutil
99
import subprocess
1010
import sys
11-
import tarfile
1211
import tempfile
1312
import urllib
1413
import urllib.request
@@ -195,26 +194,12 @@ def install_arduino_cli(self):
195194
arduino_cli_archive_download_url_prefix = "https://downloads.arduino.cc/arduino-cli/"
196195
arduino_cli_archive_file_name = "arduino-cli_" + self.cli_version + "_Linux_64bit.tar.gz"
197196

198-
# Create temporary folder
199-
with tempfile.TemporaryDirectory(prefix="arduino-cli-" + self.cli_version + "-") as download_folder:
200-
arduino_cli_archive_file_path = pathlib.PurePath(download_folder, arduino_cli_archive_file_name)
201-
202-
# https://stackoverflow.com/a/38358646
203-
with open(file=str(arduino_cli_archive_file_path), mode="wb") as out_file:
204-
with contextlib.closing(thing=urllib.request.urlopen(
205-
url=arduino_cli_archive_download_url_prefix
206-
+ arduino_cli_archive_file_name)
207-
) as file_pointer:
208-
block_size = 1024 * 8
209-
while True:
210-
block = file_pointer.read(block_size)
211-
if not block:
212-
break
213-
out_file.write(block)
214-
215-
# Extract arduino-cli archive
216-
with tarfile.open(name=arduino_cli_archive_file_path) as tar:
217-
tar.extractall(path=self.arduino_cli_installation_path)
197+
install_from_download(
198+
url=arduino_cli_archive_download_url_prefix + arduino_cli_archive_file_name,
199+
# The Arduino CLI has no root folder, so just install the arduino-cli executable from the archive root
200+
source_path="arduino-cli",
201+
destination_parent_path=self.arduino_cli_installation_path
202+
)
218203

219204
# Configure the location of the Arduino CLI user directory
220205
os.environ["ARDUINO_DIRECTORIES_USER"] = str(self.arduino_cli_user_directory_path)
@@ -990,6 +975,83 @@ def list_to_string(list_input):
990975
return " ".join([str(item) for item in list_input])
991976

992977

978+
def install_from_download(url, source_path, destination_parent_path, destination_name=None):
979+
"""Download an archive, extract, and install.
980+
981+
Keyword arguments:
982+
url -- URL to download the archive from
983+
source_path -- path relative to the root folder of the archive to install.
984+
destination_parent_path -- path under which to install
985+
destination_name -- folder name to use for the installation. Set to None to take the name from source_path.
986+
(default None)
987+
"""
988+
destination_parent_path = pathlib.Path(destination_parent_path)
989+
990+
# Create temporary folder for the download
991+
with tempfile.TemporaryDirectory("-compilesketches-download_folder") as download_folder:
992+
download_file_path = pathlib.PurePath(download_folder, url.rsplit(sep="/", maxsplit=1)[1])
993+
994+
# https://stackoverflow.com/a/38358646
995+
with open(file=str(download_file_path), mode="wb") as out_file:
996+
with contextlib.closing(thing=urllib.request.urlopen(url=url)) as file_pointer:
997+
block_size = 1024 * 8
998+
while True:
999+
block = file_pointer.read(block_size)
1000+
if not block:
1001+
break
1002+
out_file.write(block)
1003+
1004+
# Create temporary folder for the extraction
1005+
with tempfile.TemporaryDirectory("-compilesketches-extract_folder") as extract_folder:
1006+
# Extract archive
1007+
shutil.unpack_archive(filename=str(download_file_path), extract_dir=extract_folder)
1008+
1009+
archive_root_path = get_archive_root_path(extract_folder)
1010+
1011+
absolute_source_path = pathlib.Path(archive_root_path, source_path).resolve()
1012+
1013+
if not absolute_source_path.exists():
1014+
print("::error::Archive source path:", source_path, "not found")
1015+
sys.exit(1)
1016+
1017+
if destination_name is None:
1018+
destination_name = absolute_source_path.name
1019+
1020+
# Create the parent path if it doesn't already exist
1021+
destination_parent_path.mkdir(parents=True, exist_ok=True)
1022+
1023+
# Install by moving the source folder
1024+
shutil.move(src=str(absolute_source_path),
1025+
dst=str(destination_parent_path.joinpath(destination_name)))
1026+
1027+
1028+
def get_archive_root_path(archive_extract_path):
1029+
"""Return the path of the archive's root folder.
1030+
1031+
Keyword arguments:
1032+
archive_extract_path -- path the archive was extracted to
1033+
"""
1034+
archive_root_folder_name = archive_extract_path
1035+
for extract_folder_content in pathlib.Path(archive_extract_path).glob("*"):
1036+
if extract_folder_content.is_dir():
1037+
# Path is a folder
1038+
# Ignore the __MACOSX folder
1039+
if extract_folder_content.name != "__MACOSX":
1040+
if archive_root_folder_name == archive_extract_path:
1041+
# This is the first folder found
1042+
archive_root_folder_name = extract_folder_content
1043+
else:
1044+
# Multiple folders found
1045+
archive_root_folder_name = archive_extract_path
1046+
break
1047+
else:
1048+
# Path is a file
1049+
archive_root_folder_name = archive_extract_path
1050+
break
1051+
1052+
return archive_root_folder_name
1053+
1054+
9931055
# Only execute the following code if the script is run directly, not imported
9941056
if __name__ == "__main__":
9951057
main()

compilesketches/tests/test_compilesketches.py

Lines changed: 63 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,6 @@
77
import tarfile
88
import tempfile
99
import unittest.mock
10-
import urllib
11-
import urllib.request
1210

1311
import git
1412
import github
@@ -286,33 +284,23 @@ def test_compile_sketches(mocker, compilation_success_list, expected_success, do
286284
)
287285

288286

289-
def test_install_arduino_cli(tmpdir, mocker):
287+
def test_install_arduino_cli(mocker):
290288
cli_version = "1.2.3"
289+
arduino_cli_installation_path = unittest.mock.sentinel.arduino_cli_installation_path
291290
arduino_cli_user_directory_path = pathlib.PurePath("/foo/arduino_cli_user_directory_path")
292-
source_file_path = test_data_path.joinpath("githubevent.json")
293-
# Create temporary folder
294-
arduino_cli_installation_path = pathlib.PurePath(tmpdir.mkdir("test_install_arduino_cli"))
295-
output_archive_path = arduino_cli_installation_path.joinpath("foo_archive.tar.gz")
296-
297-
# Create an archive file
298-
with tarfile.open(name=output_archive_path, mode="w:gz") as tar:
299-
tar.add(name=source_file_path, arcname=source_file_path.name)
300291

301292
compile_sketches = get_compilesketches_object(cli_version=cli_version)
302293
compile_sketches.arduino_cli_installation_path = arduino_cli_installation_path
303294
compile_sketches.arduino_cli_user_directory_path = arduino_cli_user_directory_path
304295

305-
# Patch urllib.request.urlopen so that the generated archive file is opened instead of the Arduino CLI download
306-
mocker.patch("urllib.request.urlopen",
307-
return_value=urllib.request.urlopen(url="file:///" + str(output_archive_path)))
296+
mocker.patch("compilesketches.install_from_download", autospec=True)
308297

309298
compile_sketches.install_arduino_cli()
310299

311-
urllib.request.urlopen.assert_called_once_with(url="https://downloads.arduino.cc/arduino-cli/arduino-cli_"
312-
+ cli_version + "_Linux_64bit.tar.gz")
313-
314-
# Verify that the installation matches the source file
315-
assert filecmp.cmp(f1=source_file_path, f2=arduino_cli_installation_path.joinpath(source_file_path.name)) is True
300+
compilesketches.install_from_download.assert_called_once_with(
301+
url="https://downloads.arduino.cc/arduino-cli/arduino-cli_" + cli_version + "_Linux_64bit.tar.gz",
302+
source_path="arduino-cli",
303+
destination_parent_path=arduino_cli_installation_path)
316304

317305
assert os.environ["ARDUINO_DIRECTORIES_USER"] == str(arduino_cli_user_directory_path)
318306

@@ -1288,6 +1276,62 @@ def test_list_to_string():
12881276
assert compilesketches.list_to_string([42, path]) == "42 " + str(path)
12891277

12901278

1279+
@pytest.mark.parametrize("arcname, source_path, destination_name, expected_destination_name, expected_success",
1280+
[("FooArcname", ".", None, "FooArcname", True),
1281+
("FooArcname", "./Sketch1", "FooDestinationName", "FooDestinationName", True),
1282+
("FooArcname", "Sketch1", None, "Sketch1", True),
1283+
(".", "Sketch1", None, "Sketch1", True),
1284+
("FooArcname", "Nonexistent", None, "", False), ])
1285+
def test_install_from_download(capsys,
1286+
tmp_path,
1287+
arcname,
1288+
source_path,
1289+
destination_name,
1290+
expected_destination_name,
1291+
expected_success):
1292+
url_source_path = test_data_path.joinpath("HasSketches")
1293+
1294+
# Create temporary folder
1295+
url_path = tmp_path.joinpath("url_path")
1296+
url_path.mkdir()
1297+
url_archive_path = url_path.joinpath("foo_archive.tar.gz")
1298+
url = url_archive_path.as_uri()
1299+
1300+
# Create an archive file
1301+
with tarfile.open(name=url_archive_path, mode="w:gz", format=tarfile.GNU_FORMAT) as tar:
1302+
tar.add(name=url_source_path, arcname=arcname)
1303+
1304+
destination_parent_path = tmp_path.joinpath("destination_parent_path")
1305+
1306+
if expected_success:
1307+
compilesketches.install_from_download(url=url,
1308+
source_path=source_path,
1309+
destination_parent_path=destination_parent_path,
1310+
destination_name=destination_name)
1311+
1312+
# Verify that the installation matches the source
1313+
assert directories_are_same(left_directory=url_source_path.joinpath(source_path),
1314+
right_directory=destination_parent_path.joinpath(expected_destination_name))
1315+
else:
1316+
with pytest.raises(expected_exception=SystemExit, match="1"):
1317+
compilesketches.install_from_download(url=url,
1318+
source_path=source_path,
1319+
destination_parent_path=destination_parent_path,
1320+
destination_name=destination_name)
1321+
assert capsys.readouterr().out.strip() == ("::error::Archive source path: " + source_path + " not found")
1322+
1323+
1324+
@pytest.mark.parametrize("archive_extract_path, expected_archive_root_path",
1325+
[(test_data_path.joinpath("test_get_archive_root_folder_name", "has-root"),
1326+
test_data_path.joinpath("test_get_archive_root_folder_name", "has-root", "root")),
1327+
(test_data_path.joinpath("test_get_archive_root_folder_name", "has-file"),
1328+
test_data_path.joinpath("test_get_archive_root_folder_name", "has-file")),
1329+
(test_data_path.joinpath("test_get_archive_root_folder_name", "has-folders"),
1330+
test_data_path.joinpath("test_get_archive_root_folder_name", "has-folders"))])
1331+
def test_get_archive_root_path(archive_extract_path, expected_archive_root_path):
1332+
assert compilesketches.get_archive_root_path(archive_extract_path) == expected_archive_root_path
1333+
1334+
12911335
@pytest.mark.parametrize("url, source_path, destination_name, expected_destination_name",
12921336
[("https://example.com/foo/FooRepositoryName.git", ".", None, "FooRepositoryName"),
12931337
("https://example.com/foo/FooRepositoryName.git/", "./examples", "FooDestinationName",

compilesketches/tests/testdata/test_get_archive_root_folder_name/has-file/not-root.foo

Whitespace-only changes.

compilesketches/tests/testdata/test_get_archive_root_folder_name/has-file/not-root/.gitkeep

Whitespace-only changes.

compilesketches/tests/testdata/test_get_archive_root_folder_name/has-folders/also-not-root/.gitkeep

Whitespace-only changes.

compilesketches/tests/testdata/test_get_archive_root_folder_name/has-folders/not-root/.gitkeep

Whitespace-only changes.

compilesketches/tests/testdata/test_get_archive_root_folder_name/has-root/__MACOSX/.gitkeep

Whitespace-only changes.

compilesketches/tests/testdata/test_get_archive_root_folder_name/has-root/root/.gitkeep

Whitespace-only changes.

0 commit comments

Comments
 (0)