test_jupyterlab.py 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809
  1. """Test installation of JupyterLab extensions"""
  2. # Copyright (c) Jupyter Development Team.
  3. # Distributed under the terms of the Modified BSD License.
  4. import glob
  5. import json
  6. import logging
  7. import os
  8. import platform
  9. import shutil
  10. import subprocess
  11. import sys
  12. from os.path import join as pjoin
  13. from pathlib import Path
  14. from tempfile import TemporaryDirectory
  15. from unittest import TestCase
  16. from unittest.mock import patch
  17. import pytest
  18. from jupyter_core import paths
  19. from jupyterlab import commands
  20. from jupyterlab.commands import (
  21. DEV_DIR,
  22. AppOptions,
  23. _compare_ranges,
  24. _test_overlap,
  25. build,
  26. build_check,
  27. check_extension,
  28. disable_extension,
  29. enable_extension,
  30. get_app_info,
  31. get_app_version,
  32. install_extension,
  33. link_package,
  34. list_extensions,
  35. lock_extension,
  36. uninstall_extension,
  37. unlink_package,
  38. unlock_extension,
  39. update_extension,
  40. )
  41. from jupyterlab.coreconfig import CoreConfig, _get_default_core_data
  42. here = os.path.dirname(os.path.abspath(__file__))
  43. def touch(file, mtime=None):
  44. """ensure a file exists, and set its modification time
  45. returns the modification time of the file
  46. """
  47. dirname = os.path.dirname(file)
  48. if not os.path.exists(dirname):
  49. os.makedirs(dirname)
  50. open(file, "a").close()
  51. # set explicit mtime
  52. if mtime:
  53. atime = os.stat(file).st_atime
  54. os.utime(file, (atime, mtime))
  55. return os.stat(file).st_mtime
  56. # @pytest.fixture()
  57. # def resource():
  58. # print("setup")
  59. # yield "resource"
  60. # print("teardown")
  61. class AppHandlerTest(TestCase):
  62. def tempdir(self):
  63. td = TemporaryDirectory()
  64. self.tempdirs.append(td)
  65. return td.name
  66. def setUp(self):
  67. # Any TemporaryDirectory objects appended to this list will be cleaned
  68. # up at the end of the test run.
  69. self.tempdirs = []
  70. self.devnull = open(os.devnull, "w") # noqa
  71. @self.addCleanup
  72. def cleanup_tempdirs():
  73. for d in self.tempdirs:
  74. d.cleanup()
  75. self.test_dir = self.tempdir()
  76. self.data_dir = pjoin(self.test_dir, "data")
  77. self.config_dir = pjoin(self.test_dir, "config")
  78. self.pkg_names = {}
  79. # Copy in the mock packages.
  80. for name in ["extension", "incompat", "package", "mimeextension"]:
  81. src = pjoin(here, "mock_packages", name)
  82. def ignore(dname, files):
  83. if "node_modules" in dname:
  84. files = []
  85. if "node_modules" in files:
  86. files.remove("node_modules")
  87. return dname, files
  88. dest = pjoin(self.test_dir, name)
  89. shutil.copytree(src, dest, ignore=ignore)
  90. # Make a node modules folder so npm install is not called.
  91. if not os.path.exists(pjoin(dest, "node_modules")):
  92. os.makedirs(pjoin(dest, "node_modules"))
  93. setattr(self, "mock_" + name, dest)
  94. with open(pjoin(dest, "package.json")) as fid:
  95. data = json.load(fid)
  96. self.pkg_names[name] = data["name"]
  97. self.patches = []
  98. p = patch.dict(
  99. "os.environ",
  100. {
  101. "JUPYTER_CONFIG_DIR": self.config_dir,
  102. "JUPYTER_DATA_DIR": self.data_dir,
  103. "JUPYTERLAB_DIR": pjoin(self.data_dir, "lab"),
  104. },
  105. )
  106. self.patches.append(p)
  107. for mod in [paths]:
  108. if hasattr(mod, "ENV_JUPYTER_PATH"):
  109. p = patch.object(mod, "ENV_JUPYTER_PATH", [self.data_dir])
  110. self.patches.append(p)
  111. if hasattr(mod, "ENV_CONFIG_PATH"):
  112. p = patch.object(mod, "ENV_CONFIG_PATH", [self.config_dir])
  113. self.patches.append(p)
  114. if hasattr(mod, "CONFIG_PATH"):
  115. p = patch.object(mod, "CONFIG_PATH", self.config_dir)
  116. self.patches.append(p)
  117. if hasattr(mod, "BUILD_PATH"):
  118. p = patch.object(mod, "BUILD_PATH", self.data_dir)
  119. self.patches.append(p)
  120. for p in self.patches:
  121. p.start()
  122. self.addCleanup(p.stop)
  123. # verify our patches
  124. self.assertEqual(paths.ENV_CONFIG_PATH, [self.config_dir])
  125. self.assertEqual(paths.ENV_JUPYTER_PATH, [self.data_dir])
  126. self.assertEqual(
  127. Path(commands.get_app_dir()).resolve(), (Path(self.data_dir) / "lab").resolve()
  128. )
  129. self.app_dir = commands.get_app_dir()
  130. # Set pinned extension names
  131. self.pinned_packages = ["jupyterlab-test-extension@1.0", "jupyterlab-test-extension@2.0"]
  132. class TestExtension(AppHandlerTest):
  133. def test_install_extension(self):
  134. assert install_extension(self.mock_extension) is True
  135. path = pjoin(self.app_dir, "extensions", "*.tgz")
  136. assert glob.glob(path)
  137. extensions = get_app_info()["extensions"]
  138. name = self.pkg_names["extension"]
  139. assert name in extensions
  140. assert check_extension(name)
  141. def test_install_twice(self):
  142. assert install_extension(self.mock_extension) is True
  143. path = pjoin(self.app_dir, "extensions", "*.tgz")
  144. assert install_extension(self.mock_extension) is True
  145. assert glob.glob(path)
  146. extensions = get_app_info()["extensions"]
  147. name = self.pkg_names["extension"]
  148. assert name in extensions
  149. assert check_extension(name)
  150. def test_install_mime_renderer(self):
  151. install_extension(self.mock_mimeextension)
  152. name = self.pkg_names["mimeextension"]
  153. assert name in get_app_info()["extensions"]
  154. assert check_extension(name)
  155. assert uninstall_extension(name) is True
  156. assert name not in get_app_info()["extensions"]
  157. assert not check_extension(name)
  158. def test_install_incompatible(self):
  159. with pytest.raises(ValueError) as excinfo:
  160. install_extension(self.mock_incompat)
  161. assert "Conflicting Dependencies" in str(excinfo.value)
  162. assert not check_extension(self.pkg_names["incompat"])
  163. def test_install_failed(self):
  164. path = self.mock_package
  165. with pytest.raises(ValueError):
  166. install_extension(path)
  167. with open(pjoin(path, "package.json")) as fid:
  168. data = json.load(fid)
  169. extensions = get_app_info()["extensions"]
  170. name = data["name"]
  171. assert name not in extensions
  172. assert not check_extension(name)
  173. def test_validation(self):
  174. path = self.mock_extension
  175. os.remove(pjoin(path, "index.js"))
  176. with pytest.raises(ValueError):
  177. install_extension(path)
  178. assert not check_extension(self.pkg_names["extension"])
  179. path = self.mock_mimeextension
  180. os.remove(pjoin(path, "index.js"))
  181. with pytest.raises(ValueError):
  182. install_extension(path)
  183. assert not check_extension(self.pkg_names["mimeextension"])
  184. def test_uninstall_extension(self):
  185. assert install_extension(self.mock_extension) is True
  186. name = self.pkg_names["extension"]
  187. assert check_extension(name)
  188. assert uninstall_extension(self.pkg_names["extension"]) is True
  189. path = pjoin(self.app_dir, "extensions", "*.tgz")
  190. assert not glob.glob(path)
  191. extensions = get_app_info()["extensions"]
  192. assert name not in extensions
  193. assert not check_extension(name)
  194. def test_uninstall_all_extensions(self):
  195. install_extension(self.mock_extension)
  196. install_extension(self.mock_mimeextension)
  197. ext_name = self.pkg_names["extension"]
  198. mime_ext_name = self.pkg_names["mimeextension"]
  199. assert check_extension(ext_name) is True
  200. assert check_extension(mime_ext_name) is True
  201. assert uninstall_extension(all_=True) is True
  202. extensions = get_app_info()["extensions"]
  203. assert ext_name not in extensions
  204. assert mime_ext_name not in extensions
  205. @pytest.mark.slow
  206. def test_uninstall_core_extension(self):
  207. assert uninstall_extension("@jupyterlab/console-extension") is True
  208. app_dir = self.app_dir
  209. build()
  210. with open(pjoin(app_dir, "staging", "package.json")) as fid:
  211. data = json.load(fid)
  212. extensions = data["jupyterlab"]["extensions"]
  213. assert "@jupyterlab/console-extension" not in extensions
  214. assert not check_extension("@jupyterlab/console-extension")
  215. assert install_extension("@jupyterlab/console-extension") is True
  216. build()
  217. with open(pjoin(app_dir, "staging", "package.json")) as fid:
  218. data = json.load(fid)
  219. extensions = data["jupyterlab"]["extensions"]
  220. assert "@jupyterlab/console-extension" in extensions
  221. assert check_extension("@jupyterlab/console-extension")
  222. def test_install_and_uninstall_pinned(self):
  223. """
  224. You should be able to install different versions of the same extension with different
  225. pinned names and uninstall them with those names.
  226. """
  227. NAMES = ["test-1", "test-2"] # noqa
  228. assert install_extension(self.pinned_packages[0], pin=NAMES[0])
  229. assert install_extension(self.pinned_packages[1], pin=NAMES[1])
  230. extensions = get_app_info()["extensions"]
  231. assert NAMES[0] in extensions
  232. assert NAMES[1] in extensions
  233. assert check_extension(NAMES[0])
  234. assert check_extension(NAMES[1])
  235. # Uninstall
  236. assert uninstall_extension(NAMES[0])
  237. assert uninstall_extension(NAMES[1])
  238. extensions = get_app_info()["extensions"]
  239. assert NAMES[0] not in extensions
  240. assert NAMES[1] not in extensions
  241. assert not check_extension(NAMES[0])
  242. assert not check_extension(NAMES[1])
  243. @pytest.mark.skipif(
  244. platform.system() == "Windows", reason="running npm pack fails on windows CI"
  245. )
  246. def test_install_and_uninstall_pinned_folder(self):
  247. """
  248. Same as above test, but installs from a local folder instead of from npm.
  249. """
  250. # Download each version of the package from NPM:
  251. base_dir = Path(self.tempdir())
  252. # The archive file names are printed to stdout when run `npm pack`
  253. packages = [
  254. subprocess.run( # noqa S603
  255. ["npm", "pack", name], # noqa S607
  256. stdout=subprocess.PIPE,
  257. text=True,
  258. check=True,
  259. cwd=str(base_dir),
  260. ).stdout.strip()
  261. for name in self.pinned_packages
  262. ]
  263. shutil.unpack_archive(str(base_dir / packages[0]), str(base_dir / "1"))
  264. shutil.unpack_archive(str(base_dir / packages[1]), str(base_dir / "2"))
  265. # Change pinned packages to be these directories now, so we install from these folders
  266. self.pinned_packages = [str(base_dir / "1" / "package"), str(base_dir / "2" / "package")]
  267. self.test_install_and_uninstall_pinned()
  268. def test_link_extension(self):
  269. path = self.mock_extension
  270. name = self.pkg_names["extension"]
  271. link_package(path)
  272. linked = get_app_info()["linked_packages"]
  273. assert name not in linked
  274. assert name in get_app_info()["extensions"]
  275. assert check_extension(name)
  276. assert unlink_package(path) is True
  277. linked = get_app_info()["linked_packages"]
  278. assert name not in linked
  279. assert name not in get_app_info()["extensions"]
  280. assert not check_extension(name)
  281. def test_link_package(self):
  282. path = self.mock_package
  283. name = self.pkg_names["package"]
  284. assert link_package(path) is True
  285. linked = get_app_info()["linked_packages"]
  286. assert name in linked
  287. assert name not in get_app_info()["extensions"]
  288. assert check_extension(name)
  289. assert unlink_package(path)
  290. linked = get_app_info()["linked_packages"]
  291. assert name not in linked
  292. assert not check_extension(name)
  293. def test_unlink_package(self):
  294. target = self.mock_package
  295. assert link_package(target) is True
  296. assert unlink_package(target) is True
  297. linked = get_app_info()["linked_packages"]
  298. name = self.pkg_names["package"]
  299. assert name not in linked
  300. assert not check_extension(name)
  301. def test_list_extensions(self):
  302. assert install_extension(self.mock_extension) is True
  303. list_extensions()
  304. def test_app_dir(self):
  305. app_dir = self.tempdir()
  306. options = AppOptions(app_dir=app_dir)
  307. assert install_extension(self.mock_extension, app_options=options) is True
  308. path = pjoin(app_dir, "extensions", "*.tgz")
  309. assert glob.glob(path)
  310. extensions = get_app_info(app_options=options)["extensions"]
  311. ext_name = self.pkg_names["extension"]
  312. assert ext_name in extensions
  313. assert check_extension(ext_name, app_options=options)
  314. assert uninstall_extension(self.pkg_names["extension"], app_options=options) is True
  315. path = pjoin(app_dir, "extensions", "*.tgz")
  316. assert not glob.glob(path)
  317. extensions = get_app_info(app_options=options)["extensions"]
  318. assert ext_name not in extensions
  319. assert not check_extension(ext_name, app_options=options)
  320. assert link_package(self.mock_package, app_options=options) is True
  321. linked = get_app_info(app_options=options)["linked_packages"]
  322. pkg_name = self.pkg_names["package"]
  323. assert pkg_name in linked
  324. assert check_extension(pkg_name, app_options=options)
  325. assert unlink_package(self.mock_package, app_options=options) is True
  326. linked = get_app_info(app_options=options)["linked_packages"]
  327. assert pkg_name not in linked
  328. assert not check_extension(pkg_name, app_options=options)
  329. def test_app_dir_use_sys_prefix(self):
  330. app_dir = self.tempdir()
  331. options = AppOptions(app_dir=app_dir)
  332. if os.path.exists(self.app_dir):
  333. os.removedirs(self.app_dir)
  334. assert install_extension(self.mock_extension) is True
  335. path = pjoin(app_dir, "extensions", "*.tgz")
  336. assert not glob.glob(path)
  337. extensions = get_app_info(app_options=options)["extensions"]
  338. ext_name = self.pkg_names["extension"]
  339. assert ext_name in extensions
  340. assert check_extension(ext_name, app_options=options)
  341. def test_app_dir_disable_sys_prefix(self):
  342. app_dir = self.tempdir()
  343. options = AppOptions(app_dir=app_dir, use_sys_dir=False)
  344. if os.path.exists(self.app_dir):
  345. os.removedirs(self.app_dir)
  346. assert install_extension(self.mock_extension) is True
  347. path = pjoin(app_dir, "extensions", "*.tgz")
  348. assert not glob.glob(path)
  349. extensions = get_app_info(app_options=options)["extensions"]
  350. ext_name = self.pkg_names["extension"]
  351. assert ext_name not in extensions
  352. assert not check_extension(ext_name, app_options=options)
  353. def test_app_dir_shadowing(self):
  354. app_dir = self.tempdir()
  355. sys_dir = self.app_dir
  356. app_options = AppOptions(app_dir=app_dir)
  357. if os.path.exists(sys_dir):
  358. os.removedirs(sys_dir)
  359. assert install_extension(self.mock_extension) is True
  360. sys_path = pjoin(sys_dir, "extensions", "*.tgz")
  361. assert glob.glob(sys_path)
  362. app_path = pjoin(app_dir, "extensions", "*.tgz")
  363. assert not glob.glob(app_path)
  364. extensions = get_app_info(app_options=app_options)["extensions"]
  365. ext_name = self.pkg_names["extension"]
  366. assert ext_name in extensions
  367. assert check_extension(ext_name, app_options=app_options)
  368. assert install_extension(self.mock_extension, app_options=app_options) is True
  369. assert glob.glob(app_path)
  370. extensions = get_app_info(app_options=app_options)["extensions"]
  371. assert ext_name in extensions
  372. assert check_extension(ext_name, app_options=app_options)
  373. assert uninstall_extension(self.pkg_names["extension"], app_options=app_options) is True
  374. assert not glob.glob(app_path)
  375. assert glob.glob(sys_path)
  376. extensions = get_app_info(app_options=app_options)["extensions"]
  377. assert ext_name in extensions
  378. assert check_extension(ext_name, app_options=app_options)
  379. assert uninstall_extension(self.pkg_names["extension"], app_options=app_options) is True
  380. assert not glob.glob(app_path)
  381. assert not glob.glob(sys_path)
  382. extensions = get_app_info(app_options=app_options)["extensions"]
  383. assert ext_name not in extensions
  384. assert not check_extension(ext_name, app_options=app_options)
  385. @pytest.mark.slow
  386. def test_build(self):
  387. assert install_extension(self.mock_extension) is True
  388. build()
  389. # check staging directory.
  390. entry = pjoin(self.app_dir, "staging", "build", "index.out.js")
  391. with open(entry) as fid:
  392. data = fid.read()
  393. assert self.pkg_names["extension"] in data
  394. # check static directory.
  395. entry = pjoin(self.app_dir, "static", "index.out.js")
  396. with open(entry) as fid:
  397. data = fid.read()
  398. assert self.pkg_names["extension"] in data
  399. @pytest.mark.slow
  400. @pytest.mark.skipif(not os.path.exists(DEV_DIR), reason="Not in git checkout")
  401. def test_build_splice_packages(self):
  402. app_options = AppOptions(splice_source=True)
  403. assert install_extension(self.mock_extension) is True
  404. build(app_options=app_options)
  405. assert "-spliced" in get_app_version(app_options)
  406. # check staging directory.
  407. entry = pjoin(self.app_dir, "staging", "build", "index.out.js")
  408. with open(entry) as fid:
  409. data = fid.read()
  410. assert self.pkg_names["extension"] in data
  411. # check static directory.
  412. entry = pjoin(self.app_dir, "static", "index.out.js")
  413. with open(entry) as fid:
  414. data = fid.read()
  415. assert self.pkg_names["extension"] in data
  416. @pytest.mark.slow
  417. def test_build_custom(self):
  418. assert install_extension(self.mock_extension) is True
  419. build(name="foo", version="1.0", static_url="bar")
  420. # check static directory.
  421. entry = pjoin(self.app_dir, "static", "index.out.js")
  422. with open(entry) as fid:
  423. data = fid.read()
  424. assert self.pkg_names["extension"] in data
  425. pkg = pjoin(self.app_dir, "static", "package.json")
  426. with open(pkg) as fid:
  427. data = json.load(fid)
  428. assert data["jupyterlab"]["name"] == "foo"
  429. assert data["jupyterlab"]["version"] == "1.0"
  430. assert data["jupyterlab"]["staticUrl"] == "bar"
  431. @pytest.mark.slow
  432. def test_build_custom_minimal_core_config(self):
  433. default_config = CoreConfig()
  434. core_config = CoreConfig()
  435. core_config.clear_packages()
  436. logger = logging.getLogger("jupyterlab_test_logger")
  437. logger.setLevel("DEBUG")
  438. app_dir = self.tempdir()
  439. options = AppOptions(
  440. app_dir=app_dir,
  441. core_config=core_config,
  442. logger=logger,
  443. use_sys_dir=False,
  444. )
  445. extensions = (
  446. "@jupyterlab/application-extension",
  447. "@jupyterlab/apputils-extension",
  448. )
  449. singletons = (
  450. "@jupyterlab/application",
  451. "@jupyterlab/apputils",
  452. "@jupyterlab/coreutils",
  453. "@jupyterlab/services",
  454. )
  455. for name in extensions:
  456. semver = default_config.extensions[name]
  457. core_config.add(name, semver, extension=True)
  458. for name in singletons:
  459. semver = default_config.singletons[name]
  460. core_config.add(name, semver)
  461. assert install_extension(self.mock_extension, app_options=options) is True
  462. build(app_options=options)
  463. # check static directory.
  464. entry = pjoin(app_dir, "static", "index.out.js")
  465. with open(entry) as fid:
  466. data = fid.read()
  467. assert self.pkg_names["extension"] in data
  468. pkg = pjoin(app_dir, "static", "package.json")
  469. with open(pkg) as fid:
  470. data = json.load(fid)
  471. assert sorted(data["jupyterlab"]["extensions"].keys()) == [
  472. "@jupyterlab/application-extension",
  473. "@jupyterlab/apputils-extension",
  474. "@jupyterlab/mock-extension",
  475. ]
  476. assert data["jupyterlab"]["mimeExtensions"] == {}
  477. for pkg in data["jupyterlab"]["singletonPackages"]:
  478. if pkg.startswith("@jupyterlab/"):
  479. assert pkg in singletons
  480. def test_disable_extension(self):
  481. options = AppOptions(app_dir=self.tempdir())
  482. assert install_extension(self.mock_extension, app_options=options) is True
  483. assert disable_extension(self.pkg_names["extension"], app_options=options) is True
  484. info = get_app_info(app_options=options)
  485. name = self.pkg_names["extension"]
  486. assert info["disabled"].get(name) is True
  487. assert not check_extension(name, app_options=options)
  488. assert check_extension(name, installed=True, app_options=options)
  489. assert disable_extension("@jupyterlab/notebook-extension", app_options=options) is True
  490. info = get_app_info(app_options=options)
  491. assert info["disabled"].get("@jupyterlab/notebook-extension") is True
  492. assert not check_extension("@jupyterlab/notebook-extension", app_options=options)
  493. assert check_extension(
  494. "@jupyterlab/notebook-extension", installed=True, app_options=options
  495. )
  496. assert info["disabled"].get(name) is True
  497. assert not check_extension(name, app_options=options)
  498. assert check_extension(name, installed=True, app_options=options)
  499. def test_enable_extension(self):
  500. options = AppOptions(app_dir=self.tempdir())
  501. assert install_extension(self.mock_extension, app_options=options) is True
  502. assert disable_extension(self.pkg_names["extension"], app_options=options) is True
  503. assert enable_extension(self.pkg_names["extension"], app_options=options) is True
  504. info = get_app_info(app_options=options)
  505. assert "@jupyterlab/notebook-extension" not in info["disabled"]
  506. name = self.pkg_names["extension"]
  507. assert info["disabled"].get(name, False) is False
  508. assert check_extension(name, app_options=options)
  509. assert disable_extension("@jupyterlab/notebook-extension", app_options=options) is True
  510. assert check_extension(name, app_options=options)
  511. assert not check_extension("@jupyterlab/notebook-extension", app_options=options)
  512. def test_lock_unlock_extension(self):
  513. options = AppOptions(app_dir=self.tempdir())
  514. assert install_extension(self.mock_extension, app_options=options) is True
  515. name = self.pkg_names["extension"]
  516. info = get_app_info(app_options=options)
  517. assert info["locked"].get(name, False) is False
  518. lock_extension(self.pkg_names["extension"], app_options=options)
  519. info = get_app_info(app_options=options)
  520. assert info["locked"].get(name, False) is True
  521. unlock_extension(self.pkg_names["extension"], app_options=options)
  522. info = get_app_info(app_options=options)
  523. assert info["locked"].get(name, False) is False
  524. @pytest.mark.slow
  525. def test_build_check(self):
  526. # Do the initial build.
  527. assert build_check()
  528. assert install_extension(self.mock_extension) is True
  529. assert link_package(self.mock_package) is True
  530. build()
  531. assert not build_check()
  532. # Check installed extensions.
  533. assert install_extension(self.mock_mimeextension) is True
  534. assert build_check()
  535. assert uninstall_extension(self.pkg_names["mimeextension"]) is True
  536. assert not build_check()
  537. # Check local extensions.
  538. pkg_path = pjoin(self.mock_extension, "package.json")
  539. with open(pkg_path) as fid:
  540. data = json.load(fid)
  541. with open(pkg_path, "rb") as fid:
  542. orig = fid.read()
  543. data["foo"] = "bar"
  544. with open(pkg_path, "w") as fid:
  545. json.dump(data, fid)
  546. assert build_check()
  547. assert build_check()
  548. with open(pkg_path, "wb") as fid:
  549. fid.write(orig)
  550. assert not build_check()
  551. # Check linked packages.
  552. pkg_path = pjoin(self.mock_package, "index.js")
  553. with open(pkg_path, "rb") as fid:
  554. orig = fid.read()
  555. with open(pkg_path, "wb") as fid:
  556. fid.write(orig + b'\nconsole.log("hello");')
  557. assert build_check()
  558. assert build_check()
  559. with open(pkg_path, "wb") as fid:
  560. fid.write(orig)
  561. assert not build_check()
  562. def test_compatibility(self):
  563. assert _test_overlap("^0.6.0", "^0.6.1")
  564. assert _test_overlap(">0.1", "0.6")
  565. assert _test_overlap("~0.5.0", "~0.5.2")
  566. assert _test_overlap("0.5.2", "^0.5.0")
  567. assert not _test_overlap("^0.5.0", "^0.6.0")
  568. assert not _test_overlap("~1.5.0", "^1.6.0")
  569. assert _test_overlap("*", "0.6") is None
  570. assert _test_overlap("<0.6", "0.1") is None
  571. assert _test_overlap("^1 || ^2", "^1")
  572. assert _test_overlap("^1 || ^2", "^2")
  573. assert _test_overlap("^1", "^1 || ^2")
  574. assert _test_overlap("^2", "^1 || ^2")
  575. assert _test_overlap("^1 || ^2", "^2 || ^3")
  576. assert not _test_overlap("^1 || ^2", "^3 || ^4")
  577. assert not _test_overlap("^2", "^1 || ^3")
  578. def test_compare_ranges(self):
  579. assert _compare_ranges("^1 || ^2", "^1") == 0
  580. assert _compare_ranges("^1 || ^2", "^2 || ^3") == 0
  581. assert _compare_ranges("^1 || ^2", "^3 || ^4") == 1
  582. assert _compare_ranges("^3 || ^4", "^1 || ^2") == -1
  583. assert _compare_ranges("^2 || ^3", "^1 || ^4") is None
  584. def test_install_compatible(self):
  585. core_data = _get_default_core_data()
  586. current_app_dep = core_data["dependencies"]["@jupyterlab/application"]
  587. def _gen_dep(ver):
  588. return {"dependencies": {"@jupyterlab/application": ver}}
  589. def _mock_metadata(registry, name, logger):
  590. assert name == "mockextension"
  591. return {
  592. "name": name,
  593. "versions": {
  594. "0.9.0": _gen_dep(current_app_dep),
  595. "1.0.0": _gen_dep(current_app_dep),
  596. "1.1.0": _gen_dep(current_app_dep),
  597. "2.0.0": _gen_dep("^2000.0.0"),
  598. "2.0.0-b0": _gen_dep(current_app_dep),
  599. "2.1.0-b0": _gen_dep("^2000.0.0"),
  600. "2.1.0": _gen_dep("^2000.0.0"),
  601. },
  602. }
  603. def _mock_extract(self, source, tempdir, *args, **kwargs):
  604. data = {
  605. "name": source,
  606. "version": "2.1.0",
  607. "jupyterlab": {"extension": True},
  608. "jupyterlab_extracted_files": ["index.js"],
  609. }
  610. data.update(_gen_dep("^2000.0.0"))
  611. info = {
  612. "source": source,
  613. "is_dir": False,
  614. "data": data,
  615. "name": source,
  616. "version": data["version"],
  617. "filename": "mockextension.tgz",
  618. "path": pjoin(tempdir, "mockextension.tgz"),
  619. }
  620. return info
  621. class Success(Exception): # noqa
  622. pass
  623. def _mock_install(self, name, *args, **kwargs):
  624. assert name in ("mockextension", "mockextension@1.1.0")
  625. if name == "mockextension@1.1.0":
  626. raise Success()
  627. return orig_install(self, name, *args, **kwargs)
  628. p1 = patch.object(commands, "_fetch_package_metadata", _mock_metadata)
  629. p2 = patch.object(commands._AppHandler, "_extract_package", _mock_extract)
  630. p3 = patch.object(commands._AppHandler, "_install_extension", _mock_install)
  631. with p1, p2:
  632. orig_install = commands._AppHandler._install_extension
  633. with p3, pytest.raises(Success):
  634. assert install_extension("mockextension") is True
  635. def test_update_single(self):
  636. installed = []
  637. def _mock_install(self, name, *args, **kwargs):
  638. installed.append(name[0] + name[1:].split("@")[0])
  639. return {"name": name, "is_dir": False, "path": "foo/bar/" + name}
  640. def _mock_latest(self, name):
  641. return "10000.0.0"
  642. p1 = patch.object(commands._AppHandler, "_install_extension", _mock_install)
  643. p2 = patch.object(commands._AppHandler, "_latest_compatible_package_version", _mock_latest)
  644. assert install_extension(self.mock_extension) is True
  645. assert install_extension(self.mock_mimeextension) is True
  646. with p1, p2:
  647. assert update_extension(self.pkg_names["extension"]) is True
  648. assert installed == [self.pkg_names["extension"]]
  649. def test_update_missing_extension(self):
  650. assert update_extension("foo") is False
  651. def test_update_multiple(self):
  652. installed = []
  653. def _mock_install(self, name, *args, **kwargs):
  654. installed.append(name[0] + name[1:].split("@")[0])
  655. return {"name": name, "is_dir": False, "path": "foo/bar/" + name}
  656. def _mock_latest(self, name):
  657. return "10000.0.0"
  658. p1 = patch.object(commands._AppHandler, "_install_extension", _mock_install)
  659. p2 = patch.object(commands._AppHandler, "_latest_compatible_package_version", _mock_latest)
  660. install_extension(self.mock_extension)
  661. install_extension(self.mock_mimeextension)
  662. with p1, p2:
  663. assert update_extension(self.pkg_names["extension"]) is True
  664. assert update_extension(self.pkg_names["mimeextension"]) is True
  665. assert installed == [self.pkg_names["extension"], self.pkg_names["mimeextension"]]
  666. def test_update_all(self):
  667. updated = []
  668. def _mock_update(self, name, *args, **kwargs):
  669. updated.append(name[0] + name[1:].split("@")[0])
  670. return True
  671. original_app_info = commands._AppHandler._get_app_info
  672. def _mock_app_info(self):
  673. info = original_app_info(self)
  674. info["local_extensions"] = []
  675. return info
  676. assert install_extension(self.mock_extension) is True
  677. assert install_extension(self.mock_mimeextension) is True
  678. p1 = patch.object(commands._AppHandler, "_update_extension", _mock_update)
  679. # local packages are not updated, so mock them as non-local:
  680. p2 = patch.object(commands._AppHandler, "_get_app_info", _mock_app_info)
  681. with p1, p2:
  682. assert update_extension(None, all_=True) is True
  683. assert sorted(updated) == [self.pkg_names["extension"], self.pkg_names["mimeextension"]]
  684. def test_load_extension(jp_serverapp, make_lab_app):
  685. app = make_lab_app()
  686. stderr = sys.stderr
  687. # sys.stderr = self.devnull
  688. app._link_jupyter_server_extension(jp_serverapp)
  689. app.initialize()
  690. sys.stderr = stderr