1 # SPDX-FileCopyrightText: 2023 Blender Foundation
3 # SPDX-License-Identifier: GPL-2.0-or-later
7 make test_blender BLENDER_BIN=$PWD/../../../blender.bin
30 # For more useful output that isn't clipped.
31 unittest
.util
._MAX
_LENGTH
= 10_000
33 IS_WIN32
= sys
.platform
== "win32"
35 # See the variable with the same name in `blender_ext.py`.
36 REMOTE_REPO_HAS_JSON_IMPLIED
= True
40 # PKG_REPO_LIST_FILENAME = "bl_ext_repo.json"
41 PKG_MANIFEST_FILENAME
= "bl_ext_pkg_manifest.json"
43 PKG_MANIFEST_FILENAME_TOML
= "blender_manifest.toml"
45 # Use an in-memory temp, when available.
46 TEMP_PREFIX
= tempfile
.gettempdir()
47 if os
.path
.exists("/ramcache/tmp"):
48 TEMP_PREFIX
= "/ramcache/tmp"
50 TEMP_DIR_REMOTE
= os
.path
.join(TEMP_PREFIX
, "bl_ext_remote")
51 TEMP_DIR_LOCAL
= os
.path
.join(TEMP_PREFIX
, "bl_ext_local")
53 if TEMP_DIR_LOCAL
and not os
.path
.isdir(TEMP_DIR_LOCAL
):
54 os
.makedirs(TEMP_DIR_LOCAL
)
55 if TEMP_DIR_REMOTE
and not os
.path
.isdir(TEMP_DIR_REMOTE
):
56 os
.makedirs(TEMP_DIR_REMOTE
)
59 BASE_DIR
= os
.path
.abspath(os
.path
.dirname(__file__
))
60 # PYTHON_CMD = sys.executable
64 os
.path
.normpath(os
.path
.join(BASE_DIR
, "..", "cli", "blender_ext.py")),
67 # Simulate communicating with a web-server.
68 USE_HTTP
= os
.environ
.get("USE_HTTP", "0") != "0"
71 VERBOSE
= os
.environ
.get("VERBOSE", "0") != "0"
73 sys
.path
.append(os
.path
.join(BASE_DIR
, "modules"))
74 from http_server_context
import HTTPServerContext
# noqa: E402
76 STATUS_NON_ERROR
= {'STATUS', 'PROGRESS'}
79 # -----------------------------------------------------------------------------
84 def rmdir_contents(directory
: str) -> None:
86 Remove all directory contents without removing the directory.
88 for entry
in os
.scandir(directory
):
89 filepath
= os
.path
.join(directory
, entry
.name
)
91 shutil
.rmtree(filepath
)
96 # -----------------------------------------------------------------------------
97 # HTTP Server (simulate remote access)
100 # -----------------------------------------------------------------------------
101 # Generate Repository
105 def my_create_package(dirpath
: str, filename
: str, *, metadata
: Dict
[str, Any
], files
: Dict
[str, bytes
]) -> None:
107 Create a package using the command line interface.
109 assert filename
.endswith(PKG_EXT
)
110 outfile
= os
.path
.join(dirpath
, filename
)
112 # NOTE: use the command line packaging utility to ensure 1:1 behavior with actual packages.
113 metadata_copy
= metadata
.copy()
115 with tempfile
.TemporaryDirectory() as temp_dir_pkg
:
116 temp_dir_pkg_manifest_toml
= os
.path
.join(temp_dir_pkg
, PKG_MANIFEST_FILENAME_TOML
)
117 with
open(temp_dir_pkg_manifest_toml
, "wb") as fh
:
118 # NOTE: escaping is not supported, this is primitive TOML writing for tests.
121 """schema_version = "{:s}"\n""".format(metadata_copy
.pop("schema_version")),
122 """id = "{:s}"\n""".format(metadata_copy
.pop("id")),
123 """name = "{:s}"\n""".format(metadata_copy
.pop("name")),
124 """tagline = "{:s}"\n""".format(metadata_copy
.pop("tagline")),
125 """version = "{:s}"\n""".format(metadata_copy
.pop("version")),
126 """type = "{:s}"\n""".format(metadata_copy
.pop("type")),
127 """tags = [{:s}]\n""".format(", ".join("\"{:s}\"".format(v
) for v
in metadata_copy
.pop("tags"))),
128 """blender_version_min = "{:s}"\n""".format(metadata_copy
.pop("blender_version_min")),
129 """maintainer = "{:s}"\n""".format(metadata_copy
.pop("maintainer")),
130 """license = [{:s}]\n""".format(", ".join("\"{:s}\"".format(v
) for v
in metadata_copy
.pop("license"))),
135 raise Exception("Unexpected mata-data: {!r}".format(metadata_copy
))
137 for filename_iter
, data
in files
.items():
138 with
open(os
.path
.join(temp_dir_pkg
, filename_iter
), "wb") as fh
:
141 output_json
= command_output_from_json_0(
144 "--source-dir", temp_dir_pkg
,
145 "--output-filepath", outfile
,
147 exclude_types
={"PROGRESS"},
150 output_json_error
= command_output_filter_exclude(
152 exclude_types
=STATUS_NON_ERROR
,
155 if output_json_error
:
156 raise Exception("Creating a package produced some error output: {!r}".format(output_json_error
))
159 class PkgTemplate(NamedTuple
):
160 """Data need to create a package for testing."""
166 def my_generate_repo(
169 templates
: Sequence
[PkgTemplate
],
171 for template
in templates
:
173 dirpath
, template
.idname
+ PKG_EXT
,
175 "schema_version": "1.0.0",
176 "id": template
.idname
,
177 "name": template
.name
,
178 "tagline": """This package has a tagline""",
179 "version": template
.version
,
181 "tags": ["UV", "Modeling"],
182 "blender_version_min": "0.0.0",
183 "maintainer": "Some Developer",
184 "license": ["SPDX:GPL-2.0-or-later"],
187 "__init__.py": b
"# This is a script\n",
192 def command_output_filter_include(
193 output_json
: Sequence
[Tuple
[str, Any
]],
194 include_types
: Set
[str],
195 ) -> Sequence
[Tuple
[str, Any
]]:
196 return [(a
, b
) for a
, b
in output_json
if a
in include_types
]
199 def command_output_filter_exclude(
200 output_json
: Sequence
[Tuple
[str, Any
]],
201 exclude_types
: Set
[str],
202 ) -> Sequence
[Tuple
[str, Any
]]:
203 return [(a
, b
) for a
, b
in output_json
if a
not in exclude_types
]
208 expected_returncode
: int = 0,
210 proc
= subprocess
.run(
212 stdout
=subprocess
.PIPE
,
213 check
=expected_returncode
== 0,
215 if proc
.returncode
!= expected_returncode
:
216 raise subprocess
.CalledProcessError(proc
.returncode
, proc
.args
, output
=proc
.stdout
, stderr
=proc
.stderr
)
217 result
= proc
.stdout
.decode("utf-8")
219 result
= result
.replace("\r\n", "\n")
223 def command_output_from_json_0(
226 exclude_types
: Optional
[Set
[str]] = None,
227 expected_returncode
: int = 0,
228 ) -> Sequence
[Tuple
[str, Any
]]:
231 proc
= subprocess
.run(
232 [*CMD
, *args
, "--output-type=JSON_0"],
233 stdout
=subprocess
.PIPE
,
234 check
=expected_returncode
== 0,
236 if proc
.returncode
!= expected_returncode
:
237 raise subprocess
.CalledProcessError(proc
.returncode
, proc
.args
, output
=proc
.stdout
, stderr
=proc
.stderr
)
238 for json_bytes
in proc
.stdout
.split(b
'\0'):
241 json_str
= json_bytes
.decode("utf-8")
242 json_data
= json
.loads(json_str
)
243 assert len(json_data
) == 2
244 assert isinstance(json_data
[0], str)
245 if (exclude_types
is not None) and (json_data
[0] in exclude_types
):
247 result
.append((json_data
[0], json_data
[1]))
252 class TestCLI(unittest
.TestCase
):
254 def test_version(self
) -> None:
255 self
.assertEqual(command_output(["--version"]), "0.1\n")
258 class TestCLI_WithRepo(unittest
.TestCase
):
263 def setUpClass(cls
) -> None:
265 cls
.dirpath
= TEMP_DIR_REMOTE
266 if os
.path
.isdir(cls
.dirpath
):
267 # pylint: disable-next=using-constant-test
269 shutil
.rmtree(cls
.dirpath
)
270 os
.makedirs(TEMP_DIR_REMOTE
)
272 # Empty the path without removing it,
273 # handy so a developer can remain in the directory.
274 rmdir_contents(TEMP_DIR_REMOTE
)
276 os
.makedirs(TEMP_DIR_REMOTE
)
278 cls
.dirpath
= tempfile
.mkdtemp(prefix
="bl_ext_")
283 PkgTemplate(idname
="foo_bar", name
="Foo Bar", version
="1.0.5"),
284 PkgTemplate(idname
="another_package", name
="Another Package", version
="1.5.2"),
285 PkgTemplate(idname
="test_package", name
="Test Package", version
="1.5.2"),
290 if REMOTE_REPO_HAS_JSON_IMPLIED
:
291 cls
.dirpath_url
= "http://localhost:{:d}/bl_ext_repo.json".format(HTTP_PORT
)
293 cls
.dirpath_url
= "http://localhost:{:d}".format(HTTP_PORT
)
295 cls
.dirpath_url
= cls
.dirpath
298 def tearDownClass(cls
) -> None:
299 if not TEMP_DIR_REMOTE
:
300 shutil
.rmtree(cls
.dirpath
)
304 def test_version(self
) -> None:
305 self
.assertEqual(command_output(["--version"]), "0.1\n")
307 def test_server_generate(self
) -> None:
308 output
= command_output(["server-generate", "--repo-dir", self
.dirpath
])
309 self
.assertEqual(output
, "found 3 packages.\n")
311 def test_client_list(self
) -> None:
312 # TODO: only run once.
313 self
.test_server_generate()
315 output
= command_output(["list", "--remote-url", self
.dirpath_url
, "--local-dir", ""])
318 "another_package(1.5.2): Another Package\n"
319 "foo_bar(1.0.5): Foo Bar\n"
320 "test_package(1.5.2): Test Package\n"
325 # TODO, figure out how to split JSON & TEXT output tests, this test just checks JSON is working at all.
326 output_json
= command_output_from_json_0(
327 ["list", "--remote-url", self
.dirpath_url
, "--local-dir", ""],
328 exclude_types
={"PROGRESS"},
332 ("STATUS", "another_package(1.5.2): Another Package"),
333 ("STATUS", "foo_bar(1.0.5): Foo Bar"),
334 ("STATUS", "test_package(1.5.2): Test Package"),
338 def test_client_install_and_uninstall(self
) -> None:
339 with tempfile
.TemporaryDirectory(dir=TEMP_DIR_LOCAL
) as temp_dir_local
:
340 # TODO: only run once.
341 self
.test_server_generate()
343 output_json
= command_output_from_json_0([
345 "--remote-url", self
.dirpath_url
,
346 "--local-dir", temp_dir_local
,
347 ], exclude_types
={"PROGRESS"})
350 ('STATUS', 'Sync repo: ' + self
.dirpath_url
),
351 ('STATUS', 'Sync downloading remote data'),
352 ('STATUS', 'Sync complete: ' + self
.dirpath_url
),
357 output_json
= command_output_from_json_0(
359 "install", "another_package",
360 "--remote-url", self
.dirpath_url
,
361 "--local-dir", temp_dir_local
,
363 exclude_types
={"PROGRESS"},
367 ("STATUS", "Installed \"another_package\"")
370 self
.assertTrue(os
.path
.isdir(os
.path
.join(temp_dir_local
, "another_package")))
373 output_json
= command_output_from_json_0(
375 "install", "another_package",
376 "--remote-url", self
.dirpath_url
,
377 "--local-dir", temp_dir_local
,
379 exclude_types
={"PROGRESS"},
383 ("STATUS", "Re-Installed \"another_package\"")
386 self
.assertTrue(os
.path
.isdir(os
.path
.join(temp_dir_local
, "another_package")))
388 # Uninstall (not found).
389 output_json
= command_output_from_json_0(
391 "uninstall", "another_package_",
392 "--local-dir", temp_dir_local
,
394 expected_returncode
=1,
398 ("ERROR", "Package not found \"another_package_\"")
403 output_json
= command_output_from_json_0([
404 "uninstall", "another_package",
405 "--local-dir", temp_dir_local
,
409 ("STATUS", "Removed \"another_package\"")
412 self
.assertFalse(os
.path
.isdir(os
.path
.join(temp_dir_local
, "another_package")))
415 if __name__
== "__main__":
417 with
HTTPServerContext(directory
=TEMP_DIR_REMOTE
, port
=HTTP_PORT
):