From 8cdb15d54e18b4d69edef4b285453d3ea5c066e7 Mon Sep 17 00:00:00 2001 From: Claudio Matsuoka Date: Tue, 23 Nov 2021 19:23:19 -0300 Subject: [PATCH 001/167] many: rename snapcraft package to snapcraft_legacy Move the current `snapcraft` package to `snapcraft_legacy` to make room for a new application based on craft libraries. The application entry point has not been changed, that will be handled in a different PR. Signed-off-by: Claudio Matsuoka --- bin/snapcraftctl | 4 +- setup.py | 4 +- snapcraft.spec | 2 +- {snapcraft => snapcraft_legacy}/__init__.py | 22 ++--- .../_legacy_loader.py | 10 ++- {snapcraft => snapcraft_legacy}/_store.py | 36 ++++---- .../cli/__init__.py | 4 +- .../cli/__main__.py | 6 +- .../cli/_channel_map.py | 2 +- .../cli/_command_group.py | 2 +- .../cli/_errors.py | 16 ++-- .../cli/_metrics.py | 2 +- .../cli/_options.py | 8 +- .../cli/_review.py | 2 +- .../cli/_runner.py | 8 +- .../cli/assertions.py | 20 ++--- .../cli/containers.py | 2 +- .../cli/discovery.py | 15 ++-- {snapcraft => snapcraft_legacy}/cli/echo.py | 4 +- .../cli/extensions.py | 4 +- {snapcraft => snapcraft_legacy}/cli/help.py | 10 +-- .../cli/lifecycle.py | 10 +-- {snapcraft => snapcraft_legacy}/cli/remote.py | 6 +- .../cli/snapcraftctl/__init__.py | 0 .../cli/snapcraftctl/_runner.py | 4 +- {snapcraft => snapcraft_legacy}/cli/store.py | 22 ++--- .../cli/version.py | 4 +- {snapcraft => snapcraft_legacy}/common.py | 16 ++-- {snapcraft => snapcraft_legacy}/config.py | 2 +- .../extractors/__init__.py | 0 .../extractors/_errors.py | 2 +- .../extractors/_metadata.py | 2 +- .../extractors/appstream.py | 2 +- .../extractors/setuppy.py | 2 +- {snapcraft => snapcraft_legacy}/file_utils.py | 4 +- .../formatting_utils.py | 0 .../internal/__init__.py | 6 +- .../internal/build_providers/__init__.py | 0 .../build_providers/_base_provider.py | 10 +-- .../internal/build_providers/_factory.py | 0 .../internal/build_providers/_lxd/__init__.py | 0 .../internal/build_providers/_lxd/_images.py | 0 .../internal/build_providers/_lxd/_lxd.py | 4 +- .../build_providers/_multipass/__init__.py | 0 .../_multipass/_instance_info.py | 4 +- .../build_providers/_multipass/_multipass.py | 2 +- .../_multipass/_multipass_command.py | 6 +- .../build_providers/_multipass/_windows.py | 6 +- .../internal/build_providers/_snap.py | 4 +- .../internal/build_providers/errors.py | 4 +- .../internal/cache/__init__.py | 0 .../internal/cache/_apt.py | 0 .../internal/cache/_cache.py | 0 .../internal/cache/_file.py | 2 +- .../internal/cache/_snap.py | 2 +- .../internal/common.py | 2 +- .../internal/db/__init__.py | 0 .../internal/db/datastore.py | 4 +- .../internal/db/errors.py | 2 +- .../internal/db/migration.py | 0 .../internal/deltas/__init__.py | 0 .../internal/deltas/_deltas.py | 4 +- .../internal/deltas/_xdelta3.py | 0 .../internal/deltas/errors.py | 2 +- .../internal/deprecations.py | 0 .../internal/dirs.py | 14 +-- .../internal/elf.py | 8 +- .../internal/errors.py | 8 +- .../internal/indicators.py | 0 .../internal/lifecycle/__init__.py | 0 .../internal/lifecycle/_clean.py | 6 +- .../internal/lifecycle/_init.py | 2 +- .../internal/lifecycle/_runner.py | 8 +- .../internal/lifecycle/_status_cache.py | 4 +- .../internal/lifecycle/errors.py | 2 +- .../internal/log.py | 2 +- .../internal/lxd/__init__.py | 0 .../internal/mangling.py | 4 +- .../internal/meta/__init__.py | 0 .../internal/meta/_manifest.py | 10 +-- .../internal/meta/_snap_packaging.py | 26 +++--- .../internal/meta/_utils.py | 0 .../internal/meta/_version.py | 4 +- .../internal/meta/application.py | 2 +- .../internal/meta/command.py | 2 +- .../internal/meta/desktop.py | 0 .../internal/meta/errors.py | 4 +- .../internal/meta/hooks.py | 2 +- .../internal/meta/package_repository.py | 0 .../internal/meta/plugs.py | 2 +- .../internal/meta/slots.py | 2 +- .../internal/meta/snap.py | 18 ++-- .../internal/meta/system_user.py | 2 +- .../internal/mountinfo.py | 2 +- .../internal/os_release.py | 2 +- .../internal/pluginhandler/__init__.py | 39 +++++--- .../pluginhandler/_build_attributes.py | 0 .../internal/pluginhandler/_dependencies.py | 2 +- .../internal/pluginhandler/_dirty_report.py | 2 +- .../pluginhandler/_metadata_extraction.py | 6 +- .../pluginhandler/_outdated_report.py | 4 +- .../pluginhandler/_part_environment.py | 6 +- .../internal/pluginhandler/_patchelf.py | 4 +- .../internal/pluginhandler/_plugin_loader.py | 14 +-- .../internal/pluginhandler/_runner.py | 2 +- .../internal/project_loader/__init__.py | 2 +- .../internal/project_loader/_config.py | 14 +-- .../internal/project_loader/_env.py | 4 +- .../project_loader/_extensions/__init__.py | 0 .../project_loader/_extensions/_extension.py | 0 .../_extensions/_flutter_meta.py | 0 .../project_loader/_extensions/_utils.py | 10 +-- .../_extensions/flutter_beta.py | 0 .../project_loader/_extensions/flutter_dev.py | 0 .../_extensions/flutter_master.py | 0 .../_extensions/flutter_stable.py | 0 .../project_loader/_extensions/gnome_3_28.py | 0 .../project_loader/_extensions/gnome_3_34.py | 0 .../project_loader/_extensions/gnome_3_38.py | 0 .../project_loader/_extensions/kde_neon.py | 0 .../project_loader/_extensions/ros1_noetic.py | 0 .../project_loader/_extensions/ros2_foxy.py | 0 .../internal/project_loader/_parts_config.py | 8 +- .../internal/project_loader/errors.py | 8 +- .../project_loader/grammar/__init__.py | 0 .../project_loader/grammar/_compound.py | 0 .../internal/project_loader/grammar/_on.py | 8 +- .../project_loader/grammar/_processor.py | 4 +- .../project_loader/grammar/_statement.py | 0 .../internal/project_loader/grammar/_to.py | 4 +- .../internal/project_loader/grammar/_try.py | 2 +- .../internal/project_loader/grammar/errors.py | 2 +- .../internal/project_loader/grammar/typing.py | 0 .../grammar_processing/__init__.py | 0 .../_global_grammar_processor.py | 12 +-- .../_package_transformer.py | 4 +- .../_part_grammar_processor.py | 18 ++-- .../project_loader/inspection/__init__.py | 0 .../project_loader/inspection/_latest_step.py | 6 +- .../inspection/_lifecycle_status.py | 4 +- .../project_loader/inspection/_provides.py | 4 +- .../project_loader/inspection/errors.py | 6 +- .../internal/remote_build/__init__.py | 0 .../internal/remote_build/_info_file.py | 2 +- .../internal/remote_build/_launchpad.py | 12 +-- .../internal/remote_build/_worktree.py | 20 ++--- .../internal/remote_build/errors.py | 4 +- .../internal/repo/__init__.py | 0 .../internal/repo/_base.py | 18 ++-- .../internal/repo/_deb.py | 10 +-- .../internal/repo/_platform.py | 4 +- .../internal/repo/apt_cache.py | 8 +- .../internal/repo/apt_key_manager.py | 2 +- .../internal/repo/apt_ppa.py | 0 .../internal/repo/apt_sources_manager.py | 6 +- .../internal/repo/deb_package.py | 0 .../internal/repo/errors.py | 8 +- .../internal/repo/snaps.py | 0 .../internal/repo/ua_manager.py | 2 +- .../internal/review_tools/__init__.py | 0 .../internal/review_tools/_runner.py | 2 +- .../internal/review_tools/errors.py | 2 +- .../internal/sources/_7z.py | 0 .../internal/sources/__init__.py | 0 .../internal/sources/_base.py | 10 +-- .../internal/sources/_bazaar.py | 0 .../internal/sources/_checksum.py | 2 +- .../internal/sources/_deb.py | 0 .../internal/sources/_git.py | 2 +- .../internal/sources/_local.py | 4 +- .../internal/sources/_mercurial.py | 0 .../internal/sources/_rpm.py | 0 .../internal/sources/_script.py | 0 .../internal/sources/_snap.py | 2 +- .../internal/sources/_subversion.py | 0 .../internal/sources/_tar.py | 0 .../internal/sources/_zip.py | 0 .../internal/sources/errors.py | 4 +- .../internal/states/__init__.py | 16 ++-- .../internal/states/_build_state.py | 8 +- .../internal/states/_global_state.py | 4 +- .../internal/states/_prime_state.py | 6 +- .../internal/states/_pull_state.py | 8 +- .../internal/states/_stage_state.py | 6 +- .../internal/states/_state.py | 4 +- .../internal/steps.py | 2 +- .../internal/xattrs.py | 2 +- .../plugins/__init__.py | 0 .../plugins/_plugin_finder.py | 2 +- .../plugins/_python/__init__.py | 0 .../plugins/v1/__init__.py | 0 .../plugins/v1/_plugin.py | 6 +- .../plugins/v1/_python/__init__.py | 0 .../plugins/v1/_python/_pip.py | 12 +-- .../plugins/v1/_python/_python_finder.py | 0 .../plugins/v1/_python/_sitecustomize.py | 0 .../plugins/v1/_python/errors.py | 8 +- .../plugins/v1/_ros/__init__.py | 0 .../plugins/v1/_ros/rosdep.py | 2 +- .../plugins/v1/_ros/wstool.py | 10 +-- .../plugins/v1/ant.py | 6 +- .../plugins/v1/autotools.py | 2 +- .../plugins/v1/catkin.py | 10 +-- .../plugins/v1/catkin_tools.py | 2 +- .../plugins/v1/cmake.py | 2 +- .../plugins/v1/colcon.py | 8 +- .../plugins/v1/conda.py | 4 +- .../plugins/v1/crystal.py | 6 +- .../plugins/v1/dotnet.py | 6 +- .../plugins/v1/dump.py | 12 +-- .../plugins/v1/flutter.py | 4 +- .../plugins/v1/go.py | 8 +- .../plugins/v1/godeps.py | 4 +- .../plugins/v1/gradle.py | 6 +- .../plugins/v1/kbuild.py | 6 +- .../plugins/v1/kernel.py | 10 ++- .../plugins/v1/make.py | 10 ++- .../plugins/v1/maven.py | 6 +- .../plugins/v1/meson.py | 4 +- .../plugins/v1/nil.py | 2 +- .../plugins/v1/nodejs.py | 8 +- .../plugins/v1/plainbox_provider.py | 4 +- .../plugins/v1/python.py | 8 +- .../plugins/v1/qmake.py | 4 +- .../plugins/v1/ruby.py | 8 +- .../plugins/v1/rust.py | 6 +- .../plugins/v1/scons.py | 2 +- .../plugins/v1/waf.py | 2 +- .../plugins/v2/__init__.py | 0 .../plugins/v2/_plugin.py | 0 .../plugins/v2/_ros.py | 6 +- .../plugins/v2/autotools.py | 2 +- .../plugins/v2/catkin.py | 2 +- .../plugins/v2/catkin_tools.py | 2 +- .../plugins/v2/cmake.py | 2 +- .../plugins/v2/colcon.py | 2 +- .../plugins/v2/conda.py | 4 +- .../plugins/v2/dump.py | 2 +- .../plugins/v2/go.py | 2 +- .../plugins/v2/make.py | 2 +- .../plugins/v2/meson.py | 2 +- .../plugins/v2/nil.py | 2 +- .../plugins/v2/npm.py | 2 +- .../plugins/v2/python.py | 2 +- .../plugins/v2/qmake.py | 2 +- .../plugins/v2/rust.py | 2 +- .../project/__init__.py | 0 .../project/_get_snapcraft.py | 0 .../project/_project.py | 4 +- .../project/_project_info.py | 6 +- .../project/_project_options.py | 8 +- .../project/_sanity_checks.py | 4 +- .../project/_schema.py | 8 +- .../project/errors.py | 2 +- .../scripts/__init__.py | 0 .../scripts/generate_reference.py | 0 .../shell_utils.py | 2 +- {snapcraft => snapcraft_legacy}/sources.py | 22 ++--- .../storeapi/__init__.py | 0 .../storeapi/_dashboard_api.py | 0 .../storeapi/_metadata.py | 2 +- .../storeapi/_requests.py | 0 .../storeapi/_snap_api.py | 0 .../storeapi/_status_tracker.py | 0 .../storeapi/_store_client.py | 2 +- .../storeapi/_up_down_client.py | 0 .../storeapi/_upload.py | 2 +- .../storeapi/channels.py | 0 .../storeapi/constants.py | 0 .../storeapi/errors.py | 4 +- .../storeapi/http_clients/__init__.py | 0 .../storeapi/http_clients/_candid_client.py | 2 +- .../storeapi/http_clients/_config.py | 0 .../storeapi/http_clients/_http_client.py | 0 .../http_clients/_ubuntu_sso_client.py | 3 +- .../storeapi/http_clients/agent.py | 10 +-- .../storeapi/http_clients/errors.py | 2 +- .../storeapi/info.py | 2 +- .../storeapi/metrics.py | 0 .../storeapi/status.py | 0 .../storeapi/v2/__init__.py | 0 .../storeapi/v2/_api_schema.py | 0 .../storeapi/v2/channel_map.py | 0 .../storeapi/v2/releases.py | 0 .../storeapi/v2/validation_sets.py | 0 .../storeapi/v2/whoami.py | 0 .../yaml_utils/__init__.py | 2 +- .../yaml_utils/errors.py | 4 +- tests/fake_servers/__init__.py | 2 +- tests/fake_servers/api.py | 2 +- tests/fixture_setup/_fixtures.py | 6 +- tests/fixture_setup/_unittests.py | 35 ++++---- tests/fixture_setup/_unix.py | 2 +- tests/fixture_setup/os_release.py | 4 +- .../snap/plugins/x_local_plugin.py | 2 +- .../snap/plugins/x_local_plugin.py | 2 +- .../snap/plugins/x_local_plugin.py | 2 +- .../snap/plugins/x-local-plugin.py | 2 +- .../snap/plugins/x_local_plugin.py | 2 +- .../snap/plugins/x_local_plugin.py | 2 +- tests/unit/__init__.py | 6 +- tests/unit/build_providers/__init__.py | 12 +-- tests/unit/build_providers/conftest.py | 2 +- tests/unit/build_providers/lxd/test_lxd.py | 18 ++-- .../multipass/test_instance_info.py | 4 +- .../multipass/test_multipass.py | 18 ++-- .../multipass/test_multipass_command.py | 4 +- .../build_providers/test_base_provider.py | 8 +- tests/unit/build_providers/test_errors.py | 2 +- tests/unit/build_providers/test_snap.py | 6 +- tests/unit/cache/conftest.py | 2 +- tests/unit/cache/test_file.py | 2 +- tests/unit/cache/test_snap.py | 8 +- tests/unit/cli/conftest.py | 4 +- tests/unit/cli/test_echo.py | 18 ++-- tests/unit/cli/test_errors.py | 38 ++++---- tests/unit/cli/test_lifecycle.py | 6 +- tests/unit/cli/test_metrics.py | 4 +- tests/unit/cli/test_options.py | 4 +- tests/unit/commands/__init__.py | 25 +++--- tests/unit/commands/conftest.py | 2 +- tests/unit/commands/snapcraftctl/__init__.py | 2 +- .../unit/commands/snapcraftctl/test_build.py | 2 +- .../commands/snapcraftctl/test_set_grade.py | 2 +- .../commands/snapcraftctl/test_set_version.py | 2 +- tests/unit/commands/test_build_providers.py | 40 +++++---- tests/unit/commands/test_clean.py | 2 +- tests/unit/commands/test_close.py | 6 +- tests/unit/commands/test_create_key.py | 2 +- .../commands/test_edit_validation_sets.py | 8 +- tests/unit/commands/test_export_login.py | 2 +- tests/unit/commands/test_extensions.py | 4 +- tests/unit/commands/test_gated.py | 4 +- tests/unit/commands/test_help.py | 12 +-- tests/unit/commands/test_init.py | 4 +- tests/unit/commands/test_list.py | 4 +- tests/unit/commands/test_list_keys.py | 4 +- tests/unit/commands/test_list_plugins.py | 16 ++-- tests/unit/commands/test_list_revisions.py | 2 +- tests/unit/commands/test_list_tracks.py | 2 +- .../commands/test_list_validation_sets.py | 4 +- tests/unit/commands/test_login.py | 2 +- tests/unit/commands/test_logout.py | 2 +- tests/unit/commands/test_metrics.py | 2 +- .../commands/test_pull_build_stage_prime.py | 2 +- tests/unit/commands/test_refresh.py | 2 +- tests/unit/commands/test_register.py | 2 +- tests/unit/commands/test_register_key.py | 6 +- tests/unit/commands/test_release.py | 4 +- tests/unit/commands/test_remote.py | 30 ++++--- tests/unit/commands/test_set_default_track.py | 7 +- tests/unit/commands/test_sign_build.py | 20 ++--- tests/unit/commands/test_snap.py | 2 +- tests/unit/commands/test_status.py | 4 +- tests/unit/commands/test_upload.py | 14 +-- tests/unit/commands/test_upload_metadata.py | 16 ++-- tests/unit/commands/test_validate.py | 20 ++--- tests/unit/commands/test_whoami.py | 4 +- tests/unit/db/test_datastore.py | 2 +- tests/unit/db/test_errors.py | 2 +- tests/unit/db/test_migration.py | 2 +- tests/unit/deltas/test_deltas.py | 4 +- tests/unit/deltas/test_deltas_xdelta3.py | 2 +- tests/unit/extractors/test_appstream.py | 8 +- tests/unit/extractors/test_metadata.py | 2 +- tests/unit/extractors/test_setuppy.py | 2 +- tests/unit/lifecycle/__init__.py | 16 ++-- tests/unit/lifecycle/test_errors.py | 2 +- tests/unit/lifecycle/test_global_state.py | 16 ++-- tests/unit/lifecycle/test_lifecycle.py | 58 +++++++----- tests/unit/lifecycle/test_order.py | 10 +-- .../unit/lifecycle/test_snap_installation.py | 18 ++-- tests/unit/lifecycle/test_status_cache.py | 4 +- tests/unit/meta/test_application.py | 4 +- tests/unit/meta/test_command.py | 2 +- tests/unit/meta/test_command_mangle.py | 2 +- tests/unit/meta/test_desktop.py | 4 +- tests/unit/meta/test_errors.py | 2 +- tests/unit/meta/test_hook.py | 4 +- tests/unit/meta/test_meta.py | 16 ++-- tests/unit/meta/test_package_repository.py | 4 +- tests/unit/meta/test_plugs.py | 4 +- tests/unit/meta/test_slots.py | 4 +- tests/unit/meta/test_snap.py | 8 +- tests/unit/meta/test_snap_packaging.py | 6 +- tests/unit/meta/test_system_user.py | 4 +- tests/unit/part_loader.py | 6 +- tests/unit/pluginhandler/mocks.py | 4 +- tests/unit/pluginhandler/test_clean.py | 4 +- tests/unit/pluginhandler/test_dirty_report.py | 7 +- .../pluginhandler/test_metadata_extraction.py | 6 +- .../pluginhandler/test_missing_dependency.py | 8 +- tests/unit/pluginhandler/test_patcher.py | 20 +++-- .../unit/pluginhandler/test_plugin_loader.py | 12 +-- .../unit/pluginhandler/test_pluginhandler.py | 32 +++---- tests/unit/pluginhandler/test_runner.py | 4 +- tests/unit/pluginhandler/test_scriptlets.py | 8 +- tests/unit/pluginhandler/test_state.py | 39 ++++---- tests/unit/plugins/v1/__init__.py | 4 +- tests/unit/plugins/v1/conftest.py | 14 +-- tests/unit/plugins/v1/python/test_errors.py | 2 +- tests/unit/plugins/v1/python/test_pip.py | 6 +- .../plugins/v1/python/test_python_finder.py | 2 +- .../plugins/v1/python/test_sitecustomize.py | 2 +- tests/unit/plugins/v1/ros/test_rosdep.py | 8 +- tests/unit/plugins/v1/ros/test_wstool.py | 8 +- tests/unit/plugins/v1/test_ant.py | 12 +-- tests/unit/plugins/v1/test_autotools.py | 10 +-- tests/unit/plugins/v1/test_base.py | 34 ++++--- tests/unit/plugins/v1/test_catkin.py | 27 +++--- tests/unit/plugins/v1/test_catkin_tools.py | 6 +- tests/unit/plugins/v1/test_cmake.py | 6 +- tests/unit/plugins/v1/test_colcon.py | 19 ++-- tests/unit/plugins/v1/test_conda.py | 8 +- tests/unit/plugins/v1/test_crystal.py | 10 +-- tests/unit/plugins/v1/test_dotnet.py | 16 ++-- tests/unit/plugins/v1/test_dump.py | 8 +- tests/unit/plugins/v1/test_flutter.py | 8 +- tests/unit/plugins/v1/test_go.py | 14 +-- tests/unit/plugins/v1/test_godeps.py | 6 +- tests/unit/plugins/v1/test_gradle.py | 8 +- tests/unit/plugins/v1/test_kbuild.py | 10 +-- tests/unit/plugins/v1/test_kernel.py | 50 +++++------ tests/unit/plugins/v1/test_make.py | 8 +- tests/unit/plugins/v1/test_maven.py | 12 +-- tests/unit/plugins/v1/test_meson.py | 4 +- tests/unit/plugins/v1/test_nil.py | 2 +- tests/unit/plugins/v1/test_nodejs.py | 12 +-- .../unit/plugins/v1/test_plainbox_provider.py | 4 +- tests/unit/plugins/v1/test_python.py | 6 +- tests/unit/plugins/v1/test_qmake.py | 6 +- tests/unit/plugins/v1/test_ruby.py | 18 ++-- tests/unit/plugins/v1/test_rust.py | 20 +++-- tests/unit/plugins/v1/test_scons.py | 4 +- tests/unit/plugins/v1/test_waf.py | 6 +- tests/unit/plugins/v2/test_autotools.py | 2 +- tests/unit/plugins/v2/test_catkin.py | 4 +- tests/unit/plugins/v2/test_catkin_tools.py | 4 +- tests/unit/plugins/v2/test_cmake.py | 2 +- tests/unit/plugins/v2/test_colcon.py | 4 +- tests/unit/plugins/v2/test_conda.py | 2 +- tests/unit/plugins/v2/test_dump.py | 2 +- tests/unit/plugins/v2/test_go.py | 2 +- tests/unit/plugins/v2/test_make.py | 2 +- tests/unit/plugins/v2/test_meson.py | 2 +- tests/unit/plugins/v2/test_nil.py | 2 +- tests/unit/plugins/v2/test_npm.py | 2 +- tests/unit/plugins/v2/test_python.py | 2 +- tests/unit/plugins/v2/test_qmake.py | 2 +- tests/unit/plugins/v2/test_rust.py | 2 +- tests/unit/project/__init__.py | 6 +- tests/unit/project/test_errors.py | 2 +- tests/unit/project/test_get_snapcraft.py | 2 +- tests/unit/project/test_project.py | 2 +- tests/unit/project/test_project_info.py | 16 ++-- tests/unit/project/test_sanity_checks.py | 10 ++- tests/unit/project/test_schema.py | 88 +++++++++++-------- tests/unit/project_loader/__init__.py | 6 +- .../extensions/test_extensions.py | 5 +- .../project_loader/extensions/test_flutter.py | 4 +- .../extensions/test_gnome_3_28.py | 4 +- .../extensions/test_gnome_3_34.py | 4 +- .../extensions/test_gnome_3_38.py | 4 +- .../extensions/test_kde_neon.py | 2 +- .../extensions/test_ros1_noetic.py | 2 +- .../extensions/test_ros2_foxy.py | 2 +- .../project_loader/extensions/test_utils.py | 12 +-- .../grammar/test_compound_statement.py | 16 ++-- .../grammar/test_on_statement.py | 12 +-- .../project_loader/grammar/test_processor.py | 14 +-- .../grammar/test_to_statement.py | 14 +-- .../grammar/test_try_statement.py | 10 +-- .../test_global_grammar_processor.py | 2 +- .../test_part_grammar_processor.py | 10 ++- .../inspection/test_latest_step.py | 6 +- .../inspection/test_lifecycle_status.py | 4 +- .../inspection/test_provides.py | 4 +- .../project_loader/test_build_packages.py | 4 +- tests/unit/project_loader/test_config.py | 4 +- tests/unit/project_loader/test_environment.py | 12 +-- tests/unit/project_loader/test_errors.py | 2 +- tests/unit/project_loader/test_parts.py | 4 +- .../unit/project_loader/test_replace_attr.py | 2 +- tests/unit/project_loader/test_schema.py | 10 +-- tests/unit/remote_build/test_errors.py | 2 +- tests/unit/remote_build/test_info_file.py | 2 +- tests/unit/remote_build/test_launchpad.py | 18 ++-- tests/unit/remote_build/test_worktree.py | 6 +- tests/unit/repo/test_apt_cache.py | 12 +-- tests/unit/repo/test_apt_key_manager.py | 24 ++--- tests/unit/repo/test_apt_ppa.py | 4 +- tests/unit/repo/test_apt_sources_manager.py | 14 +-- tests/unit/repo/test_base.py | 2 +- tests/unit/repo/test_deb.py | 20 ++--- tests/unit/repo/test_deb_package.py | 2 +- tests/unit/repo/test_errors.py | 2 +- tests/unit/repo/test_snaps.py | 10 ++- tests/unit/repo/test_ua_manager.py | 2 +- tests/unit/review_tools/test_errors.py | 2 +- tests/unit/review_tools/test_runner.py | 4 +- tests/unit/sources/test_7z.py | 2 +- tests/unit/sources/test_base.py | 20 ++--- tests/unit/sources/test_bazaar.py | 4 +- tests/unit/sources/test_checksum.py | 4 +- tests/unit/sources/test_deb.py | 2 +- tests/unit/sources/test_errors.py | 2 +- tests/unit/sources/test_git.py | 6 +- tests/unit/sources/test_local.py | 4 +- tests/unit/sources/test_mercurial.py | 4 +- tests/unit/sources/test_rpm.py | 2 +- tests/unit/sources/test_script.py | 4 +- tests/unit/sources/test_snap.py | 2 +- tests/unit/sources/test_sources.py | 2 +- tests/unit/sources/test_subversion.py | 4 +- tests/unit/sources/test_tar.py | 6 +- tests/unit/sources/test_zip.py | 2 +- tests/unit/states/conftest.py | 2 +- tests/unit/states/test_build.py | 12 +-- tests/unit/states/test_global_state.py | 2 +- tests/unit/states/test_prime.py | 12 +-- tests/unit/states/test_pull.py | 12 +-- tests/unit/states/test_stage.py | 12 +-- tests/unit/states/test_state.py | 2 +- tests/unit/store/http_client/test_agent.py | 6 +- .../store/http_client/test_candid_client.py | 2 +- tests/unit/store/http_client/test_config.py | 4 +- tests/unit/store/http_client/test_errors.py | 2 +- .../test_ubuntu_one_auth_client.py | 2 +- tests/unit/store/test_channels.py | 2 +- tests/unit/store/test_errors.py | 2 +- tests/unit/store/test_metrics.py | 2 +- tests/unit/store/test_status.py | 2 +- tests/unit/store/test_store_client.py | 10 +-- tests/unit/store/v2/test_channel_map.py | 2 +- tests/unit/store/v2/test_releases.py | 2 +- tests/unit/store/v2/test_validation_sets.py | 2 +- tests/unit/store/v2/test_whoami.py | 2 +- tests/unit/test_common.py | 2 +- tests/unit/test_config.py | 4 +- tests/unit/test_elf.py | 14 +-- tests/unit/test_errors.py | 10 ++- tests/unit/test_file_utils.py | 4 +- tests/unit/test_formatting_utils.py | 2 +- tests/unit/test_indicators.py | 2 +- tests/unit/test_init.py | 4 +- tests/unit/test_log.py | 2 +- tests/unit/test_mangling.py | 2 +- tests/unit/test_mountinfo.py | 2 +- tests/unit/test_options.py | 26 +++--- tests/unit/test_os_release.py | 2 +- tests/unit/test_steps.py | 2 +- tests/unit/test_target_arch.py | 2 +- tests/unit/test_xattrs.py | 4 +- tests/unit/yaml_utils/test_errors.py | 2 +- tests/unit/yaml_utils/test_yaml_utils.py | 4 +- tools/brew_install_from_source.py | 4 +- units.py | 2 +- 557 files changed, 1660 insertions(+), 1513 deletions(-) rename {snapcraft => snapcraft_legacy}/__init__.py (95%) rename {snapcraft => snapcraft_legacy}/_legacy_loader.py (85%) rename {snapcraft => snapcraft_legacy}/_store.py (96%) rename {snapcraft => snapcraft_legacy}/cli/__init__.py (89%) rename {snapcraft => snapcraft_legacy}/cli/__main__.py (89%) rename {snapcraft => snapcraft_legacy}/cli/_channel_map.py (99%) rename {snapcraft => snapcraft_legacy}/cli/_command_group.py (97%) rename {snapcraft => snapcraft_legacy}/cli/_errors.py (96%) rename {snapcraft => snapcraft_legacy}/cli/_metrics.py (98%) rename {snapcraft => snapcraft_legacy}/cli/_options.py (98%) rename {snapcraft => snapcraft_legacy}/cli/_review.py (96%) rename {snapcraft => snapcraft_legacy}/cli/_runner.py (94%) rename {snapcraft => snapcraft_legacy}/cli/assertions.py (94%) rename {snapcraft => snapcraft_legacy}/cli/containers.py (95%) rename {snapcraft => snapcraft_legacy}/cli/discovery.py (83%) rename {snapcraft => snapcraft_legacy}/cli/echo.py (97%) rename {snapcraft => snapcraft_legacy}/cli/extensions.py (96%) rename {snapcraft => snapcraft_legacy}/cli/help.py (93%) rename {snapcraft => snapcraft_legacy}/cli/lifecycle.py (98%) rename {snapcraft => snapcraft_legacy}/cli/remote.py (97%) rename {snapcraft => snapcraft_legacy}/cli/snapcraftctl/__init__.py (100%) rename {snapcraft => snapcraft_legacy}/cli/snapcraftctl/_runner.py (97%) rename {snapcraft => snapcraft_legacy}/cli/store.py (98%) rename {snapcraft => snapcraft_legacy}/cli/version.py (95%) rename {snapcraft => snapcraft_legacy}/common.py (60%) rename {snapcraft => snapcraft_legacy}/config.py (98%) rename {snapcraft => snapcraft_legacy}/extractors/__init__.py (100%) rename {snapcraft => snapcraft_legacy}/extractors/_errors.py (96%) rename {snapcraft => snapcraft_legacy}/extractors/_metadata.py (99%) rename {snapcraft => snapcraft_legacy}/extractors/appstream.py (99%) rename {snapcraft => snapcraft_legacy}/extractors/setuppy.py (98%) rename {snapcraft => snapcraft_legacy}/file_utils.py (99%) rename {snapcraft => snapcraft_legacy}/formatting_utils.py (100%) rename {snapcraft => snapcraft_legacy}/internal/__init__.py (80%) rename {snapcraft => snapcraft_legacy}/internal/build_providers/__init__.py (100%) rename {snapcraft => snapcraft_legacy}/internal/build_providers/_base_provider.py (98%) rename {snapcraft => snapcraft_legacy}/internal/build_providers/_factory.py (100%) rename {snapcraft => snapcraft_legacy}/internal/build_providers/_lxd/__init__.py (100%) rename {snapcraft => snapcraft_legacy}/internal/build_providers/_lxd/_images.py (100%) rename {snapcraft => snapcraft_legacy}/internal/build_providers/_lxd/_lxd.py (99%) rename {snapcraft => snapcraft_legacy}/internal/build_providers/_multipass/__init__.py (100%) rename {snapcraft => snapcraft_legacy}/internal/build_providers/_multipass/_instance_info.py (95%) rename {snapcraft => snapcraft_legacy}/internal/build_providers/_multipass/_multipass.py (99%) rename {snapcraft => snapcraft_legacy}/internal/build_providers/_multipass/_multipass_command.py (98%) rename {snapcraft => snapcraft_legacy}/internal/build_providers/_multipass/_windows.py (97%) rename {snapcraft => snapcraft_legacy}/internal/build_providers/_snap.py (99%) rename {snapcraft => snapcraft_legacy}/internal/build_providers/errors.py (98%) rename {snapcraft => snapcraft_legacy}/internal/cache/__init__.py (100%) rename {snapcraft => snapcraft_legacy}/internal/cache/_apt.py (100%) rename {snapcraft => snapcraft_legacy}/internal/cache/_cache.py (100%) rename {snapcraft => snapcraft_legacy}/internal/cache/_file.py (98%) rename {snapcraft => snapcraft_legacy}/internal/cache/_snap.py (98%) rename {snapcraft => snapcraft_legacy}/internal/common.py (99%) rename {snapcraft => snapcraft_legacy}/internal/db/__init__.py (100%) rename {snapcraft => snapcraft_legacy}/internal/db/datastore.py (97%) rename {snapcraft => snapcraft_legacy}/internal/db/errors.py (95%) rename {snapcraft => snapcraft_legacy}/internal/db/migration.py (100%) rename {snapcraft => snapcraft_legacy}/internal/deltas/__init__.py (100%) rename {snapcraft => snapcraft_legacy}/internal/deltas/_deltas.py (98%) rename {snapcraft => snapcraft_legacy}/internal/deltas/_xdelta3.py (100%) rename {snapcraft => snapcraft_legacy}/internal/deltas/errors.py (96%) rename {snapcraft => snapcraft_legacy}/internal/deprecations.py (100%) rename {snapcraft => snapcraft_legacy}/internal/dirs.py (90%) rename {snapcraft => snapcraft_legacy}/internal/elf.py (99%) rename {snapcraft => snapcraft_legacy}/internal/errors.py (98%) rename {snapcraft => snapcraft_legacy}/internal/indicators.py (100%) rename {snapcraft => snapcraft_legacy}/internal/lifecycle/__init__.py (100%) rename {snapcraft => snapcraft_legacy}/internal/lifecycle/_clean.py (97%) rename {snapcraft => snapcraft_legacy}/internal/lifecycle/_init.py (98%) rename {snapcraft => snapcraft_legacy}/internal/lifecycle/_runner.py (98%) rename {snapcraft => snapcraft_legacy}/internal/lifecycle/_status_cache.py (98%) rename {snapcraft => snapcraft_legacy}/internal/lifecycle/errors.py (91%) rename {snapcraft => snapcraft_legacy}/internal/log.py (97%) rename {snapcraft => snapcraft_legacy}/internal/lxd/__init__.py (100%) rename {snapcraft => snapcraft_legacy}/internal/mangling.py (97%) rename {snapcraft => snapcraft_legacy}/internal/meta/__init__.py (100%) rename {snapcraft => snapcraft_legacy}/internal/meta/_manifest.py (91%) rename {snapcraft => snapcraft_legacy}/internal/meta/_snap_packaging.py (97%) rename {snapcraft => snapcraft_legacy}/internal/meta/_utils.py (100%) rename {snapcraft => snapcraft_legacy}/internal/meta/_version.py (95%) rename {snapcraft => snapcraft_legacy}/internal/meta/application.py (99%) rename {snapcraft => snapcraft_legacy}/internal/meta/command.py (99%) rename {snapcraft => snapcraft_legacy}/internal/meta/desktop.py (100%) rename {snapcraft => snapcraft_legacy}/internal/meta/errors.py (98%) rename {snapcraft => snapcraft_legacy}/internal/meta/hooks.py (98%) rename {snapcraft => snapcraft_legacy}/internal/meta/package_repository.py (100%) rename {snapcraft => snapcraft_legacy}/internal/meta/plugs.py (98%) rename {snapcraft => snapcraft_legacy}/internal/meta/slots.py (99%) rename {snapcraft => snapcraft_legacy}/internal/meta/snap.py (97%) rename {snapcraft => snapcraft_legacy}/internal/meta/system_user.py (98%) rename {snapcraft => snapcraft_legacy}/internal/mountinfo.py (98%) rename {snapcraft => snapcraft_legacy}/internal/os_release.py (98%) rename {snapcraft => snapcraft_legacy}/internal/pluginhandler/__init__.py (98%) rename {snapcraft => snapcraft_legacy}/internal/pluginhandler/_build_attributes.py (100%) rename {snapcraft => snapcraft_legacy}/internal/pluginhandler/_dependencies.py (98%) rename {snapcraft => snapcraft_legacy}/internal/pluginhandler/_dirty_report.py (99%) rename {snapcraft => snapcraft_legacy}/internal/pluginhandler/_metadata_extraction.py (93%) rename {snapcraft => snapcraft_legacy}/internal/pluginhandler/_outdated_report.py (96%) rename {snapcraft => snapcraft_legacy}/internal/pluginhandler/_part_environment.py (97%) rename {snapcraft => snapcraft_legacy}/internal/pluginhandler/_patchelf.py (98%) rename {snapcraft => snapcraft_legacy}/internal/pluginhandler/_plugin_loader.py (95%) rename {snapcraft => snapcraft_legacy}/internal/pluginhandler/_runner.py (99%) rename {snapcraft => snapcraft_legacy}/internal/project_loader/__init__.py (96%) rename {snapcraft => snapcraft_legacy}/internal/project_loader/_config.py (97%) rename {snapcraft => snapcraft_legacy}/internal/project_loader/_env.py (96%) rename {snapcraft => snapcraft_legacy}/internal/project_loader/_extensions/__init__.py (100%) rename {snapcraft => snapcraft_legacy}/internal/project_loader/_extensions/_extension.py (100%) rename {snapcraft => snapcraft_legacy}/internal/project_loader/_extensions/_flutter_meta.py (100%) rename {snapcraft => snapcraft_legacy}/internal/project_loader/_extensions/_utils.py (95%) rename {snapcraft => snapcraft_legacy}/internal/project_loader/_extensions/flutter_beta.py (100%) rename {snapcraft => snapcraft_legacy}/internal/project_loader/_extensions/flutter_dev.py (100%) rename {snapcraft => snapcraft_legacy}/internal/project_loader/_extensions/flutter_master.py (100%) rename {snapcraft => snapcraft_legacy}/internal/project_loader/_extensions/flutter_stable.py (100%) rename {snapcraft => snapcraft_legacy}/internal/project_loader/_extensions/gnome_3_28.py (100%) rename {snapcraft => snapcraft_legacy}/internal/project_loader/_extensions/gnome_3_34.py (100%) rename {snapcraft => snapcraft_legacy}/internal/project_loader/_extensions/gnome_3_38.py (100%) rename {snapcraft => snapcraft_legacy}/internal/project_loader/_extensions/kde_neon.py (100%) rename {snapcraft => snapcraft_legacy}/internal/project_loader/_extensions/ros1_noetic.py (100%) rename {snapcraft => snapcraft_legacy}/internal/project_loader/_extensions/ros2_foxy.py (100%) rename {snapcraft => snapcraft_legacy}/internal/project_loader/_parts_config.py (97%) rename {snapcraft => snapcraft_legacy}/internal/project_loader/errors.py (95%) rename {snapcraft => snapcraft_legacy}/internal/project_loader/grammar/__init__.py (100%) rename {snapcraft => snapcraft_legacy}/internal/project_loader/grammar/_compound.py (100%) rename {snapcraft => snapcraft_legacy}/internal/project_loader/grammar/_on.py (95%) rename {snapcraft => snapcraft_legacy}/internal/project_loader/grammar/_processor.py (99%) rename {snapcraft => snapcraft_legacy}/internal/project_loader/grammar/_statement.py (100%) rename {snapcraft => snapcraft_legacy}/internal/project_loader/grammar/_to.py (97%) rename {snapcraft => snapcraft_legacy}/internal/project_loader/grammar/_try.py (97%) rename {snapcraft => snapcraft_legacy}/internal/project_loader/grammar/errors.py (97%) rename {snapcraft => snapcraft_legacy}/internal/project_loader/grammar/typing.py (100%) rename {snapcraft => snapcraft_legacy}/internal/project_loader/grammar_processing/__init__.py (100%) rename {snapcraft => snapcraft_legacy}/internal/project_loader/grammar_processing/_global_grammar_processor.py (85%) rename {snapcraft => snapcraft_legacy}/internal/project_loader/grammar_processing/_package_transformer.py (94%) rename {snapcraft => snapcraft_legacy}/internal/project_loader/grammar_processing/_part_grammar_processor.py (92%) rename {snapcraft => snapcraft_legacy}/internal/project_loader/inspection/__init__.py (100%) rename {snapcraft => snapcraft_legacy}/internal/project_loader/inspection/_latest_step.py (89%) rename {snapcraft => snapcraft_legacy}/internal/project_loader/inspection/_lifecycle_status.py (94%) rename {snapcraft => snapcraft_legacy}/internal/project_loader/inspection/_provides.py (97%) rename {snapcraft => snapcraft_legacy}/internal/project_loader/inspection/errors.py (89%) rename {snapcraft => snapcraft_legacy}/internal/remote_build/__init__.py (100%) rename {snapcraft => snapcraft_legacy}/internal/remote_build/_info_file.py (97%) rename {snapcraft => snapcraft_legacy}/internal/remote_build/_launchpad.py (97%) rename {snapcraft => snapcraft_legacy}/internal/remote_build/_worktree.py (96%) rename {snapcraft => snapcraft_legacy}/internal/remote_build/errors.py (97%) rename {snapcraft => snapcraft_legacy}/internal/repo/__init__.py (100%) rename {snapcraft => snapcraft_legacy}/internal/repo/_base.py (95%) rename {snapcraft => snapcraft_legacy}/internal/repo/_deb.py (98%) rename {snapcraft => snapcraft_legacy}/internal/repo/_platform.py (90%) rename {snapcraft => snapcraft_legacy}/internal/repo/apt_cache.py (98%) rename {snapcraft => snapcraft_legacy}/internal/repo/apt_key_manager.py (99%) rename {snapcraft => snapcraft_legacy}/internal/repo/apt_ppa.py (100%) rename {snapcraft => snapcraft_legacy}/internal/repo/apt_sources_manager.py (97%) rename {snapcraft => snapcraft_legacy}/internal/repo/deb_package.py (100%) rename {snapcraft => snapcraft_legacy}/internal/repo/errors.py (97%) rename {snapcraft => snapcraft_legacy}/internal/repo/snaps.py (100%) rename {snapcraft => snapcraft_legacy}/internal/repo/ua_manager.py (98%) rename {snapcraft => snapcraft_legacy}/internal/review_tools/__init__.py (100%) rename {snapcraft => snapcraft_legacy}/internal/review_tools/_runner.py (98%) rename {snapcraft => snapcraft_legacy}/internal/review_tools/errors.py (97%) rename {snapcraft => snapcraft_legacy}/internal/sources/_7z.py (100%) rename {snapcraft => snapcraft_legacy}/internal/sources/__init__.py (100%) rename {snapcraft => snapcraft_legacy}/internal/sources/_base.py (94%) rename {snapcraft => snapcraft_legacy}/internal/sources/_bazaar.py (100%) rename {snapcraft => snapcraft_legacy}/internal/sources/_checksum.py (97%) rename {snapcraft => snapcraft_legacy}/internal/sources/_deb.py (100%) rename {snapcraft => snapcraft_legacy}/internal/sources/_git.py (99%) rename {snapcraft => snapcraft_legacy}/internal/sources/_local.py (98%) rename {snapcraft => snapcraft_legacy}/internal/sources/_mercurial.py (100%) rename {snapcraft => snapcraft_legacy}/internal/sources/_rpm.py (100%) rename {snapcraft => snapcraft_legacy}/internal/sources/_script.py (100%) rename {snapcraft => snapcraft_legacy}/internal/sources/_snap.py (98%) rename {snapcraft => snapcraft_legacy}/internal/sources/_subversion.py (100%) rename {snapcraft => snapcraft_legacy}/internal/sources/_tar.py (100%) rename {snapcraft => snapcraft_legacy}/internal/sources/_zip.py (100%) rename {snapcraft => snapcraft_legacy}/internal/sources/errors.py (98%) rename {snapcraft => snapcraft_legacy}/internal/states/__init__.py (53%) rename {snapcraft => snapcraft_legacy}/internal/states/_build_state.py (91%) rename {snapcraft => snapcraft_legacy}/internal/states/_global_state.py (96%) rename {snapcraft => snapcraft_legacy}/internal/states/_prime_state.py (92%) rename {snapcraft => snapcraft_legacy}/internal/states/_pull_state.py (91%) rename {snapcraft => snapcraft_legacy}/internal/states/_stage_state.py (91%) rename {snapcraft => snapcraft_legacy}/internal/states/_state.py (97%) rename {snapcraft => snapcraft_legacy}/internal/steps.py (98%) rename {snapcraft => snapcraft_legacy}/internal/xattrs.py (97%) rename {snapcraft => snapcraft_legacy}/plugins/__init__.py (100%) rename {snapcraft => snapcraft_legacy}/plugins/_plugin_finder.py (98%) rename {snapcraft => snapcraft_legacy}/plugins/_python/__init__.py (100%) rename {snapcraft => snapcraft_legacy}/plugins/v1/__init__.py (100%) rename {snapcraft => snapcraft_legacy}/plugins/v1/_plugin.py (97%) rename {snapcraft => snapcraft_legacy}/plugins/v1/_python/__init__.py (100%) rename {snapcraft => snapcraft_legacy}/plugins/v1/_python/_pip.py (98%) rename {snapcraft => snapcraft_legacy}/plugins/v1/_python/_python_finder.py (100%) rename {snapcraft => snapcraft_legacy}/plugins/v1/_python/_sitecustomize.py (100%) rename {snapcraft => snapcraft_legacy}/plugins/v1/_python/errors.py (90%) rename {snapcraft => snapcraft_legacy}/plugins/v1/_ros/__init__.py (100%) rename {snapcraft => snapcraft_legacy}/plugins/v1/_ros/rosdep.py (99%) rename {snapcraft => snapcraft_legacy}/plugins/v1/_ros/wstool.py (96%) rename {snapcraft => snapcraft_legacy}/plugins/v1/ant.py (98%) rename {snapcraft => snapcraft_legacy}/plugins/v1/autotools.py (99%) rename {snapcraft => snapcraft_legacy}/plugins/v1/catkin.py (99%) rename {snapcraft => snapcraft_legacy}/plugins/v1/catkin_tools.py (98%) rename {snapcraft => snapcraft_legacy}/plugins/v1/cmake.py (99%) rename {snapcraft => snapcraft_legacy}/plugins/v1/colcon.py (99%) rename {snapcraft => snapcraft_legacy}/plugins/v1/conda.py (97%) rename {snapcraft => snapcraft_legacy}/plugins/v1/crystal.py (96%) rename {snapcraft => snapcraft_legacy}/plugins/v1/dotnet.py (97%) rename {snapcraft => snapcraft_legacy}/plugins/v1/dump.py (90%) rename {snapcraft => snapcraft_legacy}/plugins/v1/flutter.py (97%) rename {snapcraft => snapcraft_legacy}/plugins/v1/go.py (98%) rename {snapcraft => snapcraft_legacy}/plugins/v1/godeps.py (98%) rename {snapcraft => snapcraft_legacy}/plugins/v1/gradle.py (98%) rename {snapcraft => snapcraft_legacy}/plugins/v1/kbuild.py (98%) rename {snapcraft => snapcraft_legacy}/plugins/v1/kernel.py (98%) rename {snapcraft => snapcraft_legacy}/plugins/v1/make.py (93%) rename {snapcraft => snapcraft_legacy}/plugins/v1/maven.py (98%) rename {snapcraft => snapcraft_legacy}/plugins/v1/meson.py (97%) rename {snapcraft => snapcraft_legacy}/plugins/v1/nil.py (95%) rename {snapcraft => snapcraft_legacy}/plugins/v1/nodejs.py (98%) rename {snapcraft => snapcraft_legacy}/plugins/v1/plainbox_provider.py (97%) rename {snapcraft => snapcraft_legacy}/plugins/v1/python.py (98%) rename {snapcraft => snapcraft_legacy}/plugins/v1/qmake.py (98%) rename {snapcraft => snapcraft_legacy}/plugins/v1/ruby.py (97%) rename {snapcraft => snapcraft_legacy}/plugins/v1/rust.py (98%) rename {snapcraft => snapcraft_legacy}/plugins/v1/scons.py (97%) rename {snapcraft => snapcraft_legacy}/plugins/v1/waf.py (98%) rename {snapcraft => snapcraft_legacy}/plugins/v2/__init__.py (100%) rename {snapcraft => snapcraft_legacy}/plugins/v2/_plugin.py (100%) rename {snapcraft => snapcraft_legacy}/plugins/v2/_ros.py (97%) rename {snapcraft => snapcraft_legacy}/plugins/v2/autotools.py (98%) rename {snapcraft => snapcraft_legacy}/plugins/v2/catkin.py (99%) rename {snapcraft => snapcraft_legacy}/plugins/v2/catkin_tools.py (99%) rename {snapcraft => snapcraft_legacy}/plugins/v2/cmake.py (98%) rename {snapcraft => snapcraft_legacy}/plugins/v2/colcon.py (99%) rename {snapcraft => snapcraft_legacy}/plugins/v2/conda.py (97%) rename {snapcraft => snapcraft_legacy}/plugins/v2/dump.py (97%) rename {snapcraft => snapcraft_legacy}/plugins/v2/go.py (98%) rename {snapcraft => snapcraft_legacy}/plugins/v2/make.py (98%) rename {snapcraft => snapcraft_legacy}/plugins/v2/meson.py (98%) rename {snapcraft => snapcraft_legacy}/plugins/v2/nil.py (96%) rename {snapcraft => snapcraft_legacy}/plugins/v2/npm.py (98%) rename {snapcraft => snapcraft_legacy}/plugins/v2/python.py (99%) rename {snapcraft => snapcraft_legacy}/plugins/v2/qmake.py (98%) rename {snapcraft => snapcraft_legacy}/plugins/v2/rust.py (98%) rename {snapcraft => snapcraft_legacy}/project/__init__.py (100%) rename {snapcraft => snapcraft_legacy}/project/_get_snapcraft.py (100%) rename {snapcraft => snapcraft_legacy}/project/_project.py (97%) rename {snapcraft => snapcraft_legacy}/project/_project_info.py (93%) rename {snapcraft => snapcraft_legacy}/project/_project_options.py (97%) rename {snapcraft => snapcraft_legacy}/project/_sanity_checks.py (96%) rename {snapcraft => snapcraft_legacy}/project/_schema.py (89%) rename {snapcraft => snapcraft_legacy}/project/errors.py (97%) rename {snapcraft => snapcraft_legacy}/scripts/__init__.py (100%) rename {snapcraft => snapcraft_legacy}/scripts/generate_reference.py (100%) rename {snapcraft => snapcraft_legacy}/shell_utils.py (96%) rename {snapcraft => snapcraft_legacy}/sources.py (50%) rename {snapcraft => snapcraft_legacy}/storeapi/__init__.py (100%) rename {snapcraft => snapcraft_legacy}/storeapi/_dashboard_api.py (100%) rename {snapcraft => snapcraft_legacy}/storeapi/_metadata.py (98%) rename {snapcraft => snapcraft_legacy}/storeapi/_requests.py (100%) rename {snapcraft => snapcraft_legacy}/storeapi/_snap_api.py (100%) rename {snapcraft => snapcraft_legacy}/storeapi/_status_tracker.py (100%) rename {snapcraft => snapcraft_legacy}/storeapi/_store_client.py (99%) rename {snapcraft => snapcraft_legacy}/storeapi/_up_down_client.py (100%) rename {snapcraft => snapcraft_legacy}/storeapi/_upload.py (97%) rename {snapcraft => snapcraft_legacy}/storeapi/channels.py (100%) rename {snapcraft => snapcraft_legacy}/storeapi/constants.py (100%) rename {snapcraft => snapcraft_legacy}/storeapi/errors.py (99%) rename {snapcraft => snapcraft_legacy}/storeapi/http_clients/__init__.py (100%) rename {snapcraft => snapcraft_legacy}/storeapi/http_clients/_candid_client.py (99%) rename {snapcraft => snapcraft_legacy}/storeapi/http_clients/_config.py (100%) rename {snapcraft => snapcraft_legacy}/storeapi/http_clients/_http_client.py (100%) rename {snapcraft => snapcraft_legacy}/storeapi/http_clients/_ubuntu_sso_client.py (99%) rename {snapcraft => snapcraft_legacy}/storeapi/http_clients/agent.py (84%) rename {snapcraft => snapcraft_legacy}/storeapi/http_clients/errors.py (98%) rename {snapcraft => snapcraft_legacy}/storeapi/info.py (99%) rename {snapcraft => snapcraft_legacy}/storeapi/metrics.py (100%) rename {snapcraft => snapcraft_legacy}/storeapi/status.py (100%) rename {snapcraft => snapcraft_legacy}/storeapi/v2/__init__.py (100%) rename {snapcraft => snapcraft_legacy}/storeapi/v2/_api_schema.py (100%) rename {snapcraft => snapcraft_legacy}/storeapi/v2/channel_map.py (100%) rename {snapcraft => snapcraft_legacy}/storeapi/v2/releases.py (100%) rename {snapcraft => snapcraft_legacy}/storeapi/v2/validation_sets.py (100%) rename {snapcraft => snapcraft_legacy}/storeapi/v2/whoami.py (100%) rename {snapcraft => snapcraft_legacy}/yaml_utils/__init__.py (98%) rename {snapcraft => snapcraft_legacy}/yaml_utils/errors.py (98%) diff --git a/bin/snapcraftctl b/bin/snapcraftctl index 4971dbfe97..5eefd07828 100755 --- a/bin/snapcraftctl +++ b/bin/snapcraftctl @@ -30,10 +30,10 @@ quote() python3_command="${SNAPCRAFT_INTERPRETER:-$(command -v python3)}" snapcraftctl_command="$python3_command -I -c ' -import snapcraft.cli.__main__ +import snapcraft_legacy.cli.__main__ # Click strips off the first arg by default, so the -c will not be passed -snapcraft.cli.__main__.run_snapcraftctl(prog_name=\"snapcraftctl\") +snapcraft_legacy.cli.__main__.run_snapcraftctl(prog_name=\"snapcraftctl\") '" snapcraftctl_args=$(quote "$@") diff --git a/setup.py b/setup.py index 39f9d34bf6..39d83ed76d 100755 --- a/setup.py +++ b/setup.py @@ -139,7 +139,9 @@ def recursive_data_files(directory, install_directory): license=license, classifiers=classifiers, scripts=scripts, - entry_points=dict(console_scripts=["snapcraft = snapcraft.cli.__main__:run"]), + entry_points=dict( + console_scripts=["snapcraft = snapcraft_legacy.cli.__main__:run"] + ), data_files=( recursive_data_files("schema", "share/snapcraft") + recursive_data_files("keyrings", "share/snapcraft") diff --git a/snapcraft.spec b/snapcraft.spec index b3c2a0bb76..903a33291c 100644 --- a/snapcraft.spec +++ b/snapcraft.spec @@ -14,7 +14,7 @@ data += collect_data_files("lazr.uri") data += collect_data_files("wadllib") a = Analysis( - ["snapcraft\\cli\\__main__.py"], + ["snapcraft_legacy\\cli\\__main__.py"], pathex=[], binaries=[], datas=data, diff --git a/snapcraft/__init__.py b/snapcraft_legacy/__init__.py similarity index 95% rename from snapcraft/__init__.py rename to snapcraft_legacy/__init__.py index 8613351eaa..aec5daf4a7 100644 --- a/snapcraft/__init__.py +++ b/snapcraft_legacy/__init__.py @@ -338,20 +338,20 @@ def _get_version(): __version__ = _get_version() # Workaround for potential import loops. -from snapcraft.internal import repo # noqa isort:skip +from snapcraft_legacy.internal import repo # noqa isort:skip # For backwards compatibility with external plugins. -import snapcraft._legacy_loader # noqa: F401 isort:skip -from snapcraft.plugins.v1 import PluginV1 as BasePlugin # noqa: F401 isort:skip -from snapcraft import common # noqa -from snapcraft import extractors # noqa -from snapcraft import file_utils # noqa -from snapcraft import plugins # noqa -from snapcraft import shell_utils # noqa -from snapcraft import sources # noqa +import snapcraft_legacy._legacy_loader # noqa: F401 isort:skip +from snapcraft_legacy.plugins.v1 import PluginV1 as BasePlugin # noqa: F401 isort:skip +from snapcraft_legacy import common # noqa +from snapcraft_legacy import extractors # noqa +from snapcraft_legacy import file_utils # noqa +from snapcraft_legacy import plugins # noqa +from snapcraft_legacy import shell_utils # noqa +from snapcraft_legacy import sources # noqa # FIXME LP: #1662658 -from snapcraft._store import ( # noqa +from snapcraft_legacy._store import ( # noqa create_key, download, gated, @@ -367,4 +367,4 @@ def _get_version(): validate, ) -from snapcraft.project._project_options import ProjectOptions # noqa isort:skip +from snapcraft_legacy.project._project_options import ProjectOptions # noqa isort:skip diff --git a/snapcraft/_legacy_loader.py b/snapcraft_legacy/_legacy_loader.py similarity index 85% rename from snapcraft/_legacy_loader.py rename to snapcraft_legacy/_legacy_loader.py index da8c5e081c..ecf9059eeb 100644 --- a/snapcraft/_legacy_loader.py +++ b/snapcraft_legacy/_legacy_loader.py @@ -57,13 +57,13 @@ class LegacyPluginLoader(importlib.abc.Loader): def create_module(cls, spec): # Load the plugin from the new location. plugin_name = spec.name.split(".")[-1] - return importlib.import_module(f"snapcraft.plugins.v1.{plugin_name}") + return importlib.import_module(f"snapcraft_legacy.plugins.v1.{plugin_name}") @classmethod def exec_module(cls, module): # Rewrite the module __name__ to have that of the legacy import path. plugin_name = module.__name__.split(".")[-1] - module.__name__ = f"snapcraft.plugins.{plugin_name}" + module.__name__ = f"snapcraft_legacy.plugins.{plugin_name}" return module @@ -72,8 +72,10 @@ class LegacyPluginPathFinder(importlib.machinery.PathFinder): def find_spec(cls, fullname, path=None, target=None): # Ensure plugins using their original import paths can be found and # warn about their new import path. - if fullname in [f"snapcraft.plugins.{p}" for p in _VALID_V1_PLUGINS]: - warnings.warn("Plugin import path has changed to 'snapcraft.plugins.v1'") + if fullname in [f"snapcraft_legacy.plugins.{p}" for p in _VALID_V1_PLUGINS]: + warnings.warn( + "Plugin import path has changed to 'snapcraft_legacy.plugins.v1'" + ) return importlib.machinery.ModuleSpec(fullname, LegacyPluginLoader) else: return None diff --git a/snapcraft/_store.py b/snapcraft_legacy/_store.py similarity index 96% rename from snapcraft/_store.py rename to snapcraft_legacy/_store.py index 93978fc845..0e430e61b4 100644 --- a/snapcraft/_store.py +++ b/snapcraft_legacy/_store.py @@ -31,28 +31,28 @@ from tabulate import tabulate -from snapcraft import storeapi, yaml_utils +from snapcraft_legacy import storeapi, yaml_utils # Ideally we would move stuff into more logical components -from snapcraft.cli import echo -from snapcraft.file_utils import ( +from snapcraft_legacy.cli import echo +from snapcraft_legacy.file_utils import ( calculate_sha3_384, get_host_tool_path, get_snap_tool_path, ) -from snapcraft.internal import cache, deltas -from snapcraft.internal.deltas.errors import ( +from snapcraft_legacy.internal import cache, deltas +from snapcraft_legacy.internal.deltas.errors import ( DeltaGenerationError, DeltaGenerationTooBigError, ) -from snapcraft.internal.errors import SnapDataExtractionError, ToolMissingError -from snapcraft.storeapi.constants import DEFAULT_SERIES -from snapcraft.storeapi.metrics import MetricsFilter, MetricsResults +from snapcraft_legacy.internal.errors import SnapDataExtractionError, ToolMissingError +from snapcraft_legacy.storeapi.constants import DEFAULT_SERIES +from snapcraft_legacy.storeapi.metrics import MetricsFilter, MetricsResults if TYPE_CHECKING: - from snapcraft.storeapi._status_tracker import StatusTracker - from snapcraft.storeapi.v2.channel_map import ChannelMap - from snapcraft.storeapi.v2.releases import Releases + from snapcraft_legacy.storeapi._status_tracker import StatusTracker + from snapcraft_legacy.storeapi.v2.channel_map import ChannelMap + from snapcraft_legacy.storeapi.v2.releases import Releases logger = logging.getLogger(__name__) @@ -301,20 +301,20 @@ class StoreClientCLI(storeapi.StoreClient): # features are developed for them, but still provide a simple wrapper # method around those methods for backwards compatibility. # - # This class can be thought of and extension to snapcraft.cli.store. - # It just lives in snapcraft._store due to the convenience of the + # This class can be thought of and extension to snapcraft_legacy.cli.store. + # It just lives in snapcraft_legacy._store due to the convenience of the # methods it is trying to replace. Considering this is a private module - # and this class is not exported, moving it to snapcraft.cli can take + # and this class is not exported, moving it to snapcraft_legacy.cli can take # place. # # This is the list of items that needs to be tackled to get to there: # - # TODO create an internal copy of snapcraft.storeapi + # TODO create an internal copy of snapcraft_legacy.storeapi # TODO move configuration loading to this class and out of - # snapcraft.storeapi.StoreClient - # TODO Move progressbar implementation out of snapcraft.storeapi used + # snapcraft_legacy.storeapi.StoreClient + # TODO Move progressbar implementation out of snapcraft_legacy.storeapi used # during upload into this class using click. - # TODO use an instance of this class directly from snapcraft.cli.store + # TODO use an instance of this class directly from snapcraft_legacy.cli.store @_login_wrapper def close_channels( diff --git a/snapcraft/cli/__init__.py b/snapcraft_legacy/cli/__init__.py similarity index 89% rename from snapcraft/cli/__init__.py rename to snapcraft_legacy/cli/__init__.py index 7c8e9889e3..c2aed0019d 100644 --- a/snapcraft/cli/__init__.py +++ b/snapcraft_legacy/cli/__init__.py @@ -13,6 +13,6 @@ # # You should have received a copy of the GNU General Public License # along with this program. If not, see . -import snapcraft.internal.dirs +import snapcraft_legacy.internal.dirs -snapcraft.internal.dirs.setup_dirs() +snapcraft_legacy.internal.dirs.setup_dirs() diff --git a/snapcraft/cli/__main__.py b/snapcraft_legacy/cli/__main__.py similarity index 89% rename from snapcraft/cli/__main__.py rename to snapcraft_legacy/cli/__main__.py index 58308cfe8a..be854a0d25 100644 --- a/snapcraft/cli/__main__.py +++ b/snapcraft_legacy/cli/__main__.py @@ -20,9 +20,9 @@ import os import subprocess -from snapcraft.cli._runner import run -from snapcraft.cli.echo import warning -from snapcraft.cli.snapcraftctl._runner import run as run_snapcraftctl # noqa +from snapcraft_legacy.cli._runner import run +from snapcraft_legacy.cli.echo import warning +from snapcraft_legacy.cli.snapcraftctl._runner import run as run_snapcraftctl # noqa # If the locale ends up being ascii, Click will barf. Let's try to prevent that # here by using C.UTF-8 as a last-resort fallback. This mostly happens in CI, diff --git a/snapcraft/cli/_channel_map.py b/snapcraft_legacy/cli/_channel_map.py similarity index 99% rename from snapcraft/cli/_channel_map.py rename to snapcraft_legacy/cli/_channel_map.py index bbb8824adf..6f01bb3974 100644 --- a/snapcraft/cli/_channel_map.py +++ b/snapcraft_legacy/cli/_channel_map.py @@ -22,7 +22,7 @@ from tabulate import tabulate from typing_extensions import Final -from snapcraft.storeapi.v2.channel_map import ( +from snapcraft_legacy.storeapi.v2.channel_map import ( ChannelMap, MappedChannel, Revision, diff --git a/snapcraft/cli/_command_group.py b/snapcraft_legacy/cli/_command_group.py similarity index 97% rename from snapcraft/cli/_command_group.py rename to snapcraft_legacy/cli/_command_group.py index d7775823b7..9ec0188140 100644 --- a/snapcraft/cli/_command_group.py +++ b/snapcraft_legacy/cli/_command_group.py @@ -15,7 +15,7 @@ # along with this program. If not, see . import click -from snapcraft.internal import deprecations +from snapcraft_legacy.internal import deprecations from . import echo diff --git a/snapcraft/cli/_errors.py b/snapcraft_legacy/cli/_errors.py similarity index 96% rename from snapcraft/cli/_errors.py rename to snapcraft_legacy/cli/_errors.py index 98176bc3a7..48bcfdf0c1 100644 --- a/snapcraft/cli/_errors.py +++ b/snapcraft_legacy/cli/_errors.py @@ -26,9 +26,9 @@ from raven import Client as RavenClient from raven.transport import RequestsHTTPTransport -import snapcraft -from snapcraft.config import CLIConfig as _CLIConfig -from snapcraft.internal import errors +import snapcraft_legacy +from snapcraft_legacy.config import CLIConfig as _CLIConfig +from snapcraft_legacy.internal import errors from . import echo @@ -110,7 +110,7 @@ def _is_printable_traceback(exc_info, debug) -> bool: return True # Print if not using snap. - if not snapcraft.internal.common.is_snap(): + if not snapcraft_legacy.internal.common.is_snap(): return True return False @@ -122,7 +122,7 @@ def _handle_sentry_submission(exc_info) -> None: return # Only attempt if running as snap (with Raven requirement). - if not snapcraft.internal.common.is_snap(): + if not snapcraft_legacy.internal.common.is_snap(): # Suggest manual reporting instead. click.echo(_MSG_MANUALLY_REPORT) return @@ -259,7 +259,7 @@ def exception_handler( # noqa: C901 (a) no TTY (b) not running as snap - - Use exit code from snapcraft error (if available), otherwise 1. + - Use exit code from snapcraft_legacy error (if available), otherwise 1. """ exit_code = _get_exception_exit_code(exception) exc_info = (exception_type, exception, exception_traceback) @@ -361,8 +361,8 @@ def validate(value): def _submit_trace(exc_info): kwargs: Dict[str, str] = dict() - if "+git" not in snapcraft.__version__: - kwargs["release"] = snapcraft.__version__ + if "+git" not in snapcraft_legacy.__version__: + kwargs["release"] = snapcraft_legacy.__version__ client = RavenClient( "https://b0fef3e0ced2443c92143ae0d038b0a4:" diff --git a/snapcraft/cli/_metrics.py b/snapcraft_legacy/cli/_metrics.py similarity index 98% rename from snapcraft/cli/_metrics.py rename to snapcraft_legacy/cli/_metrics.py index e858017575..a58f15edc6 100644 --- a/snapcraft/cli/_metrics.py +++ b/snapcraft_legacy/cli/_metrics.py @@ -19,7 +19,7 @@ import pkg_resources -from snapcraft.storeapi import metrics as metrics_module +from snapcraft_legacy.storeapi import metrics as metrics_module logger = logging.getLogger(__name__) diff --git a/snapcraft/cli/_options.py b/snapcraft_legacy/cli/_options.py similarity index 98% rename from snapcraft/cli/_options.py rename to snapcraft_legacy/cli/_options.py index a1492c878c..d634ca4f45 100644 --- a/snapcraft/cli/_options.py +++ b/snapcraft_legacy/cli/_options.py @@ -21,10 +21,10 @@ import click -from snapcraft.cli.echo import confirm, prompt, warning -from snapcraft.internal import common, errors -from snapcraft.internal.meta.snap import Snap -from snapcraft.project import Project, get_snapcraft_yaml +from snapcraft_legacy.cli.echo import confirm, prompt, warning +from snapcraft_legacy.internal import common, errors +from snapcraft_legacy.internal.meta.snap import Snap +from snapcraft_legacy.project import Project, get_snapcraft_yaml class PromptOption(click.Option): diff --git a/snapcraft/cli/_review.py b/snapcraft_legacy/cli/_review.py similarity index 96% rename from snapcraft/cli/_review.py rename to snapcraft_legacy/cli/_review.py index 76f69a0a68..e7690dfa93 100644 --- a/snapcraft/cli/_review.py +++ b/snapcraft_legacy/cli/_review.py @@ -18,7 +18,7 @@ import click -from snapcraft.internal import review_tools +from snapcraft_legacy.internal import review_tools from . import echo diff --git a/snapcraft/cli/_runner.py b/snapcraft_legacy/cli/_runner.py similarity index 94% rename from snapcraft/cli/_runner.py rename to snapcraft_legacy/cli/_runner.py index e38e9a310d..326cb43119 100644 --- a/snapcraft/cli/_runner.py +++ b/snapcraft_legacy/cli/_runner.py @@ -21,8 +21,8 @@ import click -import snapcraft -from snapcraft.internal import log +import snapcraft_legacy +from snapcraft_legacy.internal import log from ._command_group import SnapcraftGroup from ._errors import exception_handler @@ -86,7 +86,7 @@ def configure_requests_ca() -> None: context_settings=dict(help_option_names=["-h", "--help"]), ) @click.version_option( - message=SNAPCRAFT_VERSION_TEMPLATE, version=snapcraft.__version__ # type: ignore + message=SNAPCRAFT_VERSION_TEMPLATE, version=snapcraft_legacy.__version__ # type: ignore ) @click.pass_context @add_provider_options(hidden=True) @@ -99,7 +99,7 @@ def run(ctx, debug, catch_exceptions=False, **kwargs): log_level = logging.DEBUG click.echo( "Starting snapcraft {} from {}.".format( - snapcraft.__version__, os.path.dirname(__file__) + snapcraft_legacy.__version__, os.path.dirname(__file__) ) ) else: diff --git a/snapcraft/cli/assertions.py b/snapcraft_legacy/cli/assertions.py similarity index 94% rename from snapcraft/cli/assertions.py rename to snapcraft_legacy/cli/assertions.py index 8e9c1eb145..7016d68638 100644 --- a/snapcraft/cli/assertions.py +++ b/snapcraft_legacy/cli/assertions.py @@ -15,7 +15,7 @@ # along with this program. If not, see . import os import json -from snapcraft.internal.errors import details_from_command_error +from snapcraft_legacy.internal.errors import details_from_command_error import subprocess import tempfile from datetime import datetime @@ -25,9 +25,9 @@ import click from tabulate import tabulate -import snapcraft -from snapcraft._store import StoreClientCLI -from snapcraft import yaml_utils +import snapcraft_legacy +from snapcraft_legacy._store import StoreClientCLI +from snapcraft_legacy import yaml_utils from . import echo @@ -65,14 +65,14 @@ def list_keys(): This command has an alias of `keys`. """ - snapcraft.list_keys() + snapcraft_legacy.list_keys() @assertionscli.command("create-key") @click.argument("key-name", metavar="", required=False) def create_key(key_name: str) -> None: """Create a key to sign assertions.""" - snapcraft.create_key(key_name) + snapcraft_legacy.create_key(key_name) @assertionscli.command("register-key") @@ -85,7 +85,7 @@ def create_key(key_name: str) -> None: ) def register_key(key_name: str, experimental_login: bool) -> None: """Register a key with the store to sign assertions.""" - snapcraft.register_key(key_name, use_candid=experimental_login) + snapcraft_legacy.register_key(key_name, use_candid=experimental_login) @assertionscli.command("sign-build") @@ -100,7 +100,7 @@ def register_key(key_name: str, experimental_login: bool) -> None: ) def sign_build(snap_file: str, key_name: str, local: bool) -> None: """Sign a built snap file and assert it using the developer's key.""" - snapcraft.sign_build(snap_file, key_name=key_name, local=local) + snapcraft_legacy.sign_build(snap_file, key_name=key_name, local=local) @assertionscli.command() @@ -116,14 +116,14 @@ def validate(snap_name: str, validations: list, key_name: str, revoke: bool) -> - = - = """ - snapcraft.validate(snap_name, validations, revoke=revoke, key=key_name) + snapcraft_legacy.validate(snap_name, validations, revoke=revoke, key=key_name) @assertionscli.command() @click.argument("snap-name", metavar="") def gated(snap_name: str) -> None: """Get the list of snaps and revisions gating a snap.""" - snapcraft.gated(snap_name) + snapcraft_legacy.gated(snap_name) @assertionscli.command("list-validation-sets") diff --git a/snapcraft/cli/containers.py b/snapcraft_legacy/cli/containers.py similarity index 95% rename from snapcraft/cli/containers.py rename to snapcraft_legacy/cli/containers.py index 64bcd3f2c8..bfe5b578ac 100644 --- a/snapcraft/cli/containers.py +++ b/snapcraft_legacy/cli/containers.py @@ -16,7 +16,7 @@ import click -from snapcraft.internal import repo +from snapcraft_legacy.internal import repo @click.group() diff --git a/snapcraft/cli/discovery.py b/snapcraft_legacy/cli/discovery.py similarity index 83% rename from snapcraft/cli/discovery.py rename to snapcraft_legacy/cli/discovery.py index 71c0527488..898a27ad2a 100644 --- a/snapcraft/cli/discovery.py +++ b/snapcraft_legacy/cli/discovery.py @@ -19,10 +19,13 @@ import click -import snapcraft -from snapcraft.internal import errors -from snapcraft.internal.common import format_output_in_columns, get_terminal_width -from snapcraft.project import errors as project_errors +import snapcraft_legacy +from snapcraft_legacy.internal import errors +from snapcraft_legacy.internal.common import ( + format_output_in_columns, + get_terminal_width, +) +from snapcraft_legacy.project import errors as project_errors from ._options import get_project @@ -44,9 +47,9 @@ def _try_get_base_from_project() -> str: def _get_modules_iter(base: str) -> Iterable: if base == "core18": - modules_path = snapcraft.plugins.v1.__path__ # type: ignore # mypy issue #1422 + modules_path = snapcraft_legacy.plugins.v1.__path__ # type: ignore # mypy issue #1422 else: - modules_path = snapcraft.plugins.v2.__path__ # type: ignore # mypy issue #1422 + modules_path = snapcraft_legacy.plugins.v2.__path__ # type: ignore # mypy issue #1422 # TODO make this part of plugin_finder. return pkgutil.iter_modules(modules_path) diff --git a/snapcraft/cli/echo.py b/snapcraft_legacy/cli/echo.py similarity index 97% rename from snapcraft/cli/echo.py rename to snapcraft_legacy/cli/echo.py index 575b7abaa7..1399b0e07a 100644 --- a/snapcraft/cli/echo.py +++ b/snapcraft_legacy/cli/echo.py @@ -26,7 +26,7 @@ import click -from snapcraft.internal import common +from snapcraft_legacy.internal import common def is_tty_connected() -> bool: @@ -41,7 +41,7 @@ def wrapped(msg: str) -> None: """Output msg wrapped to the terminal width to stdout. The maximum wrapping is determined by - snapcraft.internal.common.MAX_CHARACTERS_WRAP + snapcraft_legacy.internal.common.MAX_CHARACTERS_WRAP """ click.echo( click.formatting.wrap_text( diff --git a/snapcraft/cli/extensions.py b/snapcraft_legacy/cli/extensions.py similarity index 96% rename from snapcraft/cli/extensions.py rename to snapcraft_legacy/cli/extensions.py index c0f58bb9ee..f3999efa5b 100644 --- a/snapcraft/cli/extensions.py +++ b/snapcraft_legacy/cli/extensions.py @@ -22,8 +22,8 @@ import click import tabulate -from snapcraft import yaml_utils -from snapcraft.internal import project_loader +from snapcraft_legacy import yaml_utils +from snapcraft_legacy.internal import project_loader from ._options import get_project diff --git a/snapcraft/cli/help.py b/snapcraft_legacy/cli/help.py similarity index 93% rename from snapcraft/cli/help.py rename to snapcraft_legacy/cli/help.py index 3a59fd2da2..c59bbc65b4 100644 --- a/snapcraft/cli/help.py +++ b/snapcraft_legacy/cli/help.py @@ -19,14 +19,14 @@ import click -import snapcraft -from snapcraft.internal import errors, sources -from snapcraft.project import errors as project_errors +import snapcraft_legacy +from snapcraft_legacy.internal import errors, sources +from snapcraft_legacy.project import errors as project_errors from . import echo from ._options import get_project -_TOPICS = {"sources": sources, "plugins": snapcraft} +_TOPICS = {"sources": sources, "plugins": snapcraft_legacy} @click.group() @@ -138,7 +138,7 @@ def _module_help(plugin_name: str, devel: bool, base: str): plugin_version = "v1" module = importlib.import_module( - f"snapcraft.plugins.{plugin_version}.{module_name}" + f"snapcraft_legacy.plugins.{plugin_version}.{module_name}" ) if module.__doc__ and devel: help(module) diff --git a/snapcraft/cli/lifecycle.py b/snapcraft_legacy/cli/lifecycle.py similarity index 98% rename from snapcraft/cli/lifecycle.py rename to snapcraft_legacy/cli/lifecycle.py index e8bfa7e9e8..bf1d1bb512 100644 --- a/snapcraft/cli/lifecycle.py +++ b/snapcraft_legacy/cli/lifecycle.py @@ -27,8 +27,8 @@ import click import progressbar -from snapcraft import file_utils -from snapcraft.internal import ( +from snapcraft_legacy import file_utils +from snapcraft_legacy.internal import ( build_providers, deprecations, errors, @@ -37,8 +37,8 @@ project_loader, steps, ) -from snapcraft.internal.repo import ua_manager -from snapcraft.project._sanity_checks import conduct_project_sanity_check +from snapcraft_legacy.internal.repo import ua_manager +from snapcraft_legacy.project._sanity_checks import conduct_project_sanity_check from . import echo from ._errors import TRACEBACK_HOST, TRACEBACK_MANAGED @@ -54,7 +54,7 @@ if typing.TYPE_CHECKING: - from snapcraft.internal.project import Project # noqa: F401 + from snapcraft_legacy.internal.project import Project # noqa: F401 # TODO: when snap is a real step we can simplify the arguments here. diff --git a/snapcraft/cli/remote.py b/snapcraft_legacy/cli/remote.py similarity index 97% rename from snapcraft/cli/remote.py rename to snapcraft_legacy/cli/remote.py index 4132dd4eb1..3b8d56718f 100644 --- a/snapcraft/cli/remote.py +++ b/snapcraft_legacy/cli/remote.py @@ -21,9 +21,9 @@ import click from xdg import BaseDirectory -from snapcraft.formatting_utils import humanize_list -from snapcraft.internal.remote_build import LaunchpadClient, WorkTree, errors -from snapcraft.project import Project +from snapcraft_legacy.formatting_utils import humanize_list +from snapcraft_legacy.internal.remote_build import LaunchpadClient, WorkTree, errors +from snapcraft_legacy.project import Project from . import echo from ._options import PromptOption, get_project diff --git a/snapcraft/cli/snapcraftctl/__init__.py b/snapcraft_legacy/cli/snapcraftctl/__init__.py similarity index 100% rename from snapcraft/cli/snapcraftctl/__init__.py rename to snapcraft_legacy/cli/snapcraftctl/__init__.py diff --git a/snapcraft/cli/snapcraftctl/_runner.py b/snapcraft_legacy/cli/snapcraftctl/_runner.py similarity index 97% rename from snapcraft/cli/snapcraftctl/_runner.py rename to snapcraft_legacy/cli/snapcraftctl/_runner.py index edf27183ed..b0983d492f 100644 --- a/snapcraft/cli/snapcraftctl/_runner.py +++ b/snapcraft_legacy/cli/snapcraftctl/_runner.py @@ -22,8 +22,8 @@ import click -from snapcraft.cli._errors import exception_handler -from snapcraft.internal import errors, log +from snapcraft_legacy.cli._errors import exception_handler +from snapcraft_legacy.internal import errors, log @click.group() diff --git a/snapcraft/cli/store.py b/snapcraft_legacy/cli/store.py similarity index 98% rename from snapcraft/cli/store.py rename to snapcraft_legacy/cli/store.py index 1b02fd9d0d..418fbff5b9 100644 --- a/snapcraft/cli/store.py +++ b/snapcraft_legacy/cli/store.py @@ -27,11 +27,11 @@ import click from tabulate import tabulate -import snapcraft -from snapcraft import formatting_utils, storeapi -from snapcraft._store import StoreClientCLI -from snapcraft.storeapi import metrics as metrics_module -from snapcraft.storeapi.constants import DEFAULT_SERIES +import snapcraft_legacy +from snapcraft_legacy import formatting_utils, storeapi +from snapcraft_legacy._store import StoreClientCLI +from snapcraft_legacy.storeapi import metrics as metrics_module +from snapcraft_legacy.storeapi.constants import DEFAULT_SERIES from . import echo from ._channel_map import get_tabulated_channel_map @@ -129,7 +129,7 @@ def register(snap_name, private, store, yes): if private: click.echo(_MESSAGE_REGISTER_PRIVATE.format(snap_name)) if yes or echo.confirm(_MESSAGE_REGISTER_CONFIRM.format(snap_name)): - snapcraft.register(snap_name, is_private=private, store_id=store) + snapcraft_legacy.register(snap_name, is_private=private, store_id=store) click.echo(_MESSAGE_REGISTER_SUCCESS.format(snap_name)) else: click.echo(_MESSAGE_REGISTER_NO.format(snap_name)) @@ -177,7 +177,7 @@ def upload(snap_file, release): channel_list = None review_snap(snap_file=snap_file) - snap_name, snap_revision = snapcraft.upload(snap_file, channel_list) + snap_name, snap_revision = snapcraft_legacy.upload(snap_file, channel_list) echo.info("Revision {!r} of {!r} created.".format(snap_revision, snap_name)) if channel_list: @@ -225,7 +225,7 @@ def upload_metadata(snap_file, force): snapcraft upload-metadata my-snap_0.1_amd64.snap --force """ click.echo("Uploading metadata from {!r}".format(os.path.basename(snap_file))) - snapcraft.upload_metadata(snap_file, force) + snapcraft_legacy.upload_metadata(snap_file, force) @storecli.command() @@ -648,7 +648,7 @@ def list_registered(): Examples: snapcraft list """ - snapcraft.list_registered() + snapcraft_legacy.list_registered() @storecli.command("export-login") @@ -732,7 +732,7 @@ def export_login( save=False, ) else: - snapcraft.login( + snapcraft_legacy.login( store=store_client, packages=snap_list, channels=channel_list, @@ -815,7 +815,7 @@ def login(login_file, experimental_login: bool): if store_client.use_candid: store_client.login(config_fd=login_file, save=True) else: - snapcraft.login(store=store_client, config_fd=login_file) + snapcraft_legacy.login(store=store_client, config_fd=login_file) print() diff --git a/snapcraft/cli/version.py b/snapcraft_legacy/cli/version.py similarity index 95% rename from snapcraft/cli/version.py rename to snapcraft_legacy/cli/version.py index 613aa458e7..e39e497b92 100644 --- a/snapcraft/cli/version.py +++ b/snapcraft_legacy/cli/version.py @@ -16,7 +16,7 @@ import click -import snapcraft +import snapcraft_legacy SNAPCRAFT_VERSION_TEMPLATE = "snapcraft, version %(version)s" @@ -35,4 +35,4 @@ def version(): snapcraft version snapcraft --version """ - click.echo(SNAPCRAFT_VERSION_TEMPLATE % {"version": snapcraft.__version__}) + click.echo(SNAPCRAFT_VERSION_TEMPLATE % {"version": snapcraft_legacy.__version__}) diff --git a/snapcraft/common.py b/snapcraft_legacy/common.py similarity index 60% rename from snapcraft/common.py rename to snapcraft_legacy/common.py index bdbb49078b..a36ca26006 100644 --- a/snapcraft/common.py +++ b/snapcraft_legacy/common.py @@ -15,13 +15,13 @@ # along with this program. If not, see . # These are now available via file_utils, but don't break API. -from snapcraft.file_utils import link_or_copy # noqa -from snapcraft.file_utils import replace_in_file # noqa +from snapcraft_legacy.file_utils import link_or_copy # noqa +from snapcraft_legacy.file_utils import replace_in_file # noqa # These are now available via formatting_utils, but don't break API. -from snapcraft.formatting_utils import combine_paths # noqa -from snapcraft.formatting_utils import format_path_variable # noqa -from snapcraft.internal.common import get_include_paths # noqa -from snapcraft.internal.common import get_library_paths # noqa -from snapcraft.internal.common import get_python2_path # noqa -from snapcraft.internal.common import isurl # noqa +from snapcraft_legacy.formatting_utils import combine_paths # noqa +from snapcraft_legacy.formatting_utils import format_path_variable # noqa +from snapcraft_legacy.internal.common import get_include_paths # noqa +from snapcraft_legacy.internal.common import get_library_paths # noqa +from snapcraft_legacy.internal.common import get_python2_path # noqa +from snapcraft_legacy.internal.common import isurl # noqa diff --git a/snapcraft/config.py b/snapcraft_legacy/config.py similarity index 98% rename from snapcraft/config.py rename to snapcraft_legacy/config.py index 3e7eac81b5..c33bc1682a 100644 --- a/snapcraft/config.py +++ b/snapcraft_legacy/config.py @@ -22,7 +22,7 @@ from xdg import BaseDirectory -from snapcraft.internal.errors import SnapcraftInvalidCLIConfigError +from snapcraft_legacy.internal.errors import SnapcraftInvalidCLIConfigError logger = logging.getLogger(__name__) diff --git a/snapcraft/extractors/__init__.py b/snapcraft_legacy/extractors/__init__.py similarity index 100% rename from snapcraft/extractors/__init__.py rename to snapcraft_legacy/extractors/__init__.py diff --git a/snapcraft/extractors/_errors.py b/snapcraft_legacy/extractors/_errors.py similarity index 96% rename from snapcraft/extractors/_errors.py rename to snapcraft_legacy/extractors/_errors.py index 3ca24bd20d..9d3ee346bb 100644 --- a/snapcraft/extractors/_errors.py +++ b/snapcraft_legacy/extractors/_errors.py @@ -14,7 +14,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -from snapcraft.internal.errors import MetadataExtractionError +from snapcraft_legacy.internal.errors import MetadataExtractionError class UnhandledFileError(MetadataExtractionError): diff --git a/snapcraft/extractors/_metadata.py b/snapcraft_legacy/extractors/_metadata.py similarity index 99% rename from snapcraft/extractors/_metadata.py rename to snapcraft_legacy/extractors/_metadata.py index 3dc7af5962..5678ea8112 100644 --- a/snapcraft/extractors/_metadata.py +++ b/snapcraft_legacy/extractors/_metadata.py @@ -16,7 +16,7 @@ from typing import Any, Dict, List, Optional, Set, Union -from snapcraft import yaml_utils +from snapcraft_legacy import yaml_utils class ExtractedMetadata(yaml_utils.SnapcraftYAMLObject): diff --git a/snapcraft/extractors/appstream.py b/snapcraft_legacy/extractors/appstream.py similarity index 99% rename from snapcraft/extractors/appstream.py rename to snapcraft_legacy/extractors/appstream.py index ec76f035ba..cd53eec547 100644 --- a/snapcraft/extractors/appstream.py +++ b/snapcraft_legacy/extractors/appstream.py @@ -23,7 +23,7 @@ import lxml.etree from xdg.DesktopEntry import DesktopEntry -from snapcraft.extractors import _errors +from snapcraft_legacy.extractors import _errors from ._metadata import ExtractedMetadata diff --git a/snapcraft/extractors/setuppy.py b/snapcraft_legacy/extractors/setuppy.py similarity index 98% rename from snapcraft/extractors/setuppy.py rename to snapcraft_legacy/extractors/setuppy.py index 48bcc8ba5d..771e5579a4 100644 --- a/snapcraft/extractors/setuppy.py +++ b/snapcraft_legacy/extractors/setuppy.py @@ -21,7 +21,7 @@ from typing import Dict # noqa: F401 from unittest.mock import patch -from snapcraft.extractors import _errors +from snapcraft_legacy.extractors import _errors from ._metadata import ExtractedMetadata diff --git a/snapcraft/file_utils.py b/snapcraft_legacy/file_utils.py similarity index 99% rename from snapcraft/file_utils.py rename to snapcraft_legacy/file_utils.py index 4c972db14b..24f277b32e 100644 --- a/snapcraft/file_utils.py +++ b/snapcraft_legacy/file_utils.py @@ -27,7 +27,7 @@ from contextlib import contextmanager, suppress from typing import Callable, Generator, List, Optional, Pattern, Set -from snapcraft.internal import common, errors +from snapcraft_legacy.internal import common, errors logger = logging.getLogger(__name__) @@ -396,7 +396,7 @@ def get_linker_version_from_file(linker_file: str) -> str: the linker from libc6 or related. :returns: the version extracted from the linker file. :rtype: string - :raises snapcraft.internal.errors.errors.SnapcraftEnvironmentError: + :raises snapcraft_legacy.internal.errors.errors.SnapcraftEnvironmentError: if linker_file is not of the expected format. """ m = re.search(r"ld-(?P[\d.]+).so$", linker_file) diff --git a/snapcraft/formatting_utils.py b/snapcraft_legacy/formatting_utils.py similarity index 100% rename from snapcraft/formatting_utils.py rename to snapcraft_legacy/formatting_utils.py diff --git a/snapcraft/internal/__init__.py b/snapcraft_legacy/internal/__init__.py similarity index 80% rename from snapcraft/internal/__init__.py rename to snapcraft_legacy/internal/__init__.py index cc8692c426..541746bd07 100644 --- a/snapcraft/internal/__init__.py +++ b/snapcraft_legacy/internal/__init__.py @@ -14,6 +14,6 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -from snapcraft.internal import cache # noqa -from snapcraft.internal import deltas # noqa -from snapcraft.internal import states # noqa +from snapcraft_legacy.internal import cache # noqa +from snapcraft_legacy.internal import deltas # noqa +from snapcraft_legacy.internal import states # noqa diff --git a/snapcraft/internal/build_providers/__init__.py b/snapcraft_legacy/internal/build_providers/__init__.py similarity index 100% rename from snapcraft/internal/build_providers/__init__.py rename to snapcraft_legacy/internal/build_providers/__init__.py diff --git a/snapcraft/internal/build_providers/_base_provider.py b/snapcraft_legacy/internal/build_providers/_base_provider.py similarity index 98% rename from snapcraft/internal/build_providers/_base_provider.py rename to snapcraft_legacy/internal/build_providers/_base_provider.py index 9ca9717189..fcf707a499 100644 --- a/snapcraft/internal/build_providers/_base_provider.py +++ b/snapcraft_legacy/internal/build_providers/_base_provider.py @@ -30,9 +30,9 @@ import pkg_resources from xdg import BaseDirectory -import snapcraft -from snapcraft import yaml_utils -from snapcraft.internal import common, steps +import snapcraft_legacy +from snapcraft_legacy import yaml_utils +from snapcraft_legacy.internal import common, steps from . import errors from ._snap import SnapInjector @@ -310,7 +310,7 @@ def _check_environment_needs_cleaning(self) -> bool: ) return True elif pkg_resources.parse_version( - snapcraft._get_version() + snapcraft_legacy._get_version() ) < pkg_resources.parse_version(built_by): self.echoer.warning( f"Build environment was created with newer snapcraft version {built_by!r}, cleaning first." @@ -469,7 +469,7 @@ def _setup_snapcraft(self) -> None: self._save_info( data={ "base": self.project._get_build_base(), - "created-by-snapcraft-version": snapcraft._get_version(), + "created-by-snapcraft-version": snapcraft_legacy._get_version(), "host-project-directory": self.project._project_dir, } ) diff --git a/snapcraft/internal/build_providers/_factory.py b/snapcraft_legacy/internal/build_providers/_factory.py similarity index 100% rename from snapcraft/internal/build_providers/_factory.py rename to snapcraft_legacy/internal/build_providers/_factory.py diff --git a/snapcraft/internal/build_providers/_lxd/__init__.py b/snapcraft_legacy/internal/build_providers/_lxd/__init__.py similarity index 100% rename from snapcraft/internal/build_providers/_lxd/__init__.py rename to snapcraft_legacy/internal/build_providers/_lxd/__init__.py diff --git a/snapcraft/internal/build_providers/_lxd/_images.py b/snapcraft_legacy/internal/build_providers/_lxd/_images.py similarity index 100% rename from snapcraft/internal/build_providers/_lxd/_images.py rename to snapcraft_legacy/internal/build_providers/_lxd/_images.py diff --git a/snapcraft/internal/build_providers/_lxd/_lxd.py b/snapcraft_legacy/internal/build_providers/_lxd/_lxd.py similarity index 99% rename from snapcraft/internal/build_providers/_lxd/_lxd.py rename to snapcraft_legacy/internal/build_providers/_lxd/_lxd.py index 866579a3a8..35cdb56b5e 100644 --- a/snapcraft/internal/build_providers/_lxd/_lxd.py +++ b/snapcraft_legacy/internal/build_providers/_lxd/_lxd.py @@ -24,8 +24,8 @@ from time import sleep from typing import Dict, Optional, Sequence -from snapcraft.internal import common, repo -from snapcraft.internal.errors import SnapcraftEnvironmentError +from snapcraft_legacy.internal import common, repo +from snapcraft_legacy.internal.errors import SnapcraftEnvironmentError from .._base_provider import Provider, errors from ._images import get_image_source diff --git a/snapcraft/internal/build_providers/_multipass/__init__.py b/snapcraft_legacy/internal/build_providers/_multipass/__init__.py similarity index 100% rename from snapcraft/internal/build_providers/_multipass/__init__.py rename to snapcraft_legacy/internal/build_providers/_multipass/__init__.py diff --git a/snapcraft/internal/build_providers/_multipass/_instance_info.py b/snapcraft_legacy/internal/build_providers/_multipass/_instance_info.py similarity index 95% rename from snapcraft/internal/build_providers/_multipass/_instance_info.py rename to snapcraft_legacy/internal/build_providers/_multipass/_instance_info.py index 6a710b929f..e17f8992b4 100644 --- a/snapcraft/internal/build_providers/_multipass/_instance_info.py +++ b/snapcraft_legacy/internal/build_providers/_multipass/_instance_info.py @@ -17,7 +17,7 @@ import json from typing import Any, Dict, Type -from snapcraft.internal.build_providers import errors +from snapcraft_legacy.internal.build_providers import errors class InstanceInfo: @@ -33,7 +33,7 @@ def from_json( multipass info command. :returns: an InstanceInfo. :rtype: InstanceInfo - :raises snapcraft.internal.build_providers.ProviderInfoDataKeyError: + :raises snapcraft_legacy.internal.build_providers.ProviderInfoDataKeyError: if the instance name cannot be found in the given json or if a required key is missing from that data structure for the instance. """ diff --git a/snapcraft/internal/build_providers/_multipass/_multipass.py b/snapcraft_legacy/internal/build_providers/_multipass/_multipass.py similarity index 99% rename from snapcraft/internal/build_providers/_multipass/_multipass.py rename to snapcraft_legacy/internal/build_providers/_multipass/_multipass.py index 8c34f016fe..d1c3998761 100644 --- a/snapcraft/internal/build_providers/_multipass/_multipass.py +++ b/snapcraft_legacy/internal/build_providers/_multipass/_multipass.py @@ -19,7 +19,7 @@ import sys from typing import Dict, Optional, Sequence -from snapcraft.internal.errors import SnapcraftEnvironmentError +from snapcraft_legacy.internal.errors import SnapcraftEnvironmentError from .. import errors from .._base_provider import Provider diff --git a/snapcraft/internal/build_providers/_multipass/_multipass_command.py b/snapcraft_legacy/internal/build_providers/_multipass/_multipass_command.py similarity index 98% rename from snapcraft/internal/build_providers/_multipass/_multipass_command.py rename to snapcraft_legacy/internal/build_providers/_multipass/_multipass_command.py index 0fd1a2e765..5ed4d3beb8 100644 --- a/snapcraft/internal/build_providers/_multipass/_multipass_command.py +++ b/snapcraft_legacy/internal/build_providers/_multipass/_multipass_command.py @@ -30,9 +30,9 @@ Union, ) -from snapcraft.internal import repo -from snapcraft.internal.build_providers import errors -from snapcraft.internal.errors import SnapcraftEnvironmentError +from snapcraft_legacy.internal import repo +from snapcraft_legacy.internal.build_providers import errors +from snapcraft_legacy.internal.errors import SnapcraftEnvironmentError from ._windows import windows_install_multipass, windows_reload_multipass_path_env diff --git a/snapcraft/internal/build_providers/_multipass/_windows.py b/snapcraft_legacy/internal/build_providers/_multipass/_windows.py similarity index 97% rename from snapcraft/internal/build_providers/_multipass/_windows.py rename to snapcraft_legacy/internal/build_providers/_multipass/_windows.py index ed86877afd..f6a93fb8a0 100644 --- a/snapcraft/internal/build_providers/_multipass/_windows.py +++ b/snapcraft_legacy/internal/build_providers/_multipass/_windows.py @@ -24,12 +24,12 @@ import requests import simplejson -from snapcraft.file_utils import calculate_sha3_384 -from snapcraft.internal.build_providers.errors import ( +from snapcraft_legacy.file_utils import calculate_sha3_384 +from snapcraft_legacy.internal.build_providers.errors import ( ProviderMultipassDownloadFailed, ProviderMultipassInstallationFailed, ) -from snapcraft.internal.indicators import download_requests_stream +from snapcraft_legacy.internal.indicators import download_requests_stream if sys.platform == "win32": import winreg diff --git a/snapcraft/internal/build_providers/_snap.py b/snapcraft_legacy/internal/build_providers/_snap.py similarity index 99% rename from snapcraft/internal/build_providers/_snap.py rename to snapcraft_legacy/internal/build_providers/_snap.py index fb35c65d0b..3950bf3dab 100644 --- a/snapcraft/internal/build_providers/_snap.py +++ b/snapcraft_legacy/internal/build_providers/_snap.py @@ -21,8 +21,8 @@ import tempfile from typing import Any, Callable, Dict, List, Optional # noqa: F401 -from snapcraft import storeapi, yaml_utils -from snapcraft.internal import common, repo +from snapcraft_legacy import storeapi, yaml_utils +from snapcraft_legacy.internal import common, repo logger = logging.getLogger(__name__) diff --git a/snapcraft/internal/build_providers/errors.py b/snapcraft_legacy/internal/build_providers/errors.py similarity index 98% rename from snapcraft/internal/build_providers/errors.py rename to snapcraft_legacy/internal/build_providers/errors.py index 6dda2a2da1..18e62d45b4 100644 --- a/snapcraft/internal/build_providers/errors.py +++ b/snapcraft_legacy/internal/build_providers/errors.py @@ -18,8 +18,8 @@ from typing import Sequence # noqa: F401 from typing import Any, Dict, Optional -from snapcraft.internal.errors import SnapcraftError as _SnapcraftError -from snapcraft.internal.errors import SnapcraftException as _SnapcraftException +from snapcraft_legacy.internal.errors import SnapcraftError as _SnapcraftError +from snapcraft_legacy.internal.errors import SnapcraftException as _SnapcraftException class ProviderBaseError(_SnapcraftError): diff --git a/snapcraft/internal/cache/__init__.py b/snapcraft_legacy/internal/cache/__init__.py similarity index 100% rename from snapcraft/internal/cache/__init__.py rename to snapcraft_legacy/internal/cache/__init__.py diff --git a/snapcraft/internal/cache/_apt.py b/snapcraft_legacy/internal/cache/_apt.py similarity index 100% rename from snapcraft/internal/cache/_apt.py rename to snapcraft_legacy/internal/cache/_apt.py diff --git a/snapcraft/internal/cache/_cache.py b/snapcraft_legacy/internal/cache/_cache.py similarity index 100% rename from snapcraft/internal/cache/_cache.py rename to snapcraft_legacy/internal/cache/_cache.py diff --git a/snapcraft/internal/cache/_file.py b/snapcraft_legacy/internal/cache/_file.py similarity index 98% rename from snapcraft/internal/cache/_file.py rename to snapcraft_legacy/internal/cache/_file.py index bf146966c0..20033e3aeb 100644 --- a/snapcraft/internal/cache/_file.py +++ b/snapcraft_legacy/internal/cache/_file.py @@ -18,7 +18,7 @@ import shutil from typing import Optional -from snapcraft.file_utils import calculate_hash +from snapcraft_legacy.file_utils import calculate_hash from ._cache import SnapcraftCache diff --git a/snapcraft/internal/cache/_snap.py b/snapcraft_legacy/internal/cache/_snap.py similarity index 98% rename from snapcraft/internal/cache/_snap.py rename to snapcraft_legacy/internal/cache/_snap.py index 37aacd9b63..436ae968ff 100644 --- a/snapcraft/internal/cache/_snap.py +++ b/snapcraft_legacy/internal/cache/_snap.py @@ -21,7 +21,7 @@ import tempfile from pathlib import Path -from snapcraft import file_utils, yaml_utils +from snapcraft_legacy import file_utils, yaml_utils from ._cache import SnapcraftProjectCache diff --git a/snapcraft/internal/common.py b/snapcraft_legacy/internal/common.py similarity index 99% rename from snapcraft/internal/common.py rename to snapcraft_legacy/internal/common.py index 864f5027a4..ce74c3e1ec 100644 --- a/snapcraft/internal/common.py +++ b/snapcraft_legacy/internal/common.py @@ -30,7 +30,7 @@ from pathlib import Path from typing import Callable, List, Union -from snapcraft.internal import errors +from snapcraft_legacy.internal import errors SNAPCRAFT_FILES = ["parts", "stage", "prime"] _DEFAULT_PLUGINDIR = os.path.join(sys.prefix, "share", "snapcraft", "plugins") diff --git a/snapcraft/internal/db/__init__.py b/snapcraft_legacy/internal/db/__init__.py similarity index 100% rename from snapcraft/internal/db/__init__.py rename to snapcraft_legacy/internal/db/__init__.py diff --git a/snapcraft/internal/db/datastore.py b/snapcraft_legacy/internal/db/datastore.py similarity index 97% rename from snapcraft/internal/db/datastore.py rename to snapcraft_legacy/internal/db/datastore.py index e889c47845..719225c382 100644 --- a/snapcraft/internal/db/datastore.py +++ b/snapcraft_legacy/internal/db/datastore.py @@ -21,7 +21,7 @@ import tinydb import yaml -import snapcraft +import snapcraft_legacy from . import errors, migration @@ -83,7 +83,7 @@ def __init__( path: pathlib.Path, migrations: List[Type[migration.Migration]], read_only: bool = False, - snapcraft_version: str = snapcraft.__version__, + snapcraft_version: str = snapcraft_legacy.__version__, ) -> None: self.path = path self._snapcraft_version = snapcraft_version diff --git a/snapcraft/internal/db/errors.py b/snapcraft_legacy/internal/db/errors.py similarity index 95% rename from snapcraft/internal/db/errors.py rename to snapcraft_legacy/internal/db/errors.py index 35b6d5db2c..2f06bf0191 100644 --- a/snapcraft/internal/db/errors.py +++ b/snapcraft_legacy/internal/db/errors.py @@ -16,7 +16,7 @@ import pathlib -from snapcraft.internal.errors import SnapcraftException +from snapcraft_legacy.internal.errors import SnapcraftException class SnapcraftDatastoreVersionUnsupported(SnapcraftException): diff --git a/snapcraft/internal/db/migration.py b/snapcraft_legacy/internal/db/migration.py similarity index 100% rename from snapcraft/internal/db/migration.py rename to snapcraft_legacy/internal/db/migration.py diff --git a/snapcraft/internal/deltas/__init__.py b/snapcraft_legacy/internal/deltas/__init__.py similarity index 100% rename from snapcraft/internal/deltas/__init__.py rename to snapcraft_legacy/internal/deltas/__init__.py diff --git a/snapcraft/internal/deltas/_deltas.py b/snapcraft_legacy/internal/deltas/_deltas.py similarity index 98% rename from snapcraft/internal/deltas/_deltas.py rename to snapcraft_legacy/internal/deltas/_deltas.py index 86d8f03810..ee67f57a43 100644 --- a/snapcraft/internal/deltas/_deltas.py +++ b/snapcraft_legacy/internal/deltas/_deltas.py @@ -21,8 +21,8 @@ import time from typing import BinaryIO, Tuple -from snapcraft import file_utils -from snapcraft.internal.deltas.errors import ( +from snapcraft_legacy import file_utils +from snapcraft_legacy.internal.deltas.errors import ( DeltaFormatOptionError, DeltaGenerationError, DeltaGenerationTooBigError, diff --git a/snapcraft/internal/deltas/_xdelta3.py b/snapcraft_legacy/internal/deltas/_xdelta3.py similarity index 100% rename from snapcraft/internal/deltas/_xdelta3.py rename to snapcraft_legacy/internal/deltas/_xdelta3.py diff --git a/snapcraft/internal/deltas/errors.py b/snapcraft_legacy/internal/deltas/errors.py similarity index 96% rename from snapcraft/internal/deltas/errors.py rename to snapcraft_legacy/internal/deltas/errors.py index 8fe1f00790..a3c406160d 100644 --- a/snapcraft/internal/deltas/errors.py +++ b/snapcraft_legacy/internal/deltas/errors.py @@ -15,7 +15,7 @@ # along with this program. If not, see . -from snapcraft.internal.errors import SnapcraftError +from snapcraft_legacy.internal.errors import SnapcraftError class DeltaGenerationError(SnapcraftError): diff --git a/snapcraft/internal/deprecations.py b/snapcraft_legacy/internal/deprecations.py similarity index 100% rename from snapcraft/internal/deprecations.py rename to snapcraft_legacy/internal/deprecations.py diff --git a/snapcraft/internal/dirs.py b/snapcraft_legacy/internal/dirs.py similarity index 90% rename from snapcraft/internal/dirs.py rename to snapcraft_legacy/internal/dirs.py index 8ca19a8806..de3cb4d79b 100644 --- a/snapcraft/internal/dirs.py +++ b/snapcraft_legacy/internal/dirs.py @@ -18,7 +18,7 @@ import site import sys -import snapcraft.internal.errors +import snapcraft_legacy.internal.errors def _find_windows_data_dir(topdir): @@ -66,22 +66,22 @@ def _find_windows_data_dir(topdir): if os.path.exists(data_dir): return data_dir - raise snapcraft.internal.errors.SnapcraftDataDirectoryMissingError() + raise snapcraft_legacy.internal.errors.SnapcraftDataDirectoryMissingError() def setup_dirs() -> None: """ - Ensure that snapcraft.common plugindir is setup correctly + Ensure that snapcraft_legacy.common plugindir is setup correctly and support running out of a development snapshot """ - from snapcraft.internal import common + from snapcraft_legacy.internal import common topdir = os.path.abspath(os.path.join(__file__, "..", "..", "..")) # Only change the default if we are running from a checkout or from the # snap, or in Windows. if os.path.exists(os.path.join(topdir, "setup.py")): - common.set_plugindir(os.path.join(topdir, "snapcraft", "plugins")) + common.set_plugindir(os.path.join(topdir, "snapcraft_legacy", "plugins")) common.set_schemadir(os.path.join(topdir, "schema")) common.set_extensionsdir(os.path.join(topdir, "extensions")) common.set_keyringsdir(os.path.join(topdir, "keyrings")) @@ -104,7 +104,7 @@ def setup_dirs() -> None: common.set_keyringsdir(os.path.join(parent_dir, "keyrings")) elif sys.platform == "win32": - common.set_plugindir(os.path.join(topdir, "snapcraft", "plugins")) + common.set_plugindir(os.path.join(topdir, "snapcraft_legacy", "plugins")) data_dir = _find_windows_data_dir(topdir) common.set_schemadir(os.path.join(data_dir, "schema")) @@ -120,4 +120,4 @@ def setup_dirs() -> None: common.get_keyringsdir(), ]: if not os.path.exists(d): - raise snapcraft.internal.errors.SnapcraftDataDirectoryMissingError() + raise snapcraft_legacy.internal.errors.SnapcraftDataDirectoryMissingError() diff --git a/snapcraft/internal/elf.py b/snapcraft_legacy/internal/elf.py similarity index 99% rename from snapcraft/internal/elf.py rename to snapcraft_legacy/internal/elf.py index 7ba56c5c10..3d15aeada0 100644 --- a/snapcraft/internal/elf.py +++ b/snapcraft_legacy/internal/elf.py @@ -30,9 +30,9 @@ from elftools.construct import ConstructError from pkg_resources import parse_version -from snapcraft import file_utils -from snapcraft.internal import common, errors, repo -from snapcraft.project._project_options import ProjectOptions +from snapcraft_legacy import file_utils +from snapcraft_legacy.internal import common, errors, repo +from snapcraft_legacy.project._project_options import ProjectOptions logger = logging.getLogger(__name__) @@ -565,7 +565,7 @@ def patch(self, *, elf_file: ElfFile) -> None: :param ElfFile elf: a data object representing an elf file and its relevant attributes. - :raises snapcraft.internal.errors.PatcherError: + :raises snapcraft_legacy.internal.errors.PatcherError: raised when the elf_file cannot be patched. """ patchelf_args = [] diff --git a/snapcraft/internal/errors.py b/snapcraft_legacy/internal/errors.py similarity index 98% rename from snapcraft/internal/errors.py rename to snapcraft_legacy/internal/errors.py index c75951ed4e..5f40587d8f 100644 --- a/snapcraft/internal/errors.py +++ b/snapcraft_legacy/internal/errors.py @@ -19,12 +19,12 @@ from subprocess import CalledProcessError from typing import TYPE_CHECKING, Dict, List, Optional, Union -from snapcraft import formatting_utils -from snapcraft.internal import steps +from snapcraft_legacy import formatting_utils +from snapcraft_legacy.internal import steps if TYPE_CHECKING: - from snapcraft.internal.pluginhandler._dirty_report import DirtyReport - from snapcraft.internal.pluginhandler._outdated_report import OutdatedReport + from snapcraft_legacy.internal.pluginhandler._dirty_report import DirtyReport + from snapcraft_legacy.internal.pluginhandler._outdated_report import OutdatedReport # Commonly used resolution message to clean and retry build. diff --git a/snapcraft/internal/indicators.py b/snapcraft_legacy/internal/indicators.py similarity index 100% rename from snapcraft/internal/indicators.py rename to snapcraft_legacy/internal/indicators.py diff --git a/snapcraft/internal/lifecycle/__init__.py b/snapcraft_legacy/internal/lifecycle/__init__.py similarity index 100% rename from snapcraft/internal/lifecycle/__init__.py rename to snapcraft_legacy/internal/lifecycle/__init__.py diff --git a/snapcraft/internal/lifecycle/_clean.py b/snapcraft_legacy/internal/lifecycle/_clean.py similarity index 97% rename from snapcraft/internal/lifecycle/_clean.py rename to snapcraft_legacy/internal/lifecycle/_clean.py index 9c5ad7dfbb..a6cb4fba93 100644 --- a/snapcraft/internal/lifecycle/_clean.py +++ b/snapcraft_legacy/internal/lifecycle/_clean.py @@ -19,14 +19,14 @@ import shutil from typing import TYPE_CHECKING, Optional -from snapcraft import formatting_utils -from snapcraft.internal import errors, mountinfo, project_loader, steps +from snapcraft_legacy import formatting_utils +from snapcraft_legacy.internal import errors, mountinfo, project_loader, steps logger = logging.getLogger(__name__) if TYPE_CHECKING: - from snapcraft.project import Project + from snapcraft_legacy.project import Project def _clean_part(part_name, step, config, staged_state, primed_state): diff --git a/snapcraft/internal/lifecycle/_init.py b/snapcraft_legacy/internal/lifecycle/_init.py similarity index 98% rename from snapcraft/internal/lifecycle/_init.py rename to snapcraft_legacy/internal/lifecycle/_init.py index c1c8b8187d..d0f5e22c61 100644 --- a/snapcraft/internal/lifecycle/_init.py +++ b/snapcraft_legacy/internal/lifecycle/_init.py @@ -17,7 +17,7 @@ import os from textwrap import dedent -from snapcraft.internal import errors +from snapcraft_legacy.internal import errors _TEMPLATE_YAML = dedent( """\ diff --git a/snapcraft/internal/lifecycle/_runner.py b/snapcraft_legacy/internal/lifecycle/_runner.py similarity index 98% rename from snapcraft/internal/lifecycle/_runner.py rename to snapcraft_legacy/internal/lifecycle/_runner.py index 152889b040..139eb9d30d 100644 --- a/snapcraft/internal/lifecycle/_runner.py +++ b/snapcraft_legacy/internal/lifecycle/_runner.py @@ -17,8 +17,8 @@ import logging from typing import List, Optional, Sequence, Set -from snapcraft import config, plugins, storeapi -from snapcraft.internal import ( +from snapcraft_legacy import config, plugins, storeapi +from snapcraft_legacy.internal import ( common, errors, pluginhandler, @@ -27,8 +27,8 @@ states, steps, ) -from snapcraft.internal.meta._snap_packaging import create_snap_packaging -from snapcraft.internal.pluginhandler._part_environment import ( +from snapcraft_legacy.internal.meta._snap_packaging import create_snap_packaging +from snapcraft_legacy.internal.pluginhandler._part_environment import ( get_snapcraft_part_directory_environment, ) diff --git a/snapcraft/internal/lifecycle/_status_cache.py b/snapcraft_legacy/internal/lifecycle/_status_cache.py similarity index 98% rename from snapcraft/internal/lifecycle/_status_cache.py rename to snapcraft_legacy/internal/lifecycle/_status_cache.py index a5f6cc8e9e..361950dd07 100644 --- a/snapcraft/internal/lifecycle/_status_cache.py +++ b/snapcraft_legacy/internal/lifecycle/_status_cache.py @@ -18,8 +18,8 @@ import contextlib from typing import Any, Dict, List, Optional, Set -import snapcraft.internal.project_loader._config as _config -from snapcraft.internal import errors, pluginhandler, steps +import snapcraft_legacy.internal.project_loader._config as _config +from snapcraft_legacy.internal import errors, pluginhandler, steps _DirtyReport = Dict[str, Dict[steps.Step, Optional[pluginhandler.DirtyReport]]] _OutdatedReport = Dict[str, Dict[steps.Step, Optional[pluginhandler.OutdatedReport]]] diff --git a/snapcraft/internal/lifecycle/errors.py b/snapcraft_legacy/internal/lifecycle/errors.py similarity index 91% rename from snapcraft/internal/lifecycle/errors.py rename to snapcraft_legacy/internal/lifecycle/errors.py index a4db40fb56..fde3ed3002 100644 --- a/snapcraft/internal/lifecycle/errors.py +++ b/snapcraft_legacy/internal/lifecycle/errors.py @@ -14,7 +14,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -from snapcraft.internal.errors import SnapcraftError as _SnapcraftError +from snapcraft_legacy.internal.errors import SnapcraftError as _SnapcraftError class PackVerificationError(_SnapcraftError): diff --git a/snapcraft/internal/log.py b/snapcraft_legacy/internal/log.py similarity index 97% rename from snapcraft/internal/log.py rename to snapcraft_legacy/internal/log.py index b2e442becb..ae6f4b94eb 100644 --- a/snapcraft/internal/log.py +++ b/snapcraft_legacy/internal/log.py @@ -18,7 +18,7 @@ import logging import sys -from snapcraft.internal.indicators import is_dumb_terminal +from snapcraft_legacy.internal.indicators import is_dumb_terminal class _StdoutFilter(logging.Filter): diff --git a/snapcraft/internal/lxd/__init__.py b/snapcraft_legacy/internal/lxd/__init__.py similarity index 100% rename from snapcraft/internal/lxd/__init__.py rename to snapcraft_legacy/internal/lxd/__init__.py diff --git a/snapcraft/internal/mangling.py b/snapcraft_legacy/internal/mangling.py similarity index 97% rename from snapcraft/internal/mangling.py rename to snapcraft_legacy/internal/mangling.py index 0fbb0125d8..b1f9ad732b 100644 --- a/snapcraft/internal/mangling.py +++ b/snapcraft_legacy/internal/mangling.py @@ -18,8 +18,8 @@ import subprocess from typing import FrozenSet -from snapcraft import file_utils -from snapcraft.internal import elf +from snapcraft_legacy import file_utils +from snapcraft_legacy.internal import elf logger = logging.getLogger(__name__) diff --git a/snapcraft/internal/meta/__init__.py b/snapcraft_legacy/internal/meta/__init__.py similarity index 100% rename from snapcraft/internal/meta/__init__.py rename to snapcraft_legacy/internal/meta/__init__.py diff --git a/snapcraft/internal/meta/_manifest.py b/snapcraft_legacy/internal/meta/_manifest.py similarity index 91% rename from snapcraft/internal/meta/_manifest.py rename to snapcraft_legacy/internal/meta/_manifest.py index 105ecfef20..5e5ae9cd3d 100644 --- a/snapcraft/internal/meta/_manifest.py +++ b/snapcraft_legacy/internal/meta/_manifest.py @@ -20,17 +20,17 @@ from collections import OrderedDict from typing import TYPE_CHECKING, Any, Dict, Set -import snapcraft -from snapcraft.internal import errors, os_release, steps -from snapcraft.internal.states import GlobalState, get_state +import snapcraft_legacy +from snapcraft_legacy.internal import errors, os_release, steps +from snapcraft_legacy.internal.states import GlobalState, get_state if TYPE_CHECKING: - from snapcraft.project import Project + from snapcraft_legacy.project import Project def annotate_snapcraft(project: "Project", data: Dict[str, Any]) -> Dict[str, Any]: manifest = OrderedDict() # type: Dict[str, Any] - manifest["snapcraft-version"] = snapcraft._get_version() + manifest["snapcraft-version"] = snapcraft_legacy._get_version() manifest["snapcraft-started-at"] = project._get_start_time().isoformat() + "Z" release = os_release.OsRelease() diff --git a/snapcraft/internal/meta/_snap_packaging.py b/snapcraft_legacy/internal/meta/_snap_packaging.py similarity index 97% rename from snapcraft/internal/meta/_snap_packaging.py rename to snapcraft_legacy/internal/meta/_snap_packaging.py index f355d42e96..b9af0efb09 100644 --- a/snapcraft/internal/meta/_snap_packaging.py +++ b/snapcraft_legacy/internal/meta/_snap_packaging.py @@ -29,16 +29,22 @@ import requests -from snapcraft import extractors, file_utils, formatting_utils, shell_utils, yaml_utils -from snapcraft.extractors import _metadata -from snapcraft.internal import common, errors, project_loader, states -from snapcraft.internal.deprecations import handle_deprecation_notice -from snapcraft.internal.meta import _manifest, _version -from snapcraft.internal.meta import errors as meta_errors -from snapcraft.internal.meta.application import ApplicationAdapter -from snapcraft.internal.meta.snap import Snap -from snapcraft.internal.project_loader import _config -from snapcraft.project import _schema +from snapcraft_legacy import ( + extractors, + file_utils, + formatting_utils, + shell_utils, + yaml_utils, +) +from snapcraft_legacy.extractors import _metadata +from snapcraft_legacy.internal import common, errors, project_loader, states +from snapcraft_legacy.internal.deprecations import handle_deprecation_notice +from snapcraft_legacy.internal.meta import _manifest, _version +from snapcraft_legacy.internal.meta import errors as meta_errors +from snapcraft_legacy.internal.meta.application import ApplicationAdapter +from snapcraft_legacy.internal.meta.snap import Snap +from snapcraft_legacy.internal.project_loader import _config +from snapcraft_legacy.project import _schema logger = logging.getLogger(__name__) diff --git a/snapcraft/internal/meta/_utils.py b/snapcraft_legacy/internal/meta/_utils.py similarity index 100% rename from snapcraft/internal/meta/_utils.py rename to snapcraft_legacy/internal/meta/_utils.py diff --git a/snapcraft/internal/meta/_version.py b/snapcraft_legacy/internal/meta/_version.py similarity index 95% rename from snapcraft/internal/meta/_version.py rename to snapcraft_legacy/internal/meta/_version.py index 79312c69c6..77a56d6754 100644 --- a/snapcraft/internal/meta/_version.py +++ b/snapcraft_legacy/internal/meta/_version.py @@ -17,8 +17,8 @@ import logging import subprocess -from snapcraft import shell_utils -from snapcraft.internal import sources +from snapcraft_legacy import shell_utils +from snapcraft_legacy.internal import sources from . import errors diff --git a/snapcraft/internal/meta/application.py b/snapcraft_legacy/internal/meta/application.py similarity index 99% rename from snapcraft/internal/meta/application.py rename to snapcraft_legacy/internal/meta/application.py index 079fc33407..2915828c51 100644 --- a/snapcraft/internal/meta/application.py +++ b/snapcraft_legacy/internal/meta/application.py @@ -19,7 +19,7 @@ from copy import deepcopy from typing import Any, Dict, List, Optional, Sequence # noqa: F401 -from snapcraft import yaml_utils +from snapcraft_legacy import yaml_utils from . import errors from ._utils import _executable_is_valid diff --git a/snapcraft/internal/meta/command.py b/snapcraft_legacy/internal/meta/command.py similarity index 99% rename from snapcraft/internal/meta/command.py rename to snapcraft_legacy/internal/meta/command.py index 3e204b37f5..1320544ee9 100644 --- a/snapcraft/internal/meta/command.py +++ b/snapcraft_legacy/internal/meta/command.py @@ -22,7 +22,7 @@ import shutil from typing import Optional -from snapcraft.internal import common +from snapcraft_legacy.internal import common from . import errors from ._utils import _executable_is_valid diff --git a/snapcraft/internal/meta/desktop.py b/snapcraft_legacy/internal/meta/desktop.py similarity index 100% rename from snapcraft/internal/meta/desktop.py rename to snapcraft_legacy/internal/meta/desktop.py diff --git a/snapcraft/internal/meta/errors.py b/snapcraft_legacy/internal/meta/errors.py similarity index 98% rename from snapcraft/internal/meta/errors.py rename to snapcraft_legacy/internal/meta/errors.py index 0b8d4a05f8..a98565ee58 100644 --- a/snapcraft/internal/meta/errors.py +++ b/snapcraft_legacy/internal/meta/errors.py @@ -16,8 +16,8 @@ from typing import List, Optional -from snapcraft import formatting_utils -from snapcraft.internal import errors +from snapcraft_legacy import formatting_utils +from snapcraft_legacy.internal import errors class CommandError(errors.SnapcraftError): diff --git a/snapcraft/internal/meta/hooks.py b/snapcraft_legacy/internal/meta/hooks.py similarity index 98% rename from snapcraft/internal/meta/hooks.py rename to snapcraft_legacy/internal/meta/hooks.py index d02637a824..86f03d081d 100644 --- a/snapcraft/internal/meta/hooks.py +++ b/snapcraft_legacy/internal/meta/hooks.py @@ -18,7 +18,7 @@ from collections import OrderedDict from typing import Any, Dict, List, Optional -from snapcraft.internal.meta.errors import HookValidationError +from snapcraft_legacy.internal.meta.errors import HookValidationError class Hook: diff --git a/snapcraft/internal/meta/package_repository.py b/snapcraft_legacy/internal/meta/package_repository.py similarity index 100% rename from snapcraft/internal/meta/package_repository.py rename to snapcraft_legacy/internal/meta/package_repository.py diff --git a/snapcraft/internal/meta/plugs.py b/snapcraft_legacy/internal/meta/plugs.py similarity index 98% rename from snapcraft/internal/meta/plugs.py rename to snapcraft_legacy/internal/meta/plugs.py index 68b87e14dd..9f716b9b1c 100644 --- a/snapcraft/internal/meta/plugs.py +++ b/snapcraft_legacy/internal/meta/plugs.py @@ -19,7 +19,7 @@ from copy import deepcopy from typing import Any, Dict, Optional, Type -from snapcraft.internal.meta.errors import PlugValidationError +from snapcraft_legacy.internal.meta.errors import PlugValidationError logger = logging.getLogger(__name__) diff --git a/snapcraft/internal/meta/slots.py b/snapcraft_legacy/internal/meta/slots.py similarity index 99% rename from snapcraft/internal/meta/slots.py rename to snapcraft_legacy/internal/meta/slots.py index 4703296d9d..9e3d417840 100644 --- a/snapcraft/internal/meta/slots.py +++ b/snapcraft_legacy/internal/meta/slots.py @@ -21,7 +21,7 @@ from copy import deepcopy from typing import Any, Dict, List, Optional, Set, Tuple, Type -from snapcraft.internal.meta.errors import SlotValidationError +from snapcraft_legacy.internal.meta.errors import SlotValidationError logger = logging.getLogger(__name__) diff --git a/snapcraft/internal/meta/snap.py b/snapcraft_legacy/internal/meta/snap.py similarity index 97% rename from snapcraft/internal/meta/snap.py rename to snapcraft_legacy/internal/meta/snap.py index 1be2cbc7ab..b355d10153 100644 --- a/snapcraft/internal/meta/snap.py +++ b/snapcraft_legacy/internal/meta/snap.py @@ -20,15 +20,15 @@ from copy import deepcopy from typing import Any, Dict, List, Optional, Sequence, Set -from snapcraft import yaml_utils -from snapcraft.internal import common -from snapcraft.internal.meta import errors -from snapcraft.internal.meta.application import Application -from snapcraft.internal.meta.hooks import Hook -from snapcraft.internal.meta.package_repository import PackageRepository -from snapcraft.internal.meta.plugs import ContentPlug, Plug -from snapcraft.internal.meta.slots import ContentSlot, Slot -from snapcraft.internal.meta.system_user import SystemUser +from snapcraft_legacy import yaml_utils +from snapcraft_legacy.internal import common +from snapcraft_legacy.internal.meta import errors +from snapcraft_legacy.internal.meta.application import Application +from snapcraft_legacy.internal.meta.hooks import Hook +from snapcraft_legacy.internal.meta.package_repository import PackageRepository +from snapcraft_legacy.internal.meta.plugs import ContentPlug, Plug +from snapcraft_legacy.internal.meta.slots import ContentSlot, Slot +from snapcraft_legacy.internal.meta.system_user import SystemUser logger = logging.getLogger(__name__) diff --git a/snapcraft/internal/meta/system_user.py b/snapcraft_legacy/internal/meta/system_user.py similarity index 98% rename from snapcraft/internal/meta/system_user.py rename to snapcraft_legacy/internal/meta/system_user.py index b06f0b693d..82cb67640d 100644 --- a/snapcraft/internal/meta/system_user.py +++ b/snapcraft_legacy/internal/meta/system_user.py @@ -19,7 +19,7 @@ from collections import OrderedDict from typing import Any, Dict -from snapcraft.internal.meta import errors +from snapcraft_legacy.internal.meta import errors logger = logging.getLogger(__name__) diff --git a/snapcraft/internal/mountinfo.py b/snapcraft_legacy/internal/mountinfo.py similarity index 98% rename from snapcraft/internal/mountinfo.py rename to snapcraft_legacy/internal/mountinfo.py index 260215bf8a..5aea923ef7 100644 --- a/snapcraft/internal/mountinfo.py +++ b/snapcraft_legacy/internal/mountinfo.py @@ -20,7 +20,7 @@ import logging from typing import Dict, List # noqa: F401 -from snapcraft.internal import errors +from snapcraft_legacy.internal import errors logger = logging.getLogger(__name__) diff --git a/snapcraft/internal/os_release.py b/snapcraft_legacy/internal/os_release.py similarity index 98% rename from snapcraft/internal/os_release.py rename to snapcraft_legacy/internal/os_release.py index 778a091af2..6a77728df0 100644 --- a/snapcraft/internal/os_release.py +++ b/snapcraft_legacy/internal/os_release.py @@ -20,7 +20,7 @@ # doesn't like that very much, so noqa. from typing import Dict # noqa -from snapcraft.internal import errors +from snapcraft_legacy.internal import errors _ID_TO_UBUNTU_CODENAME = { "17.10": "artful", diff --git a/snapcraft/internal/pluginhandler/__init__.py b/snapcraft_legacy/internal/pluginhandler/__init__.py similarity index 98% rename from snapcraft/internal/pluginhandler/__init__.py rename to snapcraft_legacy/internal/pluginhandler/__init__.py index 45cea0d21c..051b8746d8 100644 --- a/snapcraft/internal/pluginhandler/__init__.py +++ b/snapcraft_legacy/internal/pluginhandler/__init__.py @@ -28,10 +28,19 @@ from glob import iglob from typing import TYPE_CHECKING, Dict, List, Optional, Sequence, Set, cast -import snapcraft.extractors -from snapcraft import file_utils, plugins, yaml_utils -from snapcraft.internal import common, elf, errors, repo, sources, states, steps, xattrs -from snapcraft.internal.mangling import clear_execstack +import snapcraft_legacy.extractors +from snapcraft_legacy import file_utils, plugins, yaml_utils +from snapcraft_legacy.internal import ( + common, + elf, + errors, + repo, + sources, + states, + steps, + xattrs, +) +from snapcraft_legacy.internal.mangling import clear_execstack from ._build_attributes import BuildAttributes from ._dependencies import MissingDependencyResolver @@ -44,7 +53,7 @@ from ._runner import Runner if TYPE_CHECKING: - from snapcraft.project import Project + from snapcraft_legacy.project import Project logger = logging.getLogger(__name__) @@ -122,8 +131,8 @@ def __init__( # Scriptlet data is a dict of dicts for each step self._scriptlet_metadata: Dict[ - steps.Step, snapcraft.extractors.ExtractedMetadata - ] = collections.defaultdict(snapcraft.extractors.ExtractedMetadata) + steps.Step, snapcraft_legacy.extractors.ExtractedMetadata + ] = collections.defaultdict(snapcraft_legacy.extractors.ExtractedMetadata) if isinstance(plugin, plugins.v2.PluginV2): self._shell = "/bin/bash" @@ -213,7 +222,7 @@ def _get_source_handler(self, properties): def _set_version(self, *, version): try: self._set_scriptlet_metadata( - snapcraft.extractors.ExtractedMetadata(version=version) + snapcraft_legacy.extractors.ExtractedMetadata(version=version) ) except errors.ScriptletDuplicateDataError as e: raise errors.ScriptletDuplicateFieldError("version", e.other_step) @@ -221,13 +230,13 @@ def _set_version(self, *, version): def _set_grade(self, *, grade): try: self._set_scriptlet_metadata( - snapcraft.extractors.ExtractedMetadata(grade=grade) + snapcraft_legacy.extractors.ExtractedMetadata(grade=grade) ) except errors.ScriptletDuplicateDataError as e: raise errors.ScriptletDuplicateFieldError("grade", e.other_step) def _check_scriplet_metadata_dupe( - self, metadata: snapcraft.extractors.ExtractedMetadata, step: steps.Step + self, metadata: snapcraft_legacy.extractors.ExtractedMetadata, step: steps.Step ): # First, ensure the metadata set here doesn't conflict with metadata # already set for this step @@ -248,7 +257,9 @@ def _check_scriplet_metadata_dupe( step, other_step, list(conflicts) ) - def _set_scriptlet_metadata(self, metadata: snapcraft.extractors.ExtractedMetadata): + def _set_scriptlet_metadata( + self, metadata: snapcraft_legacy.extractors.ExtractedMetadata + ): try: step = self.next_step() self._check_scriplet_metadata_dupe(metadata, step) @@ -526,7 +537,7 @@ def mark_pull_done(self): part_build_snaps = self._grammar_processor.get_build_snaps() # Extract any requested metadata available in the source directory - metadata = snapcraft.extractors.ExtractedMetadata() + metadata = snapcraft_legacy.extractors.ExtractedMetadata() metadata_files = [] for parse_relpath in self._part_properties.get("parse-info", []): with contextlib.suppress(errors.MissingMetadataFileError): @@ -722,7 +733,7 @@ def mark_build_done(self): # Extract any requested metadata available in the build directory, # followed by the install directory (which takes precedence) metadata_files = [] - metadata = snapcraft.extractors.ExtractedMetadata() + metadata = snapcraft_legacy.extractors.ExtractedMetadata() for parse_relpath in self._part_properties.get("parse-info", []): found_path = None with contextlib.suppress(errors.MissingMetadataFileError): @@ -1266,7 +1277,7 @@ def _migrate_files( src = os.path.join(srcdir, snap_dir) dst = os.path.join(dstdir, snap_dir) - snapcraft.file_utils.create_similar_directory(src, dst) + snapcraft_legacy.file_utils.create_similar_directory(src, dst) for snap_file in sorted(snap_files): src = os.path.join(srcdir, snap_file) diff --git a/snapcraft/internal/pluginhandler/_build_attributes.py b/snapcraft_legacy/internal/pluginhandler/_build_attributes.py similarity index 100% rename from snapcraft/internal/pluginhandler/_build_attributes.py rename to snapcraft_legacy/internal/pluginhandler/_build_attributes.py diff --git a/snapcraft/internal/pluginhandler/_dependencies.py b/snapcraft_legacy/internal/pluginhandler/_dependencies.py similarity index 98% rename from snapcraft/internal/pluginhandler/_dependencies.py rename to snapcraft_legacy/internal/pluginhandler/_dependencies.py index 4185ec6b2e..d333ed7ff9 100644 --- a/snapcraft/internal/pluginhandler/_dependencies.py +++ b/snapcraft_legacy/internal/pluginhandler/_dependencies.py @@ -17,7 +17,7 @@ import os from typing import Sequence, Set -from snapcraft.internal import repo +from snapcraft_legacy.internal import repo _MSG_EXTEND_STAGE_PACKAGES = ( "The {part_name!r} part is missing libraries that are not " diff --git a/snapcraft/internal/pluginhandler/_dirty_report.py b/snapcraft_legacy/internal/pluginhandler/_dirty_report.py similarity index 99% rename from snapcraft/internal/pluginhandler/_dirty_report.py rename to snapcraft_legacy/internal/pluginhandler/_dirty_report.py index 7dde96d5c6..a708d2b249 100644 --- a/snapcraft/internal/pluginhandler/_dirty_report.py +++ b/snapcraft_legacy/internal/pluginhandler/_dirty_report.py @@ -16,7 +16,7 @@ from typing import List, Set, Union -from snapcraft import formatting_utils +from snapcraft_legacy import formatting_utils # Ideally we'd just use Collection from typing, but that wasn't introduced # until 3.6 diff --git a/snapcraft/internal/pluginhandler/_metadata_extraction.py b/snapcraft_legacy/internal/pluginhandler/_metadata_extraction.py similarity index 93% rename from snapcraft/internal/pluginhandler/_metadata_extraction.py rename to snapcraft_legacy/internal/pluginhandler/_metadata_extraction.py index 4a781e82d8..803655e6f5 100644 --- a/snapcraft/internal/pluginhandler/_metadata_extraction.py +++ b/snapcraft_legacy/internal/pluginhandler/_metadata_extraction.py @@ -19,8 +19,8 @@ import os import pkgutil -from snapcraft import extractors -from snapcraft.internal.errors import ( +from snapcraft_legacy import extractors +from snapcraft_legacy.internal.errors import ( InvalidExtractorValueError, MissingMetadataFileError, UnhandledMetadataFileTypeError, @@ -41,7 +41,7 @@ def extract_metadata( # We only care about non-private modules in here if not module_name.startswith("_"): module = importlib.import_module( - "snapcraft.extractors.{}".format(module_name) + "snapcraft_legacy.extractors.{}".format(module_name) ) try: diff --git a/snapcraft/internal/pluginhandler/_outdated_report.py b/snapcraft_legacy/internal/pluginhandler/_outdated_report.py similarity index 96% rename from snapcraft/internal/pluginhandler/_outdated_report.py rename to snapcraft_legacy/internal/pluginhandler/_outdated_report.py index 0021559fe9..79ef285141 100644 --- a/snapcraft/internal/pluginhandler/_outdated_report.py +++ b/snapcraft_legacy/internal/pluginhandler/_outdated_report.py @@ -14,8 +14,8 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -from snapcraft import formatting_utils -from snapcraft.internal import steps +from snapcraft_legacy import formatting_utils +from snapcraft_legacy.internal import steps class OutdatedReport: diff --git a/snapcraft/internal/pluginhandler/_part_environment.py b/snapcraft_legacy/internal/pluginhandler/_part_environment.py similarity index 97% rename from snapcraft/internal/pluginhandler/_part_environment.py rename to snapcraft_legacy/internal/pluginhandler/_part_environment.py index 424c925cbf..ea9f1a620c 100644 --- a/snapcraft/internal/pluginhandler/_part_environment.py +++ b/snapcraft_legacy/internal/pluginhandler/_part_environment.py @@ -16,11 +16,11 @@ from typing import TYPE_CHECKING, Dict, Optional -from snapcraft import formatting_utils -from snapcraft.internal import common, steps +from snapcraft_legacy import formatting_utils +from snapcraft_legacy.internal import common, steps if TYPE_CHECKING: - from snapcraft.project import Project + from snapcraft_legacy.project import Project from . import PluginHandler diff --git a/snapcraft/internal/pluginhandler/_patchelf.py b/snapcraft_legacy/internal/pluginhandler/_patchelf.py similarity index 98% rename from snapcraft/internal/pluginhandler/_patchelf.py rename to snapcraft_legacy/internal/pluginhandler/_patchelf.py index a57fa02ba7..68291116ef 100644 --- a/snapcraft/internal/pluginhandler/_patchelf.py +++ b/snapcraft_legacy/internal/pluginhandler/_patchelf.py @@ -19,8 +19,8 @@ from typing import Dict # noqa: F401 from typing import FrozenSet, List -from snapcraft.internal import elf, errors -from snapcraft.project import Project +from snapcraft_legacy.internal import elf, errors +from snapcraft_legacy.project import Project logger = logging.getLogger(__name__) diff --git a/snapcraft/internal/pluginhandler/_plugin_loader.py b/snapcraft_legacy/internal/pluginhandler/_plugin_loader.py similarity index 95% rename from snapcraft/internal/pluginhandler/_plugin_loader.py rename to snapcraft_legacy/internal/pluginhandler/_plugin_loader.py index 89601c884f..2e60805844 100644 --- a/snapcraft/internal/pluginhandler/_plugin_loader.py +++ b/snapcraft_legacy/internal/pluginhandler/_plugin_loader.py @@ -22,10 +22,10 @@ import jsonschema -import snapcraft.yaml_utils.errors -from snapcraft import plugins -from snapcraft.internal import errors -from snapcraft.project import Project +import snapcraft_legacy.yaml_utils.errors +from snapcraft_legacy import plugins +from snapcraft_legacy.internal import errors +from snapcraft_legacy.project import Project logger = logging.getLogger(__name__) @@ -135,7 +135,7 @@ def _get_local_plugin_class(*, plugin_name: str, local_plugins_dir: str): logger.debug( f"Plugin attribute {attr!r} has __module__: {attr.__module__!r}" ) - if attr.__module__.startswith("snapcraft.plugins"): + if attr.__module__.startswith("snapcraft_legacy.plugins"): continue return attr else: @@ -190,7 +190,9 @@ def _make_options( try: jsonschema.validate(properties, plugin_schema) except jsonschema.ValidationError as e: - error = snapcraft.yaml_utils.errors.YamlValidationError.from_validation_error(e) + error = snapcraft_legacy.yaml_utils.errors.YamlValidationError.from_validation_error( + e + ) raise errors.PluginError( "properties failed to load for {}: {}".format(part_name, error.message) ) diff --git a/snapcraft/internal/pluginhandler/_runner.py b/snapcraft_legacy/internal/pluginhandler/_runner.py similarity index 99% rename from snapcraft/internal/pluginhandler/_runner.py rename to snapcraft_legacy/internal/pluginhandler/_runner.py index 10c2a57486..54e7423031 100644 --- a/snapcraft/internal/pluginhandler/_runner.py +++ b/snapcraft_legacy/internal/pluginhandler/_runner.py @@ -24,7 +24,7 @@ import time from typing import Any, Callable, Dict -from snapcraft.internal import common, errors, steps +from snapcraft_legacy.internal import common, errors, steps class Runner: diff --git a/snapcraft/internal/project_loader/__init__.py b/snapcraft_legacy/internal/project_loader/__init__.py similarity index 96% rename from snapcraft/internal/project_loader/__init__.py rename to snapcraft_legacy/internal/project_loader/__init__.py index 860f35b5f9..f0b42413a0 100644 --- a/snapcraft/internal/project_loader/__init__.py +++ b/snapcraft_legacy/internal/project_loader/__init__.py @@ -25,7 +25,7 @@ from ._parts_config import PartsConfig # noqa: F401 if TYPE_CHECKING: - from snapcraft.project import Project # noqa: F401 + from snapcraft_legacy.project import Project # noqa: F401 def load_config(project: "Project"): diff --git a/snapcraft/internal/project_loader/_config.py b/snapcraft_legacy/internal/project_loader/_config.py similarity index 97% rename from snapcraft/internal/project_loader/_config.py rename to snapcraft_legacy/internal/project_loader/_config.py index fde42b66a9..a8628edaba 100644 --- a/snapcraft/internal/project_loader/_config.py +++ b/snapcraft_legacy/internal/project_loader/_config.py @@ -24,15 +24,15 @@ import jsonschema -from snapcraft import formatting_utils, plugins, project -from snapcraft.internal import deprecations, repo, states, steps -from snapcraft.internal.meta.package_repository import PackageRepository -from snapcraft.internal.meta.snap import Snap -from snapcraft.internal.pluginhandler._part_environment import ( +from snapcraft_legacy import formatting_utils, plugins, project +from snapcraft_legacy.internal import deprecations, repo, states, steps +from snapcraft_legacy.internal.meta.package_repository import PackageRepository +from snapcraft_legacy.internal.meta.snap import Snap +from snapcraft_legacy.internal.pluginhandler._part_environment import ( get_snapcraft_global_environment, ) -from snapcraft.internal.repo import apt_key_manager, apt_sources_manager -from snapcraft.project._schema import Validator +from snapcraft_legacy.internal.repo import apt_key_manager, apt_sources_manager +from snapcraft_legacy.project._schema import Validator from . import errors, grammar_processing, replace_attr from ._env import environment_to_replacements, runtime_env diff --git a/snapcraft/internal/project_loader/_env.py b/snapcraft_legacy/internal/project_loader/_env.py similarity index 96% rename from snapcraft/internal/project_loader/_env.py rename to snapcraft_legacy/internal/project_loader/_env.py index dd75b616f2..e3f8df3f85 100644 --- a/snapcraft/internal/project_loader/_env.py +++ b/snapcraft_legacy/internal/project_loader/_env.py @@ -16,8 +16,8 @@ from typing import Dict, List -from snapcraft import formatting_utils -from snapcraft.internal import common, elf +from snapcraft_legacy import formatting_utils +from snapcraft_legacy.internal import common, elf def runtime_env(root: str, arch_triplet: str) -> List[str]: diff --git a/snapcraft/internal/project_loader/_extensions/__init__.py b/snapcraft_legacy/internal/project_loader/_extensions/__init__.py similarity index 100% rename from snapcraft/internal/project_loader/_extensions/__init__.py rename to snapcraft_legacy/internal/project_loader/_extensions/__init__.py diff --git a/snapcraft/internal/project_loader/_extensions/_extension.py b/snapcraft_legacy/internal/project_loader/_extensions/_extension.py similarity index 100% rename from snapcraft/internal/project_loader/_extensions/_extension.py rename to snapcraft_legacy/internal/project_loader/_extensions/_extension.py diff --git a/snapcraft/internal/project_loader/_extensions/_flutter_meta.py b/snapcraft_legacy/internal/project_loader/_extensions/_flutter_meta.py similarity index 100% rename from snapcraft/internal/project_loader/_extensions/_flutter_meta.py rename to snapcraft_legacy/internal/project_loader/_extensions/_flutter_meta.py diff --git a/snapcraft/internal/project_loader/_extensions/_utils.py b/snapcraft_legacy/internal/project_loader/_extensions/_utils.py similarity index 95% rename from snapcraft/internal/project_loader/_extensions/_utils.py rename to snapcraft_legacy/internal/project_loader/_extensions/_utils.py index f303644ef9..1a8d651d54 100644 --- a/snapcraft/internal/project_loader/_extensions/_utils.py +++ b/snapcraft_legacy/internal/project_loader/_extensions/_utils.py @@ -25,8 +25,8 @@ import jsonschema -import snapcraft.yaml_utils.errors -from snapcraft.project import errors as project_errors +import snapcraft_legacy.yaml_utils.errors +from snapcraft_legacy.project import errors as project_errors from .. import errors from ._extension import Extension @@ -94,7 +94,7 @@ def find_extension(extension_name: str) -> Type[Extension]: try: extension_module = importlib.import_module( - "snapcraft.internal.project_loader._extensions.{}".format( + "snapcraft_legacy.internal.project_loader._extensions.{}".format( extension_name.replace("-", "_") ) ) @@ -227,9 +227,9 @@ def _validate_extension_format(extension_names): extension_names, extension_schema, format_checker=format_check ) except jsonschema.ValidationError as e: - raise snapcraft.yaml_utils.errors.YamlValidationError( + raise snapcraft_legacy.yaml_utils.errors.YamlValidationError( "The 'extensions' property does not match the required schema: {}".format( - snapcraft.yaml_utils.errors.YamlValidationError.from_validation_error( + snapcraft_legacy.yaml_utils.errors.YamlValidationError.from_validation_error( e ).message ) diff --git a/snapcraft/internal/project_loader/_extensions/flutter_beta.py b/snapcraft_legacy/internal/project_loader/_extensions/flutter_beta.py similarity index 100% rename from snapcraft/internal/project_loader/_extensions/flutter_beta.py rename to snapcraft_legacy/internal/project_loader/_extensions/flutter_beta.py diff --git a/snapcraft/internal/project_loader/_extensions/flutter_dev.py b/snapcraft_legacy/internal/project_loader/_extensions/flutter_dev.py similarity index 100% rename from snapcraft/internal/project_loader/_extensions/flutter_dev.py rename to snapcraft_legacy/internal/project_loader/_extensions/flutter_dev.py diff --git a/snapcraft/internal/project_loader/_extensions/flutter_master.py b/snapcraft_legacy/internal/project_loader/_extensions/flutter_master.py similarity index 100% rename from snapcraft/internal/project_loader/_extensions/flutter_master.py rename to snapcraft_legacy/internal/project_loader/_extensions/flutter_master.py diff --git a/snapcraft/internal/project_loader/_extensions/flutter_stable.py b/snapcraft_legacy/internal/project_loader/_extensions/flutter_stable.py similarity index 100% rename from snapcraft/internal/project_loader/_extensions/flutter_stable.py rename to snapcraft_legacy/internal/project_loader/_extensions/flutter_stable.py diff --git a/snapcraft/internal/project_loader/_extensions/gnome_3_28.py b/snapcraft_legacy/internal/project_loader/_extensions/gnome_3_28.py similarity index 100% rename from snapcraft/internal/project_loader/_extensions/gnome_3_28.py rename to snapcraft_legacy/internal/project_loader/_extensions/gnome_3_28.py diff --git a/snapcraft/internal/project_loader/_extensions/gnome_3_34.py b/snapcraft_legacy/internal/project_loader/_extensions/gnome_3_34.py similarity index 100% rename from snapcraft/internal/project_loader/_extensions/gnome_3_34.py rename to snapcraft_legacy/internal/project_loader/_extensions/gnome_3_34.py diff --git a/snapcraft/internal/project_loader/_extensions/gnome_3_38.py b/snapcraft_legacy/internal/project_loader/_extensions/gnome_3_38.py similarity index 100% rename from snapcraft/internal/project_loader/_extensions/gnome_3_38.py rename to snapcraft_legacy/internal/project_loader/_extensions/gnome_3_38.py diff --git a/snapcraft/internal/project_loader/_extensions/kde_neon.py b/snapcraft_legacy/internal/project_loader/_extensions/kde_neon.py similarity index 100% rename from snapcraft/internal/project_loader/_extensions/kde_neon.py rename to snapcraft_legacy/internal/project_loader/_extensions/kde_neon.py diff --git a/snapcraft/internal/project_loader/_extensions/ros1_noetic.py b/snapcraft_legacy/internal/project_loader/_extensions/ros1_noetic.py similarity index 100% rename from snapcraft/internal/project_loader/_extensions/ros1_noetic.py rename to snapcraft_legacy/internal/project_loader/_extensions/ros1_noetic.py diff --git a/snapcraft/internal/project_loader/_extensions/ros2_foxy.py b/snapcraft_legacy/internal/project_loader/_extensions/ros2_foxy.py similarity index 100% rename from snapcraft/internal/project_loader/_extensions/ros2_foxy.py rename to snapcraft_legacy/internal/project_loader/_extensions/ros2_foxy.py diff --git a/snapcraft/internal/project_loader/_parts_config.py b/snapcraft_legacy/internal/project_loader/_parts_config.py similarity index 97% rename from snapcraft/internal/project_loader/_parts_config.py rename to snapcraft_legacy/internal/project_loader/_parts_config.py index a9ba01d330..ae4ffb88e0 100644 --- a/snapcraft/internal/project_loader/_parts_config.py +++ b/snapcraft_legacy/internal/project_loader/_parts_config.py @@ -20,9 +20,9 @@ from typing import Set # noqa: F401 from typing import List -import snapcraft -from snapcraft.internal import elf, pluginhandler, repo -from snapcraft.internal.pluginhandler._part_environment import ( +import snapcraft_legacy +from snapcraft_legacy.internal import elf, pluginhandler, repo +from snapcraft_legacy.internal.pluginhandler._part_environment import ( get_snapcraft_global_environment, get_snapcraft_part_directory_environment, ) @@ -167,7 +167,7 @@ def clean_part(self, part_name, staged_state, primed_state, step): def validate(self, part_names): for part_name in part_names: if part_name not in self._part_names: - raise snapcraft.internal.errors.SnapcraftEnvironmentError( + raise snapcraft_legacy.internal.errors.SnapcraftEnvironmentError( "The part named {!r} is not defined in " "{!r}".format( part_name, self._project.info.snapcraft_yaml_file_path diff --git a/snapcraft/internal/project_loader/errors.py b/snapcraft_legacy/internal/project_loader/errors.py similarity index 95% rename from snapcraft/internal/project_loader/errors.py rename to snapcraft_legacy/internal/project_loader/errors.py index 30a5643f42..87dfa7df4a 100644 --- a/snapcraft/internal/project_loader/errors.py +++ b/snapcraft_legacy/internal/project_loader/errors.py @@ -16,10 +16,10 @@ import pathlib -import snapcraft.internal.errors +import snapcraft_legacy.internal.errors -class ProjectLoaderError(snapcraft.internal.errors.SnapcraftError): +class ProjectLoaderError(snapcraft_legacy.internal.errors.SnapcraftError): fmt = "" @@ -123,7 +123,9 @@ def __init__(self, part_name, after_part_name): super().__init__(part_name=part_name, after_part_name=after_part_name) -class SnapcraftProjectUnusedKeyAssetError(snapcraft.internal.errors.SnapcraftException): +class SnapcraftProjectUnusedKeyAssetError( + snapcraft_legacy.internal.errors.SnapcraftException +): def __init__(self, key_path: pathlib.Path): self.key_path = key_path diff --git a/snapcraft/internal/project_loader/grammar/__init__.py b/snapcraft_legacy/internal/project_loader/grammar/__init__.py similarity index 100% rename from snapcraft/internal/project_loader/grammar/__init__.py rename to snapcraft_legacy/internal/project_loader/grammar/__init__.py diff --git a/snapcraft/internal/project_loader/grammar/_compound.py b/snapcraft_legacy/internal/project_loader/grammar/_compound.py similarity index 100% rename from snapcraft/internal/project_loader/grammar/_compound.py rename to snapcraft_legacy/internal/project_loader/grammar/_compound.py diff --git a/snapcraft/internal/project_loader/grammar/_on.py b/snapcraft_legacy/internal/project_loader/grammar/_on.py similarity index 95% rename from snapcraft/internal/project_loader/grammar/_on.py rename to snapcraft_legacy/internal/project_loader/grammar/_on.py index 7e6269b9c7..75fccbdfc4 100644 --- a/snapcraft/internal/project_loader/grammar/_on.py +++ b/snapcraft_legacy/internal/project_loader/grammar/_on.py @@ -17,7 +17,7 @@ import re from typing import TYPE_CHECKING, Optional, Set -import snapcraft +import snapcraft_legacy from . import typing from ._statement import Statement @@ -35,8 +35,8 @@ class OnStatement(Statement): """Process an 'on' statement in the grammar. For example: - >>> from snapcraft import ProjectOptions - >>> from snapcraft.internal.project_loader import grammar + >>> from snapcraft_legacy import ProjectOptions + >>> from snapcraft_legacy.internal.project_loader import grammar >>> from unittest import mock >>> >>> def checker(primitive): @@ -82,7 +82,7 @@ def _check(self) -> bool: """ # A new ProjectOptions instance defaults to the host architecture # whereas self._project_options would yield the target architecture - host_arch = snapcraft.ProjectOptions().deb_arch + host_arch = snapcraft_legacy.ProjectOptions().deb_arch # The only selector currently supported is the host arch. Since # selectors are matched with an AND, not OR, there should only be one diff --git a/snapcraft/internal/project_loader/grammar/_processor.py b/snapcraft_legacy/internal/project_loader/grammar/_processor.py similarity index 99% rename from snapcraft/internal/project_loader/grammar/_processor.py rename to snapcraft_legacy/internal/project_loader/grammar/_processor.py index 741e8e9958..1bf5c3ec17 100644 --- a/snapcraft/internal/project_loader/grammar/_processor.py +++ b/snapcraft_legacy/internal/project_loader/grammar/_processor.py @@ -17,7 +17,7 @@ import re from typing import Any, Callable, Dict, List, Optional, Tuple -from snapcraft import project +from snapcraft_legacy import project from . import typing from ._compound import CompoundStatement @@ -51,7 +51,7 @@ def __init__( :param list grammar: Unprocessed grammar. :param project: Instance of Project to use to determine appropriate primitives. - :type project: snapcraft.project.Project + :type project: snapcraft_legacy.project.Project :param callable checker: callable accepting a single primitive, returning true if it is valid. :param callable transformer: callable accepting a call stack, single diff --git a/snapcraft/internal/project_loader/grammar/_statement.py b/snapcraft_legacy/internal/project_loader/grammar/_statement.py similarity index 100% rename from snapcraft/internal/project_loader/grammar/_statement.py rename to snapcraft_legacy/internal/project_loader/grammar/_statement.py diff --git a/snapcraft/internal/project_loader/grammar/_to.py b/snapcraft_legacy/internal/project_loader/grammar/_to.py similarity index 97% rename from snapcraft/internal/project_loader/grammar/_to.py rename to snapcraft_legacy/internal/project_loader/grammar/_to.py index 8166e11fbe..f9a8b31c1b 100644 --- a/snapcraft/internal/project_loader/grammar/_to.py +++ b/snapcraft_legacy/internal/project_loader/grammar/_to.py @@ -34,8 +34,8 @@ class ToStatement(Statement): For example: >>> import tempfile - >>> from snapcraft import ProjectOptions - >>> from snapcraft.internal.project_loader import grammar + >>> from snapcraft_legacy import ProjectOptions + >>> from snapcraft_legacy.internal.project_loader import grammar >>> def checker(primitive): ... return True >>> options = ProjectOptions(target_deb_arch='i386') diff --git a/snapcraft/internal/project_loader/grammar/_try.py b/snapcraft_legacy/internal/project_loader/grammar/_try.py similarity index 97% rename from snapcraft/internal/project_loader/grammar/_try.py rename to snapcraft_legacy/internal/project_loader/grammar/_try.py index 9521e599a3..663f9ba5bc 100644 --- a/snapcraft/internal/project_loader/grammar/_try.py +++ b/snapcraft_legacy/internal/project_loader/grammar/_try.py @@ -28,7 +28,7 @@ class TryStatement(Statement): """Process a 'try' statement in the grammar. For example: - >>> from snapcraft import ProjectOptions + >>> from snapcraft_legacy import ProjectOptions >>> from ._processor import GrammarProcessor >>> def checker(primitive): ... return 'invalid' not in primitive diff --git a/snapcraft/internal/project_loader/grammar/errors.py b/snapcraft_legacy/internal/project_loader/grammar/errors.py similarity index 97% rename from snapcraft/internal/project_loader/grammar/errors.py rename to snapcraft_legacy/internal/project_loader/grammar/errors.py index 50768e9f76..e5de341e5d 100644 --- a/snapcraft/internal/project_loader/grammar/errors.py +++ b/snapcraft_legacy/internal/project_loader/grammar/errors.py @@ -14,7 +14,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -from snapcraft.internal import errors +from snapcraft_legacy.internal import errors class GrammarError(errors.SnapcraftError): diff --git a/snapcraft/internal/project_loader/grammar/typing.py b/snapcraft_legacy/internal/project_loader/grammar/typing.py similarity index 100% rename from snapcraft/internal/project_loader/grammar/typing.py rename to snapcraft_legacy/internal/project_loader/grammar/typing.py diff --git a/snapcraft/internal/project_loader/grammar_processing/__init__.py b/snapcraft_legacy/internal/project_loader/grammar_processing/__init__.py similarity index 100% rename from snapcraft/internal/project_loader/grammar_processing/__init__.py rename to snapcraft_legacy/internal/project_loader/grammar_processing/__init__.py diff --git a/snapcraft/internal/project_loader/grammar_processing/_global_grammar_processor.py b/snapcraft_legacy/internal/project_loader/grammar_processing/_global_grammar_processor.py similarity index 85% rename from snapcraft/internal/project_loader/grammar_processing/_global_grammar_processor.py rename to snapcraft_legacy/internal/project_loader/grammar_processing/_global_grammar_processor.py index 2725665b64..2e44eef96c 100644 --- a/snapcraft/internal/project_loader/grammar_processing/_global_grammar_processor.py +++ b/snapcraft_legacy/internal/project_loader/grammar_processing/_global_grammar_processor.py @@ -16,20 +16,20 @@ from typing import Any, Dict, Set -from snapcraft import project -from snapcraft.internal import repo -from snapcraft.internal.project_loader import grammar +from snapcraft_legacy import project +from snapcraft_legacy.internal import repo +from snapcraft_legacy.internal.project_loader import grammar class GlobalGrammarProcessor: """Process global properties that support grammar. Build packages example: - >>> import snapcraft - >>> from snapcraft import repo + >>> import snapcraft_legacy + >>> from snapcraft_legacy import repo >>> processor = GlobalGrammarProcessor( ... properties={'build-packages': [{'try': ['hello']}]}, - ... project=snapcraft.project.Project()) + ... project=snapcraft_legacy.project.Project()) >>> processor.get_build_packages() {'hello'} """ diff --git a/snapcraft/internal/project_loader/grammar_processing/_package_transformer.py b/snapcraft_legacy/internal/project_loader/grammar_processing/_package_transformer.py similarity index 94% rename from snapcraft/internal/project_loader/grammar_processing/_package_transformer.py rename to snapcraft_legacy/internal/project_loader/grammar_processing/_package_transformer.py index 03f6f22891..ffba98f35a 100644 --- a/snapcraft/internal/project_loader/grammar_processing/_package_transformer.py +++ b/snapcraft_legacy/internal/project_loader/grammar_processing/_package_transformer.py @@ -14,8 +14,8 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -from snapcraft import project -from snapcraft.internal.project_loader.grammar import ( +from snapcraft_legacy import project +from snapcraft_legacy.internal.project_loader.grammar import ( CompoundStatement, Statement, ToStatement, diff --git a/snapcraft/internal/project_loader/grammar_processing/_part_grammar_processor.py b/snapcraft_legacy/internal/project_loader/grammar_processing/_part_grammar_processor.py similarity index 92% rename from snapcraft/internal/project_loader/grammar_processing/_part_grammar_processor.py rename to snapcraft_legacy/internal/project_loader/grammar_processing/_part_grammar_processor.py index 126a19e2df..a2dc1aec80 100644 --- a/snapcraft/internal/project_loader/grammar_processing/_part_grammar_processor.py +++ b/snapcraft_legacy/internal/project_loader/grammar_processing/_part_grammar_processor.py @@ -16,9 +16,9 @@ from typing import Any, Dict, List, Set -from snapcraft import BasePlugin, project -from snapcraft.internal import repo -from snapcraft.internal.project_loader import grammar +from snapcraft_legacy import BasePlugin, project +from snapcraft_legacy.internal import repo +from snapcraft_legacy.internal.project_loader import grammar from ._package_transformer import package_transformer @@ -28,7 +28,7 @@ class PartGrammarProcessor: Stage packages example: >>> from unittest import mock - >>> import snapcraft + >>> import snapcraft_legacy >>> # Pretend that all packages are valid >>> repo = mock.Mock() >>> repo.is_valid.return_value = True @@ -37,14 +37,14 @@ class PartGrammarProcessor: >>> processor = PartGrammarProcessor( ... plugin=plugin, ... properties={}, - ... project=snapcraft.project.Project(), + ... project=snapcraft_legacy.project.Project(), ... repo=repo) >>> processor.get_stage_packages() {'foo'} Build packages example: >>> from unittest import mock - >>> import snapcraft + >>> import snapcraft_legacy >>> # Pretend that all packages are valid >>> repo = mock.Mock() >>> repo.is_valid.return_value = True @@ -53,20 +53,20 @@ class PartGrammarProcessor: >>> processor = PartGrammarProcessor( ... plugin=plugin, ... properties={}, - ... project=snapcraft.project.Project(), + ... project=snapcraft_legacy.project.Project(), ... repo=repo) >>> processor.get_build_packages() {'foo'} Source example: >>> from unittest import mock - >>> import snapcraft + >>> import snapcraft_legacy >>> plugin = mock.Mock() >>> plugin.properties = {'source': [{'on amd64': 'foo'}, 'else fail']} >>> processor = PartGrammarProcessor( ... plugin=plugin, ... properties=plugin.properties, - ... project=snapcraft.project.Project(), + ... project=snapcraft_legacy.project.Project(), ... repo=None) >>> processor.get_source() 'foo' diff --git a/snapcraft/internal/project_loader/inspection/__init__.py b/snapcraft_legacy/internal/project_loader/inspection/__init__.py similarity index 100% rename from snapcraft/internal/project_loader/inspection/__init__.py rename to snapcraft_legacy/internal/project_loader/inspection/__init__.py diff --git a/snapcraft/internal/project_loader/inspection/_latest_step.py b/snapcraft_legacy/internal/project_loader/inspection/_latest_step.py similarity index 89% rename from snapcraft/internal/project_loader/inspection/_latest_step.py rename to snapcraft_legacy/internal/project_loader/inspection/_latest_step.py index d7510abc72..2bb99436a9 100644 --- a/snapcraft/internal/project_loader/inspection/_latest_step.py +++ b/snapcraft_legacy/internal/project_loader/inspection/_latest_step.py @@ -17,8 +17,8 @@ import contextlib from typing import List, Tuple -import snapcraft.internal.errors -from snapcraft.internal import pluginhandler, steps +import snapcraft_legacy.internal.errors +from snapcraft_legacy.internal import pluginhandler, steps from . import errors @@ -35,7 +35,7 @@ def latest_step( latest_step = None latest_timestamp = 0 for part in parts: - with contextlib.suppress(snapcraft.internal.errors.NoLatestStepError): + with contextlib.suppress(snapcraft_legacy.internal.errors.NoLatestStepError): step = part.latest_step() timestamp = part.step_timestamp(step) if latest_timestamp < timestamp: diff --git a/snapcraft/internal/project_loader/inspection/_lifecycle_status.py b/snapcraft_legacy/internal/project_loader/inspection/_lifecycle_status.py similarity index 94% rename from snapcraft/internal/project_loader/inspection/_lifecycle_status.py rename to snapcraft_legacy/internal/project_loader/inspection/_lifecycle_status.py index 5931f84316..ad0eb7193f 100644 --- a/snapcraft/internal/project_loader/inspection/_lifecycle_status.py +++ b/snapcraft_legacy/internal/project_loader/inspection/_lifecycle_status.py @@ -16,8 +16,8 @@ from typing import Dict, List -from snapcraft.internal import lifecycle, steps -from snapcraft.internal.project_loader import _config +from snapcraft_legacy.internal import lifecycle, steps +from snapcraft_legacy.internal.project_loader import _config def lifecycle_status(config: _config.Config) -> List[Dict[str, str]]: diff --git a/snapcraft/internal/project_loader/inspection/_provides.py b/snapcraft_legacy/internal/project_loader/inspection/_provides.py similarity index 97% rename from snapcraft/internal/project_loader/inspection/_provides.py rename to snapcraft_legacy/internal/project_loader/inspection/_provides.py index b83ec70358..2ad9068c96 100644 --- a/snapcraft/internal/project_loader/inspection/_provides.py +++ b/snapcraft_legacy/internal/project_loader/inspection/_provides.py @@ -17,8 +17,8 @@ import os from typing import Callable, Iterable, Optional, Set, Tuple, Union -from snapcraft import project -from snapcraft.internal import pluginhandler, states, steps +from snapcraft_legacy import project +from snapcraft_legacy.internal import pluginhandler, states, steps from . import errors diff --git a/snapcraft/internal/project_loader/inspection/errors.py b/snapcraft_legacy/internal/project_loader/inspection/errors.py similarity index 89% rename from snapcraft/internal/project_loader/inspection/errors.py rename to snapcraft_legacy/internal/project_loader/inspection/errors.py index b0da97398d..7358972f45 100644 --- a/snapcraft/internal/project_loader/inspection/errors.py +++ b/snapcraft_legacy/internal/project_loader/inspection/errors.py @@ -14,10 +14,10 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -import snapcraft.internal.errors +import snapcraft_legacy.internal.errors -class NoSuchFileError(snapcraft.internal.errors.SnapcraftError): +class NoSuchFileError(snapcraft_legacy.internal.errors.SnapcraftError): fmt = ( "Failed to find part that provided path: {path!r} does not " @@ -29,7 +29,7 @@ def __init__(self, path): super().__init__(path=path) -class SnapcraftInspectError(snapcraft.internal.errors.SnapcraftError): +class SnapcraftInspectError(snapcraft_legacy.internal.errors.SnapcraftError): # Use a different exit code for these errors so the orchestrating snapcraft can # differentiate them. def get_exit_code(self): diff --git a/snapcraft/internal/remote_build/__init__.py b/snapcraft_legacy/internal/remote_build/__init__.py similarity index 100% rename from snapcraft/internal/remote_build/__init__.py rename to snapcraft_legacy/internal/remote_build/__init__.py diff --git a/snapcraft/internal/remote_build/_info_file.py b/snapcraft_legacy/internal/remote_build/_info_file.py similarity index 97% rename from snapcraft/internal/remote_build/_info_file.py rename to snapcraft_legacy/internal/remote_build/_info_file.py index 685cc46971..66e2c25288 100644 --- a/snapcraft/internal/remote_build/_info_file.py +++ b/snapcraft_legacy/internal/remote_build/_info_file.py @@ -17,7 +17,7 @@ import os from typing import Any -from snapcraft import yaml_utils +from snapcraft_legacy import yaml_utils class InfoFile(dict): diff --git a/snapcraft/internal/remote_build/_launchpad.py b/snapcraft_legacy/internal/remote_build/_launchpad.py similarity index 97% rename from snapcraft/internal/remote_build/_launchpad.py rename to snapcraft_legacy/internal/remote_build/_launchpad.py index d5c6c974a7..6eb0b02009 100644 --- a/snapcraft/internal/remote_build/_launchpad.py +++ b/snapcraft_legacy/internal/remote_build/_launchpad.py @@ -29,10 +29,10 @@ from lazr.restfulclient.resource import Entry from xdg import BaseDirectory -import snapcraft -from snapcraft.internal.sources._git import Git -from snapcraft.internal.sources.errors import SnapcraftPullError -from snapcraft.project import Project +import snapcraft_legacy +from snapcraft_legacy.internal.sources._git import Git +from snapcraft_legacy.internal.sources.errors import SnapcraftPullError +from snapcraft_legacy.project import Project from . import errors @@ -91,7 +91,7 @@ def __init__( snapcraft_channel: str = "stable", deadline: int = 0, git_class: Type[Git] = Git, - running_snapcraft_version: str = snapcraft.__version__, + running_snapcraft_version: str = snapcraft_legacy.__version__, ) -> None: self._git_class = git_class if not self._git_class.check_command_installed(): @@ -241,7 +241,7 @@ def _wait_for_build_request_acceptance( def login(self) -> Launchpad: try: return Launchpad.login_with( - "snapcraft remote-build {}".format(snapcraft.__version__), + "snapcraft remote-build {}".format(snapcraft_legacy.__version__), "production", self._cache_dir, credentials_file=self._credentials, diff --git a/snapcraft/internal/remote_build/_worktree.py b/snapcraft_legacy/internal/remote_build/_worktree.py similarity index 96% rename from snapcraft/internal/remote_build/_worktree.py rename to snapcraft_legacy/internal/remote_build/_worktree.py index 929995e171..e41c052ee2 100644 --- a/snapcraft/internal/remote_build/_worktree.py +++ b/snapcraft_legacy/internal/remote_build/_worktree.py @@ -22,13 +22,13 @@ from collections import OrderedDict from copy import deepcopy -import snapcraft -import snapcraft.internal.sources -from snapcraft import yaml_utils -from snapcraft.file_utils import rmtree -from snapcraft.internal.meta import _version -from snapcraft.internal.remote_build import errors -from snapcraft.project import Project +import snapcraft_legacy +import snapcraft_legacy.internal.sources +from snapcraft_legacy import yaml_utils +from snapcraft_legacy.file_utils import rmtree +from snapcraft_legacy.internal.meta import _version +from snapcraft_legacy.internal.remote_build import errors +from snapcraft_legacy.project import Project logger = logging.getLogger(__name__) @@ -88,7 +88,7 @@ def _get_part_source_handler(self, part_name: str, source: str, source_dir: str) part_config["source"] = source source_type = part_config.get("source-type") - handler_class = snapcraft.internal.sources.get_source_handler( + handler_class = snapcraft_legacy.internal.sources.get_source_handler( source, source_type=source_type ) return handler_class( @@ -169,9 +169,9 @@ def _pull_source(self, part_name: str, source: str, selector=None) -> str: # Skip non-local sources (the remote builder can fetch those directly), # unless configured to package all sources. is_local_source = isinstance( - source_handler, snapcraft.internal.sources.Local + source_handler, snapcraft_legacy.internal.sources.Local ) or ( - isinstance(source_handler, snapcraft.internal.sources.Git) + isinstance(source_handler, snapcraft_legacy.internal.sources.Git) and source_handler.is_local() ) if not self._package_all_sources and not is_local_source: diff --git a/snapcraft/internal/remote_build/errors.py b/snapcraft_legacy/internal/remote_build/errors.py similarity index 97% rename from snapcraft/internal/remote_build/errors.py rename to snapcraft_legacy/internal/remote_build/errors.py index 7c546d17aa..c276d94196 100644 --- a/snapcraft/internal/remote_build/errors.py +++ b/snapcraft_legacy/internal/remote_build/errors.py @@ -16,8 +16,8 @@ from typing import List, Sequence # noqa: F401 -from snapcraft.internal.errors import SnapcraftError as _SnapcraftError -from snapcraft.internal.errors import SnapcraftException +from snapcraft_legacy.internal.errors import SnapcraftError as _SnapcraftError +from snapcraft_legacy.internal.errors import SnapcraftException class RemoteBuildBaseError(_SnapcraftError): diff --git a/snapcraft/internal/repo/__init__.py b/snapcraft_legacy/internal/repo/__init__.py similarity index 100% rename from snapcraft/internal/repo/__init__.py rename to snapcraft_legacy/internal/repo/__init__.py diff --git a/snapcraft/internal/repo/_base.py b/snapcraft_legacy/internal/repo/_base.py similarity index 95% rename from snapcraft/internal/repo/_base.py rename to snapcraft_legacy/internal/repo/_base.py index a2b7b4f850..bffcbc8f0a 100644 --- a/snapcraft/internal/repo/_base.py +++ b/snapcraft_legacy/internal/repo/_base.py @@ -26,8 +26,8 @@ import stat from typing import List, Optional, Set -from snapcraft import file_utils -from snapcraft.internal import mangling, xattrs +from snapcraft_legacy import file_utils +from snapcraft_legacy.internal import mangling, xattrs from . import errors @@ -72,7 +72,7 @@ def get_package_for_file(cls, file_path: str) -> str: :param str file_path: the absolute path to the file to search for. :returns: package name that provides file_path. :rtype: str - :raises snapcraft.repo.errors.FileProviderNotFound: + :raises snapcraft_legacy.repo.errors.FileProviderNotFound: if file_path is not provided by any package. """ raise errors.NoNativeBackendError() @@ -105,9 +105,9 @@ def refresh_build_packages(cls) -> None: """Refresh the build packages cache. If refreshing is not possible - snapcraft.repo.errors.CacheUpdateFailedError should be raised + snapcraft_legacy.repo.errors.CacheUpdateFailedError should be raised - :raises snapcraft.repo.errors.NoNativeBackendError: + :raises snapcraft_legacy.repo.errors.NoNativeBackendError: if the method is not implemented in the subclass. """ raise errors.NoNativeBackendError() @@ -123,17 +123,17 @@ def install_build_packages(cls, package_names: List[str]) -> List[str]: in the form "package=version". If one of the packages cannot be found - snapcraft.repo.errors.BuildPackageNotFoundError should be raised. + snapcraft_legacy.repo.errors.BuildPackageNotFoundError should be raised. If dependencies for a package cannot be resolved - snapcraft.repo.errors.PackageBrokenError should be raised. + snapcraft_legacy.repo.errors.PackageBrokenError should be raised. If installing a package on the host failed - snapcraft.repo.errors.BuildPackagesNotInstalledError should be raised. + snapcraft_legacy.repo.errors.BuildPackagesNotInstalledError should be raised. :param package_names: a list of package names to install. :type package_names: a list of strings. :return: a list with the packages installed and their versions. :rtype: list of strings. - :raises snapcraft.repo.errors.NoNativeBackendError: + :raises snapcraft_legacy.repo.errors.NoNativeBackendError: if the method is not implemented in the subclass. """ raise errors.NoNativeBackendError() diff --git a/snapcraft/internal/repo/_deb.py b/snapcraft_legacy/internal/repo/_deb.py similarity index 98% rename from snapcraft/internal/repo/_deb.py rename to snapcraft_legacy/internal/repo/_deb.py index d796e6771d..4ad513627f 100644 --- a/snapcraft/internal/repo/_deb.py +++ b/snapcraft_legacy/internal/repo/_deb.py @@ -27,8 +27,8 @@ from xdg import BaseDirectory -from snapcraft import file_utils -from snapcraft.internal.indicators import is_dumb_terminal +from snapcraft_legacy import file_utils +from snapcraft_legacy.internal.indicators import is_dumb_terminal from . import errors from ._base import BaseRepo, get_pkg_name_parts @@ -368,11 +368,11 @@ def install_build_packages(cls, package_names: List[str]) -> List[str]: :type package_names: a list of strings. :return: a list with the packages installed and their versions. :rtype: list of strings. - :raises snapcraft.repo.errors.BuildPackageNotFoundError: + :raises snapcraft_legacy.repo.errors.BuildPackageNotFoundError: if one of the packages was not found. - :raises snapcraft.repo.errors.PackageBrokenError: + :raises snapcraft_legacy.repo.errors.PackageBrokenError: if dependencies for one of the packages cannot be resolved. - :raises snapcraft.repo.errors.BuildPackagesNotInstalledError: + :raises snapcraft_legacy.repo.errors.BuildPackagesNotInstalledError: if installing the packages on the host failed. """ install_required = False diff --git a/snapcraft/internal/repo/_platform.py b/snapcraft_legacy/internal/repo/_platform.py similarity index 90% rename from snapcraft/internal/repo/_platform.py rename to snapcraft_legacy/internal/repo/_platform.py index 0e0e575f4b..5b450636c6 100644 --- a/snapcraft/internal/repo/_platform.py +++ b/snapcraft_legacy/internal/repo/_platform.py @@ -16,8 +16,8 @@ import logging -from snapcraft.internal.errors import OsReleaseIdError -from snapcraft.internal.os_release import OsRelease +from snapcraft_legacy.internal.errors import OsReleaseIdError +from snapcraft_legacy.internal.os_release import OsRelease logger = logging.getLogger(__name__) diff --git a/snapcraft/internal/repo/apt_cache.py b/snapcraft_legacy/internal/repo/apt_cache.py similarity index 98% rename from snapcraft/internal/repo/apt_cache.py rename to snapcraft_legacy/internal/repo/apt_cache.py index 7b48ed9563..11be8a58f5 100644 --- a/snapcraft/internal/repo/apt_cache.py +++ b/snapcraft_legacy/internal/repo/apt_cache.py @@ -24,10 +24,10 @@ import apt -from snapcraft.internal import common -from snapcraft.internal.indicators import is_dumb_terminal -from snapcraft.internal.repo import errors -from snapcraft.internal.repo._base import get_pkg_name_parts +from snapcraft_legacy.internal import common +from snapcraft_legacy.internal.indicators import is_dumb_terminal +from snapcraft_legacy.internal.repo import errors +from snapcraft_legacy.internal.repo._base import get_pkg_name_parts logger = logging.getLogger(__name__) diff --git a/snapcraft/internal/repo/apt_key_manager.py b/snapcraft_legacy/internal/repo/apt_key_manager.py similarity index 99% rename from snapcraft/internal/repo/apt_key_manager.py rename to snapcraft_legacy/internal/repo/apt_key_manager.py index 85e446eace..0ea6f82a54 100644 --- a/snapcraft/internal/repo/apt_key_manager.py +++ b/snapcraft_legacy/internal/repo/apt_key_manager.py @@ -22,7 +22,7 @@ import gnupg -from snapcraft.internal.meta import package_repository +from snapcraft_legacy.internal.meta import package_repository from . import apt_ppa, errors diff --git a/snapcraft/internal/repo/apt_ppa.py b/snapcraft_legacy/internal/repo/apt_ppa.py similarity index 100% rename from snapcraft/internal/repo/apt_ppa.py rename to snapcraft_legacy/internal/repo/apt_ppa.py diff --git a/snapcraft/internal/repo/apt_sources_manager.py b/snapcraft_legacy/internal/repo/apt_sources_manager.py similarity index 97% rename from snapcraft/internal/repo/apt_sources_manager.py rename to snapcraft_legacy/internal/repo/apt_sources_manager.py index 58209d04f9..5186af1cd1 100644 --- a/snapcraft/internal/repo/apt_sources_manager.py +++ b/snapcraft_legacy/internal/repo/apt_sources_manager.py @@ -25,9 +25,9 @@ import tempfile from typing import List, Optional -from snapcraft.internal import os_release -from snapcraft.internal.meta import package_repository -from snapcraft.project._project_options import ProjectOptions +from snapcraft_legacy.internal import os_release +from snapcraft_legacy.internal.meta import package_repository +from snapcraft_legacy.project._project_options import ProjectOptions from . import apt_ppa diff --git a/snapcraft/internal/repo/deb_package.py b/snapcraft_legacy/internal/repo/deb_package.py similarity index 100% rename from snapcraft/internal/repo/deb_package.py rename to snapcraft_legacy/internal/repo/deb_package.py diff --git a/snapcraft/internal/repo/errors.py b/snapcraft_legacy/internal/repo/errors.py similarity index 97% rename from snapcraft/internal/repo/errors.py rename to snapcraft_legacy/internal/repo/errors.py index 137490f61b..a9c0cda007 100644 --- a/snapcraft/internal/repo/errors.py +++ b/snapcraft_legacy/internal/repo/errors.py @@ -16,10 +16,10 @@ from typing import List, Optional, Sequence -from snapcraft import formatting_utils -from snapcraft.internal import errors -from snapcraft.internal.errors import SnapcraftException -from snapcraft.internal.os_release import OsRelease +from snapcraft_legacy import formatting_utils +from snapcraft_legacy.internal import errors +from snapcraft_legacy.internal.errors import SnapcraftException +from snapcraft_legacy.internal.os_release import OsRelease from ._platform import _is_deb_based diff --git a/snapcraft/internal/repo/snaps.py b/snapcraft_legacy/internal/repo/snaps.py similarity index 100% rename from snapcraft/internal/repo/snaps.py rename to snapcraft_legacy/internal/repo/snaps.py diff --git a/snapcraft/internal/repo/ua_manager.py b/snapcraft_legacy/internal/repo/ua_manager.py similarity index 98% rename from snapcraft/internal/repo/ua_manager.py rename to snapcraft_legacy/internal/repo/ua_manager.py index 4f3c8c624e..de1dfadf3d 100644 --- a/snapcraft/internal/repo/ua_manager.py +++ b/snapcraft_legacy/internal/repo/ua_manager.py @@ -20,7 +20,7 @@ import subprocess from typing import Any, Dict, Iterator, Optional -from snapcraft.internal import repo +from snapcraft_legacy.internal import repo logger = logging.getLogger(__name__) diff --git a/snapcraft/internal/review_tools/__init__.py b/snapcraft_legacy/internal/review_tools/__init__.py similarity index 100% rename from snapcraft/internal/review_tools/__init__.py rename to snapcraft_legacy/internal/review_tools/__init__.py diff --git a/snapcraft/internal/review_tools/_runner.py b/snapcraft_legacy/internal/review_tools/_runner.py similarity index 98% rename from snapcraft/internal/review_tools/_runner.py rename to snapcraft_legacy/internal/review_tools/_runner.py index 2f7ff3bda4..e6b04db3c4 100644 --- a/snapcraft/internal/review_tools/_runner.py +++ b/snapcraft_legacy/internal/review_tools/_runner.py @@ -18,7 +18,7 @@ import pathlib import subprocess -from snapcraft import file_utils +from snapcraft_legacy import file_utils from . import errors diff --git a/snapcraft/internal/review_tools/errors.py b/snapcraft_legacy/internal/review_tools/errors.py similarity index 97% rename from snapcraft/internal/review_tools/errors.py rename to snapcraft_legacy/internal/review_tools/errors.py index b97362c0f3..6e7a674429 100644 --- a/snapcraft/internal/review_tools/errors.py +++ b/snapcraft_legacy/internal/review_tools/errors.py @@ -16,7 +16,7 @@ from typing import Any, Dict, Optional -from snapcraft.internal.errors import SnapcraftException +from snapcraft_legacy.internal.errors import SnapcraftException class ReviewToolMissing(SnapcraftException): diff --git a/snapcraft/internal/sources/_7z.py b/snapcraft_legacy/internal/sources/_7z.py similarity index 100% rename from snapcraft/internal/sources/_7z.py rename to snapcraft_legacy/internal/sources/_7z.py diff --git a/snapcraft/internal/sources/__init__.py b/snapcraft_legacy/internal/sources/__init__.py similarity index 100% rename from snapcraft/internal/sources/__init__.py rename to snapcraft_legacy/internal/sources/__init__.py diff --git a/snapcraft/internal/sources/_base.py b/snapcraft_legacy/internal/sources/_base.py similarity index 94% rename from snapcraft/internal/sources/_base.py rename to snapcraft_legacy/internal/sources/_base.py index 8f908d00a3..da2f67cacc 100644 --- a/snapcraft/internal/sources/_base.py +++ b/snapcraft_legacy/internal/sources/_base.py @@ -20,9 +20,9 @@ import requests -import snapcraft.internal.common -from snapcraft.internal.cache import FileCache -from snapcraft.internal.indicators import ( +import snapcraft_legacy.internal.common +from snapcraft_legacy.internal.cache import FileCache +from snapcraft_legacy.internal.indicators import ( download_requests_stream, download_urllib_source, ) @@ -104,7 +104,7 @@ def _run_output(self, command, **kwargs): class FileBase(Base): def pull(self): source_file = None - is_source_url = snapcraft.internal.common.isurl(self.source) + is_source_url = snapcraft_legacy.internal.common.isurl(self.source) # First check if it is a url and download and if not # it is probably locally referenced. @@ -146,7 +146,7 @@ def download(self, filepath: str = None) -> str: return self.file # If not we download and store - if snapcraft.internal.common.get_url_scheme(self.source) == "ftp": + if snapcraft_legacy.internal.common.get_url_scheme(self.source) == "ftp": download_urllib_source(self.source, self.file) else: try: diff --git a/snapcraft/internal/sources/_bazaar.py b/snapcraft_legacy/internal/sources/_bazaar.py similarity index 100% rename from snapcraft/internal/sources/_bazaar.py rename to snapcraft_legacy/internal/sources/_bazaar.py diff --git a/snapcraft/internal/sources/_checksum.py b/snapcraft_legacy/internal/sources/_checksum.py similarity index 97% rename from snapcraft/internal/sources/_checksum.py rename to snapcraft_legacy/internal/sources/_checksum.py index 4fd0012313..3961d7eec3 100644 --- a/snapcraft/internal/sources/_checksum.py +++ b/snapcraft_legacy/internal/sources/_checksum.py @@ -15,7 +15,7 @@ # along with this program. If not, see . from typing import Tuple -from snapcraft.file_utils import calculate_hash +from snapcraft_legacy.file_utils import calculate_hash from . import errors diff --git a/snapcraft/internal/sources/_deb.py b/snapcraft_legacy/internal/sources/_deb.py similarity index 100% rename from snapcraft/internal/sources/_deb.py rename to snapcraft_legacy/internal/sources/_deb.py diff --git a/snapcraft/internal/sources/_git.py b/snapcraft_legacy/internal/sources/_git.py similarity index 99% rename from snapcraft/internal/sources/_git.py rename to snapcraft_legacy/internal/sources/_git.py index 5953868e9c..b6d2b5d3cc 100644 --- a/snapcraft/internal/sources/_git.py +++ b/snapcraft_legacy/internal/sources/_git.py @@ -251,7 +251,7 @@ def add(self, file): command = [self.command, "-C", self.source_dir, "add", file] self._run_git_command(command) - def commit(self, message, author="snapcraft "): + def commit(self, message, author="snapcraft "): command = [ self.command, "-C", diff --git a/snapcraft/internal/sources/_local.py b/snapcraft_legacy/internal/sources/_local.py similarity index 98% rename from snapcraft/internal/sources/_local.py rename to snapcraft_legacy/internal/sources/_local.py index e54b56d8b3..a658d388c0 100644 --- a/snapcraft/internal/sources/_local.py +++ b/snapcraft_legacy/internal/sources/_local.py @@ -19,8 +19,8 @@ import glob import os -from snapcraft import file_utils -from snapcraft.internal import common +from snapcraft_legacy import file_utils +from snapcraft_legacy.internal import common from ._base import Base diff --git a/snapcraft/internal/sources/_mercurial.py b/snapcraft_legacy/internal/sources/_mercurial.py similarity index 100% rename from snapcraft/internal/sources/_mercurial.py rename to snapcraft_legacy/internal/sources/_mercurial.py diff --git a/snapcraft/internal/sources/_rpm.py b/snapcraft_legacy/internal/sources/_rpm.py similarity index 100% rename from snapcraft/internal/sources/_rpm.py rename to snapcraft_legacy/internal/sources/_rpm.py diff --git a/snapcraft/internal/sources/_script.py b/snapcraft_legacy/internal/sources/_script.py similarity index 100% rename from snapcraft/internal/sources/_script.py rename to snapcraft_legacy/internal/sources/_script.py diff --git a/snapcraft/internal/sources/_snap.py b/snapcraft_legacy/internal/sources/_snap.py similarity index 98% rename from snapcraft/internal/sources/_snap.py rename to snapcraft_legacy/internal/sources/_snap.py index 72524f0846..f171725442 100644 --- a/snapcraft/internal/sources/_snap.py +++ b/snapcraft_legacy/internal/sources/_snap.py @@ -18,7 +18,7 @@ import shutil import tempfile -from snapcraft import file_utils, yaml_utils +from snapcraft_legacy import file_utils, yaml_utils from . import errors from ._base import FileBase diff --git a/snapcraft/internal/sources/_subversion.py b/snapcraft_legacy/internal/sources/_subversion.py similarity index 100% rename from snapcraft/internal/sources/_subversion.py rename to snapcraft_legacy/internal/sources/_subversion.py diff --git a/snapcraft/internal/sources/_tar.py b/snapcraft_legacy/internal/sources/_tar.py similarity index 100% rename from snapcraft/internal/sources/_tar.py rename to snapcraft_legacy/internal/sources/_tar.py diff --git a/snapcraft/internal/sources/_zip.py b/snapcraft_legacy/internal/sources/_zip.py similarity index 100% rename from snapcraft/internal/sources/_zip.py rename to snapcraft_legacy/internal/sources/_zip.py diff --git a/snapcraft/internal/sources/errors.py b/snapcraft_legacy/internal/sources/errors.py similarity index 98% rename from snapcraft/internal/sources/errors.py rename to snapcraft_legacy/internal/sources/errors.py index ec612171cf..aba3d79630 100644 --- a/snapcraft/internal/sources/errors.py +++ b/snapcraft_legacy/internal/sources/errors.py @@ -17,8 +17,8 @@ import shlex from typing import List -from snapcraft import formatting_utils -from snapcraft.internal import errors +from snapcraft_legacy import formatting_utils +from snapcraft_legacy.internal import errors class SnapcraftSourceError(errors.SnapcraftError): diff --git a/snapcraft/internal/states/__init__.py b/snapcraft_legacy/internal/states/__init__.py similarity index 53% rename from snapcraft/internal/states/__init__.py rename to snapcraft_legacy/internal/states/__init__.py index c1f6e1251e..0a88af5f71 100644 --- a/snapcraft/internal/states/__init__.py +++ b/snapcraft_legacy/internal/states/__init__.py @@ -14,11 +14,11 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -from snapcraft.internal.states._build_state import BuildState # noqa -from snapcraft.internal.states._global_state import GlobalState # noqa -from snapcraft.internal.states._prime_state import PrimeState # noqa -from snapcraft.internal.states._pull_state import PullState # noqa -from snapcraft.internal.states._stage_state import StageState # noqa -from snapcraft.internal.states._state import PartState # noqa -from snapcraft.internal.states._state import get_state # noqa -from snapcraft.internal.states._state import get_step_state_file # noqa +from snapcraft_legacy.internal.states._build_state import BuildState # noqa +from snapcraft_legacy.internal.states._global_state import GlobalState # noqa +from snapcraft_legacy.internal.states._prime_state import PrimeState # noqa +from snapcraft_legacy.internal.states._pull_state import PullState # noqa +from snapcraft_legacy.internal.states._stage_state import StageState # noqa +from snapcraft_legacy.internal.states._state import PartState # noqa +from snapcraft_legacy.internal.states._state import get_state # noqa +from snapcraft_legacy.internal.states._state import get_step_state_file # noqa diff --git a/snapcraft/internal/states/_build_state.py b/snapcraft_legacy/internal/states/_build_state.py similarity index 91% rename from snapcraft/internal/states/_build_state.py rename to snapcraft_legacy/internal/states/_build_state.py index eb9503e420..505eb2539d 100644 --- a/snapcraft/internal/states/_build_state.py +++ b/snapcraft_legacy/internal/states/_build_state.py @@ -14,8 +14,8 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -import snapcraft.extractors -from snapcraft.internal.states._state import PartState +import snapcraft_legacy.extractors +from snapcraft_legacy.internal.states._state import PartState def _schema_properties(): @@ -55,10 +55,10 @@ def __init__( self.assets.update(machine_assets) if not scriptlet_metadata: - scriptlet_metadata = snapcraft.extractors.ExtractedMetadata() + scriptlet_metadata = snapcraft_legacy.extractors.ExtractedMetadata() if not metadata: - metadata = snapcraft.extractors.ExtractedMetadata() + metadata = snapcraft_legacy.extractors.ExtractedMetadata() if not metadata_files: metadata_files = [] diff --git a/snapcraft/internal/states/_global_state.py b/snapcraft_legacy/internal/states/_global_state.py similarity index 96% rename from snapcraft/internal/states/_global_state.py rename to snapcraft_legacy/internal/states/_global_state.py index cd90c12f80..23d2db12a7 100644 --- a/snapcraft/internal/states/_global_state.py +++ b/snapcraft_legacy/internal/states/_global_state.py @@ -19,8 +19,8 @@ from mypy_extensions import TypedDict -from snapcraft import yaml_utils -from snapcraft.internal.states._state import State +from snapcraft_legacy import yaml_utils +from snapcraft_legacy.internal.states._state import State StateDict = TypedDict( "StateDict", diff --git a/snapcraft/internal/states/_prime_state.py b/snapcraft_legacy/internal/states/_prime_state.py similarity index 92% rename from snapcraft/internal/states/_prime_state.py rename to snapcraft_legacy/internal/states/_prime_state.py index 740b7086cc..6f5e0e5049 100644 --- a/snapcraft/internal/states/_prime_state.py +++ b/snapcraft_legacy/internal/states/_prime_state.py @@ -14,8 +14,8 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -import snapcraft.extractors -from snapcraft.internal.states._state import PartState +import snapcraft_legacy.extractors +from snapcraft_legacy.internal.states._state import PartState class PrimeState(PartState): @@ -34,7 +34,7 @@ def __init__( super().__init__(part_properties, project) if not scriptlet_metadata: - scriptlet_metadata = snapcraft.extractors.ExtractedMetadata() + scriptlet_metadata = snapcraft_legacy.extractors.ExtractedMetadata() self.files = files self.directories = directories diff --git a/snapcraft/internal/states/_pull_state.py b/snapcraft_legacy/internal/states/_pull_state.py similarity index 91% rename from snapcraft/internal/states/_pull_state.py rename to snapcraft_legacy/internal/states/_pull_state.py index a94ee96e45..e77b021013 100644 --- a/snapcraft/internal/states/_pull_state.py +++ b/snapcraft_legacy/internal/states/_pull_state.py @@ -14,8 +14,8 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -import snapcraft.extractors -from snapcraft.internal.states._state import PartState +import snapcraft_legacy.extractors +from snapcraft_legacy.internal.states._state import PartState def _schema_properties(): @@ -62,10 +62,10 @@ def __init__( } if not scriptlet_metadata: - scriptlet_metadata = snapcraft.extractors.ExtractedMetadata() + scriptlet_metadata = snapcraft_legacy.extractors.ExtractedMetadata() if not metadata: - metadata = snapcraft.extractors.ExtractedMetadata() + metadata = snapcraft_legacy.extractors.ExtractedMetadata() if not metadata_files: metadata_files = [] diff --git a/snapcraft/internal/states/_stage_state.py b/snapcraft_legacy/internal/states/_stage_state.py similarity index 91% rename from snapcraft/internal/states/_stage_state.py rename to snapcraft_legacy/internal/states/_stage_state.py index c8756bf7ac..54d6e33ed8 100644 --- a/snapcraft/internal/states/_stage_state.py +++ b/snapcraft_legacy/internal/states/_stage_state.py @@ -14,8 +14,8 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -import snapcraft.extractors -from snapcraft.internal.states._state import PartState +import snapcraft_legacy.extractors +from snapcraft_legacy.internal.states._state import PartState class StageState(PartState): @@ -32,7 +32,7 @@ def __init__( super().__init__(part_properties, project) if not scriptlet_metadata: - scriptlet_metadata = snapcraft.extractors.ExtractedMetadata() + scriptlet_metadata = snapcraft_legacy.extractors.ExtractedMetadata() self.files = files self.directories = directories diff --git a/snapcraft/internal/states/_state.py b/snapcraft_legacy/internal/states/_state.py similarity index 97% rename from snapcraft/internal/states/_state.py rename to snapcraft_legacy/internal/states/_state.py index 3794975e31..70e4cb8bd2 100644 --- a/snapcraft/internal/states/_state.py +++ b/snapcraft_legacy/internal/states/_state.py @@ -16,8 +16,8 @@ import os -from snapcraft import yaml_utils -from snapcraft.internal import steps +from snapcraft_legacy import yaml_utils +from snapcraft_legacy.internal import steps class State(yaml_utils.SnapcraftYAMLObject): diff --git a/snapcraft/internal/steps.py b/snapcraft_legacy/internal/steps.py similarity index 98% rename from snapcraft/internal/steps.py rename to snapcraft_legacy/internal/steps.py index a860bae7f2..4b6dd146d9 100644 --- a/snapcraft/internal/steps.py +++ b/snapcraft_legacy/internal/steps.py @@ -16,7 +16,7 @@ from typing import List, Optional -from snapcraft.internal import errors +from snapcraft_legacy.internal import errors class Step: diff --git a/snapcraft/internal/xattrs.py b/snapcraft_legacy/internal/xattrs.py similarity index 97% rename from snapcraft/internal/xattrs.py rename to snapcraft_legacy/internal/xattrs.py index 0eca53e41b..9167a33eff 100644 --- a/snapcraft/internal/xattrs.py +++ b/snapcraft_legacy/internal/xattrs.py @@ -19,7 +19,7 @@ import sys from typing import Optional -from snapcraft.internal.errors import XAttributeError, XAttributeTooLongError +from snapcraft_legacy.internal.errors import XAttributeError, XAttributeTooLongError def _get_snapcraft_xattr_key(snapcraft_key: str) -> str: diff --git a/snapcraft/plugins/__init__.py b/snapcraft_legacy/plugins/__init__.py similarity index 100% rename from snapcraft/plugins/__init__.py rename to snapcraft_legacy/plugins/__init__.py diff --git a/snapcraft/plugins/_plugin_finder.py b/snapcraft_legacy/plugins/_plugin_finder.py similarity index 98% rename from snapcraft/plugins/_plugin_finder.py rename to snapcraft_legacy/plugins/_plugin_finder.py index c0fab77fe4..f111a99fe7 100644 --- a/snapcraft/plugins/_plugin_finder.py +++ b/snapcraft_legacy/plugins/_plugin_finder.py @@ -17,7 +17,7 @@ import sys from typing import TYPE_CHECKING, Dict, Type, Union -from snapcraft.internal import errors +from snapcraft_legacy.internal import errors from . import v1, v2 diff --git a/snapcraft/plugins/_python/__init__.py b/snapcraft_legacy/plugins/_python/__init__.py similarity index 100% rename from snapcraft/plugins/_python/__init__.py rename to snapcraft_legacy/plugins/_python/__init__.py diff --git a/snapcraft/plugins/v1/__init__.py b/snapcraft_legacy/plugins/v1/__init__.py similarity index 100% rename from snapcraft/plugins/v1/__init__.py rename to snapcraft_legacy/plugins/v1/__init__.py diff --git a/snapcraft/plugins/v1/_plugin.py b/snapcraft_legacy/plugins/v1/_plugin.py similarity index 97% rename from snapcraft/plugins/v1/_plugin.py rename to snapcraft_legacy/plugins/v1/_plugin.py index d1ee35d139..aeb0965221 100644 --- a/snapcraft/plugins/v1/_plugin.py +++ b/snapcraft_legacy/plugins/v1/_plugin.py @@ -21,9 +21,9 @@ from subprocess import CalledProcessError from typing import List -from snapcraft.project import Project -from snapcraft.internal import common, errors -from snapcraft.internal.meta.package_repository import PackageRepository +from snapcraft_legacy.project import Project +from snapcraft_legacy.internal import common, errors +from snapcraft_legacy.internal.meta.package_repository import PackageRepository logger = logging.getLogger(__name__) diff --git a/snapcraft/plugins/v1/_python/__init__.py b/snapcraft_legacy/plugins/v1/_python/__init__.py similarity index 100% rename from snapcraft/plugins/v1/_python/__init__.py rename to snapcraft_legacy/plugins/v1/_python/__init__.py diff --git a/snapcraft/plugins/v1/_python/_pip.py b/snapcraft_legacy/plugins/v1/_python/_pip.py similarity index 98% rename from snapcraft/plugins/v1/_python/_pip.py rename to snapcraft_legacy/plugins/v1/_python/_pip.py index 7c0bedf65e..9c9dd27cae 100644 --- a/snapcraft/plugins/v1/_python/_pip.py +++ b/snapcraft_legacy/plugins/v1/_python/_pip.py @@ -27,9 +27,9 @@ import tempfile from typing import Dict, List, Optional, Sequence, Set -import snapcraft -from snapcraft import file_utils -from snapcraft.internal import mangling +import snapcraft_legacy +from snapcraft_legacy import file_utils +from snapcraft_legacy.internal import mangling from . import errors from ._python_finder import get_python_command, get_python_headers, get_python_home @@ -529,11 +529,13 @@ def _run(self, args, runner=None, **kwargs): # Using None as the default value instead of common.run so we can mock # common.run. if runner is None: - runner = snapcraft.internal.common.run + runner = snapcraft_legacy.internal.common.run return runner( [self._python_command, "-m", "pip"] + list(args), env=env, **kwargs ) def _run_output(self, args, **kwargs): - return self._run(args, runner=snapcraft.internal.common.run_output, **kwargs) + return self._run( + args, runner=snapcraft_legacy.internal.common.run_output, **kwargs + ) diff --git a/snapcraft/plugins/v1/_python/_python_finder.py b/snapcraft_legacy/plugins/v1/_python/_python_finder.py similarity index 100% rename from snapcraft/plugins/v1/_python/_python_finder.py rename to snapcraft_legacy/plugins/v1/_python/_python_finder.py diff --git a/snapcraft/plugins/v1/_python/_sitecustomize.py b/snapcraft_legacy/plugins/v1/_python/_sitecustomize.py similarity index 100% rename from snapcraft/plugins/v1/_python/_sitecustomize.py rename to snapcraft_legacy/plugins/v1/_python/_sitecustomize.py diff --git a/snapcraft/plugins/v1/_python/errors.py b/snapcraft_legacy/plugins/v1/_python/errors.py similarity index 90% rename from snapcraft/plugins/v1/_python/errors.py rename to snapcraft_legacy/plugins/v1/_python/errors.py index 62ad545878..84315c1a7e 100644 --- a/snapcraft/plugins/v1/_python/errors.py +++ b/snapcraft_legacy/plugins/v1/_python/errors.py @@ -14,11 +14,11 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -import snapcraft.formatting_utils -import snapcraft.internal.errors +import snapcraft_legacy.formatting_utils +import snapcraft_legacy.internal.errors -class PythonPluginError(snapcraft.internal.errors.SnapcraftError): +class PythonPluginError(snapcraft_legacy.internal.errors.SnapcraftError): pass @@ -29,7 +29,7 @@ class MissingPythonCommandError(PythonPluginError): def __init__(self, python_version, search_paths): super().__init__( python_version=python_version, - search_paths=snapcraft.formatting_utils.combine_paths( + search_paths=snapcraft_legacy.formatting_utils.combine_paths( search_paths, "", ":" ), ) diff --git a/snapcraft/plugins/v1/_ros/__init__.py b/snapcraft_legacy/plugins/v1/_ros/__init__.py similarity index 100% rename from snapcraft/plugins/v1/_ros/__init__.py rename to snapcraft_legacy/plugins/v1/_ros/__init__.py diff --git a/snapcraft/plugins/v1/_ros/rosdep.py b/snapcraft_legacy/plugins/v1/_ros/rosdep.py similarity index 99% rename from snapcraft/plugins/v1/_ros/rosdep.py rename to snapcraft_legacy/plugins/v1/_ros/rosdep.py index 1e3fd9f31a..c2e9da2be1 100644 --- a/snapcraft/plugins/v1/_ros/rosdep.py +++ b/snapcraft_legacy/plugins/v1/_ros/rosdep.py @@ -23,7 +23,7 @@ import sys from typing import Dict, Set -from snapcraft.internal import errors, repo +from snapcraft_legacy.internal import errors, repo logger = logging.getLogger(__name__) diff --git a/snapcraft/plugins/v1/_ros/wstool.py b/snapcraft_legacy/plugins/v1/_ros/wstool.py similarity index 96% rename from snapcraft/plugins/v1/_ros/wstool.py rename to snapcraft_legacy/plugins/v1/_ros/wstool.py index 37a236fd79..c915250620 100644 --- a/snapcraft/plugins/v1/_ros/wstool.py +++ b/snapcraft_legacy/plugins/v1/_ros/wstool.py @@ -21,9 +21,9 @@ import sys from typing import List -import snapcraft -from snapcraft.internal import errors, repo -from snapcraft.project import Project +import snapcraft_legacy +from snapcraft_legacy.internal import errors, repo +from snapcraft_legacy.project import Project logger = logging.getLogger(__name__) @@ -164,8 +164,8 @@ def _run(self, arguments: List[str]) -> str: ld_library_path = env.get("LD_LIBRARY_PATH", "") env["LD_LIBRARY_PATH"] = ( ld_library_path - + snapcraft.formatting_utils.combine_paths( - snapcraft.common.get_library_paths( + + snapcraft_legacy.formatting_utils.combine_paths( + snapcraft_legacy.common.get_library_paths( self._wstool_install_path, self._project.arch_triplet ), prepend="", diff --git a/snapcraft/plugins/v1/ant.py b/snapcraft_legacy/plugins/v1/ant.py similarity index 98% rename from snapcraft/plugins/v1/ant.py rename to snapcraft_legacy/plugins/v1/ant.py index 35e6760127..11af6d4c3f 100644 --- a/snapcraft/plugins/v1/ant.py +++ b/snapcraft_legacy/plugins/v1/ant.py @@ -65,9 +65,9 @@ from typing import Sequence from urllib.parse import urlsplit -from snapcraft import formatting_utils -from snapcraft.internal import errors, sources -from snapcraft.plugins.v1 import PluginV1 +from snapcraft_legacy import formatting_utils +from snapcraft_legacy.internal import errors, sources +from snapcraft_legacy.plugins.v1 import PluginV1 logger = logging.getLogger(__name__) diff --git a/snapcraft/plugins/v1/autotools.py b/snapcraft_legacy/plugins/v1/autotools.py similarity index 99% rename from snapcraft/plugins/v1/autotools.py rename to snapcraft_legacy/plugins/v1/autotools.py index cd7f322682..0932c94c20 100644 --- a/snapcraft/plugins/v1/autotools.py +++ b/snapcraft_legacy/plugins/v1/autotools.py @@ -42,7 +42,7 @@ import os from pathlib import Path -from snapcraft.plugins.v1 import make +from snapcraft_legacy.plugins.v1 import make class AutotoolsPlugin(make.MakePlugin): diff --git a/snapcraft/plugins/v1/catkin.py b/snapcraft_legacy/plugins/v1/catkin.py similarity index 99% rename from snapcraft/plugins/v1/catkin.py rename to snapcraft_legacy/plugins/v1/catkin.py index 1023d8000b..227bb0f9d0 100644 --- a/snapcraft/plugins/v1/catkin.py +++ b/snapcraft_legacy/plugins/v1/catkin.py @@ -81,16 +81,16 @@ import textwrap from typing import TYPE_CHECKING, List, Set -from snapcraft import file_utils, formatting_utils -from snapcraft.internal import common, errors, mangling, os_release, repo -from snapcraft.internal.meta.package_repository import ( +from snapcraft_legacy import file_utils, formatting_utils +from snapcraft_legacy.internal import common, errors, mangling, os_release, repo +from snapcraft_legacy.internal.meta.package_repository import ( PackageRepository, PackageRepositoryApt, ) -from snapcraft.plugins.v1 import PluginV1, _python, _ros +from snapcraft_legacy.plugins.v1 import PluginV1, _python, _ros if TYPE_CHECKING: - from snapcraft.project import Project + from snapcraft_legacy.project import Project logger = logging.getLogger(__name__) diff --git a/snapcraft/plugins/v1/catkin_tools.py b/snapcraft_legacy/plugins/v1/catkin_tools.py similarity index 98% rename from snapcraft/plugins/v1/catkin_tools.py rename to snapcraft_legacy/plugins/v1/catkin_tools.py index 7ffd25470a..831848f260 100644 --- a/snapcraft/plugins/v1/catkin_tools.py +++ b/snapcraft_legacy/plugins/v1/catkin_tools.py @@ -25,7 +25,7 @@ import logging import os -from snapcraft.plugins.v1 import catkin +from snapcraft_legacy.plugins.v1 import catkin logger = logging.getLogger(__name__) diff --git a/snapcraft/plugins/v1/cmake.py b/snapcraft_legacy/plugins/v1/cmake.py similarity index 99% rename from snapcraft/plugins/v1/cmake.py rename to snapcraft_legacy/plugins/v1/cmake.py index 9ba8c0ef44..a712a62e30 100644 --- a/snapcraft/plugins/v1/cmake.py +++ b/snapcraft_legacy/plugins/v1/cmake.py @@ -37,7 +37,7 @@ import os from typing import List, Optional -from snapcraft.plugins.v1 import PluginV1 +from snapcraft_legacy.plugins.v1 import PluginV1 logger = logging.getLogger(name=__name__) diff --git a/snapcraft/plugins/v1/colcon.py b/snapcraft_legacy/plugins/v1/colcon.py similarity index 99% rename from snapcraft/plugins/v1/colcon.py rename to snapcraft_legacy/plugins/v1/colcon.py index 8b077a7055..522e2db918 100644 --- a/snapcraft/plugins/v1/colcon.py +++ b/snapcraft_legacy/plugins/v1/colcon.py @@ -66,13 +66,13 @@ import textwrap from typing import List -from snapcraft import file_utils -from snapcraft.internal import errors, mangling, os_release, repo -from snapcraft.internal.meta.package_repository import ( +from snapcraft_legacy import file_utils +from snapcraft_legacy.internal import errors, mangling, os_release, repo +from snapcraft_legacy.internal.meta.package_repository import ( PackageRepository, PackageRepositoryApt, ) -from snapcraft.plugins.v1 import PluginV1, _python, _ros +from snapcraft_legacy.plugins.v1 import PluginV1, _python, _ros logger = logging.getLogger(__name__) diff --git a/snapcraft/plugins/v1/conda.py b/snapcraft_legacy/plugins/v1/conda.py similarity index 97% rename from snapcraft/plugins/v1/conda.py rename to snapcraft_legacy/plugins/v1/conda.py index 7fcf7a696e..aa2bbaa9e9 100644 --- a/snapcraft/plugins/v1/conda.py +++ b/snapcraft_legacy/plugins/v1/conda.py @@ -30,8 +30,8 @@ import subprocess from typing import Optional, Tuple -from snapcraft.internal import errors, sources -from snapcraft.plugins.v1 import PluginV1 +from snapcraft_legacy.internal import errors, sources +from snapcraft_legacy.plugins.v1 import PluginV1 _MINICONDA_CHECKSUMS = {"4.6.14": "md5/718259965f234088d785cad1fbd7de03"} diff --git a/snapcraft/plugins/v1/crystal.py b/snapcraft_legacy/plugins/v1/crystal.py similarity index 96% rename from snapcraft/plugins/v1/crystal.py rename to snapcraft_legacy/plugins/v1/crystal.py index 9b0c8736fd..2b765b9c22 100644 --- a/snapcraft/plugins/v1/crystal.py +++ b/snapcraft_legacy/plugins/v1/crystal.py @@ -35,9 +35,9 @@ import os import shutil -from snapcraft import file_utils -from snapcraft.internal import common, elf, errors -from snapcraft.plugins.v1 import PluginV1 +from snapcraft_legacy import file_utils +from snapcraft_legacy.internal import common, elf, errors +from snapcraft_legacy.plugins.v1 import PluginV1 _CRYSTAL_CHANNEL = "latest/stable" diff --git a/snapcraft/plugins/v1/dotnet.py b/snapcraft_legacy/plugins/v1/dotnet.py similarity index 97% rename from snapcraft/plugins/v1/dotnet.py rename to snapcraft_legacy/plugins/v1/dotnet.py index 41a294a953..0065518435 100644 --- a/snapcraft/plugins/v1/dotnet.py +++ b/snapcraft_legacy/plugins/v1/dotnet.py @@ -37,9 +37,9 @@ import urllib.request from typing import List -from snapcraft import formatting_utils, sources -from snapcraft.internal import errors -from snapcraft.plugins.v1 import PluginV1 +from snapcraft_legacy import formatting_utils, sources +from snapcraft_legacy.internal import errors +from snapcraft_legacy.plugins.v1 import PluginV1 _DOTNET_RELEASE_METADATA_URL = "https://dotnetcli.blob.core.windows.net/dotnet/release-metadata/{version}/releases.json" # noqa _RUNTIME_DEFAULT = "2.0.9" diff --git a/snapcraft/plugins/v1/dump.py b/snapcraft_legacy/plugins/v1/dump.py similarity index 90% rename from snapcraft/plugins/v1/dump.py rename to snapcraft_legacy/plugins/v1/dump.py index dc4765a6b6..5bb97f423f 100644 --- a/snapcraft/plugins/v1/dump.py +++ b/snapcraft_legacy/plugins/v1/dump.py @@ -27,9 +27,9 @@ import os -import snapcraft -from snapcraft.internal import errors -from snapcraft.plugins.v1 import PluginV1 +import snapcraft_legacy +from snapcraft_legacy.internal import errors +from snapcraft_legacy.plugins.v1 import PluginV1 class DumpInvalidSymlinkError(errors.SnapcraftError): @@ -54,7 +54,7 @@ def enable_cross_compilation(self): def build(self): super().build() - snapcraft.file_utils.link_or_copy_tree( + snapcraft_legacy.file_utils.link_or_copy_tree( self.builddir, self.installdir, copy_function=lambda src, dst: _link_or_copy(src, dst, self.installdir), @@ -75,11 +75,11 @@ def _link_or_copy(source, destination, boundary): normalized = os.path.normpath(os.path.join(destination_dirname, link)) if os.path.isabs(link) or not normalized.startswith(boundary): # Only follow symlinks that are NOT pointing at libc (LP: #1658774) - if link not in snapcraft.repo.Repo.get_package_libraries("libc6"): + if link not in snapcraft_legacy.repo.Repo.get_package_libraries("libc6"): follow_symlinks = True try: - snapcraft.file_utils.link_or_copy( + snapcraft_legacy.file_utils.link_or_copy( source, destination, follow_symlinks=follow_symlinks ) except errors.SnapcraftCopyFileNotFoundError: diff --git a/snapcraft/plugins/v1/flutter.py b/snapcraft_legacy/plugins/v1/flutter.py similarity index 97% rename from snapcraft/plugins/v1/flutter.py rename to snapcraft_legacy/plugins/v1/flutter.py index 408c55fd4a..aa11ac8b4d 100644 --- a/snapcraft/plugins/v1/flutter.py +++ b/snapcraft_legacy/plugins/v1/flutter.py @@ -38,8 +38,8 @@ import subprocess from typing import Any, Dict, List -from snapcraft import file_utils -from snapcraft.plugins.v1 import PluginV1 +from snapcraft_legacy import file_utils +from snapcraft_legacy.plugins.v1 import PluginV1 logger = logging.getLogger(__name__) diff --git a/snapcraft/plugins/v1/go.py b/snapcraft_legacy/plugins/v1/go.py similarity index 98% rename from snapcraft/plugins/v1/go.py rename to snapcraft_legacy/plugins/v1/go.py index 67d6636767..e3ff1c6f13 100644 --- a/snapcraft/plugins/v1/go.py +++ b/snapcraft_legacy/plugins/v1/go.py @@ -67,12 +67,12 @@ from pkg_resources import parse_version -from snapcraft import common -from snapcraft.internal import elf, errors -from snapcraft.plugins.v1 import PluginV1 +from snapcraft_legacy import common +from snapcraft_legacy.internal import elf, errors +from snapcraft_legacy.plugins.v1 import PluginV1 if TYPE_CHECKING: - from snapcraft.project import Project + from snapcraft_legacy.project import Project logger = logging.getLogger(__name__) diff --git a/snapcraft/plugins/v1/godeps.py b/snapcraft_legacy/plugins/v1/godeps.py similarity index 98% rename from snapcraft/plugins/v1/godeps.py rename to snapcraft_legacy/plugins/v1/godeps.py index 96cb99a927..9f495c6196 100644 --- a/snapcraft/plugins/v1/godeps.py +++ b/snapcraft_legacy/plugins/v1/godeps.py @@ -58,8 +58,8 @@ import os import shutil -from snapcraft import common -from snapcraft.plugins.v1 import PluginV1 +from snapcraft_legacy import common +from snapcraft_legacy.plugins.v1 import PluginV1 logger = logging.getLogger(__name__) diff --git a/snapcraft/plugins/v1/gradle.py b/snapcraft_legacy/plugins/v1/gradle.py similarity index 98% rename from snapcraft/plugins/v1/gradle.py rename to snapcraft_legacy/plugins/v1/gradle.py index 753087a6df..47752e1f29 100644 --- a/snapcraft/plugins/v1/gradle.py +++ b/snapcraft_legacy/plugins/v1/gradle.py @@ -59,9 +59,9 @@ from glob import glob from typing import Sequence -from snapcraft import file_utils, formatting_utils -from snapcraft.internal import errors, sources -from snapcraft.plugins.v1 import PluginV1 +from snapcraft_legacy import file_utils, formatting_utils +from snapcraft_legacy.internal import errors, sources +from snapcraft_legacy.plugins.v1 import PluginV1 logger = logging.getLogger(__name__) diff --git a/snapcraft/plugins/v1/kbuild.py b/snapcraft_legacy/plugins/v1/kbuild.py similarity index 98% rename from snapcraft/plugins/v1/kbuild.py rename to snapcraft_legacy/plugins/v1/kbuild.py index 919c7274d8..4de12303da 100644 --- a/snapcraft/plugins/v1/kbuild.py +++ b/snapcraft_legacy/plugins/v1/kbuild.py @@ -17,7 +17,7 @@ """The kbuild plugin is used for building kbuild based projects as snapcraft parts. -This plugin is based on the snapcraft.BasePlugin and supports the properties +This plugin is based on the snapcraft_legacy.BasePlugin and supports the properties provided by that plus the following kbuild specific options with semantics as explained above: @@ -65,8 +65,8 @@ import re import subprocess -from snapcraft import file_utils -from snapcraft.plugins.v1 import PluginV1 +from snapcraft_legacy import file_utils +from snapcraft_legacy.plugins.v1 import PluginV1 logger = logging.getLogger(__name__) diff --git a/snapcraft/plugins/v1/kernel.py b/snapcraft_legacy/plugins/v1/kernel.py similarity index 98% rename from snapcraft/plugins/v1/kernel.py rename to snapcraft_legacy/plugins/v1/kernel.py index 03f58dbe4f..7efbdd089b 100644 --- a/snapcraft/plugins/v1/kernel.py +++ b/snapcraft_legacy/plugins/v1/kernel.py @@ -63,8 +63,8 @@ import subprocess import tempfile -import snapcraft -from snapcraft.plugins.v1 import kbuild +import snapcraft_legacy +from snapcraft_legacy.plugins.v1 import kbuild logger = logging.getLogger(__name__) @@ -273,7 +273,9 @@ def _unpack_generic_initrd(self): os.makedirs(initrd_unpacked_path) with tempfile.TemporaryDirectory() as temp_dir: - unsquashfs_path = snapcraft.file_utils.get_snap_tool_path("unsquashfs") + unsquashfs_path = snapcraft_legacy.file_utils.get_snap_tool_path( + "unsquashfs" + ) subprocess.check_call( [unsquashfs_path, self.os_snap, os.path.dirname(initrd_path)], cwd=temp_dir, @@ -507,7 +509,7 @@ def _do_check_initrd(self, builtin, modules): def pull(self): super().pull() - snapcraft.download( + snapcraft_legacy.download( "core", risk="stable", download_path=self.os_snap, diff --git a/snapcraft/plugins/v1/make.py b/snapcraft_legacy/plugins/v1/make.py similarity index 93% rename from snapcraft/plugins/v1/make.py rename to snapcraft_legacy/plugins/v1/make.py index 8921de29a0..f8e3ee4e7d 100644 --- a/snapcraft/plugins/v1/make.py +++ b/snapcraft_legacy/plugins/v1/make.py @@ -49,8 +49,8 @@ import os -import snapcraft.common -from snapcraft.plugins.v1 import PluginV1 +import snapcraft_legacy.common +from snapcraft_legacy.plugins.v1 import PluginV1 class MakePlugin(PluginV1): @@ -105,11 +105,13 @@ def make(self, env=None): source_path = os.path.join(self.builddir, artifact) destination_path = os.path.join(self.installdir, artifact) if os.path.isdir(source_path): - snapcraft.file_utils.link_or_copy_tree( + snapcraft_legacy.file_utils.link_or_copy_tree( source_path, destination_path ) else: - snapcraft.file_utils.link_or_copy(source_path, destination_path) + snapcraft_legacy.file_utils.link_or_copy( + source_path, destination_path + ) else: install_command = command + ["install"] + self.options.make_parameters if self.options.make_install_var: diff --git a/snapcraft/plugins/v1/maven.py b/snapcraft_legacy/plugins/v1/maven.py similarity index 98% rename from snapcraft/plugins/v1/maven.py rename to snapcraft_legacy/plugins/v1/maven.py index 970e4fc60a..0f7e871a85 100644 --- a/snapcraft/plugins/v1/maven.py +++ b/snapcraft_legacy/plugins/v1/maven.py @@ -61,9 +61,9 @@ from urllib.parse import urlparse from xml.etree import ElementTree -from snapcraft import file_utils, formatting_utils -from snapcraft.internal import errors, sources -from snapcraft.plugins.v1 import PluginV1 +from snapcraft_legacy import file_utils, formatting_utils +from snapcraft_legacy.internal import errors, sources +from snapcraft_legacy.plugins.v1 import PluginV1 logger = logging.getLogger(__name__) diff --git a/snapcraft/plugins/v1/meson.py b/snapcraft_legacy/plugins/v1/meson.py similarity index 97% rename from snapcraft/plugins/v1/meson.py rename to snapcraft_legacy/plugins/v1/meson.py index 1f8af8ed3b..9a88a408f3 100644 --- a/snapcraft/plugins/v1/meson.py +++ b/snapcraft_legacy/plugins/v1/meson.py @@ -37,8 +37,8 @@ import os import subprocess -from snapcraft.internal import errors -from snapcraft.plugins.v1 import PluginV1 +from snapcraft_legacy.internal import errors +from snapcraft_legacy.plugins.v1 import PluginV1 class MesonPlugin(PluginV1): diff --git a/snapcraft/plugins/v1/nil.py b/snapcraft_legacy/plugins/v1/nil.py similarity index 95% rename from snapcraft/plugins/v1/nil.py rename to snapcraft_legacy/plugins/v1/nil.py index 50aadfa59c..f963af71b8 100644 --- a/snapcraft/plugins/v1/nil.py +++ b/snapcraft_legacy/plugins/v1/nil.py @@ -20,7 +20,7 @@ included by Snapcraft, e.g. stage-packages. """ -from snapcraft.plugins.v1 import PluginV1 +from snapcraft_legacy.plugins.v1 import PluginV1 class NilPlugin(PluginV1): diff --git a/snapcraft/plugins/v1/nodejs.py b/snapcraft_legacy/plugins/v1/nodejs.py similarity index 98% rename from snapcraft/plugins/v1/nodejs.py rename to snapcraft_legacy/plugins/v1/nodejs.py index 07a8b88edf..3b1c232d84 100644 --- a/snapcraft/plugins/v1/nodejs.py +++ b/snapcraft_legacy/plugins/v1/nodejs.py @@ -49,10 +49,10 @@ import subprocess import sys -from snapcraft import sources -from snapcraft.file_utils import link_or_copy, link_or_copy_tree -from snapcraft.internal import errors -from snapcraft.plugins.v1 import PluginV1 +from snapcraft_legacy import sources +from snapcraft_legacy.file_utils import link_or_copy, link_or_copy_tree +from snapcraft_legacy.internal import errors +from snapcraft_legacy.plugins.v1 import PluginV1 _NODEJS_BASE = "node-v{version}-linux-{arch}" _NODEJS_VERSION = "8.12.0" diff --git a/snapcraft/plugins/v1/plainbox_provider.py b/snapcraft_legacy/plugins/v1/plainbox_provider.py similarity index 97% rename from snapcraft/plugins/v1/plainbox_provider.py rename to snapcraft_legacy/plugins/v1/plainbox_provider.py index 18e9e088be..90dbad37e2 100644 --- a/snapcraft/plugins/v1/plainbox_provider.py +++ b/snapcraft_legacy/plugins/v1/plainbox_provider.py @@ -31,8 +31,8 @@ import os -from snapcraft.internal import mangling -from snapcraft.plugins.v1 import PluginV1 +from snapcraft_legacy.internal import mangling +from snapcraft_legacy.plugins.v1 import PluginV1 class PlainboxProviderPlugin(PluginV1): diff --git a/snapcraft/plugins/v1/python.py b/snapcraft_legacy/plugins/v1/python.py similarity index 98% rename from snapcraft/plugins/v1/python.py rename to snapcraft_legacy/plugins/v1/python.py index df7b635a81..69df13d07a 100644 --- a/snapcraft/plugins/v1/python.py +++ b/snapcraft_legacy/plugins/v1/python.py @@ -63,10 +63,10 @@ import requests -from snapcraft.common import isurl -from snapcraft.internal import errors, mangling -from snapcraft.internal.errors import SnapcraftPluginCommandError -from snapcraft.plugins.v1 import PluginV1, _python +from snapcraft_legacy.common import isurl +from snapcraft_legacy.internal import errors, mangling +from snapcraft_legacy.internal.errors import SnapcraftPluginCommandError +from snapcraft_legacy.plugins.v1 import PluginV1, _python logger = logging.getLogger(__name__) diff --git a/snapcraft/plugins/v1/qmake.py b/snapcraft_legacy/plugins/v1/qmake.py similarity index 98% rename from snapcraft/plugins/v1/qmake.py rename to snapcraft_legacy/plugins/v1/qmake.py index 1c1f52a60f..51f9bffee2 100644 --- a/snapcraft/plugins/v1/qmake.py +++ b/snapcraft_legacy/plugins/v1/qmake.py @@ -37,8 +37,8 @@ import os -from snapcraft import common -from snapcraft.plugins.v1 import PluginV1 +from snapcraft_legacy import common +from snapcraft_legacy.plugins.v1 import PluginV1 class QmakePlugin(PluginV1): diff --git a/snapcraft/plugins/v1/ruby.py b/snapcraft_legacy/plugins/v1/ruby.py similarity index 97% rename from snapcraft/plugins/v1/ruby.py rename to snapcraft_legacy/plugins/v1/ruby.py index c2c9ec0a73..51851d06a4 100644 --- a/snapcraft/plugins/v1/ruby.py +++ b/snapcraft_legacy/plugins/v1/ruby.py @@ -36,10 +36,10 @@ import os import re -from snapcraft import file_utils -from snapcraft.internal import errors -from snapcraft.plugins.v1 import PluginV1 -from snapcraft.sources import Tar +from snapcraft_legacy import file_utils +from snapcraft_legacy.internal import errors +from snapcraft_legacy.plugins.v1 import PluginV1 +from snapcraft_legacy.sources import Tar logger = logging.getLogger(__name__) diff --git a/snapcraft/plugins/v1/rust.py b/snapcraft_legacy/plugins/v1/rust.py similarity index 98% rename from snapcraft/plugins/v1/rust.py rename to snapcraft_legacy/plugins/v1/rust.py index 8bd131dc03..0be0f20ed7 100644 --- a/snapcraft/plugins/v1/rust.py +++ b/snapcraft_legacy/plugins/v1/rust.py @@ -51,9 +51,9 @@ import toml -from snapcraft import file_utils, shell_utils, sources -from snapcraft.internal import errors -from snapcraft.plugins.v1 import PluginV1 +from snapcraft_legacy import file_utils, shell_utils, sources +from snapcraft_legacy.internal import errors +from snapcraft_legacy.plugins.v1 import PluginV1 _RUSTUP = "https://sh.rustup.rs/" logger = logging.getLogger(__name__) diff --git a/snapcraft/plugins/v1/scons.py b/snapcraft_legacy/plugins/v1/scons.py similarity index 97% rename from snapcraft/plugins/v1/scons.py rename to snapcraft_legacy/plugins/v1/scons.py index 9b578609af..7cbfe2a55d 100644 --- a/snapcraft/plugins/v1/scons.py +++ b/snapcraft_legacy/plugins/v1/scons.py @@ -31,7 +31,7 @@ import os -from snapcraft.plugins.v1 import PluginV1 +from snapcraft_legacy.plugins.v1 import PluginV1 class SconsPlugin(PluginV1): diff --git a/snapcraft/plugins/v1/waf.py b/snapcraft_legacy/plugins/v1/waf.py similarity index 98% rename from snapcraft/plugins/v1/waf.py rename to snapcraft_legacy/plugins/v1/waf.py index bab4121d47..4303a34d3e 100644 --- a/snapcraft/plugins/v1/waf.py +++ b/snapcraft_legacy/plugins/v1/waf.py @@ -32,7 +32,7 @@ ./waf --help """ -from snapcraft.plugins.v1 import PluginV1 +from snapcraft_legacy.plugins.v1 import PluginV1 class WafPlugin(PluginV1): diff --git a/snapcraft/plugins/v2/__init__.py b/snapcraft_legacy/plugins/v2/__init__.py similarity index 100% rename from snapcraft/plugins/v2/__init__.py rename to snapcraft_legacy/plugins/v2/__init__.py diff --git a/snapcraft/plugins/v2/_plugin.py b/snapcraft_legacy/plugins/v2/_plugin.py similarity index 100% rename from snapcraft/plugins/v2/_plugin.py rename to snapcraft_legacy/plugins/v2/_plugin.py diff --git a/snapcraft/plugins/v2/_ros.py b/snapcraft_legacy/plugins/v2/_ros.py similarity index 97% rename from snapcraft/plugins/v2/_ros.py rename to snapcraft_legacy/plugins/v2/_ros.py index 97a190e1df..139e53f9be 100644 --- a/snapcraft/plugins/v2/_ros.py +++ b/snapcraft_legacy/plugins/v2/_ros.py @@ -24,9 +24,9 @@ import click from catkin_pkg import packages as catkin_packages -from snapcraft.internal.repo import Repo -from snapcraft.plugins.v1._ros.rosdep import _parse_rosdep_resolve_dependencies -from snapcraft.plugins.v2 import PluginV2 +from snapcraft_legacy.internal.repo import Repo +from snapcraft_legacy.plugins.v1._ros.rosdep import _parse_rosdep_resolve_dependencies +from snapcraft_legacy.plugins.v2 import PluginV2 class RosPlugin(PluginV2): diff --git a/snapcraft/plugins/v2/autotools.py b/snapcraft_legacy/plugins/v2/autotools.py similarity index 98% rename from snapcraft/plugins/v2/autotools.py rename to snapcraft_legacy/plugins/v2/autotools.py index 829d7f6a5b..59c6bf67b0 100644 --- a/snapcraft/plugins/v2/autotools.py +++ b/snapcraft_legacy/plugins/v2/autotools.py @@ -36,7 +36,7 @@ from typing import Any, Dict, List, Set -from snapcraft.plugins.v2 import PluginV2 +from snapcraft_legacy.plugins.v2 import PluginV2 class AutotoolsPlugin(PluginV2): diff --git a/snapcraft/plugins/v2/catkin.py b/snapcraft_legacy/plugins/v2/catkin.py similarity index 99% rename from snapcraft/plugins/v2/catkin.py rename to snapcraft_legacy/plugins/v2/catkin.py index 86b5050e1b..cda2e9fa49 100644 --- a/snapcraft/plugins/v2/catkin.py +++ b/snapcraft_legacy/plugins/v2/catkin.py @@ -38,7 +38,7 @@ from typing import Any, Dict, List, Set -from snapcraft.plugins.v2 import _ros +from snapcraft_legacy.plugins.v2 import _ros class CatkinPlugin(_ros.RosPlugin): diff --git a/snapcraft/plugins/v2/catkin_tools.py b/snapcraft_legacy/plugins/v2/catkin_tools.py similarity index 99% rename from snapcraft/plugins/v2/catkin_tools.py rename to snapcraft_legacy/plugins/v2/catkin_tools.py index 03b62a7c2f..f3c1183c3b 100644 --- a/snapcraft/plugins/v2/catkin_tools.py +++ b/snapcraft_legacy/plugins/v2/catkin_tools.py @@ -33,7 +33,7 @@ from typing import Any, Dict, List, Set -from snapcraft.plugins.v2 import _ros +from snapcraft_legacy.plugins.v2 import _ros class CatkinToolsPlugin(_ros.RosPlugin): diff --git a/snapcraft/plugins/v2/cmake.py b/snapcraft_legacy/plugins/v2/cmake.py similarity index 98% rename from snapcraft/plugins/v2/cmake.py rename to snapcraft_legacy/plugins/v2/cmake.py index 3fa1bee649..01a0b9df02 100644 --- a/snapcraft/plugins/v2/cmake.py +++ b/snapcraft_legacy/plugins/v2/cmake.py @@ -42,7 +42,7 @@ from typing import Any, Dict, List, Set -from snapcraft.plugins.v2 import PluginV2 +from snapcraft_legacy.plugins.v2 import PluginV2 class CMakePlugin(PluginV2): diff --git a/snapcraft/plugins/v2/colcon.py b/snapcraft_legacy/plugins/v2/colcon.py similarity index 99% rename from snapcraft/plugins/v2/colcon.py rename to snapcraft_legacy/plugins/v2/colcon.py index ea87caa327..0845d40e0a 100644 --- a/snapcraft/plugins/v2/colcon.py +++ b/snapcraft_legacy/plugins/v2/colcon.py @@ -56,7 +56,7 @@ from typing import Any, Dict, List, Set -from snapcraft.plugins.v2 import _ros +from snapcraft_legacy.plugins.v2 import _ros class ColconPlugin(_ros.RosPlugin): diff --git a/snapcraft/plugins/v2/conda.py b/snapcraft_legacy/plugins/v2/conda.py similarity index 97% rename from snapcraft/plugins/v2/conda.py rename to snapcraft_legacy/plugins/v2/conda.py index 42415f2859..9d842bfe23 100644 --- a/snapcraft/plugins/v2/conda.py +++ b/snapcraft_legacy/plugins/v2/conda.py @@ -40,8 +40,8 @@ from textwrap import dedent from typing import Any, Dict, List, Set -from snapcraft.internal.errors import SnapcraftException -from snapcraft.plugins.v2 import PluginV2 +from snapcraft_legacy.internal.errors import SnapcraftException +from snapcraft_legacy.plugins.v2 import PluginV2 _MINICONDA_ARCH_FROM_SNAP_ARCH = { diff --git a/snapcraft/plugins/v2/dump.py b/snapcraft_legacy/plugins/v2/dump.py similarity index 97% rename from snapcraft/plugins/v2/dump.py rename to snapcraft_legacy/plugins/v2/dump.py index 5bce7f8230..534411f5d5 100644 --- a/snapcraft/plugins/v2/dump.py +++ b/snapcraft_legacy/plugins/v2/dump.py @@ -27,7 +27,7 @@ from typing import Any, Dict, List, Set -from snapcraft.plugins.v2 import PluginV2 +from snapcraft_legacy.plugins.v2 import PluginV2 class DumpPlugin(PluginV2): diff --git a/snapcraft/plugins/v2/go.py b/snapcraft_legacy/plugins/v2/go.py similarity index 98% rename from snapcraft/plugins/v2/go.py rename to snapcraft_legacy/plugins/v2/go.py index 552077ebec..88749f1972 100644 --- a/snapcraft/plugins/v2/go.py +++ b/snapcraft_legacy/plugins/v2/go.py @@ -33,7 +33,7 @@ from typing import Any, Dict, List, Set -from snapcraft.plugins.v2 import PluginV2 +from snapcraft_legacy.plugins.v2 import PluginV2 class GoPlugin(PluginV2): diff --git a/snapcraft/plugins/v2/make.py b/snapcraft_legacy/plugins/v2/make.py similarity index 98% rename from snapcraft/plugins/v2/make.py rename to snapcraft_legacy/plugins/v2/make.py index 6f8a6d5b53..164b9b728d 100644 --- a/snapcraft/plugins/v2/make.py +++ b/snapcraft_legacy/plugins/v2/make.py @@ -35,7 +35,7 @@ from typing import Any, Dict, List, Set -from snapcraft.plugins.v2 import PluginV2 +from snapcraft_legacy.plugins.v2 import PluginV2 class MakePlugin(PluginV2): diff --git a/snapcraft/plugins/v2/meson.py b/snapcraft_legacy/plugins/v2/meson.py similarity index 98% rename from snapcraft/plugins/v2/meson.py rename to snapcraft_legacy/plugins/v2/meson.py index 8c724771dd..531aa2f803 100644 --- a/snapcraft/plugins/v2/meson.py +++ b/snapcraft_legacy/plugins/v2/meson.py @@ -35,7 +35,7 @@ from typing import Any, Dict, List, Set -from snapcraft.plugins.v2 import PluginV2 +from snapcraft_legacy.plugins.v2 import PluginV2 class MesonPlugin(PluginV2): diff --git a/snapcraft/plugins/v2/nil.py b/snapcraft_legacy/plugins/v2/nil.py similarity index 96% rename from snapcraft/plugins/v2/nil.py rename to snapcraft_legacy/plugins/v2/nil.py index fb5b8e9dcb..996695e3b4 100644 --- a/snapcraft/plugins/v2/nil.py +++ b/snapcraft_legacy/plugins/v2/nil.py @@ -22,7 +22,7 @@ from typing import Any, Dict, List, Set -from snapcraft.plugins.v2 import PluginV2 +from snapcraft_legacy.plugins.v2 import PluginV2 class NilPlugin(PluginV2): diff --git a/snapcraft/plugins/v2/npm.py b/snapcraft_legacy/plugins/v2/npm.py similarity index 98% rename from snapcraft/plugins/v2/npm.py rename to snapcraft_legacy/plugins/v2/npm.py index a92b68cdef..51fb5ff220 100644 --- a/snapcraft/plugins/v2/npm.py +++ b/snapcraft_legacy/plugins/v2/npm.py @@ -36,7 +36,7 @@ from textwrap import dedent from typing import Any, Dict, List, Set -from snapcraft.plugins.v2 import PluginV2 +from snapcraft_legacy.plugins.v2 import PluginV2 _NODE_ARCH_FROM_SNAP_ARCH = { "i386": "x86", diff --git a/snapcraft/plugins/v2/python.py b/snapcraft_legacy/plugins/v2/python.py similarity index 99% rename from snapcraft/plugins/v2/python.py rename to snapcraft_legacy/plugins/v2/python.py index 0589bca0a3..2eb4e21efd 100644 --- a/snapcraft/plugins/v2/python.py +++ b/snapcraft_legacy/plugins/v2/python.py @@ -67,7 +67,7 @@ from textwrap import dedent from typing import Any, Dict, List, Set -from snapcraft.plugins.v2 import PluginV2 +from snapcraft_legacy.plugins.v2 import PluginV2 class PythonPlugin(PluginV2): diff --git a/snapcraft/plugins/v2/qmake.py b/snapcraft_legacy/plugins/v2/qmake.py similarity index 98% rename from snapcraft/plugins/v2/qmake.py rename to snapcraft_legacy/plugins/v2/qmake.py index 9da6feebd0..714f36fd39 100644 --- a/snapcraft/plugins/v2/qmake.py +++ b/snapcraft_legacy/plugins/v2/qmake.py @@ -36,7 +36,7 @@ from typing import Any, Dict, List, Set -from snapcraft.plugins.v2 import PluginV2 +from snapcraft_legacy.plugins.v2 import PluginV2 class QMakePlugin(PluginV2): diff --git a/snapcraft/plugins/v2/rust.py b/snapcraft_legacy/plugins/v2/rust.py similarity index 98% rename from snapcraft/plugins/v2/rust.py rename to snapcraft_legacy/plugins/v2/rust.py index 7f31a8628a..afb1c17e0e 100644 --- a/snapcraft/plugins/v2/rust.py +++ b/snapcraft_legacy/plugins/v2/rust.py @@ -37,7 +37,7 @@ from textwrap import dedent from typing import Any, Dict, List, Set -from snapcraft.plugins.v2 import PluginV2 +from snapcraft_legacy.plugins.v2 import PluginV2 class RustPlugin(PluginV2): diff --git a/snapcraft/project/__init__.py b/snapcraft_legacy/project/__init__.py similarity index 100% rename from snapcraft/project/__init__.py rename to snapcraft_legacy/project/__init__.py diff --git a/snapcraft/project/_get_snapcraft.py b/snapcraft_legacy/project/_get_snapcraft.py similarity index 100% rename from snapcraft/project/_get_snapcraft.py rename to snapcraft_legacy/project/_get_snapcraft.py diff --git a/snapcraft/project/_project.py b/snapcraft_legacy/project/_project.py similarity index 97% rename from snapcraft/project/_project.py rename to snapcraft_legacy/project/_project.py index 038bfea466..e479cf1ff7 100644 --- a/snapcraft/project/_project.py +++ b/snapcraft_legacy/project/_project.py @@ -20,8 +20,8 @@ from pathlib import Path from typing import List, Set -from snapcraft.internal.deprecations import handle_deprecation_notice -from snapcraft.internal.meta.snap import Snap +from snapcraft_legacy.internal.deprecations import handle_deprecation_notice +from snapcraft_legacy.internal.meta.snap import Snap from ._project_info import ProjectInfo # noqa: F401 from ._project_options import ProjectOptions diff --git a/snapcraft/project/_project_info.py b/snapcraft_legacy/project/_project_info.py similarity index 93% rename from snapcraft/project/_project_info.py rename to snapcraft_legacy/project/_project_info.py index b525eba369..e509ccd4ee 100644 --- a/snapcraft/project/_project_info.py +++ b/snapcraft_legacy/project/_project_info.py @@ -16,8 +16,8 @@ from copy import deepcopy -import snapcraft.yaml_utils.errors -from snapcraft import yaml_utils +import snapcraft_legacy.yaml_utils.errors +from snapcraft_legacy import yaml_utils from . import _schema @@ -32,7 +32,7 @@ def __init__(self, *, snapcraft_yaml_file_path) -> None: try: self.name = self.__raw_snapcraft["name"] except KeyError as key_error: - raise snapcraft.yaml_utils.errors.YamlValidationError( + raise snapcraft_legacy.yaml_utils.errors.YamlValidationError( "'name' is a required property in {!r}".format(snapcraft_yaml_file_path) ) from key_error self.version = self.__raw_snapcraft.get("version") diff --git a/snapcraft/project/_project_options.py b/snapcraft_legacy/project/_project_options.py similarity index 97% rename from snapcraft/project/_project_options.py rename to snapcraft_legacy/project/_project_options.py index 9ab9971a05..eaee204c05 100644 --- a/snapcraft/project/_project_options.py +++ b/snapcraft_legacy/project/_project_options.py @@ -21,8 +21,8 @@ import sys from typing import Set -from snapcraft import file_utils -from snapcraft.internal import common, errors, os_release +from snapcraft_legacy import file_utils +from snapcraft_legacy.internal import common, errors, os_release logger = logging.getLogger(__name__) @@ -309,9 +309,9 @@ def get_core_dynamic_linker(self, base: str, expand: bool = True) -> str: projects architecture. :return: the absolute path to the linker :rtype: str - :raises snapcraft.internal.errors.SnapcraftMissingLinkerInBaseError: + :raises snapcraft_legacy.internal.errors.SnapcraftMissingLinkerInBaseError: if the linker cannot be found in the base. - :raises snapcraft.internal.errors.SnapcraftEnvironmentError: + :raises snapcraft_legacy.internal.errors.SnapcraftEnvironmentError: if a loop is found while resolving the real path to the linker. """ core_path = common.get_installed_snap_path(base) diff --git a/snapcraft/project/_sanity_checks.py b/snapcraft_legacy/project/_sanity_checks.py similarity index 96% rename from snapcraft/project/_sanity_checks.py rename to snapcraft_legacy/project/_sanity_checks.py index 10d8e725f3..d83d804b5e 100644 --- a/snapcraft/project/_sanity_checks.py +++ b/snapcraft_legacy/project/_sanity_checks.py @@ -18,8 +18,8 @@ import os import re -from snapcraft.internal.errors import SnapcraftEnvironmentError -from snapcraft.project import Project, errors +from snapcraft_legacy.internal.errors import SnapcraftEnvironmentError +from snapcraft_legacy.project import Project, errors logger = logging.getLogger(__name__) diff --git a/snapcraft/project/_schema.py b/snapcraft_legacy/project/_schema.py similarity index 89% rename from snapcraft/project/_schema.py rename to snapcraft_legacy/project/_schema.py index d8e26ef0f8..4801291f11 100644 --- a/snapcraft/project/_schema.py +++ b/snapcraft_legacy/project/_schema.py @@ -20,8 +20,8 @@ import jsonschema -import snapcraft.yaml_utils.errors -from snapcraft.internal import common +import snapcraft_legacy.yaml_utils.errors +from snapcraft_legacy.internal import common class Validator: @@ -58,7 +58,7 @@ def _load_schema(self): with open(schema_file) as fp: self._schema = json.load(fp) except FileNotFoundError: - raise snapcraft.yaml_utils.errors.YamlValidationError( + raise snapcraft_legacy.yaml_utils.errors.YamlValidationError( "snapcraft validation file is missing from installation path" ) @@ -69,6 +69,6 @@ def validate(self, *, source="snapcraft.yaml"): self._snapcraft, self._schema, format_checker=format_check ) except jsonschema.ValidationError as e: - raise snapcraft.yaml_utils.errors.YamlValidationError.from_validation_error( + raise snapcraft_legacy.yaml_utils.errors.YamlValidationError.from_validation_error( e, source=source ) diff --git a/snapcraft/project/errors.py b/snapcraft_legacy/project/errors.py similarity index 97% rename from snapcraft/project/errors.py rename to snapcraft_legacy/project/errors.py index fe87633be0..0e98f73ba9 100644 --- a/snapcraft/project/errors.py +++ b/snapcraft_legacy/project/errors.py @@ -16,7 +16,7 @@ from typing import Optional -from snapcraft.internal.errors import SnapcraftError, SnapcraftException +from snapcraft_legacy.internal.errors import SnapcraftError, SnapcraftException # dict of jsonschema validator -> cause pairs. Wish jsonschema just gave us # better messages. diff --git a/snapcraft/scripts/__init__.py b/snapcraft_legacy/scripts/__init__.py similarity index 100% rename from snapcraft/scripts/__init__.py rename to snapcraft_legacy/scripts/__init__.py diff --git a/snapcraft/scripts/generate_reference.py b/snapcraft_legacy/scripts/generate_reference.py similarity index 100% rename from snapcraft/scripts/generate_reference.py rename to snapcraft_legacy/scripts/generate_reference.py diff --git a/snapcraft/shell_utils.py b/snapcraft_legacy/shell_utils.py similarity index 96% rename from snapcraft/shell_utils.py rename to snapcraft_legacy/shell_utils.py index 515e97ebb4..3fc7ea0016 100644 --- a/snapcraft/shell_utils.py +++ b/snapcraft_legacy/shell_utils.py @@ -19,7 +19,7 @@ import tempfile -from snapcraft.internal import common +from snapcraft_legacy.internal import common def which(command, **kwargs): diff --git a/snapcraft/sources.py b/snapcraft_legacy/sources.py similarity index 50% rename from snapcraft/sources.py rename to snapcraft_legacy/sources.py index 7704dafe2e..502c9e519f 100644 --- a/snapcraft/sources.py +++ b/snapcraft_legacy/sources.py @@ -17,14 +17,14 @@ import sys as _sys if _sys.platform == "linux": - from snapcraft.internal.sources import Bazaar # noqa - from snapcraft.internal.sources import Deb # noqa - from snapcraft.internal.sources import Git # noqa - from snapcraft.internal.sources import Local # noqa - from snapcraft.internal.sources import Mercurial # noqa - from snapcraft.internal.sources import Rpm # noqa - from snapcraft.internal.sources import Script # noqa - from snapcraft.internal.sources import Subversion # noqa - from snapcraft.internal.sources import Tar # noqa - from snapcraft.internal.sources import Zip # noqa - from snapcraft.internal.sources import get # noqa + from snapcraft_legacy.internal.sources import Bazaar # noqa + from snapcraft_legacy.internal.sources import Deb # noqa + from snapcraft_legacy.internal.sources import Git # noqa + from snapcraft_legacy.internal.sources import Local # noqa + from snapcraft_legacy.internal.sources import Mercurial # noqa + from snapcraft_legacy.internal.sources import Rpm # noqa + from snapcraft_legacy.internal.sources import Script # noqa + from snapcraft_legacy.internal.sources import Subversion # noqa + from snapcraft_legacy.internal.sources import Tar # noqa + from snapcraft_legacy.internal.sources import Zip # noqa + from snapcraft_legacy.internal.sources import get # noqa diff --git a/snapcraft/storeapi/__init__.py b/snapcraft_legacy/storeapi/__init__.py similarity index 100% rename from snapcraft/storeapi/__init__.py rename to snapcraft_legacy/storeapi/__init__.py diff --git a/snapcraft/storeapi/_dashboard_api.py b/snapcraft_legacy/storeapi/_dashboard_api.py similarity index 100% rename from snapcraft/storeapi/_dashboard_api.py rename to snapcraft_legacy/storeapi/_dashboard_api.py diff --git a/snapcraft/storeapi/_metadata.py b/snapcraft_legacy/storeapi/_metadata.py similarity index 98% rename from snapcraft/storeapi/_metadata.py rename to snapcraft_legacy/storeapi/_metadata.py index 8bb1bd109a..1eadf6880d 100644 --- a/snapcraft/storeapi/_metadata.py +++ b/snapcraft_legacy/storeapi/_metadata.py @@ -18,7 +18,7 @@ import json import os -from snapcraft.storeapi.errors import StoreMetadataError +from snapcraft_legacy.storeapi.errors import StoreMetadataError def _media_hash(media_file): diff --git a/snapcraft/storeapi/_requests.py b/snapcraft_legacy/storeapi/_requests.py similarity index 100% rename from snapcraft/storeapi/_requests.py rename to snapcraft_legacy/storeapi/_requests.py diff --git a/snapcraft/storeapi/_snap_api.py b/snapcraft_legacy/storeapi/_snap_api.py similarity index 100% rename from snapcraft/storeapi/_snap_api.py rename to snapcraft_legacy/storeapi/_snap_api.py diff --git a/snapcraft/storeapi/_status_tracker.py b/snapcraft_legacy/storeapi/_status_tracker.py similarity index 100% rename from snapcraft/storeapi/_status_tracker.py rename to snapcraft_legacy/storeapi/_status_tracker.py diff --git a/snapcraft/storeapi/_store_client.py b/snapcraft_legacy/storeapi/_store_client.py similarity index 99% rename from snapcraft/storeapi/_store_client.py rename to snapcraft_legacy/storeapi/_store_client.py index 60c6afb8c7..7888c3baa6 100644 --- a/snapcraft/storeapi/_store_client.py +++ b/snapcraft_legacy/storeapi/_store_client.py @@ -21,7 +21,7 @@ import requests -from snapcraft.internal.indicators import download_requests_stream +from snapcraft_legacy.internal.indicators import download_requests_stream from . import _upload, errors, http_clients, metrics from ._dashboard_api import DashboardAPI diff --git a/snapcraft/storeapi/_up_down_client.py b/snapcraft_legacy/storeapi/_up_down_client.py similarity index 100% rename from snapcraft/storeapi/_up_down_client.py rename to snapcraft_legacy/storeapi/_up_down_client.py diff --git a/snapcraft/storeapi/_upload.py b/snapcraft_legacy/storeapi/_upload.py similarity index 97% rename from snapcraft/storeapi/_upload.py rename to snapcraft_legacy/storeapi/_upload.py index 025f611052..24dc3e60f7 100644 --- a/snapcraft/storeapi/_upload.py +++ b/snapcraft_legacy/storeapi/_upload.py @@ -21,7 +21,7 @@ from progressbar import Bar, Percentage, ProgressBar from requests_toolbelt import MultipartEncoder, MultipartEncoderMonitor -from snapcraft.storeapi.errors import StoreUpDownError +from snapcraft_legacy.storeapi.errors import StoreUpDownError logger = logging.getLogger(__name__) diff --git a/snapcraft/storeapi/channels.py b/snapcraft_legacy/storeapi/channels.py similarity index 100% rename from snapcraft/storeapi/channels.py rename to snapcraft_legacy/storeapi/channels.py diff --git a/snapcraft/storeapi/constants.py b/snapcraft_legacy/storeapi/constants.py similarity index 100% rename from snapcraft/storeapi/constants.py rename to snapcraft_legacy/storeapi/constants.py diff --git a/snapcraft/storeapi/errors.py b/snapcraft_legacy/storeapi/errors.py similarity index 99% rename from snapcraft/storeapi/errors.py rename to snapcraft_legacy/storeapi/errors.py index ead6653dfb..61bb0fdc56 100644 --- a/snapcraft/storeapi/errors.py +++ b/snapcraft_legacy/storeapi/errors.py @@ -20,8 +20,8 @@ from simplejson.scanner import JSONDecodeError -from snapcraft import formatting_utils -from snapcraft.internal.errors import ( +from snapcraft_legacy import formatting_utils +from snapcraft_legacy.internal.errors import ( SnapcraftError, SnapcraftException, SnapcraftReportableException, diff --git a/snapcraft/storeapi/http_clients/__init__.py b/snapcraft_legacy/storeapi/http_clients/__init__.py similarity index 100% rename from snapcraft/storeapi/http_clients/__init__.py rename to snapcraft_legacy/storeapi/http_clients/__init__.py diff --git a/snapcraft/storeapi/http_clients/_candid_client.py b/snapcraft_legacy/storeapi/http_clients/_candid_client.py similarity index 99% rename from snapcraft/storeapi/http_clients/_candid_client.py rename to snapcraft_legacy/storeapi/http_clients/_candid_client.py index 93de96b389..888c2c656f 100644 --- a/snapcraft/storeapi/http_clients/_candid_client.py +++ b/snapcraft_legacy/storeapi/http_clients/_candid_client.py @@ -10,7 +10,7 @@ from macaroonbakery import bakery, httpbakery from xdg import BaseDirectory -from snapcraft.storeapi import constants +from snapcraft_legacy.storeapi import constants from . import agent, errors, _config, _http_client diff --git a/snapcraft/storeapi/http_clients/_config.py b/snapcraft_legacy/storeapi/http_clients/_config.py similarity index 100% rename from snapcraft/storeapi/http_clients/_config.py rename to snapcraft_legacy/storeapi/http_clients/_config.py diff --git a/snapcraft/storeapi/http_clients/_http_client.py b/snapcraft_legacy/storeapi/http_clients/_http_client.py similarity index 100% rename from snapcraft/storeapi/http_clients/_http_client.py rename to snapcraft_legacy/storeapi/http_clients/_http_client.py diff --git a/snapcraft/storeapi/http_clients/_ubuntu_sso_client.py b/snapcraft_legacy/storeapi/http_clients/_ubuntu_sso_client.py similarity index 99% rename from snapcraft/storeapi/http_clients/_ubuntu_sso_client.py rename to snapcraft_legacy/storeapi/http_clients/_ubuntu_sso_client.py index 604b80f84f..f66b05cb3f 100644 --- a/snapcraft/storeapi/http_clients/_ubuntu_sso_client.py +++ b/snapcraft_legacy/storeapi/http_clients/_ubuntu_sso_client.py @@ -81,7 +81,8 @@ def _get_section_name(self) -> str: def _get_config_path(self) -> pathlib.Path: return ( - pathlib.Path(BaseDirectory.save_config_path("snapcraft")) / "snapcraft.cfg" + pathlib.Path(BaseDirectory.save_config_path("snapcraft")) + / "snapcraft_legacy.cfg" ) diff --git a/snapcraft/storeapi/http_clients/agent.py b/snapcraft_legacy/storeapi/http_clients/agent.py similarity index 84% rename from snapcraft/storeapi/http_clients/agent.py rename to snapcraft_legacy/storeapi/http_clients/agent.py index e540f6ae5f..04d516e75b 100644 --- a/snapcraft/storeapi/http_clients/agent.py +++ b/snapcraft_legacy/storeapi/http_clients/agent.py @@ -17,10 +17,10 @@ import os import sys -import snapcraft -from snapcraft import project -from snapcraft.internal import os_release -from snapcraft.internal.errors import OsReleaseNameError, OsReleaseVersionIdError +import snapcraft_legacy +from snapcraft_legacy import project +from snapcraft_legacy.internal import os_release +from snapcraft_legacy.internal.errors import OsReleaseNameError, OsReleaseVersionIdError def _is_ci_env(): @@ -55,4 +55,4 @@ def get_user_agent(platform: str = sys.platform) -> str: else: os_platform = platform.title() - return f"snapcraft/{snapcraft.__version__} {testing}{os_platform} ({arch})" + return f"snapcraft/{snapcraft_legacy.__version__} {testing}{os_platform} ({arch})" diff --git a/snapcraft/storeapi/http_clients/errors.py b/snapcraft_legacy/storeapi/http_clients/errors.py similarity index 98% rename from snapcraft/storeapi/http_clients/errors.py rename to snapcraft_legacy/storeapi/http_clients/errors.py index 41d2029328..f927230ab0 100644 --- a/snapcraft/storeapi/http_clients/errors.py +++ b/snapcraft_legacy/storeapi/http_clients/errors.py @@ -19,7 +19,7 @@ import urllib3 from simplejson.scanner import JSONDecodeError -from snapcraft.internal.errors import SnapcraftError +from snapcraft_legacy.internal.errors import SnapcraftError logger = logging.getLogger(__name__) diff --git a/snapcraft/storeapi/info.py b/snapcraft_legacy/storeapi/info.py similarity index 99% rename from snapcraft/storeapi/info.py rename to snapcraft_legacy/storeapi/info.py index dff4ff0705..0dbc427338 100644 --- a/snapcraft/storeapi/info.py +++ b/snapcraft_legacy/storeapi/info.py @@ -17,7 +17,7 @@ import os from typing import Any, Dict, List, Optional -from snapcraft.file_utils import calculate_hash +from snapcraft_legacy.file_utils import calculate_hash from . import errors diff --git a/snapcraft/storeapi/metrics.py b/snapcraft_legacy/storeapi/metrics.py similarity index 100% rename from snapcraft/storeapi/metrics.py rename to snapcraft_legacy/storeapi/metrics.py diff --git a/snapcraft/storeapi/status.py b/snapcraft_legacy/storeapi/status.py similarity index 100% rename from snapcraft/storeapi/status.py rename to snapcraft_legacy/storeapi/status.py diff --git a/snapcraft/storeapi/v2/__init__.py b/snapcraft_legacy/storeapi/v2/__init__.py similarity index 100% rename from snapcraft/storeapi/v2/__init__.py rename to snapcraft_legacy/storeapi/v2/__init__.py diff --git a/snapcraft/storeapi/v2/_api_schema.py b/snapcraft_legacy/storeapi/v2/_api_schema.py similarity index 100% rename from snapcraft/storeapi/v2/_api_schema.py rename to snapcraft_legacy/storeapi/v2/_api_schema.py diff --git a/snapcraft/storeapi/v2/channel_map.py b/snapcraft_legacy/storeapi/v2/channel_map.py similarity index 100% rename from snapcraft/storeapi/v2/channel_map.py rename to snapcraft_legacy/storeapi/v2/channel_map.py diff --git a/snapcraft/storeapi/v2/releases.py b/snapcraft_legacy/storeapi/v2/releases.py similarity index 100% rename from snapcraft/storeapi/v2/releases.py rename to snapcraft_legacy/storeapi/v2/releases.py diff --git a/snapcraft/storeapi/v2/validation_sets.py b/snapcraft_legacy/storeapi/v2/validation_sets.py similarity index 100% rename from snapcraft/storeapi/v2/validation_sets.py rename to snapcraft_legacy/storeapi/v2/validation_sets.py diff --git a/snapcraft/storeapi/v2/whoami.py b/snapcraft_legacy/storeapi/v2/whoami.py similarity index 100% rename from snapcraft/storeapi/v2/whoami.py rename to snapcraft_legacy/storeapi/v2/whoami.py diff --git a/snapcraft/yaml_utils/__init__.py b/snapcraft_legacy/yaml_utils/__init__.py similarity index 98% rename from snapcraft/yaml_utils/__init__.py rename to snapcraft_legacy/yaml_utils/__init__.py index 984330219f..9ecb94e8db 100644 --- a/snapcraft/yaml_utils/__init__.py +++ b/snapcraft_legacy/yaml_utils/__init__.py @@ -21,7 +21,7 @@ import yaml -from snapcraft.yaml_utils.errors import YamlValidationError +from snapcraft_legacy.yaml_utils.errors import YamlValidationError logger = logging.getLogger(__name__) diff --git a/snapcraft/yaml_utils/errors.py b/snapcraft_legacy/yaml_utils/errors.py similarity index 98% rename from snapcraft/yaml_utils/errors.py rename to snapcraft_legacy/yaml_utils/errors.py index 4539d67188..c3adb14954 100644 --- a/snapcraft/yaml_utils/errors.py +++ b/snapcraft_legacy/yaml_utils/errors.py @@ -18,8 +18,8 @@ from collections import OrderedDict from typing import Dict, List -from snapcraft import formatting_utils -from snapcraft.internal.errors import SnapcraftError +from snapcraft_legacy import formatting_utils +from snapcraft_legacy.internal.errors import SnapcraftError _VALIDATION_ERROR_CAUSES = { "maxLength": "maximum length is {validator_value}", diff --git a/tests/fake_servers/__init__.py b/tests/fake_servers/__init__.py index 1c5f9c75d9..30debdbc4b 100644 --- a/tests/fake_servers/__init__.py +++ b/tests/fake_servers/__init__.py @@ -26,7 +26,7 @@ # we do not want snapcraft imports for the integration tests try: - from snapcraft import yaml_utils + from snapcraft_legacy import yaml_utils except ImportError: import yaml as yaml_utils # type: ignore diff --git a/tests/fake_servers/api.py b/tests/fake_servers/api.py index 83b9a9c6e4..3a1468f9d3 100644 --- a/tests/fake_servers/api.py +++ b/tests/fake_servers/api.py @@ -932,7 +932,7 @@ def snap_binary_metadata(self, request): ) else: # POST/PUT - # snapcraft.storeapi._metadata._build_binary_request_data + # snapcraft_legacy.storeapi._metadata._build_binary_request_data if type(request.params["info"]) == bytes: info = json.loads(request.params["info"].decode()) else: diff --git a/tests/fixture_setup/_fixtures.py b/tests/fixture_setup/_fixtures.py index ef3f78c742..0ada4f1041 100644 --- a/tests/fixture_setup/_fixtures.py +++ b/tests/fixture_setup/_fixtures.py @@ -37,7 +37,7 @@ # we do not want snapcraft imports for the integration tests try: - from snapcraft import yaml_utils + from snapcraft_legacy import yaml_utils except ImportError: import yaml as yaml_utils # type: ignore @@ -707,14 +707,14 @@ def _setUp(self): self.addCleanup(patcher.stop) self.core_path = self.useFixture(fixtures.TempDir()).path - patcher = mock.patch("snapcraft.internal.common.get_installed_snap_path") + patcher = mock.patch("snapcraft_legacy.internal.common.get_installed_snap_path") mock_core_path = patcher.start() mock_core_path.return_value = self.core_path self.addCleanup(patcher.stop) self.content_dirs = set([]) mock_content_dirs = fixtures.MockPatch( - "snapcraft.project._project.Project._get_provider_content_dirs", + "snapcraft_legacy.project._project.Project._get_provider_content_dirs", return_value=self.content_dirs, ) self.useFixture(mock_content_dirs) diff --git a/tests/fixture_setup/_unittests.py b/tests/fixture_setup/_unittests.py index 308ad7b63c..7209784c9c 100644 --- a/tests/fixture_setup/_unittests.py +++ b/tests/fixture_setup/_unittests.py @@ -27,9 +27,9 @@ import fixtures import jsonschema -import snapcraft -from snapcraft.internal import elf -from snapcraft.plugins._plugin_finder import get_plugin_for_base +import snapcraft_legacy +from snapcraft_legacy.internal import elf +from snapcraft_legacy.plugins._plugin_finder import get_plugin_for_base from tests.file_utils import get_snapcraft_path @@ -50,13 +50,13 @@ def __init__(self, **kwargs): def setUp(self): super().setUp() - patcher = mock.patch("snapcraft.project.Project") + patcher = mock.patch("snapcraft_legacy.project.Project") patcher.start() self.addCleanup(patcher.stop) # Special handling is required as ProjectOptions attributes are # handled with the @property decorator. - project_options_t = type(snapcraft.project.Project.return_value) + project_options_t = type(snapcraft_legacy.project.Project.return_value) for key in self._kwargs: setattr(project_options_t, key, self._kwargs[key]) @@ -67,13 +67,13 @@ class FakeMetadataExtractor(fixtures.Fixture): def __init__( self, extractor_name: str, - extractor: Callable[[str], snapcraft.extractors.ExtractedMetadata], + extractor: Callable[[str], snapcraft_legacy.extractors.ExtractedMetadata], exported_name="extract", ) -> None: super().__init__() self._extractor_name = extractor_name self._exported_name = exported_name - self._import_name = "snapcraft.extractors.{}".format(extractor_name) + self._import_name = "snapcraft_legacy.extractors.{}".format(extractor_name) self._extractor = extractor def _setUp(self) -> None: @@ -85,7 +85,7 @@ def _setUp(self) -> None: real_iter_modules = pkgutil.iter_modules def _fake_iter_modules(path): - if path == snapcraft.extractors.__path__: + if path == snapcraft_legacy.extractors.__path__: yield None, self._extractor_name, False else: yield real_iter_modules(path) @@ -109,7 +109,8 @@ def __init__(self, plugin_name, plugin_class): def _setUp(self): self.useFixture( fixtures.MockPatch( - "snapcraft.plugins.get_plugin_for_base", side_effect=self.get_plugin + "snapcraft_legacy.plugins.get_plugin_for_base", + side_effect=self.get_plugin, ) ) @@ -368,7 +369,7 @@ class FakeExtension(fixtures.Fixture): def __init__(self, extension_name, extension_class): super().__init__() - self._import_name = "snapcraft.internal.project_loader._extensions.{}".format( + self._import_name = "snapcraft_legacy.internal.project_loader._extensions.{}".format( extension_name ) self._extension_class = extension_class @@ -410,8 +411,8 @@ def __init__(self): self._email = "-" def _setUp(self): - original_check_call = snapcraft.internal.repo.snaps.check_call - original_check_output = snapcraft.internal.repo.snaps.check_output + original_check_call = snapcraft_legacy.internal.repo.snaps.check_call + original_check_output = snapcraft_legacy.internal.repo.snaps.check_output def side_effect_check_call(cmd, *args, **kwargs): return side_effect(original_check_call, cmd, *args, **kwargs) @@ -432,12 +433,14 @@ def side_effect(original, cmd, *args, **kwargs): self.useFixture( fixtures.MonkeyPatch( - "snapcraft.internal.repo.snaps.check_call", side_effect_check_call + "snapcraft_legacy.internal.repo.snaps.check_call", + side_effect_check_call, ) ) self.useFixture( fixtures.MonkeyPatch( - "snapcraft.internal.repo.snaps.check_output", side_effect_check_output + "snapcraft_legacy.internal.repo.snaps.check_output", + side_effect_check_output, ) ) @@ -498,10 +501,10 @@ def _setUp(self): import sys sys.path.append('{snapcraft_path!s}') - import snapcraft.cli.__main__ + import snapcraft_legacy.cli.__main__ if __name__ == '__main__': - snapcraft.cli.__main__.run_snapcraftctl( + snapcraft_legacy.cli.__main__.run_snapcraftctl( prog_name='snapcraftctl') """.format( snapcraft_path=snapcraft_path diff --git a/tests/fixture_setup/_unix.py b/tests/fixture_setup/_unix.py index 39f53cc9b2..1bfbd13963 100644 --- a/tests/fixture_setup/_unix.py +++ b/tests/fixture_setup/_unix.py @@ -73,7 +73,7 @@ def setUp(self): os.unlink(snapd_fake_socket_path) socket_path_patcher = mock.patch( - "snapcraft.internal.repo.snaps.get_snapd_socket_path_template" + "snapcraft_legacy.internal.repo.snaps.get_snapd_socket_path_template" ) mock_socket_path = socket_path_patcher.start() mock_socket_path.return_value = "http+unix://{}/v2/{{}}".format( diff --git a/tests/fixture_setup/os_release.py b/tests/fixture_setup/os_release.py index 723cfe3dd4..a0ed4cfd8b 100644 --- a/tests/fixture_setup/os_release.py +++ b/tests/fixture_setup/os_release.py @@ -20,7 +20,7 @@ import fixtures -from snapcraft.internal import os_release +from snapcraft_legacy.internal import os_release class FakeOsRelease(fixtures.Fixture): @@ -73,7 +73,7 @@ def _create_os_release(*args, **kwargs): return release patcher = mock.patch( - "snapcraft.internal.os_release.OsRelease", wraps=_create_os_release + "snapcraft_legacy.internal.os_release.OsRelease", wraps=_create_os_release ) patcher.start() self.addCleanup(patcher.stop) diff --git a/tests/spread/plugins/v1/x-local/snaps/from-baseplugin/snap/plugins/x_local_plugin.py b/tests/spread/plugins/v1/x-local/snaps/from-baseplugin/snap/plugins/x_local_plugin.py index d3d58bde73..1ac67e0472 100644 --- a/tests/spread/plugins/v1/x-local/snaps/from-baseplugin/snap/plugins/x_local_plugin.py +++ b/tests/spread/plugins/v1/x-local/snaps/from-baseplugin/snap/plugins/x_local_plugin.py @@ -14,7 +14,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -from snapcraft import BasePlugin +from snapcraft_legacy import BasePlugin class LocalPlugin(BasePlugin): diff --git a/tests/spread/plugins/v1/x-local/snaps/from-nilplugin/snap/plugins/x_local_plugin.py b/tests/spread/plugins/v1/x-local/snaps/from-nilplugin/snap/plugins/x_local_plugin.py index fe965e13f0..0da5cb7461 100644 --- a/tests/spread/plugins/v1/x-local/snaps/from-nilplugin/snap/plugins/x_local_plugin.py +++ b/tests/spread/plugins/v1/x-local/snaps/from-nilplugin/snap/plugins/x_local_plugin.py @@ -14,7 +14,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -from snapcraft.plugins.v1 import NilPlugin +from snapcraft_legacy.plugins.v1 import NilPlugin class LocalPlugin(NilPlugin): diff --git a/tests/spread/plugins/v1/x-local/snaps/from-pluginv1/snap/plugins/x_local_plugin.py b/tests/spread/plugins/v1/x-local/snaps/from-pluginv1/snap/plugins/x_local_plugin.py index ddb62be586..d92443122b 100644 --- a/tests/spread/plugins/v1/x-local/snaps/from-pluginv1/snap/plugins/x_local_plugin.py +++ b/tests/spread/plugins/v1/x-local/snaps/from-pluginv1/snap/plugins/x_local_plugin.py @@ -14,7 +14,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -from snapcraft.plugins.v1 import PluginV1 +from snapcraft_legacy.plugins.v1 import PluginV1 class LocalPlugin(PluginV1): diff --git a/tests/spread/plugins/v1/x-local/snaps/x-compat-name/snap/plugins/x-local-plugin.py b/tests/spread/plugins/v1/x-local/snaps/x-compat-name/snap/plugins/x-local-plugin.py index d3d58bde73..1ac67e0472 100644 --- a/tests/spread/plugins/v1/x-local/snaps/x-compat-name/snap/plugins/x-local-plugin.py +++ b/tests/spread/plugins/v1/x-local/snaps/x-compat-name/snap/plugins/x-local-plugin.py @@ -14,7 +14,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -from snapcraft import BasePlugin +from snapcraft_legacy import BasePlugin class LocalPlugin(BasePlugin): diff --git a/tests/spread/plugins/v2/snaps/local-plugin-from-base-hello/snap/plugins/x_local_plugin.py b/tests/spread/plugins/v2/snaps/local-plugin-from-base-hello/snap/plugins/x_local_plugin.py index 9fe6ad7e73..955add8cc2 100644 --- a/tests/spread/plugins/v2/snaps/local-plugin-from-base-hello/snap/plugins/x_local_plugin.py +++ b/tests/spread/plugins/v2/snaps/local-plugin-from-base-hello/snap/plugins/x_local_plugin.py @@ -16,7 +16,7 @@ from typing import Any, Dict, List, Set -from snapcraft.plugins.v2 import PluginV2 +from snapcraft_legacy.plugins.v2 import PluginV2 class PluginImpl(PluginV2): diff --git a/tests/spread/plugins/v2/snaps/local-plugin-from-nil-hello/snap/plugins/x_local_plugin.py b/tests/spread/plugins/v2/snaps/local-plugin-from-nil-hello/snap/plugins/x_local_plugin.py index b4f8e84f2a..d2d37f78f4 100644 --- a/tests/spread/plugins/v2/snaps/local-plugin-from-nil-hello/snap/plugins/x_local_plugin.py +++ b/tests/spread/plugins/v2/snaps/local-plugin-from-nil-hello/snap/plugins/x_local_plugin.py @@ -16,7 +16,7 @@ from typing import List, Set -from snapcraft.plugins.v2 import nil +from snapcraft_legacy.plugins.v2 import nil class PluginImpl(nil.NilPlugin): diff --git a/tests/unit/__init__.py b/tests/unit/__init__.py index 426168457c..331f195e30 100644 --- a/tests/unit/__init__.py +++ b/tests/unit/__init__.py @@ -27,7 +27,7 @@ import testscenarios import testtools -from snapcraft.internal import common, steps +from snapcraft_legacy.internal import common, steps from tests import fake_servers, fixture_setup from tests.file_utils import get_snapcraft_path from tests.unit.part_loader import load_part @@ -159,13 +159,13 @@ def setUp(self): # We do not want the paths to affect every test we have. patcher = mock.patch( - "snapcraft.file_utils.get_snap_tool_path", side_effect=lambda x: x + "snapcraft_legacy.file_utils.get_snap_tool_path", side_effect=lambda x: x ) patcher.start() self.addCleanup(patcher.stop) patcher = mock.patch( - "snapcraft.internal.indicators.ProgressBar", new=SilentProgressBar + "snapcraft_legacy.internal.indicators.ProgressBar", new=SilentProgressBar ) patcher.start() self.addCleanup(patcher.stop) diff --git a/tests/unit/build_providers/__init__.py b/tests/unit/build_providers/__init__.py index 092e836044..c10a42706b 100644 --- a/tests/unit/build_providers/__init__.py +++ b/tests/unit/build_providers/__init__.py @@ -18,9 +18,9 @@ from typing import Dict, Optional from unittest import mock -from snapcraft.internal.build_providers._base_provider import Provider -from snapcraft.internal.meta.snap import Snap -from snapcraft.project import Project +from snapcraft_legacy.internal.build_providers._base_provider import Provider +from snapcraft_legacy.internal.meta.snap import Snap +from snapcraft_legacy.project import Project from tests import fixture_setup, unit @@ -146,7 +146,7 @@ def setUp(self): self.instance_name = "snapcraft-project-name" patcher = mock.patch( - "snapcraft.internal.build_providers._base_provider.SnapInjector" + "snapcraft_legacy.internal.build_providers._base_provider.SnapInjector" ) self.snap_injector_mock = patcher.start() self.addCleanup(patcher.stop) @@ -163,7 +163,7 @@ def setUp(self): self.instance_name = "snapcraft-project-name" patcher = mock.patch( - "snapcraft.internal.build_providers._base_provider.SnapInjector" + "snapcraft_legacy.internal.build_providers._base_provider.SnapInjector" ) self.snap_injector_mock = patcher.start() self.addCleanup(patcher.stop) @@ -171,7 +171,7 @@ def setUp(self): self.project = get_project() patcher = mock.patch( - "snapcraft.internal.build_providers._base_provider._get_platform", + "snapcraft_legacy.internal.build_providers._base_provider._get_platform", return_value="darwin", ) patcher.start() diff --git a/tests/unit/build_providers/conftest.py b/tests/unit/build_providers/conftest.py index bc812338ee..f589bb2834 100644 --- a/tests/unit/build_providers/conftest.py +++ b/tests/unit/build_providers/conftest.py @@ -23,7 +23,7 @@ def snap_injector(): """Fake SnapManager""" patcher = mock.patch( - "snapcraft.internal.build_providers._base_provider.SnapInjector" + "snapcraft_legacy.internal.build_providers._base_provider.SnapInjector" ) snap_injector_mock = patcher.start() yield snap_injector_mock diff --git a/tests/unit/build_providers/lxd/test_lxd.py b/tests/unit/build_providers/lxd/test_lxd.py index 6d67aeedc3..dda8efe63b 100644 --- a/tests/unit/build_providers/lxd/test_lxd.py +++ b/tests/unit/build_providers/lxd/test_lxd.py @@ -22,10 +22,10 @@ from testtools.matchers import Equals, FileContains, FileExists -from snapcraft.internal.build_providers import _base_provider, errors -from snapcraft.internal.build_providers._lxd import LXD -from snapcraft.internal.errors import SnapcraftEnvironmentError -from snapcraft.internal.repo.errors import SnapdConnectionError +from snapcraft_legacy.internal.build_providers import _base_provider, errors +from snapcraft_legacy.internal.build_providers._lxd import LXD +from snapcraft_legacy.internal.errors import SnapcraftEnvironmentError +from snapcraft_legacy.internal.repo.errors import SnapdConnectionError from tests.unit.build_providers import BaseProviderBaseTest if sys.platform == "linux": @@ -173,7 +173,7 @@ def setUp(self): self.addCleanup(patcher.stop) patcher = mock.patch( - "snapcraft.internal.build_providers._base_provider.Provider.clean_project", + "snapcraft_legacy.internal.build_providers._base_provider.Provider.clean_project", return_value=True, ) @@ -483,7 +483,8 @@ def test_linux(self): # Thou shall not fail with mock.patch( - "snapcraft.internal.repo.Repo.is_package_installed", return_value=False + "snapcraft_legacy.internal.repo.Repo.is_package_installed", + return_value=False, ): LXD.ensure_provider() @@ -494,7 +495,8 @@ def test_linux_with_snap_and_deb_installed(self): # Thou shall not fail with mock.patch( - "snapcraft.internal.repo.Repo.is_package_installed", return_value=True + "snapcraft_legacy.internal.repo.Repo.is_package_installed", + return_value=True, ): raised = self.assertRaises(SnapcraftEnvironmentError, LXD.ensure_provider) @@ -519,7 +521,7 @@ def test_lxd_snap_not_installed(self): def test_snap_support_missing(self): with mock.patch( - "snapcraft.internal.repo.snaps.SnapPackage.is_snap_installed", + "snapcraft_legacy.internal.repo.snaps.SnapPackage.is_snap_installed", side_effect=SnapdConnectionError(snap_name="lxd", url="fake"), ): raised = self.assertRaises(errors.ProviderNotFound, LXD.ensure_provider) diff --git a/tests/unit/build_providers/multipass/test_instance_info.py b/tests/unit/build_providers/multipass/test_instance_info.py index f532d71072..a8b138fbde 100644 --- a/tests/unit/build_providers/multipass/test_instance_info.py +++ b/tests/unit/build_providers/multipass/test_instance_info.py @@ -18,8 +18,8 @@ from testtools.matchers import Equals -from snapcraft.internal.build_providers import errors -from snapcraft.internal.build_providers._multipass._instance_info import ( # noqa: E501 +from snapcraft_legacy.internal.build_providers import errors +from snapcraft_legacy.internal.build_providers._multipass._instance_info import ( # noqa: E501 InstanceInfo, ) from tests import unit diff --git a/tests/unit/build_providers/multipass/test_multipass.py b/tests/unit/build_providers/multipass/test_multipass.py index f33826d819..24c136517a 100644 --- a/tests/unit/build_providers/multipass/test_multipass.py +++ b/tests/unit/build_providers/multipass/test_multipass.py @@ -23,10 +23,13 @@ import pytest from testtools.matchers import Equals -from snapcraft.internal import steps -from snapcraft.internal.build_providers import _base_provider, errors -from snapcraft.internal.build_providers._multipass import Multipass, MultipassCommand -from snapcraft.internal.errors import SnapcraftEnvironmentError +from snapcraft_legacy.internal import steps +from snapcraft_legacy.internal.build_providers import _base_provider, errors +from snapcraft_legacy.internal.build_providers._multipass import ( + Multipass, + MultipassCommand, +) +from snapcraft_legacy.internal.errors import SnapcraftEnvironmentError from tests.unit.build_providers import BaseProviderBaseTest, get_project _DEFAULT_INSTANCE_INFO = dedent( @@ -77,7 +80,8 @@ def execute_effect(*, command, instance_name, hide_output): return b"" patcher = mock.patch( - "snapcraft.internal.build_providers._multipass." "_multipass.MultipassCommand", + "snapcraft_legacy.internal.build_providers._multipass." + "_multipass.MultipassCommand", spec=MultipassCommand, ) multipass_cmd_mock = patcher.start() @@ -113,7 +117,7 @@ def setUp(self): super().setUp() patcher = mock.patch( - "snapcraft.internal.build_providers._multipass." + "snapcraft_legacy.internal.build_providers._multipass." "_multipass.MultipassCommand", spec=MultipassCommand, ) @@ -121,7 +125,7 @@ def setUp(self): self.addCleanup(patcher.stop) patcher = mock.patch( - "snapcraft.internal.build_providers._base_provider.Provider.clean_project", + "snapcraft_legacy.internal.build_providers._base_provider.Provider.clean_project", return_value=True, ) patcher.start() diff --git a/tests/unit/build_providers/multipass/test_multipass_command.py b/tests/unit/build_providers/multipass/test_multipass_command.py index e7f329353c..69bca2ae3f 100644 --- a/tests/unit/build_providers/multipass/test_multipass_command.py +++ b/tests/unit/build_providers/multipass/test_multipass_command.py @@ -23,8 +23,8 @@ import pytest from testtools.matchers import Equals -from snapcraft.internal.build_providers import errors -from snapcraft.internal.build_providers._multipass import MultipassCommand +from snapcraft_legacy.internal.build_providers import errors +from snapcraft_legacy.internal.build_providers._multipass import MultipassCommand from tests import unit diff --git a/tests/unit/build_providers/test_base_provider.py b/tests/unit/build_providers/test_base_provider.py index a73b9e4b8c..e30236269c 100644 --- a/tests/unit/build_providers/test_base_provider.py +++ b/tests/unit/build_providers/test_base_provider.py @@ -26,10 +26,10 @@ import pytest from testtools.matchers import DirExists, EndsWith, Equals, Not -from snapcraft.internal import steps -from snapcraft.internal.build_providers import errors -from snapcraft.internal.meta.snap import Snap -from snapcraft.project import Project +from snapcraft_legacy.internal import steps +from snapcraft_legacy.internal.build_providers import errors +from snapcraft_legacy.internal.meta.snap import Snap +from snapcraft_legacy.project import Project from . import ( BaseProviderBaseTest, diff --git a/tests/unit/build_providers/test_errors.py b/tests/unit/build_providers/test_errors.py index 6a0b2a0fb4..28bf314394 100644 --- a/tests/unit/build_providers/test_errors.py +++ b/tests/unit/build_providers/test_errors.py @@ -14,7 +14,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -from snapcraft.internal.build_providers import errors +from snapcraft_legacy.internal.build_providers import errors class TestErrorFormatting: diff --git a/tests/unit/build_providers/test_snap.py b/tests/unit/build_providers/test_snap.py index f85244c629..fa8b503e16 100644 --- a/tests/unit/build_providers/test_snap.py +++ b/tests/unit/build_providers/test_snap.py @@ -22,7 +22,7 @@ import fixtures from testtools.matchers import Contains, Equals, FileContains, Not -from snapcraft.internal.build_providers._snap import ( +from snapcraft_legacy.internal.build_providers._snap import ( SnapInjector, _get_snap_channel, repo, @@ -36,7 +36,7 @@ class SnapInjectionTest(unit.TestCase): def setUp(self): super().setUp() - patcher = patch("snapcraft.internal.repo.snaps.get_assertion") + patcher = patch("snapcraft_legacy.internal.repo.snaps.get_assertion") self.get_assertion_mock = patcher.start() self.addCleanup(patcher.stop) @@ -502,7 +502,7 @@ def test_snapd_not_on_host_installs_from_store(self): snap_injector.add("snapcraft") with patch( - "snapcraft.internal.repo.snaps.SnapPackage.get_local_snap_info", + "snapcraft_legacy.internal.repo.snaps.SnapPackage.get_local_snap_info", side_effect=repo.errors.SnapdConnectionError("core", "url"), ): snap_injector.apply() diff --git a/tests/unit/cache/conftest.py b/tests/unit/cache/conftest.py index 6b730c7a88..88b40d0eae 100644 --- a/tests/unit/cache/conftest.py +++ b/tests/unit/cache/conftest.py @@ -16,7 +16,7 @@ import pytest -from snapcraft.internal import cache +from snapcraft_legacy.internal import cache @pytest.fixture() diff --git a/tests/unit/cache/test_file.py b/tests/unit/cache/test_file.py index e3aa424d29..75df63d771 100644 --- a/tests/unit/cache/test_file.py +++ b/tests/unit/cache/test_file.py @@ -17,7 +17,7 @@ import os import shutil -from snapcraft.file_utils import calculate_hash +from snapcraft_legacy.file_utils import calculate_hash class TestFileCache: diff --git a/tests/unit/cache/test_snap.py b/tests/unit/cache/test_snap.py index cad7dcb26c..b09c2d9d14 100644 --- a/tests/unit/cache/test_snap.py +++ b/tests/unit/cache/test_snap.py @@ -22,10 +22,10 @@ import fixtures from testtools.matchers import Contains, Equals, Not -import snapcraft +import snapcraft_legacy import tests -from snapcraft import file_utils -from snapcraft.internal import cache +from snapcraft_legacy import file_utils +from snapcraft_legacy.internal import cache from tests.unit.commands import CommandBaseTestCase @@ -33,7 +33,7 @@ class SnapCacheBaseTestCase(CommandBaseTestCase): def setUp(self): super().setUp() - self.deb_arch = snapcraft.ProjectOptions().deb_arch + self.deb_arch = snapcraft_legacy.ProjectOptions().deb_arch self.snap_path = os.path.join( os.path.dirname(tests.__file__), "data", "test-snap.snap" ) diff --git a/tests/unit/cli/conftest.py b/tests/unit/cli/conftest.py index 5ad8db1841..a183d9f5b6 100644 --- a/tests/unit/cli/conftest.py +++ b/tests/unit/cli/conftest.py @@ -21,8 +21,8 @@ @pytest.fixture def mock_echo_error(): - """Return a mock for snapcraft.cli.echo.error.""" - patcher = mock.patch("snapcraft.cli.echo.error") + """Return a mock for snapcraft_legacy.cli.echo.error.""" + patcher = mock.patch("snapcraft_legacy.cli.echo.error") yield patcher.start() patcher.stop() diff --git a/tests/unit/cli/test_echo.py b/tests/unit/cli/test_echo.py index 721daed5b2..7b2bf5ebf7 100644 --- a/tests/unit/cli/test_echo.py +++ b/tests/unit/cli/test_echo.py @@ -21,13 +21,13 @@ import fixtures import pytest -from snapcraft.cli import echo +from snapcraft_legacy.cli import echo from tests import unit @pytest.fixture() def mock_click(): - with mock.patch("snapcraft.cli.echo.click", autospec=True) as mock_click: + with mock.patch("snapcraft_legacy.cli.echo.click", autospec=True) as mock_click: yield mock_click @@ -35,7 +35,7 @@ def mock_click(): def mock_shutil_get_terminal_size(): fake_terminal = os.terminal_size([80, 24]) with mock.patch( - "snapcraft.cli.echo.shutil.get_terminal_size", return_value=fake_terminal + "snapcraft_legacy.cli.echo.shutil.get_terminal_size", return_value=fake_terminal ) as mock_terminal_size: yield mock_terminal_size @@ -63,13 +63,13 @@ def test_is_tty_connected(self, tty_mock): self.assertEqual(result, True) - @mock.patch("snapcraft.cli.echo.is_tty_connected", return_value=False) + @mock.patch("snapcraft_legacy.cli.echo.is_tty_connected", return_value=False) def test_echo_confirm_is_not_tty(self, tty_mock): echo.confirm("message") self.click_confirm.mock.assert_not_called() - @mock.patch("snapcraft.cli.echo.is_tty_connected", return_value=True) + @mock.patch("snapcraft_legacy.cli.echo.is_tty_connected", return_value=True) def test_echo_confirm_is_tty(self, tty_mock): echo.confirm("message") @@ -82,7 +82,7 @@ def test_echo_confirm_is_tty(self, tty_mock): err=False, ) - @mock.patch("snapcraft.cli.echo.is_tty_connected", return_value=True) + @mock.patch("snapcraft_legacy.cli.echo.is_tty_connected", return_value=True) def test_echo_confirm_default(self, tty_mock): echo.confirm("message", default="the new default") @@ -95,13 +95,13 @@ def test_echo_confirm_default(self, tty_mock): err=False, ) - @mock.patch("snapcraft.cli.echo.is_tty_connected", return_value=False) + @mock.patch("snapcraft_legacy.cli.echo.is_tty_connected", return_value=False) def test_echo_prompt_is_not_tty(self, tty_mock): echo.prompt("message") self.click_prompt.mock.assert_not_called() - @mock.patch("snapcraft.cli.echo.is_tty_connected", return_value=True) + @mock.patch("snapcraft_legacy.cli.echo.is_tty_connected", return_value=True) def test_echo_prompt_is_tty(self, tty_mock): echo.prompt("message") @@ -117,7 +117,7 @@ def test_echo_prompt_is_tty(self, tty_mock): err=False, ) - @mock.patch("snapcraft.cli.echo.is_tty_connected", return_value=True) + @mock.patch("snapcraft_legacy.cli.echo.is_tty_connected", return_value=True) def test_echo_prompt_default(self, tty_mock): echo.prompt("message", default="the new default") diff --git a/tests/unit/cli/test_errors.py b/tests/unit/cli/test_errors.py index bd2830d2ad..5b96f19c53 100644 --- a/tests/unit/cli/test_errors.py +++ b/tests/unit/cli/test_errors.py @@ -25,19 +25,19 @@ import xdg from testtools.matchers import Equals, FileContains -import snapcraft.cli.echo -import snapcraft.internal.errors -from snapcraft.cli._errors import ( +import snapcraft_legacy.cli.echo +import snapcraft_legacy.internal.errors +from snapcraft_legacy.cli._errors import ( _get_exception_exit_code, _is_reportable_error, _print_exception_message, exception_handler, ) -from snapcraft.internal.build_providers.errors import ProviderExecError +from snapcraft_legacy.internal.build_providers.errors import ProviderExecError from tests import fixture_setup, unit -class SnapcraftTError(snapcraft.internal.errors.SnapcraftError): +class SnapcraftTError(snapcraft_legacy.internal.errors.SnapcraftError): fmt = "{message}" @@ -48,7 +48,7 @@ def get_exit_code(self): return 123 -class SnapcraftTException(snapcraft.internal.errors.SnapcraftException): +class SnapcraftTException(snapcraft_legacy.internal.errors.SnapcraftException): def __init__(self): self._brief = "" self._resolution = "" @@ -80,7 +80,7 @@ class TestSnapcraftExceptionHandling(unit.TestCase): def setUp(self): super().setUp() - patcher = mock.patch("snapcraft.cli._errors.echo.error") + patcher = mock.patch("snapcraft_legacy.cli._errors.echo.error") self.error_mock = patcher.start() self.addCleanup(patcher.stop) @@ -146,7 +146,11 @@ def test_snapcraft_exception_minimal_with_resolution_and_url(self): def test_snapcraft_exception_reportable(self): exception = SnapcraftTException() exception._brief = "something's strange, in the neighborhood" - exc_info = (snapcraft.internal.errors.SnapcraftException, exception, None) + exc_info = ( + snapcraft_legacy.internal.errors.SnapcraftException, + exception, + None, + ) # Test default (is false). self.assertFalse(_is_reportable_error(exc_info)) @@ -192,7 +196,7 @@ def setUp(self): self.print_mock = patcher.start() self.addCleanup(patcher.stop) - patcher = mock.patch("snapcraft.cli._errors.echo.error") + patcher = mock.patch("snapcraft_legacy.cli._errors.echo.error") self.error_mock = patcher.start() self.addCleanup(patcher.stop) @@ -241,12 +245,12 @@ class ErrorsTestCase(ErrorsBaseTestCase): def setUp(self): super().setUp() - @mock.patch.object(snapcraft.cli._errors, "RavenClient") - @mock.patch("snapcraft.internal.common.is_snap", return_value=False) + @mock.patch.object(snapcraft_legacy.cli._errors, "RavenClient") + @mock.patch("snapcraft_legacy.internal.common.is_snap", return_value=False) def test_handler_no_raven_traceback_non_snapcraft_exceptions_debug( self, is_snap_mock, raven_client_mock ): - snapcraft.cli._errors.RavenClient = None + snapcraft_legacy.cli._errors.RavenClient = None try: self.call_handler(RuntimeError("not a SnapcraftError"), True) except Exception: @@ -318,13 +322,13 @@ def test_provider_error_host(self, isfile_function): self.assertThat(self.print_exception_mock.call_count, Equals(1)) @mock.patch("os.path.isfile", return_value=False) - @mock.patch.object(snapcraft.cli._errors, "RavenClient") + @mock.patch.object(snapcraft_legacy.cli._errors, "RavenClient") def test_provider_error_inner(self, isfile_function, raven_client_mock): # Error raised inside the build provider self.useFixture( fixtures.EnvironmentVariable("SNAPCRAFT_BUILD_ENVIRONMENT", "managed-host") ) - snapcraft.cli._errors.RavenClient = "something" + snapcraft_legacy.cli._errors.RavenClient = "something" self._raise_other_error() self.move_mock.assert_not_called() self.assertThat(self.print_exception_mock.call_count, Equals(2)) @@ -347,15 +351,15 @@ def setUp(self): except ImportError: self.skipTest("raven needs to be installed for this test.") - patcher = mock.patch("snapcraft.cli.echo.prompt") + patcher = mock.patch("snapcraft_legacy.cli.echo.prompt") self.prompt_mock = patcher.start() self.addCleanup(patcher.stop) - patcher = mock.patch("snapcraft.cli._errors.RequestsHTTPTransport") + patcher = mock.patch("snapcraft_legacy.cli._errors.RequestsHTTPTransport") self.raven_request_mock = patcher.start() self.addCleanup(patcher.stop) - patcher = mock.patch("snapcraft.cli._errors.RavenClient") + patcher = mock.patch("snapcraft_legacy.cli._errors.RavenClient") self.raven_client_mock = patcher.start() self.addCleanup(patcher.stop) diff --git a/tests/unit/cli/test_lifecycle.py b/tests/unit/cli/test_lifecycle.py index 2ef10b1fdd..cfc703f136 100644 --- a/tests/unit/cli/test_lifecycle.py +++ b/tests/unit/cli/test_lifecycle.py @@ -18,7 +18,7 @@ import pytest -from snapcraft.cli import lifecycle +from snapcraft_legacy.cli import lifecycle @pytest.mark.parametrize( @@ -32,8 +32,8 @@ @pytest.mark.parametrize( "compression", ["xz", "lzo", None], ) -@mock.patch("snapcraft.file_utils.get_host_tool_path", return_value="/bin/snap") -@mock.patch("snapcraft.cli.lifecycle._run_pack", return_value="ignore.snap") +@mock.patch("snapcraft_legacy.file_utils.get_host_tool_path", return_value="/bin/snap") +@mock.patch("snapcraft_legacy.cli.lifecycle._run_pack", return_value="ignore.snap") def test_pack(mock_run_pack, mock_host_tool, compression, output, pack_name, pack_dir): lifecycle._pack(directory="/my/snap", compression=compression, output=output) diff --git a/tests/unit/cli/test_metrics.py b/tests/unit/cli/test_metrics.py index 1154ccce78..6378729fa8 100644 --- a/tests/unit/cli/test_metrics.py +++ b/tests/unit/cli/test_metrics.py @@ -16,11 +16,11 @@ import pytest -from snapcraft.cli._metrics import ( +from snapcraft_legacy.cli._metrics import ( convert_metrics_to_table, get_series_label_from_metric_name, ) -from snapcraft.storeapi import metrics +from snapcraft_legacy.storeapi import metrics def test_get_series_label_from_metric_name(): diff --git a/tests/unit/cli/test_options.py b/tests/unit/cli/test_options.py index af16e5ce1f..3138512e6c 100644 --- a/tests/unit/cli/test_options.py +++ b/tests/unit/cli/test_options.py @@ -20,7 +20,7 @@ import fixtures from testtools.matchers import Equals -import snapcraft.cli._options as options +import snapcraft_legacy.cli._options as options from tests import unit @@ -320,7 +320,7 @@ def setUp(self): fixtures.MockPatch("os.geteuid", return_value=0) ).mock - @mock.patch("snapcraft.cli._options.warning") + @mock.patch("snapcraft_legacy.cli._options.warning") def test_click_warn_sudo(self, warning_mock): options._sanity_check_build_provider_flags("host") warning_mock.assert_called_once_with( diff --git a/tests/unit/commands/__init__.py b/tests/unit/commands/__init__.py index 888c8979b0..3ec204f686 100644 --- a/tests/unit/commands/__init__.py +++ b/tests/unit/commands/__init__.py @@ -23,11 +23,11 @@ import fixtures from click.testing import CliRunner -from snapcraft import storeapi -from snapcraft.cli._runner import run -from snapcraft.storeapi import metrics -from snapcraft.storeapi.v2.channel_map import ChannelMap -from snapcraft.storeapi.v2.releases import Releases +from snapcraft_legacy import storeapi +from snapcraft_legacy.cli._runner import run +from snapcraft_legacy.storeapi import metrics +from snapcraft_legacy.storeapi.v2.channel_map import ChannelMap +from snapcraft_legacy.storeapi.v2.releases import Releases from tests import fixture_setup, unit _sample_keys = [ @@ -94,7 +94,9 @@ def run_command(self, args, **kwargs): # For click testing, runner will overwrite the descriptors for stdio - # ensure TTY always appears connected. self.useFixture( - fixtures.MockPatch("snapcraft.cli.echo.is_tty_connected", return_value=True) + fixtures.MockPatch( + "snapcraft_legacy.cli.echo.is_tty_connected", return_value=True + ) ) with mock.patch("sys.argv", args): @@ -108,16 +110,16 @@ def setUp(self): self.useFixture(fixtures.EnvironmentVariable("SNAPCRAFT_BUILD_ENVIRONMENT")) self.fake_lifecycle_clean = fixtures.MockPatch( - "snapcraft.internal.lifecycle.clean" + "snapcraft_legacy.internal.lifecycle.clean" ) self.useFixture(self.fake_lifecycle_clean) self.fake_lifecycle_execute = fixtures.MockPatch( - "snapcraft.internal.lifecycle.execute" + "snapcraft_legacy.internal.lifecycle.execute" ) self.useFixture(self.fake_lifecycle_execute) - self.fake_pack = fixtures.MockPatch("snapcraft.cli.lifecycle._pack") + self.fake_pack = fixtures.MockPatch("snapcraft_legacy.cli.lifecycle._pack") self.useFixture(self.fake_pack) self.snapcraft_yaml = fixture_setup.SnapcraftYaml( @@ -137,7 +139,7 @@ def setUp(self): ) self.fake_get_provider_for = fixtures.MockPatch( - "snapcraft.internal.build_providers.get_provider_for", + "snapcraft_legacy.internal.build_providers.get_provider_for", return_value=self.provider_class_mock, ) self.useFixture(self.fake_get_provider_for) @@ -458,6 +460,7 @@ def setUp(self): # Pretend that the snap command is available self.fake_package_installed = fixtures.MockPatch( - "snapcraft.internal.repo.Repo.is_package_installed", return_value=True + "snapcraft_legacy.internal.repo.Repo.is_package_installed", + return_value=True, ) self.useFixture(self.fake_package_installed) diff --git a/tests/unit/commands/conftest.py b/tests/unit/commands/conftest.py index 15d466ab79..e5f552e309 100644 --- a/tests/unit/commands/conftest.py +++ b/tests/unit/commands/conftest.py @@ -19,7 +19,7 @@ import pytest from click.testing import CliRunner -from snapcraft.cli._runner import run +from snapcraft_legacy.cli._runner import run @pytest.fixture diff --git a/tests/unit/commands/snapcraftctl/__init__.py b/tests/unit/commands/snapcraftctl/__init__.py index 8e4c4cc247..890b71472e 100644 --- a/tests/unit/commands/snapcraftctl/__init__.py +++ b/tests/unit/commands/snapcraftctl/__init__.py @@ -19,7 +19,7 @@ import fixtures from click.testing import CliRunner -from snapcraft.cli.snapcraftctl._runner import run +from snapcraft_legacy.cli.snapcraftctl._runner import run from tests import unit diff --git a/tests/unit/commands/snapcraftctl/test_build.py b/tests/unit/commands/snapcraftctl/test_build.py index 3f754f5186..a96089088d 100644 --- a/tests/unit/commands/snapcraftctl/test_build.py +++ b/tests/unit/commands/snapcraftctl/test_build.py @@ -18,7 +18,7 @@ from testtools.matchers import Contains, Equals, FileExists -from snapcraft.internal import errors +from snapcraft_legacy.internal import errors from . import CommandBaseNoFifoTestCase, CommandBaseTestCase diff --git a/tests/unit/commands/snapcraftctl/test_set_grade.py b/tests/unit/commands/snapcraftctl/test_set_grade.py index be43522108..41a1e5d094 100644 --- a/tests/unit/commands/snapcraftctl/test_set_grade.py +++ b/tests/unit/commands/snapcraftctl/test_set_grade.py @@ -18,7 +18,7 @@ from testtools.matchers import Contains, Equals, FileExists -from snapcraft.internal import errors +from snapcraft_legacy.internal import errors from . import CommandBaseNoFifoTestCase, CommandBaseTestCase diff --git a/tests/unit/commands/snapcraftctl/test_set_version.py b/tests/unit/commands/snapcraftctl/test_set_version.py index 0b30e27132..b264e76370 100644 --- a/tests/unit/commands/snapcraftctl/test_set_version.py +++ b/tests/unit/commands/snapcraftctl/test_set_version.py @@ -18,7 +18,7 @@ from testtools.matchers import Contains, Equals, FileExists -from snapcraft.internal import errors +from snapcraft_legacy.internal import errors from . import CommandBaseNoFifoTestCase, CommandBaseTestCase diff --git a/tests/unit/commands/test_build_providers.py b/tests/unit/commands/test_build_providers.py index a01ec5dd25..454d4771f0 100644 --- a/tests/unit/commands/test_build_providers.py +++ b/tests/unit/commands/test_build_providers.py @@ -21,9 +21,9 @@ import fixtures from testtools.matchers import Equals -import snapcraft.yaml_utils.errors -from snapcraft.internal import steps -from snapcraft.internal.build_providers.errors import ProviderExecError +import snapcraft_legacy.yaml_utils.errors +from snapcraft_legacy.internal import steps +from snapcraft_legacy.internal.build_providers.errors import ProviderExecError from tests import fixture_setup from tests.unit.build_providers import ProviderImpl @@ -75,7 +75,7 @@ def setUp(self): # Don't actually run clean - we only want to test the command # line interface flag parsing. - self.useFixture(fixtures.MockPatch("snapcraft.internal.lifecycle.clean")) + self.useFixture(fixtures.MockPatch("snapcraft_legacy.internal.lifecycle.clean")) # tests.unit.TestCase sets SNAPCRAFT_BUILD_ENVIRONMENT to host. # These build provider tests will want to set this explicitly. @@ -85,7 +85,7 @@ def setUp(self): self.mock_get_provider_for = self.useFixture( fixtures.MockPatch( - "snapcraft.internal.build_providers.get_provider_for", + "snapcraft_legacy.internal.build_providers.get_provider_for", return_value=ProviderImpl, ) ).mock @@ -93,7 +93,8 @@ def setUp(self): # Tests need to dictate this (or not). self.useFixture( fixtures.MockPatch( - "snapcraft.internal.common.is_process_container", return_value=False + "snapcraft_legacy.internal.common.is_process_container", + return_value=False, ) ) @@ -108,7 +109,8 @@ class AssortedBuildEnvironmentParsingTests(BuildEnvironmentParsingTest): def test_host_container(self): self.useFixture( fixtures.MockPatch( - "snapcraft.internal.common.is_process_container", return_value=True + "snapcraft_legacy.internal.common.is_process_container", + return_value=True, ) ) result = self.run_command([self.step]) @@ -242,7 +244,7 @@ def setUp(self): ) patcher = mock.patch( - "snapcraft.internal.build_providers.get_provider_for", + "snapcraft_legacy.internal.build_providers.get_provider_for", return_value=ProviderImpl, ) self.provider = patcher.start() @@ -267,7 +269,9 @@ def test_validation_fails(self): self.useFixture(snapcraft_yaml) self.assertRaises( - snapcraft.yaml_utils.errors.YamlValidationError, self.run_command, ["pull"] + snapcraft_legacy.yaml_utils.errors.YamlValidationError, + self.run_command, + ["pull"], ) @@ -300,7 +304,8 @@ def shell(self): shell_mock() patcher = mock.patch( - "snapcraft.internal.build_providers.get_provider_for", return_value=Provider + "snapcraft_legacy.internal.build_providers.get_provider_for", + return_value=Provider, ) self.provider = patcher.start() self.addCleanup(patcher.stop) @@ -352,7 +357,8 @@ def shell(self): shell_mock() patcher = mock.patch( - "snapcraft.internal.build_providers.get_provider_for", return_value=Provider + "snapcraft_legacy.internal.build_providers.get_provider_for", + return_value=Provider, ) self.provider = patcher.start() self.addCleanup(patcher.stop) @@ -456,7 +462,8 @@ def _mount_prime_directory(self) -> bool: return mount_prime_mock() patcher = mock.patch( - "snapcraft.internal.build_providers.get_provider_for", return_value=Provider + "snapcraft_legacy.internal.build_providers.get_provider_for", + return_value=Provider, ) self.provider = patcher.start() self.addCleanup(patcher.stop) @@ -505,7 +512,8 @@ def clean_parts(self, part_names): clean_mock(part_names=part_names) patcher = mock.patch( - "snapcraft.internal.build_providers.get_provider_for", return_value=Provider + "snapcraft_legacy.internal.build_providers.get_provider_for", + return_value=Provider, ) self.provider = patcher.start() self.addCleanup(patcher.stop) @@ -515,7 +523,7 @@ def clean_parts(self, part_names): self.make_snapcraft_yaml("pull", base="core20") - @mock.patch("snapcraft.internal.lifecycle.clean") + @mock.patch("snapcraft_legacy.internal.lifecycle.clean") def test_clean(self, lifecycle_clean_mock): result = self.run_command(["clean"]) @@ -545,8 +553,8 @@ def test_unprime_with_build_environment_errors(self): self.clean_project_mock.assert_not_called() self.clean_mock.assert_not_called() - @mock.patch("snapcraft.cli.lifecycle.get_project") - @mock.patch("snapcraft.internal.lifecycle.clean") + @mock.patch("snapcraft_legacy.cli.lifecycle.get_project") + @mock.patch("snapcraft_legacy.internal.lifecycle.clean") def test_unprime_in_managed_host(self, lifecycle_clean_mock, get_project_mock): self.useFixture( fixtures.EnvironmentVariable("SNAPCRAFT_BUILD_ENVIRONMENT", "managed-host") diff --git a/tests/unit/commands/test_clean.py b/tests/unit/commands/test_clean.py index 08be3f633f..fe63441c7d 100644 --- a/tests/unit/commands/test_clean.py +++ b/tests/unit/commands/test_clean.py @@ -18,7 +18,7 @@ from testtools.matchers import Equals -from snapcraft.internal import steps +from snapcraft_legacy.internal import steps from . import LifecycleCommandsBaseTestCase diff --git a/tests/unit/commands/test_close.py b/tests/unit/commands/test_close.py index 031e113914..81a93212bb 100644 --- a/tests/unit/commands/test_close.py +++ b/tests/unit/commands/test_close.py @@ -18,8 +18,8 @@ import fixtures from testtools.matchers import Contains, Equals -import snapcraft.storeapi.errors -from snapcraft import storeapi +import snapcraft_legacy.storeapi.errors +from snapcraft_legacy import storeapi from . import FakeStoreCommandsBaseTestCase @@ -43,7 +43,7 @@ def test_close_missing_permission(self): } raised = self.assertRaises( - snapcraft.storeapi.errors.StoreChannelClosingPermissionError, + snapcraft_legacy.storeapi.errors.StoreChannelClosingPermissionError, self.run_command, ["close", "foo", "beta"], ) diff --git a/tests/unit/commands/test_create_key.py b/tests/unit/commands/test_create_key.py index bed827ee3d..c9619e485f 100644 --- a/tests/unit/commands/test_create_key.py +++ b/tests/unit/commands/test_create_key.py @@ -17,7 +17,7 @@ import fixtures from testtools.matchers import Equals -from snapcraft import storeapi +from snapcraft_legacy import storeapi from . import FakeStoreCommandsBaseTestCase, get_sample_key diff --git a/tests/unit/commands/test_edit_validation_sets.py b/tests/unit/commands/test_edit_validation_sets.py index 8c14232738..0129c8cde4 100644 --- a/tests/unit/commands/test_edit_validation_sets.py +++ b/tests/unit/commands/test_edit_validation_sets.py @@ -20,8 +20,8 @@ import pytest -from snapcraft.storeapi.v2 import validation_sets -from snapcraft.storeapi import StoreClient +from snapcraft_legacy.storeapi.v2 import validation_sets +from snapcraft_legacy.storeapi import StoreClient @pytest.fixture @@ -93,7 +93,7 @@ def sign(assertion: Dict[str, Any], *, key_name: str) -> bytes: return (json.dumps(assertion) + f"\n\nSIGNED{key_name}").encode() patched_snap_sign = mock.patch( - "snapcraft.cli.assertions._sign_assertion", side_effect=sign + "snapcraft_legacy.cli.assertions._sign_assertion", side_effect=sign ) yield patched_snap_sign.start() patched_snap_sign.stop() @@ -129,7 +129,7 @@ def test_edit_validation_sets_with_no_changes_to_existing_set( @pytest.fixture def fake_edit_validation_sets(): patched_edit_validation_sets = mock.patch( - "snapcraft.cli.assertions._edit_validation_sets" + "snapcraft_legacy.cli.assertions._edit_validation_sets" ) yield patched_edit_validation_sets.start() patched_edit_validation_sets.stop() diff --git a/tests/unit/commands/test_export_login.py b/tests/unit/commands/test_export_login.py index 6fa00374eb..2c4fc789e6 100644 --- a/tests/unit/commands/test_export_login.py +++ b/tests/unit/commands/test_export_login.py @@ -21,7 +21,7 @@ import pytest from testtools.matchers import Contains, Equals, MatchesRegex, Not -from snapcraft import storeapi +from snapcraft_legacy import storeapi from . import FakeStoreCommandsBaseTestCase diff --git a/tests/unit/commands/test_extensions.py b/tests/unit/commands/test_extensions.py index 299b6897f8..2a4ab75b52 100644 --- a/tests/unit/commands/test_extensions.py +++ b/tests/unit/commands/test_extensions.py @@ -20,8 +20,8 @@ from testtools.matchers import Equals -from snapcraft.internal.project_loader import errors, supported_extension_names -from snapcraft.internal.project_loader._extensions._extension import Extension +from snapcraft_legacy.internal.project_loader import errors, supported_extension_names +from snapcraft_legacy.internal.project_loader._extensions._extension import Extension from tests import fixture_setup from . import CommandBaseTestCase diff --git a/tests/unit/commands/test_gated.py b/tests/unit/commands/test_gated.py index 722c0da17d..ecd3c215a6 100644 --- a/tests/unit/commands/test_gated.py +++ b/tests/unit/commands/test_gated.py @@ -18,7 +18,7 @@ from testtools.matchers import Contains, Equals -import snapcraft.storeapi.errors +import snapcraft_legacy.storeapi.errors from . import StoreCommandsBaseTestCase @@ -30,7 +30,7 @@ def test_gated_unknown_snap(self): self.client.login(email="dummy", password="test correct password") raised = self.assertRaises( - snapcraft.storeapi.errors.SnapNotFoundError, + snapcraft_legacy.storeapi.errors.SnapNotFoundError, self.run_command, ["gated", "notfound"], ) diff --git a/tests/unit/commands/test_help.py b/tests/unit/commands/test_help.py index 7d76fc8f43..ff813fa681 100644 --- a/tests/unit/commands/test_help.py +++ b/tests/unit/commands/test_help.py @@ -21,8 +21,8 @@ import fixtures from testtools.matchers import Contains, Equals, StartsWith -from snapcraft.cli._runner import run -from snapcraft.cli.help import _TOPICS +from snapcraft_legacy.cli._runner import run +from snapcraft_legacy.cli.help import _TOPICS from tests import fixture_setup from . import CommandBaseTestCase @@ -115,7 +115,9 @@ def test_print_module_named_with_dashes_help_for_valid_plugin(self): def test_show_module_help_with_devel_for_valid_plugin(self): result = self.run_command(["help", "nil", "--devel"]) - expected = "Help on module snapcraft.plugins.v2.nil in snapcraft.plugins" + expected = ( + "Help on module snapcraft_legacy.plugins.v2.nil in snapcraft_legacy.plugins" + ) output = result.output[: len(expected)] self.assertThat( @@ -164,9 +166,9 @@ def test_no_unicode_in_help_strings(self): import os from pathlib import Path - import snapcraft.plugins + import snapcraft_legacy.plugins - for plugin in Path(snapcraft.plugins.__path__[0]).glob("*.py"): + for plugin in Path(snapcraft_legacy.plugins.__path__[0]).glob("*.py"): if os.path.isfile(str(plugin)) and not os.path.basename( str(plugin) ).startswith("_"): diff --git a/tests/unit/commands/test_init.py b/tests/unit/commands/test_init.py index 05bfe3f3fa..301dcd12f5 100644 --- a/tests/unit/commands/test_init.py +++ b/tests/unit/commands/test_init.py @@ -18,7 +18,7 @@ from testtools.matchers import Contains, Equals, FileContains -import snapcraft.internal.errors +import snapcraft_legacy.internal.errors from . import CommandBaseTestCase @@ -75,7 +75,7 @@ def test_init_with_existing_yaml(self): open(yaml_path, "w").close() raised = self.assertRaises( - snapcraft.internal.errors.SnapcraftEnvironmentError, + snapcraft_legacy.internal.errors.SnapcraftEnvironmentError, self.run_command, ["init"], ) diff --git a/tests/unit/commands/test_list.py b/tests/unit/commands/test_list.py index 8332351c82..faa9fb48eb 100644 --- a/tests/unit/commands/test_list.py +++ b/tests/unit/commands/test_list.py @@ -18,7 +18,7 @@ from testtools.matchers import Contains, Equals -from snapcraft import storeapi +from snapcraft_legacy import storeapi from . import FakeStoreCommandsBaseTestCase @@ -28,7 +28,7 @@ class ListTest(FakeStoreCommandsBaseTestCase): command_name = "list" def test_command_without_login_must_ask(self): - # TODO: look into why this many calls are done inside snapcraft.storeapi + # TODO: look into why this many calls are done inside snapcraft_legacy.storeapi self.fake_store_account_info.mock.side_effect = [ storeapi.http_clients.errors.InvalidCredentialsError("error"), {"account_id": "abcd", "snaps": dict()}, diff --git a/tests/unit/commands/test_list_keys.py b/tests/unit/commands/test_list_keys.py index 2c454a0352..301925579e 100644 --- a/tests/unit/commands/test_list_keys.py +++ b/tests/unit/commands/test_list_keys.py @@ -19,7 +19,7 @@ from testtools.matchers import Contains, Equals import fixtures -from snapcraft import storeapi +from snapcraft_legacy import storeapi from . import FakeStoreCommandsBaseTestCase, get_sample_key @@ -29,7 +29,7 @@ class ListKeysCommandTestCase(FakeStoreCommandsBaseTestCase): command_name = "list-keys" def test_command_without_login_must_ask(self): - # TODO: look into why this many calls are done inside snapcraft.storeapi + # TODO: look into why this many calls are done inside snapcraft_legacy.storeapi self.fake_store_account_info.mock.side_effect = [ storeapi.http_clients.errors.InvalidCredentialsError("error"), {"account_id": "abcd", "account_keys": list()}, diff --git a/tests/unit/commands/test_list_plugins.py b/tests/unit/commands/test_list_plugins.py index 62f1e90f78..90257a9b46 100644 --- a/tests/unit/commands/test_list_plugins.py +++ b/tests/unit/commands/test_list_plugins.py @@ -17,7 +17,7 @@ import fixtures from testtools.matchers import Contains, Equals -import snapcraft +import snapcraft_legacy from tests import fixture_setup from . import CommandBaseTestCase @@ -89,7 +89,7 @@ def test_default_from_snapcraft_yaml(self): result.output, Contains("Displaying plugins available for 'core18") ) self.fake_iter_modules.mock.assert_called_once_with( - snapcraft.plugins.v1.__path__ + snapcraft_legacy.plugins.v1.__path__ ) def test_alias(self): @@ -115,15 +115,15 @@ def test_core20_list(self): ) self.fake_iter_modules.mock.assert_called_once_with( - snapcraft.plugins.v2.__path__ + snapcraft_legacy.plugins.v2.__path__ ) def test_core2y_list(self): # Note that core2y is some future base, _not_ allowed to be used from cmdline # This tests that addition of the next base will use the latest version of plugins - snapcraft.cli.discovery.list_plugins.callback("core2y") + snapcraft_legacy.cli.discovery.list_plugins.callback("core2y") self.fake_iter_modules.mock.assert_called_once_with( - snapcraft.plugins.v2.__path__ + snapcraft_legacy.plugins.v2.__path__ ) def test_list_plugins_non_tty(self): @@ -135,7 +135,7 @@ def test_list_plugins_non_tty(self): self.assertThat(result.exit_code, Equals(0)) self.assertThat(result.output, Contains(self.default_plugin_output)) self.fake_iter_modules.mock.assert_called_once_with( - snapcraft.plugins.v1.__path__ + snapcraft_legacy.plugins.v1.__path__ ) def test_list_plugins_large_terminal(self): @@ -147,7 +147,7 @@ def test_list_plugins_large_terminal(self): self.assertThat(result.exit_code, Equals(0)) self.assertThat(result.output, Contains(self.default_plugin_output)) self.fake_iter_modules.mock.assert_called_once_with( - snapcraft.plugins.v1.__path__ + snapcraft_legacy.plugins.v1.__path__ ) def test_list_plugins_small_terminal(self): @@ -169,5 +169,5 @@ def test_list_plugins_small_terminal(self): output_slice = [o.strip() for o in result.output.splitlines()][1:] self.assertThat(output_slice, Equals(expected_output)) self.fake_iter_modules.mock.assert_called_once_with( - snapcraft.plugins.v1.__path__ + snapcraft_legacy.plugins.v1.__path__ ) diff --git a/tests/unit/commands/test_list_revisions.py b/tests/unit/commands/test_list_revisions.py index ed095a0c7d..44f7c759fe 100644 --- a/tests/unit/commands/test_list_revisions.py +++ b/tests/unit/commands/test_list_revisions.py @@ -20,7 +20,7 @@ import fixtures from testtools.matchers import Contains, Equals -from snapcraft import storeapi +from snapcraft_legacy import storeapi from . import FakeStoreCommandsBaseTestCase diff --git a/tests/unit/commands/test_list_tracks.py b/tests/unit/commands/test_list_tracks.py index 4e85b806b8..ad4b013109 100644 --- a/tests/unit/commands/test_list_tracks.py +++ b/tests/unit/commands/test_list_tracks.py @@ -18,7 +18,7 @@ from testtools.matchers import Contains, Equals -from snapcraft import storeapi +from snapcraft_legacy import storeapi from . import FakeStoreCommandsBaseTestCase diff --git a/tests/unit/commands/test_list_validation_sets.py b/tests/unit/commands/test_list_validation_sets.py index ac5e2171bc..5184daaa62 100644 --- a/tests/unit/commands/test_list_validation_sets.py +++ b/tests/unit/commands/test_list_validation_sets.py @@ -19,8 +19,8 @@ import pytest -from snapcraft.storeapi.v2 import validation_sets -from snapcraft.storeapi import StoreClient +from snapcraft_legacy.storeapi.v2 import validation_sets +from snapcraft_legacy.storeapi import StoreClient @pytest.fixture diff --git a/tests/unit/commands/test_login.py b/tests/unit/commands/test_login.py index f46d17cf33..40cf5ce48e 100644 --- a/tests/unit/commands/test_login.py +++ b/tests/unit/commands/test_login.py @@ -23,7 +23,7 @@ from simplejson.scanner import JSONDecodeError from testtools.matchers import Contains, Equals, MatchesRegex, Not -from snapcraft import storeapi +from snapcraft_legacy import storeapi from . import FakeStoreCommandsBaseTestCase diff --git a/tests/unit/commands/test_logout.py b/tests/unit/commands/test_logout.py index 1fc57e5839..e2856ac12f 100644 --- a/tests/unit/commands/test_logout.py +++ b/tests/unit/commands/test_logout.py @@ -18,7 +18,7 @@ from testtools.matchers import Equals, MatchesRegex -from snapcraft.storeapi import StoreClient +from snapcraft_legacy.storeapi import StoreClient from . import CommandBaseTestCase diff --git a/tests/unit/commands/test_metrics.py b/tests/unit/commands/test_metrics.py index b4dffb436e..e941daa4c8 100644 --- a/tests/unit/commands/test_metrics.py +++ b/tests/unit/commands/test_metrics.py @@ -18,7 +18,7 @@ import pytest -from snapcraft import storeapi +from snapcraft_legacy import storeapi from . import FakeStoreCommandsBaseTestCase diff --git a/tests/unit/commands/test_pull_build_stage_prime.py b/tests/unit/commands/test_pull_build_stage_prime.py index d127d4be7d..acb54ee65a 100644 --- a/tests/unit/commands/test_pull_build_stage_prime.py +++ b/tests/unit/commands/test_pull_build_stage_prime.py @@ -18,7 +18,7 @@ from testtools.matchers import Equals -from snapcraft.internal import steps +from snapcraft_legacy.internal import steps from . import LifecycleCommandsBaseTestCase diff --git a/tests/unit/commands/test_refresh.py b/tests/unit/commands/test_refresh.py index a1a24145b3..b967a28e58 100644 --- a/tests/unit/commands/test_refresh.py +++ b/tests/unit/commands/test_refresh.py @@ -51,7 +51,7 @@ def make_snapcraft_yaml(self, n=1, snap_type="app", snapcraft_yaml=None): class RefreshCommandTestCase(RefreshCommandBaseTestCase): - @mock.patch("snapcraft.cli.containers.repo.Repo.refresh_build_packages") + @mock.patch("snapcraft_legacy.cli.containers.repo.Repo.refresh_build_packages") def test_refresh(self, mock_repo_refresh): self.make_snapcraft_yaml() diff --git a/tests/unit/commands/test_register.py b/tests/unit/commands/test_register.py index 230ad881ee..720540f5a2 100644 --- a/tests/unit/commands/test_register.py +++ b/tests/unit/commands/test_register.py @@ -19,7 +19,7 @@ from simplejson.scanner import JSONDecodeError from testtools.matchers import Contains, Equals, Not -from snapcraft import storeapi +from snapcraft_legacy import storeapi from . import FakeStoreCommandsBaseTestCase diff --git a/tests/unit/commands/test_register_key.py b/tests/unit/commands/test_register_key.py index 1e7924e898..5cd8a4d685 100644 --- a/tests/unit/commands/test_register_key.py +++ b/tests/unit/commands/test_register_key.py @@ -22,7 +22,7 @@ from simplejson.scanner import JSONDecodeError from testtools.matchers import Contains, Equals -from snapcraft import storeapi +from snapcraft_legacy import storeapi from . import FakeStoreCommandsBaseTestCase, get_sample_key @@ -109,7 +109,9 @@ def test_register_key_account_info_failed(self): ) # Fake the login check - self.useFixture(fixtures.MockPatch("snapcraft._store.login", return_value=True)) + self.useFixture( + fixtures.MockPatch("snapcraft_legacy._store.login", return_value=True) + ) raised = self.assertRaises( storeapi.errors.StoreAccountInformationError, diff --git a/tests/unit/commands/test_release.py b/tests/unit/commands/test_release.py index 3c14549a36..8bed58cc05 100644 --- a/tests/unit/commands/test_release.py +++ b/tests/unit/commands/test_release.py @@ -18,8 +18,8 @@ from testtools.matchers import Contains, Equals -from snapcraft import storeapi -from snapcraft.storeapi.v2.channel_map import ( +from snapcraft_legacy import storeapi +from snapcraft_legacy.storeapi.v2.channel_map import ( MappedChannel, Progressive, Revision, diff --git a/tests/unit/commands/test_remote.py b/tests/unit/commands/test_remote.py index 73df602071..e60cf4e161 100644 --- a/tests/unit/commands/test_remote.py +++ b/tests/unit/commands/test_remote.py @@ -19,8 +19,8 @@ import fixtures from testtools.matchers import Contains, Equals -import snapcraft.internal.remote_build.errors as errors -import snapcraft.project +import snapcraft_legacy.internal.remote_build.errors as errors +import snapcraft_legacy.project from tests import fixture_setup from . import CommandBaseTestCase @@ -35,7 +35,9 @@ def setUp(self): self.useFixture(self.snapcraft_yaml) self.mock_lc_init = self.useFixture( - fixtures.MockPatch("snapcraft.cli.remote.LaunchpadClient", autospec=True) + fixtures.MockPatch( + "snapcraft_legacy.cli.remote.LaunchpadClient", autospec=True + ) ).mock self.mock_lc = self.mock_lc_init.return_value self.mock_lc_architectures = mock.PropertyMock(return_value=["i386"]) @@ -44,13 +46,13 @@ def setUp(self): self.mock_project = self.useFixture( fixtures.MockPatchObject( - snapcraft.project.Project, + snapcraft_legacy.project.Project, "_get_project_directory_hash", return_value="fakehash123", ) ) - @mock.patch("snapcraft.cli.remote.echo.confirm") + @mock.patch("snapcraft_legacy.cli.remote.echo.confirm") def test_remote_build_prompts(self, mock_confirm): result = self.run_command(["remote-build"]) @@ -69,7 +71,7 @@ def test_remote_build_prompts(self, mock_confirm): default=True, ) - @mock.patch("snapcraft.cli.remote.echo.confirm") + @mock.patch("snapcraft_legacy.cli.remote.echo.confirm") def test_remote_build_with_accept_option_doesnt_prompt(self, mock_confirm): result = self.run_command(["remote-build", "--launchpad-accept-public-upload"]) @@ -79,7 +81,7 @@ def test_remote_build_with_accept_option_doesnt_prompt(self, mock_confirm): self.assertThat(result.exit_code, Equals(0)) mock_confirm.assert_not_called() - @mock.patch("snapcraft.cli.remote.echo.confirm") + @mock.patch("snapcraft_legacy.cli.remote.echo.confirm") def test_remote_build_without_acceptance_raises(self, mock_confirm): mock_confirm.return_value = False self.assertRaises( @@ -136,7 +138,7 @@ def test_remote_build_invalid_user_arch(self): self.mock_lc.start_build.assert_not_called() self.mock_lc.cleanup.assert_not_called() - @mock.patch("snapcraft.cli.remote.echo") + @mock.patch("snapcraft_legacy.cli.remote.echo") def test_remote_build_sudo_errors(self, mock_echo): self.useFixture(fixtures.EnvironmentVariable("SUDO_USER", "testuser")) self.useFixture(fixtures.MockPatch("os.geteuid", return_value=0)) @@ -150,7 +152,7 @@ def test_remote_build_sudo_errors(self, mock_echo): ] ) - @mock.patch("snapcraft.cli.remote.echo") + @mock.patch("snapcraft_legacy.cli.remote.echo") def test_remote_build_recover_doesnt_prompt(self, mock_echo): result = self.run_command(["remote-build", "--recover"]) @@ -159,7 +161,7 @@ def test_remote_build_recover_doesnt_prompt(self, mock_echo): mock_echo.info.assert_called_with("No build found.") mock_echo.confirm.assert_not_called() - @mock.patch("snapcraft.cli.remote.echo") + @mock.patch("snapcraft_legacy.cli.remote.echo") def test_remote_build_status_doesnt_prompt(self, mock_echo): result = self.run_command(["remote-build", "--status"]) @@ -168,7 +170,7 @@ def test_remote_build_status_doesnt_prompt(self, mock_echo): mock_echo.info.assert_called_with("No build found.") mock_echo.confirm.assert_not_called() - @mock.patch("snapcraft.cli.remote.echo") + @mock.patch("snapcraft_legacy.cli.remote.echo") def test_remote_build_recover_uses_calculated_hash(self, mock_echo): result = self.run_command( ["remote-build", "--launchpad-accept-public-upload", "--recover"] @@ -181,7 +183,7 @@ def test_remote_build_recover_uses_calculated_hash(self, mock_echo): build_id="snapcraft-test-snap-fakehash123", ) - @mock.patch("snapcraft.cli.remote.echo") + @mock.patch("snapcraft_legacy.cli.remote.echo") def test_remote_build_recover_uses_build_id(self, mock_echo): result = self.run_command( [ @@ -200,7 +202,7 @@ def test_remote_build_recover_uses_build_id(self, mock_echo): build_id="snapcraft-test-snap-foo", ) - @mock.patch("snapcraft.cli.remote.echo") + @mock.patch("snapcraft_legacy.cli.remote.echo") def test_remote_build_status_uses_calculated_hash(self, mock_echo): result = self.run_command( ["remote-build", "--launchpad-accept-public-upload", "--status"] @@ -213,7 +215,7 @@ def test_remote_build_status_uses_calculated_hash(self, mock_echo): build_id="snapcraft-test-snap-fakehash123", ) - @mock.patch("snapcraft.cli.remote.echo") + @mock.patch("snapcraft_legacy.cli.remote.echo") def test_remote_build_status_uses_build_id(self, mock_echo): result = self.run_command( [ diff --git a/tests/unit/commands/test_set_default_track.py b/tests/unit/commands/test_set_default_track.py index 37f22173c6..2424121b62 100644 --- a/tests/unit/commands/test_set_default_track.py +++ b/tests/unit/commands/test_set_default_track.py @@ -17,8 +17,8 @@ import fixtures from testtools.matchers import Contains, Equals -import snapcraft -from snapcraft import storeapi +import snapcraft_legacy +from snapcraft_legacy import storeapi from . import FakeStoreCommandsBaseTestCase @@ -63,7 +63,8 @@ def test_set_default_track(self): def test_invalid_track_fails(self): mock_wrap = self.useFixture( fixtures.MockPatch( - "snapcraft.cli.echo.exit_error", wraps=snapcraft.cli.echo.exit_error + "snapcraft_legacy.cli.echo.exit_error", + wraps=snapcraft_legacy.cli.echo.exit_error, ) ).mock diff --git a/tests/unit/commands/test_sign_build.py b/tests/unit/commands/test_sign_build.py index 391325a9ee..cda53dd7ff 100644 --- a/tests/unit/commands/test_sign_build.py +++ b/tests/unit/commands/test_sign_build.py @@ -22,7 +22,7 @@ from testtools.matchers import Contains, Equals, FileExists, Not import tests -from snapcraft import internal, storeapi +from snapcraft_legacy import internal, storeapi from . import CommandBaseTestCase @@ -78,7 +78,7 @@ def test_sign_build_invalid_snap(self): self.assertThat(str(raised), Contains("Cannot read data from snap")) @mock.patch.object(storeapi._dashboard_api.DashboardAPI, "get_account_information") - @mock.patch("snapcraft._store._get_data_from_snap_file") + @mock.patch("snapcraft_legacy._store._get_data_from_snap_file") def test_sign_build_missing_account_info( self, mock_get_snap_data, mock_get_account_info, ): @@ -101,7 +101,7 @@ def test_sign_build_missing_account_info( ) @mock.patch.object(storeapi._dashboard_api.DashboardAPI, "get_account_information") - @mock.patch("snapcraft._store._get_data_from_snap_file") + @mock.patch("snapcraft_legacy._store._get_data_from_snap_file") def test_sign_build_no_usable_keys( self, mock_get_snap_data, mock_get_account_info, ): @@ -133,7 +133,7 @@ def test_sign_build_no_usable_keys( self.assertThat(snap_build_path, Not(FileExists())) @mock.patch.object(storeapi._dashboard_api.DashboardAPI, "get_account_information") - @mock.patch("snapcraft._store._get_data_from_snap_file") + @mock.patch("snapcraft_legacy._store._get_data_from_snap_file") def test_sign_build_no_usable_named_key( self, mock_get_snap_data, mock_get_account_info, ): @@ -164,7 +164,7 @@ def test_sign_build_no_usable_named_key( self.assertThat(snap_build_path, Not(FileExists())) @mock.patch.object(storeapi._dashboard_api.DashboardAPI, "get_account_information") - @mock.patch("snapcraft._store._get_data_from_snap_file") + @mock.patch("snapcraft_legacy._store._get_data_from_snap_file") def test_sign_build_unregistered_key( self, mock_get_snap_data, mock_get_account_info, ): @@ -201,7 +201,7 @@ def test_sign_build_unregistered_key( self.assertThat(snap_build_path, Not(FileExists())) @mock.patch.object(storeapi._dashboard_api.DashboardAPI, "get_account_information") - @mock.patch("snapcraft._store._get_data_from_snap_file") + @mock.patch("snapcraft_legacy._store._get_data_from_snap_file") def test_sign_build_snapd_failure( self, mock_get_snap_data, mock_get_account_info, ): @@ -239,7 +239,7 @@ def test_sign_build_snapd_failure( self.assertThat(snap_build_path, Not(FileExists())) @mock.patch.object(storeapi._dashboard_api.DashboardAPI, "get_account_information") - @mock.patch("snapcraft._store._get_data_from_snap_file") + @mock.patch("snapcraft_legacy._store._get_data_from_snap_file") def test_sign_build_locally_successfully( self, mock_get_snap_data, mock_get_account_info, ): @@ -277,7 +277,7 @@ def test_sign_build_locally_successfully( ) @mock.patch.object(storeapi._dashboard_api.DashboardAPI, "get_account_information") - @mock.patch("snapcraft._store._get_data_from_snap_file") + @mock.patch("snapcraft_legacy._store._get_data_from_snap_file") def test_sign_build_missing_grade( self, mock_get_snap_data, mock_get_account_info, ): @@ -317,7 +317,7 @@ def test_sign_build_missing_grade( @mock.patch.object(storeapi._dashboard_api.DashboardAPI, "push_snap_build") @mock.patch.object(storeapi._dashboard_api.DashboardAPI, "get_account_information") - @mock.patch("snapcraft._store._get_data_from_snap_file") + @mock.patch("snapcraft_legacy._store._get_data_from_snap_file") def test_sign_build_upload_successfully( self, mock_get_snap_data, mock_get_account_info, mock_push_snap_build, ): @@ -367,7 +367,7 @@ def test_sign_build_upload_successfully( @mock.patch.object(storeapi._dashboard_api.DashboardAPI, "push_snap_build") @mock.patch.object(storeapi._dashboard_api.DashboardAPI, "get_account_information") - @mock.patch("snapcraft._store._get_data_from_snap_file") + @mock.patch("snapcraft_legacy._store._get_data_from_snap_file") def test_sign_build_upload_existing( self, mock_get_snap_data, mock_get_account_info, mock_push_snap_build, ): diff --git a/tests/unit/commands/test_snap.py b/tests/unit/commands/test_snap.py index 71a93a2435..365033c29f 100644 --- a/tests/unit/commands/test_snap.py +++ b/tests/unit/commands/test_snap.py @@ -20,7 +20,7 @@ from testtools.matchers import Contains, Equals -from snapcraft.internal import steps +from snapcraft_legacy.internal import steps from . import LifecycleCommandsBaseTestCase diff --git a/tests/unit/commands/test_status.py b/tests/unit/commands/test_status.py index 2f45e2e48f..49267d9584 100644 --- a/tests/unit/commands/test_status.py +++ b/tests/unit/commands/test_status.py @@ -18,8 +18,8 @@ from testtools.matchers import Contains, Equals -from snapcraft import storeapi -from snapcraft.storeapi.v2.channel_map import ( +from snapcraft_legacy import storeapi +from snapcraft_legacy.storeapi.v2.channel_map import ( MappedChannel, Progressive, Revision, diff --git a/tests/unit/commands/test_upload.py b/tests/unit/commands/test_upload.py index a7757c8ac5..2a9582cb69 100644 --- a/tests/unit/commands/test_upload.py +++ b/tests/unit/commands/test_upload.py @@ -23,9 +23,9 @@ from xdg import BaseDirectory import tests -from snapcraft import file_utils, internal, storeapi -from snapcraft.internal import review_tools -from snapcraft.storeapi.errors import ( +from snapcraft_legacy import file_utils, internal, storeapi +from snapcraft_legacy.internal import review_tools +from snapcraft_legacy.storeapi.errors import ( StoreDeltaApplicationError, StoreUpDownError, StoreUploadError, @@ -43,12 +43,12 @@ def setUp(self): ) self.fake_review_tools_run = fixtures.MockPatch( - "snapcraft.internal.review_tools.run" + "snapcraft_legacy.internal.review_tools.run" ) self.useFixture(self.fake_review_tools_run) self.fake_review_tools_is_available = fixtures.MockPatch( - "snapcraft.internal.review_tools.is_available", return_value=False + "snapcraft_legacy.internal.review_tools.is_available", return_value=False ) self.useFixture(self.fake_review_tools_is_available) @@ -390,7 +390,7 @@ def test_upload_revision_uses_available_delta(self): def test_upload_with_delta_generation_failure_falls_back(self): # Upload and ensure fallback is called with mock.patch( - "snapcraft._store._upload_delta", + "snapcraft_legacy._store._upload_delta", side_effect=StoreDeltaApplicationError("error"), ): result = self.run_command(["upload", self.snap_file]) @@ -477,7 +477,7 @@ def json(self): ] # Upload and ensure fallback is called - with mock.patch("snapcraft.storeapi._status_tracker.StatusTracker"): + with mock.patch("snapcraft_legacy.storeapi._status_tracker.StatusTracker"): result = self.run_command(["upload", self.snap_file]) self.assertThat(result.exit_code, Equals(0)) self.fake_store_upload.mock.assert_has_calls( diff --git a/tests/unit/commands/test_upload_metadata.py b/tests/unit/commands/test_upload_metadata.py index 62e909b432..71e81afdb1 100644 --- a/tests/unit/commands/test_upload_metadata.py +++ b/tests/unit/commands/test_upload_metadata.py @@ -22,8 +22,8 @@ from testtools.matchers import Contains, Equals, Not import tests -from snapcraft import storeapi -from snapcraft.storeapi.errors import StoreUploadError +from snapcraft_legacy import storeapi +from snapcraft_legacy.storeapi.errors import StoreUploadError from . import CommandBaseTestCase @@ -33,7 +33,7 @@ def setUp(self): super().setUp() self.fake_precheck = fixtures.MockPatch( - "snapcraft.storeapi.StoreClient.upload_precheck" + "snapcraft_legacy.storeapi.StoreClient.upload_precheck" ) self.useFixture(self.fake_precheck) @@ -89,7 +89,7 @@ def test_without_snap_must_raise_exception(self): def test_simple(self): # upload metadata - with mock.patch("snapcraft.storeapi._status_tracker.StatusTracker"): + with mock.patch("snapcraft_legacy.storeapi._status_tracker.StatusTracker"): result = self.run_command(["upload-metadata", self.snap_file]) self.assertThat(result.exit_code, Equals(0)) @@ -108,7 +108,7 @@ def test_with_license_and_title(self): ) # upload metadata - with mock.patch("snapcraft.storeapi._status_tracker.StatusTracker"): + with mock.patch("snapcraft_legacy.storeapi._status_tracker.StatusTracker"): result = self.run_command(["upload-metadata", self.snap_file]) self.assertThat(result.exit_code, Equals(0)) @@ -126,7 +126,7 @@ def test_simple_debug(self): fixtures.EnvironmentVariable("SNAPCRAFT_ENABLE_DEVELOPER_DEBUG", "yes") ) # upload metadata - with mock.patch("snapcraft.storeapi._status_tracker.StatusTracker"): + with mock.patch("snapcraft_legacy.storeapi._status_tracker.StatusTracker"): result = self.run_command(["upload-metadata", self.snap_file]) self.assertThat(result.exit_code, Equals(0)) @@ -223,7 +223,7 @@ def test_snap_without_icon(self): ) # upload metadata - with mock.patch("snapcraft.storeapi._status_tracker.StatusTracker"): + with mock.patch("snapcraft_legacy.storeapi._status_tracker.StatusTracker"): result = self.run_command(["upload-metadata", snap_file]) self.assertThat(result.exit_code, Equals(0)) @@ -236,7 +236,7 @@ def test_push_raises_deprecation_warning(self): self.useFixture(fake_logger) # upload metadata - with mock.patch("snapcraft.storeapi._status_tracker.StatusTracker"): + with mock.patch("snapcraft_legacy.storeapi._status_tracker.StatusTracker"): result = self.run_command(["push-metadata", self.snap_file]) self.assertThat(result.exit_code, Equals(0)) self.assertThat( diff --git a/tests/unit/commands/test_validate.py b/tests/unit/commands/test_validate.py index 417a047d9a..a9e04cc736 100644 --- a/tests/unit/commands/test_validate.py +++ b/tests/unit/commands/test_validate.py @@ -18,7 +18,7 @@ import fixtures from testtools.matchers import Contains, Equals, FileExists -import snapcraft.storeapi.errors +import snapcraft_legacy.storeapi.errors from . import StoreCommandsBaseTestCase @@ -27,7 +27,7 @@ class ValidateCommandTestCase(StoreCommandsBaseTestCase): def setUp(self): super().setUp() - patcher = mock.patch("snapcraft._store.Popen") + patcher = mock.patch("snapcraft_legacy._store.Popen") self.popen_mock = patcher.start() rv_mock = mock.Mock() rv_mock.returncode = 0 @@ -81,7 +81,7 @@ def test_validate_from_branded_store(self): def test_validate_unknown_snap(self): raised = self.assertRaises( - snapcraft.storeapi.errors.SnapNotFoundError, + snapcraft_legacy.storeapi.errors.SnapNotFoundError, self.run_command, ["validate", "notfound", "core=3", "test-snap=4"], ) @@ -90,7 +90,7 @@ def test_validate_unknown_snap(self): def test_validate_bad_argument(self): raised = self.assertRaises( - snapcraft.storeapi.errors.InvalidValidationRequestsError, + snapcraft_legacy.storeapi.errors.InvalidValidationRequestsError, self.run_command, ["validate", "core", "core=foo"], ) @@ -99,7 +99,7 @@ def test_validate_bad_argument(self): def test_validate_with_snap_name(self): self.fake_sign = fixtures.MockPatch( - "snapcraft._store._sign_assertion", return_value=b"" + "snapcraft_legacy._store._sign_assertion", return_value=b"" ) self.useFixture(self.fake_sign) @@ -124,7 +124,7 @@ def test_validate_with_snap_name(self): def test_revoke(self): self.fake_sign = fixtures.MockPatch( - "snapcraft._store._sign_assertion", return_value=b"" + "snapcraft_legacy._store._sign_assertion", return_value=b"" ) self.useFixture(self.fake_sign) @@ -149,7 +149,7 @@ def test_revoke(self): def test_no_revoke(self): self.fake_sign = fixtures.MockPatch( - "snapcraft._store._sign_assertion", return_value=b"" + "snapcraft_legacy._store._sign_assertion", return_value=b"" ) self.useFixture(self.fake_sign) @@ -174,7 +174,7 @@ def test_no_revoke(self): def test_validate_fallback_to_snap_id(self): self.fake_sign = fixtures.MockPatch( - "snapcraft._store._sign_assertion", return_value=b"" + "snapcraft_legacy._store._sign_assertion", return_value=b"" ) self.useFixture(self.fake_sign) @@ -200,7 +200,7 @@ def test_validate_fallback_to_snap_id(self): def test_validate_with_revoke(self): self.fake_sign = fixtures.MockPatch( - "snapcraft._store._sign_assertion", return_value=b"" + "snapcraft_legacy._store._sign_assertion", return_value=b"" ) self.useFixture(self.fake_sign) @@ -225,7 +225,7 @@ def test_validate_with_revoke(self): def test_validate_with_no_revoke(self): self.fake_sign = fixtures.MockPatch( - "snapcraft._store._sign_assertion", return_value=b"" + "snapcraft_legacy._store._sign_assertion", return_value=b"" ) self.useFixture(self.fake_sign) diff --git a/tests/unit/commands/test_whoami.py b/tests/unit/commands/test_whoami.py index 285d276b97..3b5feeda9a 100644 --- a/tests/unit/commands/test_whoami.py +++ b/tests/unit/commands/test_whoami.py @@ -18,8 +18,8 @@ import pytest -from snapcraft.storeapi.v2 import whoami -from snapcraft.storeapi import StoreClient +from snapcraft_legacy.storeapi.v2 import whoami +from snapcraft_legacy.storeapi import StoreClient @pytest.fixture diff --git a/tests/unit/db/test_datastore.py b/tests/unit/db/test_datastore.py index ad66ecfaae..8fb258d79a 100644 --- a/tests/unit/db/test_datastore.py +++ b/tests/unit/db/test_datastore.py @@ -21,7 +21,7 @@ import pytest import tinydb -from snapcraft.internal.db import datastore, errors, migration +from snapcraft_legacy.internal.db import datastore, errors, migration @pytest.fixture(autouse=True) diff --git a/tests/unit/db/test_errors.py b/tests/unit/db/test_errors.py index 7f3752669f..cd4b439b9f 100644 --- a/tests/unit/db/test_errors.py +++ b/tests/unit/db/test_errors.py @@ -16,7 +16,7 @@ import pathlib -from snapcraft.internal.db import errors +from snapcraft_legacy.internal.db import errors def test_SnapcraftDatastoreVersionUnsupported(): diff --git a/tests/unit/db/test_migration.py b/tests/unit/db/test_migration.py index 5005cd0473..3dbc7d1d5d 100644 --- a/tests/unit/db/test_migration.py +++ b/tests/unit/db/test_migration.py @@ -20,7 +20,7 @@ import pytest import tinydb -from snapcraft.internal.db import migration +from snapcraft_legacy.internal.db import migration @pytest.fixture diff --git a/tests/unit/deltas/test_deltas.py b/tests/unit/deltas/test_deltas.py index 29dc77afc5..3ebd1b9e96 100644 --- a/tests/unit/deltas/test_deltas.py +++ b/tests/unit/deltas/test_deltas.py @@ -21,7 +21,7 @@ from testtools import TestCase from testtools import matchers as m -from snapcraft.internal import deltas +from snapcraft_legacy.internal import deltas from tests import fixture_setup @@ -44,7 +44,7 @@ def setUp(self): self.useFixture( fixtures.MockPatch( - "snapcraft.file_utils.get_snap_tool_path", + "snapcraft_legacy.file_utils.get_snap_tool_path", side_effect=lambda x: os.path.join("/usr", "bin", x), ) ) diff --git a/tests/unit/deltas/test_deltas_xdelta3.py b/tests/unit/deltas/test_deltas_xdelta3.py index e8d1e5b91e..89ad01d559 100644 --- a/tests/unit/deltas/test_deltas_xdelta3.py +++ b/tests/unit/deltas/test_deltas_xdelta3.py @@ -23,7 +23,7 @@ from progressbar import AnimatedMarker, ProgressBar from testtools import matchers as m -from snapcraft.internal import deltas +from snapcraft_legacy.internal import deltas from tests import fixture_setup, unit diff --git a/tests/unit/extractors/test_appstream.py b/tests/unit/extractors/test_appstream.py index 5254b1ac6c..89c832ad2a 100644 --- a/tests/unit/extractors/test_appstream.py +++ b/tests/unit/extractors/test_appstream.py @@ -20,7 +20,7 @@ import testscenarios from testtools.matchers import Equals -from snapcraft.extractors import ExtractedMetadata, _errors, appstream +from snapcraft_legacy.extractors import ExtractedMetadata, _errors, appstream from tests import unit @@ -269,7 +269,7 @@ def test_appstream_no_icon_theme_fallback_svgz(self): class AppstreamTest(unit.TestCase): def test_appstream_with_ul(self): - file_name = "snapcraft.appdata.xml" + file_name = "snapcraft_legacy.appdata.xml" content = textwrap.dedent( """\ @@ -322,7 +322,7 @@ def test_appstream_with_ul(self): ) def test_appstream_with_ol(self): - file_name = "snapcraft.appdata.xml" + file_name = "snapcraft_legacy.appdata.xml" content = textwrap.dedent( """\ @@ -375,7 +375,7 @@ def test_appstream_with_ol(self): ) def test_appstream_with_ul_in_p(self): - file_name = "snapcraft.appdata.xml" + file_name = "snapcraft_legacy.appdata.xml" content = textwrap.dedent( """\ diff --git a/tests/unit/extractors/test_metadata.py b/tests/unit/extractors/test_metadata.py index ee77160c9c..f6e4a0f82a 100644 --- a/tests/unit/extractors/test_metadata.py +++ b/tests/unit/extractors/test_metadata.py @@ -16,7 +16,7 @@ from testtools.matchers import Equals, Not -from snapcraft.extractors._metadata import ExtractedMetadata +from snapcraft_legacy.extractors._metadata import ExtractedMetadata from tests import unit diff --git a/tests/unit/extractors/test_setuppy.py b/tests/unit/extractors/test_setuppy.py index e4c2c0e336..6ea6bdb5ad 100644 --- a/tests/unit/extractors/test_setuppy.py +++ b/tests/unit/extractors/test_setuppy.py @@ -19,7 +19,7 @@ from testscenarios import multiply_scenarios from testtools.matchers import Equals -from snapcraft.extractors import ExtractedMetadata, _errors, setuppy +from snapcraft_legacy.extractors import ExtractedMetadata, _errors, setuppy from tests import unit diff --git a/tests/unit/lifecycle/__init__.py b/tests/unit/lifecycle/__init__.py index 65b044de7e..0c3f9c09f4 100644 --- a/tests/unit/lifecycle/__init__.py +++ b/tests/unit/lifecycle/__init__.py @@ -19,8 +19,8 @@ import fixtures -import snapcraft -from snapcraft.internal import project_loader +import snapcraft_legacy +from snapcraft_legacy.internal import project_loader from tests import unit @@ -30,30 +30,30 @@ def setUp(self): self.fake_logger = fixtures.FakeLogger(level=logging.INFO) self.useFixture(self.fake_logger) - self.project_options = snapcraft.ProjectOptions() + self.project_options = snapcraft_legacy.ProjectOptions() self.fake_install_build_packages = fixtures.MockPatch( - "snapcraft.internal.lifecycle._runner._install_build_packages", + "snapcraft_legacy.internal.lifecycle._runner._install_build_packages", return_value=list(), ) self.useFixture(self.fake_install_build_packages) self.useFixture( fixtures.MockPatch( - "snapcraft.internal.project_loader._config.Config.get_build_packages", + "snapcraft_legacy.internal.project_loader._config.Config.get_build_packages", return_value=set(), ) ) self.fake_install_build_snaps = fixtures.MockPatch( - "snapcraft.internal.lifecycle._runner._install_build_snaps", + "snapcraft_legacy.internal.lifecycle._runner._install_build_snaps", return_value=list(), ) self.useFixture(self.fake_install_build_snaps) self.useFixture( fixtures.MockPatch( - "snapcraft.internal.project_loader._config.Config.get_build_snaps", + "snapcraft_legacy.internal.project_loader._config.Config.get_build_snaps", return_value=set(), ) ) @@ -77,7 +77,7 @@ def make_snapcraft_project(self, parts, snap_type=""): self.snapcraft_yaml_file_path = self.make_snapcraft_yaml( yaml.format(parts=parts, type=snap_type) ) - project = snapcraft.project.Project( + project = snapcraft_legacy.project.Project( snapcraft_yaml_file_path=self.snapcraft_yaml_file_path ) return project_loader.load_config(project) diff --git a/tests/unit/lifecycle/test_errors.py b/tests/unit/lifecycle/test_errors.py index d2c8c47aec..318ffa47d4 100644 --- a/tests/unit/lifecycle/test_errors.py +++ b/tests/unit/lifecycle/test_errors.py @@ -14,7 +14,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -from snapcraft.internal.lifecycle import errors +from snapcraft_legacy.internal.lifecycle import errors class TestErrorFormatting: diff --git a/tests/unit/lifecycle/test_global_state.py b/tests/unit/lifecycle/test_global_state.py index 60165bb05d..ad071103e8 100644 --- a/tests/unit/lifecycle/test_global_state.py +++ b/tests/unit/lifecycle/test_global_state.py @@ -18,10 +18,10 @@ from testtools import TestCase from testtools.matchers import Equals -from snapcraft.internal import lifecycle, project_loader, states, steps -from snapcraft.project import Project -from snapcraft.storeapi.errors import SnapNotFoundError -from snapcraft.storeapi.info import SnapInfo +from snapcraft_legacy.internal import lifecycle, project_loader, states, steps +from snapcraft_legacy.project import Project +from snapcraft_legacy.storeapi.errors import SnapNotFoundError +from snapcraft_legacy.storeapi.info import SnapInfo from tests import fixture_setup @@ -54,11 +54,13 @@ def setUp(self): ) self.useFixture( - fixtures.MockPatch("snapcraft.internal.lifecycle._runner._Executor.run") + fixtures.MockPatch( + "snapcraft_legacy.internal.lifecycle._runner._Executor.run" + ) ) self.useFixture( - fixtures.MockPatch("snapcraft.internal.repo.snaps.install_snaps") + fixtures.MockPatch("snapcraft_legacy.internal.repo.snaps.install_snaps") ) # Avoid unnecessary calls to info. @@ -100,7 +102,7 @@ def setUp(self): "snap-id": "CSO04Jhav2yK0uz97cr0ipQRyqg0qQL6", } self.fake_storeapi_get_info = fixtures.MockPatch( - "snapcraft.storeapi._snap_api.SnapAPI.get_info", + "snapcraft_legacy.storeapi._snap_api.SnapAPI.get_info", return_value=SnapInfo(info), ) self.useFixture(self.fake_storeapi_get_info) diff --git a/tests/unit/lifecycle/test_lifecycle.py b/tests/unit/lifecycle/test_lifecycle.py index 82a5f641e3..d09811c9d6 100644 --- a/tests/unit/lifecycle/test_lifecycle.py +++ b/tests/unit/lifecycle/test_lifecycle.py @@ -29,10 +29,16 @@ Not, ) -import snapcraft -from snapcraft.internal import errors, lifecycle, pluginhandler, project_loader, steps -from snapcraft.internal.lifecycle._runner import _replace_in_part -from snapcraft.project import Project +import snapcraft_legacy +from snapcraft_legacy.internal import ( + errors, + lifecycle, + pluginhandler, + project_loader, + steps, +) +from snapcraft_legacy.internal.lifecycle._runner import _replace_in_part +from snapcraft_legacy.project import Project from tests import fixture_setup, unit from . import LifecycleTestBase @@ -62,7 +68,7 @@ def __init__(self): self.assertThat(new_part.plugin.options.source, Equals(part.part_install_dir)) - @mock.patch("snapcraft.repo.snaps.install_snaps") + @mock.patch("snapcraft_legacy.repo.snaps.install_snaps") def test_dependency_is_staged_when_required(self, mock_install_build_snaps): project_config = self.make_snapcraft_project( textwrap.dedent( @@ -85,7 +91,7 @@ def test_dependency_is_staged_when_required(self, mock_install_build_snaps): Contains("'part2' has dependencies that need to be staged: part1"), ) - @mock.patch("snapcraft.repo.snaps.install_snaps") + @mock.patch("snapcraft_legacy.repo.snaps.install_snaps") def test_no_exception_when_dependency_is_required_but_already_staged( self, mock_install_build_snaps ): @@ -116,9 +122,9 @@ def _fake_should_step_run(self, step, force=False): def test_dirty_stage_part_with_built_dependent_raises(self): # Set the option to error on dirty/outdated steps - with snapcraft.config.CLIConfig() as cli_config: + with snapcraft_legacy.config.CLIConfig() as cli_config: cli_config.set_outdated_step_action( - snapcraft.config.OutdatedStepAction.ERROR + snapcraft_legacy.config.OutdatedStepAction.ERROR ) project_config = self.make_snapcraft_project( @@ -169,12 +175,12 @@ def _fake_dirty_report(self, step): self.assertThat(raised.part, Equals("part2")) self.assertThat(raised.report, Equals("A dependency has changed: 'part1'\n")) - @mock.patch("snapcraft.repo.snaps.install_snaps") + @mock.patch("snapcraft_legacy.repo.snaps.install_snaps") def test_dirty_build_raises(self, mock_install_build_snaps): # Set the option to error on dirty/outdated steps - with snapcraft.config.CLIConfig() as cli_config: + with snapcraft_legacy.config.CLIConfig() as cli_config: cli_config.set_outdated_step_action( - snapcraft.config.OutdatedStepAction.ERROR + snapcraft_legacy.config.OutdatedStepAction.ERROR ) project_config = self.make_snapcraft_project( @@ -219,12 +225,12 @@ def _fake_dirty_report(self, step): ) self.assertThat(raised.parts_names, Equals("part1")) - @mock.patch("snapcraft.repo.snaps.install_snaps") + @mock.patch("snapcraft_legacy.repo.snaps.install_snaps") def test_dirty_pull_raises(self, mock_install_build_snaps): # Set the option to error on dirty/outdated steps - with snapcraft.config.CLIConfig() as cli_config: + with snapcraft_legacy.config.CLIConfig() as cli_config: cli_config.set_outdated_step_action( - snapcraft.config.OutdatedStepAction.ERROR + snapcraft_legacy.config.OutdatedStepAction.ERROR ) project_config = self.make_snapcraft_project( @@ -266,9 +272,9 @@ def _fake_dirty_report(self, step): Equals("The 'bar' and 'foo' project options appear to have changed.\n"), ) - @mock.patch.object(snapcraft.BasePlugin, "enable_cross_compilation") - @mock.patch("snapcraft.repo.Repo.install_build_packages") - @mock.patch("snapcraft.repo.snaps.install_snaps") + @mock.patch.object(snapcraft_legacy.BasePlugin, "enable_cross_compilation") + @mock.patch("snapcraft_legacy.repo.Repo.install_build_packages") + @mock.patch("snapcraft_legacy.repo.snaps.install_snaps") def test_pull_is_dirty_if_target_arch_changes( self, mock_install_build_snaps, @@ -276,9 +282,9 @@ def test_pull_is_dirty_if_target_arch_changes( mock_enable_cross_compilation, ): # Set the option to error on dirty/outdated steps - with snapcraft.config.CLIConfig() as cli_config: + with snapcraft_legacy.config.CLIConfig() as cli_config: cli_config.set_outdated_step_action( - snapcraft.config.OutdatedStepAction.ERROR + snapcraft_legacy.config.OutdatedStepAction.ERROR ) mock_install_build_packages.return_value = [] @@ -375,7 +381,7 @@ def test_clean_removes_global_state(self): lifecycle.clean(project_config.project, parts=None) self.assertThat(os.path.join("snap", ".snapcraft"), Not(DirExists())) - @mock.patch("snapcraft.internal.mountinfo.MountInfo.for_root") + @mock.patch("snapcraft_legacy.internal.mountinfo.MountInfo.for_root") def test_clean_leaves_prime_alone_for_tried(self, mock_for_root): project_config = self.make_snapcraft_project( textwrap.dedent( @@ -453,7 +459,9 @@ def test_prime_with_build_info_records_snapcraft_yaml(self): class OfflineTestCase(unit.TestCase): def test_install_build_packages(self): - with mock.patch("snapcraft.repo.Repo.install_build_packages") as mock_install: + with mock.patch( + "snapcraft_legacy.repo.Repo.install_build_packages" + ) as mock_install: lifecycle._runner._install_build_packages({"pkg1", "pkg2"}) assert mock_install.mock_calls == [mock.call({"pkg1", "pkg2"})] @@ -461,14 +469,16 @@ def test_install_build_packages(self): def test_install_build_packages_offline(self): self.useFixture(fixtures.EnvironmentVariable("SNAPCRAFT_OFFLINE", "True")) - with mock.patch("snapcraft.repo.Repo.install_build_packages") as mock_install: + with mock.patch( + "snapcraft_legacy.repo.Repo.install_build_packages" + ) as mock_install: pkgs = lifecycle._runner._install_build_packages({"pkg1", "pkg2"}) assert mock_install.mock_calls == [] assert pkgs == [] def test_install_build_snaps(self): - with mock.patch("snapcraft.repo.snaps.install_snaps") as mock_install: + with mock.patch("snapcraft_legacy.repo.snaps.install_snaps") as mock_install: lifecycle._runner._install_build_snaps( {"build_snap1", "build_snap2"}, {"content_snap"} ) @@ -482,7 +492,7 @@ def test_install_build_snaps(self): def test_install_build_snaps_offline(self): self.useFixture(fixtures.EnvironmentVariable("SNAPCRAFT_OFFLINE", "True")) - with mock.patch("snapcraft.repo.snaps.install_snaps") as mock_install: + with mock.patch("snapcraft_legacy.repo.snaps.install_snaps") as mock_install: snaps = lifecycle._runner._install_build_snaps( {"build_snap1", "build_snap2"}, {"content_snap"} ) diff --git a/tests/unit/lifecycle/test_order.py b/tests/unit/lifecycle/test_order.py index c54342d42e..71e720d0be 100644 --- a/tests/unit/lifecycle/test_order.py +++ b/tests/unit/lifecycle/test_order.py @@ -21,9 +21,9 @@ from testtools.matchers import Contains, Equals, HasLength -import snapcraft -from snapcraft.internal import lifecycle, pluginhandler, states, steps -from snapcraft.internal.lifecycle._status_cache import StatusCache +import snapcraft_legacy +from snapcraft_legacy.internal import lifecycle, pluginhandler, states, steps +from snapcraft_legacy.internal.lifecycle._status_cache import StatusCache from . import LifecycleTestBase @@ -155,9 +155,9 @@ def setUp(self): ) # Set the option to automatically clean dirty/outdated steps - with snapcraft.config.CLIConfig() as cli_config: + with snapcraft_legacy.config.CLIConfig() as cli_config: cli_config.set_outdated_step_action( - snapcraft.config.OutdatedStepAction.CLEAN + snapcraft_legacy.config.OutdatedStepAction.CLEAN ) def set_attributes(self, kwargs): diff --git a/tests/unit/lifecycle/test_snap_installation.py b/tests/unit/lifecycle/test_snap_installation.py index 570accfc21..ead9431725 100644 --- a/tests/unit/lifecycle/test_snap_installation.py +++ b/tests/unit/lifecycle/test_snap_installation.py @@ -19,7 +19,7 @@ from testtools import TestCase from testtools.matchers import Contains -from snapcraft.internal.lifecycle._runner import _install_build_snaps +from snapcraft_legacy.internal.lifecycle._runner import _install_build_snaps class TestSnapInstall(TestCase): @@ -29,7 +29,7 @@ def setUp(self): self.fake_logger = fixtures.FakeLogger(level=logging.WARNING) self.useFixture(self.fake_logger) - @mock.patch("snapcraft.repo.snaps.install_snaps") + @mock.patch("snapcraft_legacy.repo.snaps.install_snaps") def test_install(self, mock_install_build_snaps): _install_build_snaps({"foo/latest/stable", "bar/default/edge"}, set()) @@ -37,7 +37,7 @@ def test_install(self, mock_install_build_snaps): {"foo/latest/stable", "bar/default/edge"} ) - @mock.patch("snapcraft.repo.snaps.install_snaps") + @mock.patch("snapcraft_legacy.repo.snaps.install_snaps") def test_install_with_content_snap(self, mock_install_build_snaps): _install_build_snaps({"foo/latest/stable"}, {"content1/latest/stable"}) @@ -45,8 +45,10 @@ def test_install_with_content_snap(self, mock_install_build_snaps): [mock.call({"foo/latest/stable"}), mock.call(["content1/latest/stable"])] ) - @mock.patch("snapcraft.internal.common.is_process_container", return_value=True) - @mock.patch("snapcraft.repo.snaps.install_snaps") + @mock.patch( + "snapcraft_legacy.internal.common.is_process_container", return_value=True + ) + @mock.patch("snapcraft_legacy.repo.snaps.install_snaps") def test_install_on_docker(self, mock_install_build_snaps, mock_docker_instance): _install_build_snaps({"foo/latest/stable", "bar/default/edge"}, set()) @@ -59,8 +61,10 @@ def test_install_on_docker(self, mock_install_build_snaps, mock_docker_instance) ), ) - @mock.patch("snapcraft.internal.common.is_process_container", return_value=True) - @mock.patch("snapcraft.repo.snaps.install_snaps") + @mock.patch( + "snapcraft_legacy.internal.common.is_process_container", return_value=True + ) + @mock.patch("snapcraft_legacy.repo.snaps.install_snaps") def test_install_with_content_snap_on_docker( self, mock_install_build_snaps, mock_docker_instance ): diff --git a/tests/unit/lifecycle/test_status_cache.py b/tests/unit/lifecycle/test_status_cache.py index bc96ee3f2d..09d76b1028 100644 --- a/tests/unit/lifecycle/test_status_cache.py +++ b/tests/unit/lifecycle/test_status_cache.py @@ -17,8 +17,8 @@ import os import textwrap -from snapcraft.internal import lifecycle, states, steps -from snapcraft.internal.lifecycle._status_cache import StatusCache +from snapcraft_legacy.internal import lifecycle, states, steps +from snapcraft_legacy.internal.lifecycle._status_cache import StatusCache from . import LifecycleTestBase diff --git a/tests/unit/meta/test_application.py b/tests/unit/meta/test_application.py index 810a1c61a1..24220aa531 100644 --- a/tests/unit/meta/test_application.py +++ b/tests/unit/meta/test_application.py @@ -19,8 +19,8 @@ from testtools.matchers import Contains, Equals, FileExists, Not -from snapcraft import yaml_utils -from snapcraft.internal.meta import application, desktop, errors +from snapcraft_legacy import yaml_utils +from snapcraft_legacy.internal.meta import application, desktop, errors from tests import unit diff --git a/tests/unit/meta/test_command.py b/tests/unit/meta/test_command.py index 1d22f37649..296dcfa29e 100644 --- a/tests/unit/meta/test_command.py +++ b/tests/unit/meta/test_command.py @@ -21,7 +21,7 @@ import fixtures from testtools.matchers import Equals, FileContains, FileExists, Is -from snapcraft.internal.meta import command, errors +from snapcraft_legacy.internal.meta import command, errors from tests import unit diff --git a/tests/unit/meta/test_command_mangle.py b/tests/unit/meta/test_command_mangle.py index 6dda4c58c9..c9d124cc7c 100644 --- a/tests/unit/meta/test_command_mangle.py +++ b/tests/unit/meta/test_command_mangle.py @@ -16,7 +16,7 @@ import logging -from snapcraft.internal.meta import command +from snapcraft_legacy.internal.meta import command class TestCommandMangle: diff --git a/tests/unit/meta/test_desktop.py b/tests/unit/meta/test_desktop.py index 8cc90a1b94..95b70a50cb 100644 --- a/tests/unit/meta/test_desktop.py +++ b/tests/unit/meta/test_desktop.py @@ -18,8 +18,8 @@ import pytest -from snapcraft.internal.meta import errors -from snapcraft.internal.meta.desktop import DesktopFile +from snapcraft_legacy.internal.meta import errors +from snapcraft_legacy.internal.meta.desktop import DesktopFile class TestDesktopExec: diff --git a/tests/unit/meta/test_errors.py b/tests/unit/meta/test_errors.py index d7c20282cb..ecbcffa7ad 100644 --- a/tests/unit/meta/test_errors.py +++ b/tests/unit/meta/test_errors.py @@ -14,7 +14,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -from snapcraft.internal.meta import errors +from snapcraft_legacy.internal.meta import errors class TestErrorFormatting: diff --git a/tests/unit/meta/test_hook.py b/tests/unit/meta/test_hook.py index c836e94e13..183a3cdaca 100644 --- a/tests/unit/meta/test_hook.py +++ b/tests/unit/meta/test_hook.py @@ -18,8 +18,8 @@ from testtools.matchers import Equals -from snapcraft.internal.meta import errors -from snapcraft.internal.meta.hooks import Hook +from snapcraft_legacy.internal.meta import errors +from snapcraft_legacy.internal.meta.hooks import Hook from tests import unit diff --git a/tests/unit/meta/test_meta.py b/tests/unit/meta/test_meta.py index 2178aff75c..92ad5c187a 100644 --- a/tests/unit/meta/test_meta.py +++ b/tests/unit/meta/test_meta.py @@ -34,11 +34,11 @@ Not, ) -from snapcraft import extractors, yaml_utils -from snapcraft.internal import errors, project_loader, states -from snapcraft.internal.meta import _snap_packaging -from snapcraft.internal.meta import errors as meta_errors -from snapcraft.project import Project +from snapcraft_legacy import extractors, yaml_utils +from snapcraft_legacy.internal import errors, project_loader, states +from snapcraft_legacy.internal.meta import _snap_packaging +from snapcraft_legacy.internal.meta import errors as meta_errors +from snapcraft_legacy.project import Project from tests import fixture_setup, unit @@ -1231,7 +1231,7 @@ def test_generate_hook_wrappers(self): ), ) - @patch("snapcraft.internal.project_loader._config.Config.snap_env") + @patch("snapcraft_legacy.internal.project_loader._config.Config.snap_env") def test_generated_hook_wrappers_include_environment(self, mock_snap_env): mock_snap_env.return_value = ["PATH={}/foo".format(self.prime_dir)] @@ -1517,7 +1517,7 @@ def test_stable_required(self): global_state_path = "global_state" self.useFixture( fixtures.MockPatch( - "snapcraft.project.Project._get_global_state_file_path", + "snapcraft_legacy.project.Project._get_global_state_file_path", return_value=global_state_path, ) ) @@ -1534,7 +1534,7 @@ def test_stable_but_devel_required(self): global_state_path = "global_state" self.useFixture( fixtures.MockPatch( - "snapcraft.project.Project._get_global_state_file_path", + "snapcraft_legacy.project.Project._get_global_state_file_path", return_value=global_state_path, ) ) diff --git a/tests/unit/meta/test_package_repository.py b/tests/unit/meta/test_package_repository.py index bbcaad35d5..c81d1ac277 100644 --- a/tests/unit/meta/test_package_repository.py +++ b/tests/unit/meta/test_package_repository.py @@ -17,8 +17,8 @@ import pytest -from snapcraft.internal.meta import errors -from snapcraft.internal.meta.package_repository import ( +from snapcraft_legacy.internal.meta import errors +from snapcraft_legacy.internal.meta.package_repository import ( PackageRepository, PackageRepositoryApt, PackageRepositoryAptPpa, diff --git a/tests/unit/meta/test_plugs.py b/tests/unit/meta/test_plugs.py index bdb483cfc7..06243b800e 100644 --- a/tests/unit/meta/test_plugs.py +++ b/tests/unit/meta/test_plugs.py @@ -18,8 +18,8 @@ from testtools.matchers import Equals, Is -from snapcraft.internal.meta import errors -from snapcraft.internal.meta.plugs import ContentPlug, Plug +from snapcraft_legacy.internal.meta import errors +from snapcraft_legacy.internal.meta.plugs import ContentPlug, Plug from tests import unit diff --git a/tests/unit/meta/test_slots.py b/tests/unit/meta/test_slots.py index eea7145bea..618f42bed2 100644 --- a/tests/unit/meta/test_slots.py +++ b/tests/unit/meta/test_slots.py @@ -18,8 +18,8 @@ from testtools.matchers import Equals -from snapcraft.internal.meta import errors -from snapcraft.internal.meta.slots import ContentSlot, DbusSlot, Slot +from snapcraft_legacy.internal.meta import errors +from snapcraft_legacy.internal.meta.slots import ContentSlot, DbusSlot, Slot from tests import unit diff --git a/tests/unit/meta/test_snap.py b/tests/unit/meta/test_snap.py index 6af13dac94..5aa48eab69 100644 --- a/tests/unit/meta/test_snap.py +++ b/tests/unit/meta/test_snap.py @@ -21,9 +21,9 @@ from testtools.matchers import Equals -from snapcraft.internal.meta import errors -from snapcraft.internal.meta.snap import Snap -from snapcraft.internal.meta.system_user import SystemUserScope +from snapcraft_legacy.internal.meta import errors +from snapcraft_legacy.internal.meta.snap import Snap +from snapcraft_legacy.internal.meta.system_user import SystemUserScope from tests import unit @@ -481,7 +481,7 @@ def test_get_provider_content_directories_with_content_plugs(self): snap = Snap.from_dict(snap_dict=snap_dict) snap.validate() - patcher = mock.patch("snapcraft.internal.common.get_installed_snap_path") + patcher = mock.patch("snapcraft_legacy.internal.common.get_installed_snap_path") mock_core_path = patcher.start() mock_core_path.return_value = self.path self.addCleanup(patcher.stop) diff --git a/tests/unit/meta/test_snap_packaging.py b/tests/unit/meta/test_snap_packaging.py index 521842f456..c342150cfe 100644 --- a/tests/unit/meta/test_snap_packaging.py +++ b/tests/unit/meta/test_snap_packaging.py @@ -19,9 +19,9 @@ from testtools.matchers import Equals, FileContains, Is -from snapcraft.internal.meta._snap_packaging import _SnapPackaging -from snapcraft.internal.project_loader import load_config -from snapcraft.project import Project +from snapcraft_legacy.internal.meta._snap_packaging import _SnapPackaging +from snapcraft_legacy.internal.project_loader import load_config +from snapcraft_legacy.project import Project from tests import fixture_setup, unit diff --git a/tests/unit/meta/test_system_user.py b/tests/unit/meta/test_system_user.py index a19dd2cef4..943a0b7cd5 100644 --- a/tests/unit/meta/test_system_user.py +++ b/tests/unit/meta/test_system_user.py @@ -16,8 +16,8 @@ from testtools.matchers import Equals -from snapcraft.internal.meta import errors -from snapcraft.internal.meta.system_user import SystemUser, SystemUserScope +from snapcraft_legacy.internal.meta import errors +from snapcraft_legacy.internal.meta.system_user import SystemUser, SystemUserScope from tests import unit diff --git a/tests/unit/part_loader.py b/tests/unit/part_loader.py index 459c658cb9..bac11adc81 100644 --- a/tests/unit/part_loader.py +++ b/tests/unit/part_loader.py @@ -16,9 +16,9 @@ from unittest import mock -from snapcraft.internal import elf, pluginhandler -from snapcraft.internal.project_loader import grammar_processing -from snapcraft.project import Project, _schema +from snapcraft_legacy.internal import elf, pluginhandler +from snapcraft_legacy.internal.project_loader import grammar_processing +from snapcraft_legacy.project import Project, _schema def load_part( diff --git a/tests/unit/pluginhandler/mocks.py b/tests/unit/pluginhandler/mocks.py index 431bdbb9af..d025c4f7ba 100644 --- a/tests/unit/pluginhandler/mocks.py +++ b/tests/unit/pluginhandler/mocks.py @@ -14,10 +14,10 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -import snapcraft +import snapcraft_legacy -class TestPlugin(snapcraft.BasePlugin): +class TestPlugin(snapcraft_legacy.BasePlugin): @classmethod def schema(cls): return { diff --git a/tests/unit/pluginhandler/test_clean.py b/tests/unit/pluginhandler/test_clean.py index 1da3c0eb70..34130bb546 100644 --- a/tests/unit/pluginhandler/test_clean.py +++ b/tests/unit/pluginhandler/test_clean.py @@ -18,8 +18,8 @@ from testtools.matchers import Equals -from snapcraft import file_utils -from snapcraft.internal import errors, pluginhandler, steps +from snapcraft_legacy import file_utils +from snapcraft_legacy.internal import errors, pluginhandler, steps from tests.unit import TestCase, load_part diff --git a/tests/unit/pluginhandler/test_dirty_report.py b/tests/unit/pluginhandler/test_dirty_report.py index 0afaf4371c..4d17923a85 100644 --- a/tests/unit/pluginhandler/test_dirty_report.py +++ b/tests/unit/pluginhandler/test_dirty_report.py @@ -16,8 +16,11 @@ from testscenarios import multiply_scenarios -from snapcraft.internal import steps -from snapcraft.internal.pluginhandler._dirty_report import Dependency, DirtyReport +from snapcraft_legacy.internal import steps +from snapcraft_legacy.internal.pluginhandler._dirty_report import ( + Dependency, + DirtyReport, +) class TestDirtyReportGetReport: diff --git a/tests/unit/pluginhandler/test_metadata_extraction.py b/tests/unit/pluginhandler/test_metadata_extraction.py index 6802c2af80..5226e6bc17 100644 --- a/tests/unit/pluginhandler/test_metadata_extraction.py +++ b/tests/unit/pluginhandler/test_metadata_extraction.py @@ -19,9 +19,9 @@ import fixtures from testtools.matchers import Contains, Equals -from snapcraft import extractors -from snapcraft.internal import errors -from snapcraft.internal.pluginhandler import extract_metadata +from snapcraft_legacy import extractors +from snapcraft_legacy.internal import errors +from snapcraft_legacy.internal.pluginhandler import extract_metadata from tests import fixture_setup, unit diff --git a/tests/unit/pluginhandler/test_missing_dependency.py b/tests/unit/pluginhandler/test_missing_dependency.py index 5cabbcce92..47861658ee 100644 --- a/tests/unit/pluginhandler/test_missing_dependency.py +++ b/tests/unit/pluginhandler/test_missing_dependency.py @@ -19,8 +19,10 @@ import fixtures from testtools.matchers import Equals -from snapcraft.internal import repo -from snapcraft.internal.pluginhandler._dependencies import MissingDependencyResolver +from snapcraft_legacy.internal import repo +from snapcraft_legacy.internal.pluginhandler._dependencies import ( + MissingDependencyResolver, +) from tests import unit @@ -39,7 +41,7 @@ def fake_repo_query(*args, **kwargs): self.useFixture( fixtures.MockPatch( - "snapcraft.internal.repo.Repo.get_package_for_file", + "snapcraft_legacy.internal.repo.Repo.get_package_for_file", side_effect=fake_repo_query, ) ) diff --git a/tests/unit/pluginhandler/test_patcher.py b/tests/unit/pluginhandler/test_patcher.py index ba5e7039f4..21c7ed3482 100644 --- a/tests/unit/pluginhandler/test_patcher.py +++ b/tests/unit/pluginhandler/test_patcher.py @@ -18,33 +18,35 @@ import pytest -from snapcraft import file_utils -from snapcraft.internal import errors -from snapcraft.internal.pluginhandler import PartPatcher +from snapcraft_legacy import file_utils +from snapcraft_legacy.internal import errors +from snapcraft_legacy.internal.pluginhandler import PartPatcher from tests.unit import load_part @pytest.fixture def mock_elf_patcher(): - """Return a mock for snapcraft.internal.elf.Patcher.""" - patcher = mock.patch("snapcraft.internal.elf.Patcher", autospec=True) + """Return a mock for snapcraft_legacy.internal.elf.Patcher.""" + patcher = mock.patch("snapcraft_legacy.internal.elf.Patcher", autospec=True) yield patcher.start() patcher.stop() @pytest.fixture def mock_partpatcher(): - """Return a mock for snapcraft.internal.pluginhandler.PartPatcher.""" - patcher = mock.patch("snapcraft.internal.pluginhandler.PartPatcher", autospec=True) + """Return a mock for snapcraft_legacy.internal.pluginhandler.PartPatcher.""" + patcher = mock.patch( + "snapcraft_legacy.internal.pluginhandler.PartPatcher", autospec=True + ) yield patcher.start() patcher.stop() @pytest.fixture(autouse=True) def mock_find_linker(): - """Return a mock for snapcraft.internal.elf.find_linker.""" + """Return a mock for snapcraft_legacy.internal.elf.find_linker.""" patcher = mock.patch( - "snapcraft.internal.elf.find_linker", + "snapcraft_legacy.internal.elf.find_linker", autospec=True, return_value="/snap/test-snap/current/lib/x86_64-linux-gnu/ld-2.27.so", ) diff --git a/tests/unit/pluginhandler/test_plugin_loader.py b/tests/unit/pluginhandler/test_plugin_loader.py index ba1690cbae..e91040c727 100644 --- a/tests/unit/pluginhandler/test_plugin_loader.py +++ b/tests/unit/pluginhandler/test_plugin_loader.py @@ -22,10 +22,10 @@ import fixtures from testtools.matchers import Equals, IsInstance -from snapcraft.internal import errors -from snapcraft.plugins._plugin_finder import _PLUGINS -from snapcraft.plugins.v1 import PluginV1 -from snapcraft.plugins.v2 import PluginV2 +from snapcraft_legacy.internal import errors +from snapcraft_legacy.plugins._plugin_finder import _PLUGINS +from snapcraft_legacy.plugins.v1 import PluginV1 +from snapcraft_legacy.plugins.v2 import PluginV2 from tests import unit @@ -49,8 +49,8 @@ def test_local_plugin(self): print( dedent( """\ - import snapcraft.plugins.v1 - class Local(snapcraft.plugins.v1.PluginV1): + import snapcraft_legacy.plugins.v1 + class Local(snapcraft_legacy.plugins.v1.PluginV1): pass """ ), diff --git a/tests/unit/pluginhandler/test_pluginhandler.py b/tests/unit/pluginhandler/test_pluginhandler.py index 05e46ef346..c6480ca8b7 100644 --- a/tests/unit/pluginhandler/test_pluginhandler.py +++ b/tests/unit/pluginhandler/test_pluginhandler.py @@ -26,8 +26,8 @@ import pytest from testtools.matchers import Contains, Equals, FileExists, Not -import snapcraft -from snapcraft.internal import ( +import snapcraft_legacy +from snapcraft_legacy.internal import ( common, errors, lifecycle, @@ -37,8 +37,8 @@ states, steps, ) -from snapcraft.internal.sources.errors import SnapcraftSourceUnhandledError -from snapcraft.project import Project +from snapcraft_legacy.internal.sources.errors import SnapcraftSourceUnhandledError +from snapcraft_legacy.project import Project from tests import fixture_setup, unit from . import mocks @@ -119,7 +119,7 @@ def test_fileset_include_excludes(self): ) self.assertThat(exclude, Equals(["etc", "usr/lib/*.a"])) - @patch.object(snapcraft.plugins.v1.nil.NilPlugin, "snap_fileset") + @patch.object(snapcraft_legacy.plugins.v1.nil.NilPlugin, "snap_fileset") def test_migratable_fileset_for_no_options_modification(self, mock_snap_fileset): """Making sure migratable_fileset_for() doesn't modify options""" @@ -448,7 +448,7 @@ def test_filesets_excludes_without_relative_paths(self): self.assertThat(raised.message, Equals('path "/abs/exclude" must be relative')) - @patch("snapcraft.internal.pluginhandler._organize_filesets") + @patch("snapcraft_legacy.internal.pluginhandler._organize_filesets") def test_build_organizes(self, mock_organize): handler = self.load_part("test-part") handler.build() @@ -456,9 +456,9 @@ def test_build_organizes(self, mock_organize): "test-part", {}, handler.part_install_dir, False ) - @patch("snapcraft.internal.pluginhandler._organize_filesets") + @patch("snapcraft_legacy.internal.pluginhandler._organize_filesets") def test_update_build_organizes_with_overwrite(self, mock_organize): - class TestPlugin(snapcraft.BasePlugin): + class TestPlugin(snapcraft_legacy.BasePlugin): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.out_of_source_build = True @@ -836,13 +836,13 @@ def setUp(self): super().setUp() fake_install_build_packages = fixtures.MockPatch( - "snapcraft.internal.lifecycle._runner._install_build_packages", + "snapcraft_legacy.internal.lifecycle._runner._install_build_packages", return_value=list(), ) self.useFixture(fake_install_build_packages) fake_install_build_snaps = fixtures.MockPatch( - "snapcraft.internal.lifecycle._runner._install_build_snaps", + "snapcraft_legacy.internal.lifecycle._runner._install_build_snaps", return_value=list(), ) self.useFixture(fake_install_build_snaps) @@ -1100,7 +1100,7 @@ def test_build_is_dirty_from_options(self): self.handler.is_dirty(steps.BUILD), "Expected build step to be dirty" ) - @patch.object(snapcraft.BasePlugin, "enable_cross_compilation") + @patch.object(snapcraft_legacy.BasePlugin, "enable_cross_compilation") def test_build_is_dirty_from_project(self, mock_enable_cross_compilation): project = Project(target_deb_arch="amd64") self.handler = self.load_part("test-part", project=project) @@ -1157,7 +1157,7 @@ def test_pull_is_dirty_from_options(self): self.handler.is_dirty(steps.PULL), "Expected pull step to be dirty" ) - @patch.object(snapcraft.BasePlugin, "enable_cross_compilation") + @patch.object(snapcraft_legacy.BasePlugin, "enable_cross_compilation") def test_pull_is_dirty_from_project(self, mock_enable_cross_compilation): project = Project(target_deb_arch="amd64") self.handler = self.load_part("test-part", project=project) @@ -1453,13 +1453,13 @@ def test_stage_packages_offline(self): part = self.load_part("offline-test", plugin_name="nil") with patch( - "snapcraft.internal.pluginhandler.PluginHandler._fetch_stage_packages" + "snapcraft_legacy.internal.pluginhandler.PluginHandler._fetch_stage_packages" ) as fetch_stage_packages, patch( - "snapcraft.internal.pluginhandler.PluginHandler._fetch_stage_snaps" + "snapcraft_legacy.internal.pluginhandler.PluginHandler._fetch_stage_snaps" ) as fetch_stage_snaps, patch( - "snapcraft.internal.pluginhandler.PluginHandler._unpack_stage_packages" + "snapcraft_legacy.internal.pluginhandler.PluginHandler._unpack_stage_packages" ) as unpack_stage_packages, patch( - "snapcraft.internal.pluginhandler.PluginHandler._unpack_stage_snaps" + "snapcraft_legacy.internal.pluginhandler.PluginHandler._unpack_stage_snaps" ) as unpack_stage_snaps: part.prepare_pull() diff --git a/tests/unit/pluginhandler/test_runner.py b/tests/unit/pluginhandler/test_runner.py index 570b7c99da..9262266600 100644 --- a/tests/unit/pluginhandler/test_runner.py +++ b/tests/unit/pluginhandler/test_runner.py @@ -22,8 +22,8 @@ from testtools.matchers import Contains, FileContains, FileExists -from snapcraft.internal import errors -from snapcraft.internal.pluginhandler import _runner +from snapcraft_legacy.internal import errors +from snapcraft_legacy.internal.pluginhandler import _runner from tests import fixture_setup, unit diff --git a/tests/unit/pluginhandler/test_scriptlets.py b/tests/unit/pluginhandler/test_scriptlets.py index e612874e9b..546c13b333 100644 --- a/tests/unit/pluginhandler/test_scriptlets.py +++ b/tests/unit/pluginhandler/test_scriptlets.py @@ -25,8 +25,8 @@ from testscenarios.scenarios import multiply_scenarios from testtools.matchers import Equals -from snapcraft import yaml_utils -from snapcraft.internal import errors +from snapcraft_legacy import yaml_utils +from snapcraft_legacy.internal import errors from tests import unit from tests.unit.commands import CommandBaseTestCase @@ -63,13 +63,13 @@ def setUp(self): open(os.path.join("src", "version.txt"), "w").write("v1.0") fake_install_build_packages = fixtures.MockPatch( - "snapcraft.internal.lifecycle._runner._install_build_packages", + "snapcraft_legacy.internal.lifecycle._runner._install_build_packages", return_value=list(), ) self.useFixture(fake_install_build_packages) fake_install_build_snaps = fixtures.MockPatch( - "snapcraft.internal.lifecycle._runner._install_build_snaps", + "snapcraft_legacy.internal.lifecycle._runner._install_build_snaps", return_value=list(), ) self.useFixture(fake_install_build_snaps) diff --git a/tests/unit/pluginhandler/test_state.py b/tests/unit/pluginhandler/test_state.py index 7577d6f101..8257f7f210 100644 --- a/tests/unit/pluginhandler/test_state.py +++ b/tests/unit/pluginhandler/test_state.py @@ -21,8 +21,8 @@ import fixtures from testtools.matchers import Contains, Equals -from snapcraft import extractors, plugins -from snapcraft.internal import elf, errors, states, steps +from snapcraft_legacy import extractors, plugins +from snapcraft_legacy.internal import elf, errors, states, steps from tests import fixture_setup, unit @@ -32,13 +32,15 @@ def setUp(self): self.get_pull_properties_mock = self.useFixture( fixtures.MockPatch( - "snapcraft.plugins.v1.PluginV1.get_pull_properties", return_value=[] + "snapcraft_legacy.plugins.v1.PluginV1.get_pull_properties", + return_value=[], ) ).mock self.get_build_properties_mock = self.useFixture( fixtures.MockPatch( - "snapcraft.plugins.v1.PluginV1.get_build_properties", return_value=[] + "snapcraft_legacy.plugins.v1.PluginV1.get_build_properties", + return_value=[], ) ).mock @@ -47,13 +49,14 @@ def setUp(self): self.get_elf_files_mock = self.useFixture( fixtures.MockPatch( - "snapcraft.internal.elf.get_elf_files", return_value=frozenset() + "snapcraft_legacy.internal.elf.get_elf_files", return_value=frozenset() ) ).mock self.useFixture( fixtures.MockPatch( - "snapcraft.internal.xattrs.read_origin_stage_package", return_value=None + "snapcraft_legacy.internal.xattrs.read_origin_stage_package", + return_value=None, ) ) @@ -98,7 +101,7 @@ def test_pull_build_packages_with_grammar_properties(self): class StateTestCase(StateBaseTestCase): - @patch("snapcraft.internal.repo.Repo") + @patch("snapcraft_legacy.internal.repo.Repo") def test_pull_state(self, repo_mock): self.assertRaises(errors.NoLatestStepError, self.handler.latest_step) self.assertThat(self.handler.next_step(), Equals(steps.PULL)) @@ -131,7 +134,7 @@ def test_pull_state(self, repo_mock): self.assertTrue(type(state.project_options) is OrderedDict) self.assertTrue("deb_arch" in state.project_options) - @patch("snapcraft.internal.repo.Repo") + @patch("snapcraft_legacy.internal.repo.Repo") def test_pull_state_with_extracted_metadata(self, repo_mock): self.handler = self.load_part( "test_part", @@ -202,7 +205,7 @@ def _fake_extractor(file_path, workdir): files, Equals([os.path.join(self.handler.part_source_dir, "metadata-file")]) ) - @patch("snapcraft.internal.repo.Repo") + @patch("snapcraft_legacy.internal.repo.Repo") def test_pull_state_with_scriptlet_metadata(self, repo_mock): self.handler = self.load_part( "test_part", @@ -756,9 +759,9 @@ def test_prime_state_with_stuff_already_primed(self, mock_copy): self.assertTrue(type(state.project_options) is OrderedDict) self.assertThat(len(state.project_options), Equals(0)) - @patch("snapcraft.internal.elf.ElfFile._extract_attributes") - @patch("snapcraft.internal.elf.ElfFile.load_dependencies") - @patch("snapcraft.internal.pluginhandler._migrate_files") + @patch("snapcraft_legacy.internal.elf.ElfFile._extract_attributes") + @patch("snapcraft_legacy.internal.elf.ElfFile.load_dependencies") + @patch("snapcraft_legacy.internal.pluginhandler._migrate_files") def test_prime_state_with_dependencies( self, mock_migrate_files, mock_load_dependencies, mock_get_symbols ): @@ -827,9 +830,9 @@ def test_prime_state_with_dependencies( self.assertTrue(type(state.project_options) is OrderedDict) self.assertThat(len(state.project_options), Equals(0)) - @patch("snapcraft.internal.elf.ElfFile._extract_attributes") - @patch("snapcraft.internal.elf.ElfFile.load_dependencies") - @patch("snapcraft.internal.pluginhandler._migrate_files") + @patch("snapcraft_legacy.internal.elf.ElfFile._extract_attributes") + @patch("snapcraft_legacy.internal.elf.ElfFile.load_dependencies") + @patch("snapcraft_legacy.internal.pluginhandler._migrate_files") def test_prime_state_missing_libraries( self, mock_migrate_files, mock_load_dependencies, mock_get_symbols ): @@ -885,9 +888,9 @@ def test_prime_state_missing_libraries( # The rest should be considered missing. self.assertThat(state.dependency_paths, Equals({"lib3"})) - @patch("snapcraft.internal.elf.ElfFile._extract_attributes") - @patch("snapcraft.internal.elf.ElfFile.load_dependencies") - @patch("snapcraft.internal.pluginhandler._migrate_files") + @patch("snapcraft_legacy.internal.elf.ElfFile._extract_attributes") + @patch("snapcraft_legacy.internal.elf.ElfFile.load_dependencies") + @patch("snapcraft_legacy.internal.pluginhandler._migrate_files") def test_prime_state_with_shadowed_dependencies( self, mock_migrate_files, mock_load_dependencies, mock_get_symbols ): diff --git a/tests/unit/plugins/v1/__init__.py b/tests/unit/plugins/v1/__init__.py index 663dc28647..8d0fb5ecc5 100644 --- a/tests/unit/plugins/v1/__init__.py +++ b/tests/unit/plugins/v1/__init__.py @@ -14,8 +14,8 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -from snapcraft.internal.meta.snap import Snap -from snapcraft.project import Project +from snapcraft_legacy.internal.meta.snap import Snap +from snapcraft_legacy.project import Project from tests import unit diff --git a/tests/unit/plugins/v1/conftest.py b/tests/unit/plugins/v1/conftest.py index 744a6794f4..15f9fa9069 100644 --- a/tests/unit/plugins/v1/conftest.py +++ b/tests/unit/plugins/v1/conftest.py @@ -18,8 +18,8 @@ import pytest -from snapcraft.internal.meta.snap import Snap -from snapcraft.project import Project +from snapcraft_legacy.internal.meta.snap import Snap +from snapcraft_legacy.project import Project @pytest.fixture @@ -37,7 +37,7 @@ def project(monkeypatch, tmp_work_path, request): @pytest.fixture def mock_common_run_output(): """A no-op common.run_output mock.""" - patcher = mock.patch("snapcraft.internal.common.run_output") + patcher = mock.patch("snapcraft_legacy.internal.common.run_output") yield patcher.start() patcher.stop() @@ -45,7 +45,7 @@ def mock_common_run_output(): @pytest.fixture def mock_run(): """A no-op run mock.""" - patcher = mock.patch("snapcraft.plugins.v1.PluginV1.run") + patcher = mock.patch("snapcraft_legacy.plugins.v1.PluginV1.run") yield patcher.start() patcher.stop() @@ -53,7 +53,7 @@ def mock_run(): @pytest.fixture def mock_run_output(): """A no-op run_output mock.""" - patcher = mock.patch("snapcraft.plugins.v1.PluginV1.run_output") + patcher = mock.patch("snapcraft_legacy.plugins.v1.PluginV1.run_output") yield patcher.start() patcher.stop() @@ -61,7 +61,7 @@ def mock_run_output(): @pytest.fixture def mock_tar(): """A no-op tar source mock.""" - patcher = mock.patch("snapcraft.internal.sources.Tar") + patcher = mock.patch("snapcraft_legacy.internal.sources.Tar") yield patcher.start() patcher.stop() @@ -69,6 +69,6 @@ def mock_tar(): @pytest.fixture def mock_zip(): """A no-op zip source mock.""" - patcher = mock.patch("snapcraft.internal.sources.Zip") + patcher = mock.patch("snapcraft_legacy.internal.sources.Zip") yield patcher.start() patcher.stop() diff --git a/tests/unit/plugins/v1/python/test_errors.py b/tests/unit/plugins/v1/python/test_errors.py index 430a43482c..194e107ed2 100644 --- a/tests/unit/plugins/v1/python/test_errors.py +++ b/tests/unit/plugins/v1/python/test_errors.py @@ -15,7 +15,7 @@ # along with this program. If not, see . -from snapcraft.plugins.v1._python import errors +from snapcraft_legacy.plugins.v1._python import errors class TestErrorFormatting: diff --git a/tests/unit/plugins/v1/python/test_pip.py b/tests/unit/plugins/v1/python/test_pip.py index dc4a70858d..a524026720 100644 --- a/tests/unit/plugins/v1/python/test_pip.py +++ b/tests/unit/plugins/v1/python/test_pip.py @@ -23,7 +23,7 @@ import pytest from testtools.matchers import Contains, Equals, HasLength -from snapcraft.plugins.v1._python import _pip, errors +from snapcraft_legacy.plugins.v1._python import _pip, errors from ._basesuite import PythonBaseTestCase @@ -32,11 +32,11 @@ class PipRunBaseTestCase(PythonBaseTestCase): def setUp(self): super().setUp() - patcher = mock.patch("snapcraft.internal.common.run_output") + patcher = mock.patch("snapcraft_legacy.internal.common.run_output") self.mock_run_output = patcher.start() self.addCleanup(patcher.stop) - patcher = mock.patch("snapcraft.internal.common.run") + patcher = mock.patch("snapcraft_legacy.internal.common.run") self.mock_run = patcher.start() self.addCleanup(patcher.stop) diff --git a/tests/unit/plugins/v1/python/test_python_finder.py b/tests/unit/plugins/v1/python/test_python_finder.py index 0a7e52633f..a58a3f6b3e 100644 --- a/tests/unit/plugins/v1/python/test_python_finder.py +++ b/tests/unit/plugins/v1/python/test_python_finder.py @@ -20,7 +20,7 @@ from testtools.matchers import Equals, MatchesRegex -from snapcraft.plugins.v1._python import _python_finder, errors +from snapcraft_legacy.plugins.v1._python import _python_finder, errors from ._basesuite import PythonBaseTestCase diff --git a/tests/unit/plugins/v1/python/test_sitecustomize.py b/tests/unit/plugins/v1/python/test_sitecustomize.py index 6b6465b8b7..a75aaf7dde 100644 --- a/tests/unit/plugins/v1/python/test_sitecustomize.py +++ b/tests/unit/plugins/v1/python/test_sitecustomize.py @@ -19,7 +19,7 @@ from testtools.matchers import Contains, FileContains -from snapcraft.plugins.v1 import _python +from snapcraft_legacy.plugins.v1 import _python from ._basesuite import PythonBaseTestCase diff --git a/tests/unit/plugins/v1/ros/test_rosdep.py b/tests/unit/plugins/v1/ros/test_rosdep.py index a7932ccaca..7310e8353f 100644 --- a/tests/unit/plugins/v1/ros/test_rosdep.py +++ b/tests/unit/plugins/v1/ros/test_rosdep.py @@ -21,15 +21,15 @@ from testtools.matchers import Equals -import snapcraft -from snapcraft.plugins.v1._ros import rosdep +import snapcraft_legacy +from snapcraft_legacy.plugins.v1._ros import rosdep from tests import unit class RosdepTestCase(unit.TestCase): def setUp(self): super().setUp() - self.project = snapcraft.ProjectOptions() + self.project = snapcraft_legacy.ProjectOptions() self.rosdep = rosdep.Rosdep( ros_distro="melodic", @@ -41,7 +41,7 @@ def setUp(self): target_arch=self.project._get_stage_packages_target_arch(), ) - patcher = mock.patch("snapcraft.repo.Ubuntu") + patcher = mock.patch("snapcraft_legacy.repo.Ubuntu") self.ubuntu_mock = patcher.start() self.addCleanup(patcher.stop) diff --git a/tests/unit/plugins/v1/ros/test_wstool.py b/tests/unit/plugins/v1/ros/test_wstool.py index 0c6925178a..99c4bb2277 100644 --- a/tests/unit/plugins/v1/ros/test_wstool.py +++ b/tests/unit/plugins/v1/ros/test_wstool.py @@ -21,20 +21,20 @@ from testtools.matchers import Contains, Equals -import snapcraft -from snapcraft.plugins.v1._ros import wstool +import snapcraft_legacy +from snapcraft_legacy.plugins.v1._ros import wstool from tests import unit class WstoolTestCase(unit.TestCase): def setUp(self): super().setUp() - self.project = snapcraft.ProjectOptions() + self.project = snapcraft_legacy.ProjectOptions() self.wstool = wstool.Wstool( "package_path", "wstool_path", self.project, "core18" ) - patcher = mock.patch("snapcraft.repo.Ubuntu") + patcher = mock.patch("snapcraft_legacy.repo.Ubuntu") self.ubuntu_mock = patcher.start() self.addCleanup(patcher.stop) diff --git a/tests/unit/plugins/v1/test_ant.py b/tests/unit/plugins/v1/test_ant.py index 95ce5c3d08..5a1e4ab293 100644 --- a/tests/unit/plugins/v1/test_ant.py +++ b/tests/unit/plugins/v1/test_ant.py @@ -22,10 +22,10 @@ import pytest from testtools.matchers import Equals, HasLength -from snapcraft.internal import errors -from snapcraft.internal.meta.snap import Snap -from snapcraft.plugins.v1 import ant -from snapcraft.project import Project +from snapcraft_legacy.internal import errors +from snapcraft_legacy.internal.meta.snap import Snap +from snapcraft_legacy.plugins.v1 import ant +from snapcraft_legacy.project import Project from tests import unit from . import PluginsV1BaseTestCase @@ -229,10 +229,10 @@ class Options: self.options = Options() self.run_mock = self.useFixture( - fixtures.MockPatch("snapcraft.internal.common.run") + fixtures.MockPatch("snapcraft_legacy.internal.common.run") ).mock self.tar_mock = self.useFixture( - fixtures.MockPatch("snapcraft.internal.sources.Tar") + fixtures.MockPatch("snapcraft_legacy.internal.sources.Tar") ).mock def create_assets(self, plugin): diff --git a/tests/unit/plugins/v1/test_autotools.py b/tests/unit/plugins/v1/test_autotools.py index 1300454244..e3cc6f7ff0 100644 --- a/tests/unit/plugins/v1/test_autotools.py +++ b/tests/unit/plugins/v1/test_autotools.py @@ -23,9 +23,9 @@ import pytest from testtools.matchers import Equals, HasLength -import snapcraft -from snapcraft.internal import errors, meta -from snapcraft.plugins.v1 import autotools, make +import snapcraft_legacy +from snapcraft_legacy.internal import errors, meta +from snapcraft_legacy.plugins.v1 import autotools, make from . import PluginsV1BaseTestCase @@ -443,9 +443,9 @@ def test_unsupported_base(self): ) @mock.patch.object(autotools.AutotoolsPlugin, "run") def test_cross_compile(mock_run, monkeypatch, project, options, deb_arch, triplet): - monkeypatch.setattr(snapcraft.project.Project, "is_cross_compiling", True) + monkeypatch.setattr(snapcraft_legacy.project.Project, "is_cross_compiling", True) - project = snapcraft.project.Project(target_deb_arch=deb_arch) + project = snapcraft_legacy.project.Project(target_deb_arch=deb_arch) project._snap_meta = meta.snap.Snap(name="test-snap", base="core18") plugin = autotools.AutotoolsPlugin("test-part", options, project) diff --git a/tests/unit/plugins/v1/test_base.py b/tests/unit/plugins/v1/test_base.py index d22cc999fb..630a84cc45 100644 --- a/tests/unit/plugins/v1/test_base.py +++ b/tests/unit/plugins/v1/test_base.py @@ -18,58 +18,64 @@ from testtools.matchers import Equals -import snapcraft -from snapcraft.internal import errors +import snapcraft_legacy +from snapcraft_legacy.internal import errors from tests import unit class TestBasePlugin(unit.TestCase): def setUp(self): super().setUp() - self.project_options = snapcraft.ProjectOptions() + self.project_options = snapcraft_legacy.ProjectOptions() def test_cross_compilation_raises(self): options = unit.MockOptions(disable_parallel=True) - plugin = snapcraft.BasePlugin("test_plugin", options, self.project_options) + plugin = snapcraft_legacy.BasePlugin( + "test_plugin", options, self.project_options + ) self.assertRaises( errors.CrossCompilationNotSupported, plugin.enable_cross_compilation ) def test_parallel_build_count_returns_1_when_disabled(self): options = unit.MockOptions(disable_parallel=True) - plugin = snapcraft.BasePlugin("test_plugin", options, self.project_options) + plugin = snapcraft_legacy.BasePlugin( + "test_plugin", options, self.project_options + ) self.assertThat(plugin.parallel_build_count, Equals(1)) def test_parallel_build_count_returns_build_count_from_project(self): options = unit.MockOptions(disable_parallel=False) - plugin = snapcraft.BasePlugin("test_plugin", options, self.project_options) + plugin = snapcraft_legacy.BasePlugin( + "test_plugin", options, self.project_options + ) unittest.mock.patch.object(self.project_options, "parallel_build_count", 2) self.assertThat(plugin.parallel_build_count, Equals(2)) - @unittest.mock.patch("snapcraft.internal.common.run") + @unittest.mock.patch("snapcraft_legacy.internal.common.run") def test_run_without_specifying_cwd(self, mock_run): - plugin = snapcraft.BasePlugin("test/part", options=None) + plugin = snapcraft_legacy.BasePlugin("test/part", options=None) plugin.run(["ls"]) mock_run.assert_called_once_with(["ls"], cwd=plugin.builddir) - @unittest.mock.patch("snapcraft.internal.common.run") + @unittest.mock.patch("snapcraft_legacy.internal.common.run") def test_run_specifying_a_cwd(self, mock_run): - plugin = snapcraft.BasePlugin("test/part", options=None) + plugin = snapcraft_legacy.BasePlugin("test/part", options=None) plugin.run(["ls"], cwd=plugin.sourcedir) mock_run.assert_called_once_with(["ls"], cwd=plugin.sourcedir) - @unittest.mock.patch("snapcraft.internal.common.run_output") + @unittest.mock.patch("snapcraft_legacy.internal.common.run_output") def test_run_output_without_specifying_cwd(self, mock_run): - plugin = snapcraft.BasePlugin("test/part", options=None) + plugin = snapcraft_legacy.BasePlugin("test/part", options=None) plugin.run_output(["ls"]) mock_run.assert_called_once_with(["ls"], cwd=plugin.builddir) - @unittest.mock.patch("snapcraft.internal.common.run_output") + @unittest.mock.patch("snapcraft_legacy.internal.common.run_output") def test_run_output_specifying_a_cwd(self, mock_run): - plugin = snapcraft.BasePlugin("test/part", options=None) + plugin = snapcraft_legacy.BasePlugin("test/part", options=None) plugin.run_output(["ls"], cwd=plugin.sourcedir) mock_run.assert_called_once_with(["ls"], cwd=plugin.sourcedir) diff --git a/tests/unit/plugins/v1/test_catkin.py b/tests/unit/plugins/v1/test_catkin.py index d6a77024f2..f01a19c35f 100644 --- a/tests/unit/plugins/v1/test_catkin.py +++ b/tests/unit/plugins/v1/test_catkin.py @@ -36,10 +36,10 @@ Not, ) -import snapcraft -from snapcraft import repo -from snapcraft.internal import errors -from snapcraft.plugins.v1 import _ros, catkin +import snapcraft_legacy +from snapcraft_legacy import repo +from snapcraft_legacy.internal import errors +from snapcraft_legacy.plugins.v1 import _ros, catkin from tests import unit from . import PluginsV1BaseTestCase @@ -70,29 +70,30 @@ class props: self.ros_version = "1" self.ubuntu_distro = "bionic" - patcher = mock.patch("snapcraft.repo.Ubuntu") + patcher = mock.patch("snapcraft_legacy.repo.Ubuntu") self.ubuntu_mock = patcher.start() self.addCleanup(patcher.stop) patcher = mock.patch( - "snapcraft.plugins.v1.catkin._find_system_dependencies", return_value={} + "snapcraft_legacy.plugins.v1.catkin._find_system_dependencies", + return_value={}, ) self.dependencies_mock = patcher.start() self.addCleanup(patcher.stop) - patcher = mock.patch("snapcraft.plugins.v1._ros.rosdep.Rosdep") + patcher = mock.patch("snapcraft_legacy.plugins.v1._ros.rosdep.Rosdep") self.rosdep_mock = patcher.start() self.addCleanup(patcher.stop) - patcher = mock.patch("snapcraft.plugins.v1.catkin._Catkin") + patcher = mock.patch("snapcraft_legacy.plugins.v1.catkin._Catkin") self.catkin_mock = patcher.start() self.addCleanup(patcher.stop) - patcher = mock.patch("snapcraft.plugins.v1._ros.wstool.Wstool") + patcher = mock.patch("snapcraft_legacy.plugins.v1._ros.wstool.Wstool") self.wstool_mock = patcher.start() self.addCleanup(patcher.stop) - patcher = mock.patch("snapcraft.plugins.v1._python.Pip") + patcher = mock.patch("snapcraft_legacy.plugins.v1._python.Pip") self.pip_mock = patcher.start() self.addCleanup(patcher.stop) self.pip_mock.return_value.list.return_value = {} @@ -1494,7 +1495,7 @@ class TestBuildArgs: ), ] - @mock.patch("snapcraft.plugins.v1.catkin.CatkinPlugin.run", autospec=True) + @mock.patch("snapcraft_legacy.plugins.v1.catkin.CatkinPlugin.run", autospec=True) @mock.patch.object(catkin.CatkinPlugin, "run_output", return_value="foo") @mock.patch.object(catkin.CatkinPlugin, "_prepare_build") @mock.patch.object(catkin.CatkinPlugin, "_finish_build") @@ -2118,13 +2119,13 @@ class CatkinFindTestCase(unit.TestCase): def setUp(self): super().setUp() - self.project = snapcraft.project.Project() + self.project = snapcraft_legacy.project.Project() self.project._snap_meta.build_base = "core18" self.catkin = catkin._Catkin( "kinetic", "workspace_path", "catkin_path", self.project ) - patcher = mock.patch("snapcraft.repo.Ubuntu") + patcher = mock.patch("snapcraft_legacy.repo.Ubuntu") self.ubuntu_mock = patcher.start() self.addCleanup(patcher.stop) diff --git a/tests/unit/plugins/v1/test_catkin_tools.py b/tests/unit/plugins/v1/test_catkin_tools.py index 005fdba877..b57a0e200e 100644 --- a/tests/unit/plugins/v1/test_catkin_tools.py +++ b/tests/unit/plugins/v1/test_catkin_tools.py @@ -19,7 +19,7 @@ import pytest -from snapcraft.plugins.v1 import catkin_tools +from snapcraft_legacy.plugins.v1 import catkin_tools from . import PluginsV1BaseTestCase @@ -42,7 +42,7 @@ class props: self.properties = props() - patcher = mock.patch("snapcraft.plugins.v1._python.Pip") + patcher = mock.patch("snapcraft_legacy.plugins.v1._python.Pip") self.pip_mock = patcher.start() self.addCleanup(patcher.stop) self.pip_mock.return_value.list.return_value = {} @@ -52,7 +52,7 @@ class CatkinToolsPluginTestCase(CatkinToolsPluginBaseTest): def setUp(self): super().setUp() - patcher = mock.patch("snapcraft.repo.Ubuntu") + patcher = mock.patch("snapcraft_legacy.repo.Ubuntu") self.ubuntu_mock = patcher.start() self.addCleanup(patcher.stop) diff --git a/tests/unit/plugins/v1/test_cmake.py b/tests/unit/plugins/v1/test_cmake.py index 9a6f99c558..192f4fb3b1 100644 --- a/tests/unit/plugins/v1/test_cmake.py +++ b/tests/unit/plugins/v1/test_cmake.py @@ -19,8 +19,8 @@ from testtools.matchers import Equals, HasLength -from snapcraft.internal import errors -from snapcraft.plugins.v1 import cmake +from snapcraft_legacy.internal import errors +from snapcraft_legacy.plugins.v1 import cmake from tests import fixture_setup, unit from . import PluginsV1BaseTestCase @@ -40,7 +40,7 @@ class Options: self.options = Options() - patcher = mock.patch("snapcraft.internal.common.run") + patcher = mock.patch("snapcraft_legacy.internal.common.run") self.run_mock = patcher.start() self.addCleanup(patcher.stop) diff --git a/tests/unit/plugins/v1/test_colcon.py b/tests/unit/plugins/v1/test_colcon.py index f119d26b6d..fec0540512 100644 --- a/tests/unit/plugins/v1/test_colcon.py +++ b/tests/unit/plugins/v1/test_colcon.py @@ -24,9 +24,9 @@ from testscenarios import multiply_scenarios from testtools.matchers import Contains, Equals, FileExists, HasLength, LessThan, Not -from snapcraft import repo -from snapcraft.internal import errors -from snapcraft.plugins.v1 import _ros, colcon +from snapcraft_legacy import repo +from snapcraft_legacy.internal import errors +from snapcraft_legacy.plugins.v1 import _ros, colcon from tests import unit from . import PluginsV1BaseTestCase @@ -53,21 +53,22 @@ class props: self.ubuntu_distro = "bionic" self.ubuntu_mock = self.useFixture( - fixtures.MockPatch("snapcraft.repo.Ubuntu") + fixtures.MockPatch("snapcraft_legacy.repo.Ubuntu") ).mock self.dependencies_mock = self.useFixture( fixtures.MockPatch( - "snapcraft.plugins.v1.colcon._find_system_dependencies", return_value={} + "snapcraft_legacy.plugins.v1.colcon._find_system_dependencies", + return_value={}, ) ).mock self.rosdep_mock = self.useFixture( - fixtures.MockPatch("snapcraft.plugins.v1._ros.rosdep.Rosdep") + fixtures.MockPatch("snapcraft_legacy.plugins.v1._ros.rosdep.Rosdep") ).mock self.pip_mock = self.useFixture( - fixtures.MockPatch("snapcraft.plugins.v1._python.Pip") + fixtures.MockPatch("snapcraft_legacy.plugins.v1._python.Pip") ).mock self.pip_mock.return_value.list.return_value = {} @@ -537,7 +538,7 @@ def setUp(self): super().setUp() self.plugin = colcon.ColconPlugin("test-part", self.properties, self.project) - @mock.patch("snapcraft.internal.mangling.rewrite_python_shebangs") + @mock.patch("snapcraft_legacy.internal.mangling.rewrite_python_shebangs") def test_in_snap_python_is_used(self, shebangs_mock): # Mangling has its own tests. Here we just need to make sure # _prepare_build actually uses it. @@ -676,7 +677,7 @@ def setUp(self): super().setUp() self.plugin = colcon.ColconPlugin("test-part", self.properties, self.project) - @mock.patch("snapcraft.internal.mangling.rewrite_python_shebangs") + @mock.patch("snapcraft_legacy.internal.mangling.rewrite_python_shebangs") def test_in_snap_python_is_used(self, shebangs_mock): # Mangling has its own tests. Here we just need to make sure # _prepare_build actually uses it. diff --git a/tests/unit/plugins/v1/test_conda.py b/tests/unit/plugins/v1/test_conda.py index ba64cef4b6..aabbd0be3c 100644 --- a/tests/unit/plugins/v1/test_conda.py +++ b/tests/unit/plugins/v1/test_conda.py @@ -21,8 +21,8 @@ import pytest from testtools.matchers import DirExists, Equals, HasLength, Not -from snapcraft.internal import errors -from snapcraft.plugins.v1 import conda +from snapcraft_legacy.internal import errors +from snapcraft_legacy.plugins.v1 import conda from tests import unit from . import PluginsV1BaseTestCase @@ -224,7 +224,9 @@ def test_pull(self): class Options: conda_miniconda_version = "latest" - fake_source_script = fixtures.MockPatch("snapcraft.internal.sources.Script") + fake_source_script = fixtures.MockPatch( + "snapcraft_legacy.internal.sources.Script" + ) self.useFixture(fake_source_script) plugin = conda.CondaPlugin("test-part", Options(), self.project) diff --git a/tests/unit/plugins/v1/test_crystal.py b/tests/unit/plugins/v1/test_crystal.py index 211eb0a816..36ffab746a 100644 --- a/tests/unit/plugins/v1/test_crystal.py +++ b/tests/unit/plugins/v1/test_crystal.py @@ -20,8 +20,8 @@ import fixtures from testtools.matchers import Equals, FileExists, HasLength -from snapcraft.internal import errors -from snapcraft.plugins.v1 import crystal +from snapcraft_legacy.internal import errors +from snapcraft_legacy.plugins.v1 import crystal from tests import unit from . import PluginsV1BaseTestCase @@ -31,7 +31,7 @@ class CrystalPluginBaseTest(PluginsV1BaseTestCase): def setUp(self): super().setUp() - self.fake_run = fixtures.MockPatch("snapcraft.internal.common.run") + self.fake_run = fixtures.MockPatch("snapcraft_legacy.internal.common.run") self.useFixture(self.fake_run) @@ -155,7 +155,7 @@ class Options: # fake binaries being built self.useFixture( fixtures.MockPatch( - "snapcraft.internal.elf.ElfFile", side_effect=MockElfFile + "snapcraft_legacy.internal.elf.ElfFile", side_effect=MockElfFile ) ) binaries = ["foo", "bar"] @@ -189,7 +189,7 @@ class Options: # fake binaries being built self.useFixture( fixtures.MockPatch( - "snapcraft.internal.elf.ElfFile", side_effect=MockElfFile + "snapcraft_legacy.internal.elf.ElfFile", side_effect=MockElfFile ) ) binaries = ["foo", "bar"] diff --git a/tests/unit/plugins/v1/test_dotnet.py b/tests/unit/plugins/v1/test_dotnet.py index 304f8922ce..256e476526 100644 --- a/tests/unit/plugins/v1/test_dotnet.py +++ b/tests/unit/plugins/v1/test_dotnet.py @@ -21,10 +21,10 @@ from testtools.matchers import Contains, DirExists, Equals, FileExists, Not -import snapcraft -from snapcraft import file_utils -from snapcraft.internal import sources -from snapcraft.plugins.v1 import dotnet +import snapcraft_legacy +from snapcraft_legacy import file_utils +from snapcraft_legacy.internal import sources +from snapcraft_legacy.plugins.v1 import dotnet from tests import unit from . import PluginsV1BaseTestCase @@ -70,7 +70,7 @@ class Options: # Only amd64 is supported for now. patcher = mock.patch( - "snapcraft.ProjectOptions.deb_arch", + "snapcraft_legacy.ProjectOptions.deb_arch", new_callable=mock.PropertyMock, return_value="amd64", ) @@ -117,8 +117,8 @@ def read(self): urlopen_mock.side_effect = fake_urlopen self.addCleanup(patcher.stop) - original_check_call = snapcraft.internal.common.run - patcher = mock.patch("snapcraft.internal.common.run") + original_check_call = snapcraft_legacy.internal.common.run + patcher = mock.patch("snapcraft_legacy.internal.common.run") self.mock_check_call = patcher.start() self.addCleanup(patcher.stop) @@ -177,7 +177,7 @@ def test_sdk_in_path(self): def test_init_with_non_amd64_architecture(self): with mock.patch( - "snapcraft.ProjectOptions.deb_arch", + "snapcraft_legacy.ProjectOptions.deb_arch", new_callable=mock.PropertyMock, return_value="non-amd64", ): diff --git a/tests/unit/plugins/v1/test_dump.py b/tests/unit/plugins/v1/test_dump.py index 3a92d1c0cb..b70fd05198 100644 --- a/tests/unit/plugins/v1/test_dump.py +++ b/tests/unit/plugins/v1/test_dump.py @@ -18,15 +18,15 @@ from testtools.matchers import Equals -import snapcraft -from snapcraft.plugins.v1.dump import DumpInvalidSymlinkError, DumpPlugin +import snapcraft_legacy +from snapcraft_legacy.plugins.v1.dump import DumpInvalidSymlinkError, DumpPlugin from tests import unit class DumpPluginTestCase(unit.TestCase): def setUp(self): super().setUp() - self.project_options = snapcraft.ProjectOptions() + self.project_options = snapcraft_legacy.ProjectOptions() class Options: source = "." @@ -169,7 +169,7 @@ def test_dump_symlinks_to_libc(self): # Even though this symlink is absolute, since it's to libc the copy # plugin shouldn't try to follow it or modify it. - libc_libs = snapcraft.repo.Repo.get_package_libraries("libc6") + libc_libs = snapcraft_legacy.repo.Repo.get_package_libraries("libc6") # We don't care which lib we're testing with, as long as it's a .so. libc_library_path = [lib for lib in libc_libs if ".so" in lib][0] diff --git a/tests/unit/plugins/v1/test_flutter.py b/tests/unit/plugins/v1/test_flutter.py index ad2bb9f629..187923bb5d 100644 --- a/tests/unit/plugins/v1/test_flutter.py +++ b/tests/unit/plugins/v1/test_flutter.py @@ -19,10 +19,10 @@ import pytest -from snapcraft.internal import errors -from snapcraft.internal.meta.snap import Snap -from snapcraft.plugins.v1 import flutter -from snapcraft.project import Project +from snapcraft_legacy.internal import errors +from snapcraft_legacy.internal.meta.snap import Snap +from snapcraft_legacy.plugins.v1 import flutter +from snapcraft_legacy.project import Project def test_schema(): diff --git a/tests/unit/plugins/v1/test_go.py b/tests/unit/plugins/v1/test_go.py index 8e47f3badd..7c91ed2483 100644 --- a/tests/unit/plugins/v1/test_go.py +++ b/tests/unit/plugins/v1/test_go.py @@ -23,9 +23,9 @@ import pytest from testtools.matchers import Contains, DirExists, Equals, HasLength, Not -from snapcraft.internal import errors, meta -from snapcraft.plugins.v1 import go -from snapcraft.project import Project +from snapcraft_legacy.internal import errors, meta +from snapcraft_legacy.plugins.v1 import go +from snapcraft_legacy.project import Project from tests import fixture_setup, unit from . import PluginsV1BaseTestCase @@ -47,13 +47,13 @@ def fake_go_build(command, cwd, *args, **kwargs): fake_run = self.useFixture( fixtures.MockPatch( - "snapcraft.internal.common.run", side_effect=fake_go_build + "snapcraft_legacy.internal.common.run", side_effect=fake_go_build ) ) self.run_mock = fake_run.mock fake_run_output = self.useFixture( - fixtures.MockPatch("snapcraft.internal.common.run_output") + fixtures.MockPatch("snapcraft_legacy.internal.common.run_output") ) self.run_output_mock = fake_run_output.mock @@ -716,7 +716,7 @@ class Options: ), ) - @mock.patch("snapcraft.internal.elf.ElfFile") + @mock.patch("snapcraft_legacy.internal.elf.ElfFile") def test_build_classic_dynamic_relink(self, mock_elffile): class Options: source = "" @@ -762,7 +762,7 @@ class Options: self.assert_go_paths(plugin) - @mock.patch("snapcraft.internal.elf.ElfFile") + @mock.patch("snapcraft_legacy.internal.elf.ElfFile") def test_build_go_mod_classic_dynamic_relink(self, mock_elffile): class Options: source = "" diff --git a/tests/unit/plugins/v1/test_godeps.py b/tests/unit/plugins/v1/test_godeps.py index 31ce03a4e1..65af1f2136 100644 --- a/tests/unit/plugins/v1/test_godeps.py +++ b/tests/unit/plugins/v1/test_godeps.py @@ -19,8 +19,8 @@ from testtools.matchers import Contains, Equals, HasLength, Not -from snapcraft.internal import errors -from snapcraft.plugins.v1 import godeps +from snapcraft_legacy.internal import errors +from snapcraft_legacy.plugins.v1 import godeps from tests import unit from . import PluginsV1BaseTestCase @@ -30,7 +30,7 @@ class GodepsPluginBaseTest(PluginsV1BaseTestCase): def setUp(self): super().setUp() - patcher = mock.patch("snapcraft.internal.common.run") + patcher = mock.patch("snapcraft_legacy.internal.common.run") self.run_mock = patcher.start() self.addCleanup(patcher.stop) diff --git a/tests/unit/plugins/v1/test_gradle.py b/tests/unit/plugins/v1/test_gradle.py index a9a51f3028..8f701b3e92 100644 --- a/tests/unit/plugins/v1/test_gradle.py +++ b/tests/unit/plugins/v1/test_gradle.py @@ -20,10 +20,10 @@ import pytest -from snapcraft.internal import errors -from snapcraft.internal.meta.snap import Snap -from snapcraft.plugins.v1 import gradle -from snapcraft.project import Project +from snapcraft_legacy.internal import errors +from snapcraft_legacy.internal.meta.snap import Snap +from snapcraft_legacy.plugins.v1 import gradle +from snapcraft_legacy.project import Project from . import PluginsV1BaseTestCase diff --git a/tests/unit/plugins/v1/test_kbuild.py b/tests/unit/plugins/v1/test_kbuild.py index ef2e832149..bb22941405 100644 --- a/tests/unit/plugins/v1/test_kbuild.py +++ b/tests/unit/plugins/v1/test_kbuild.py @@ -22,9 +22,9 @@ import pytest from testtools.matchers import Equals, HasLength -import snapcraft -from snapcraft.internal import errors, meta -from snapcraft.plugins.v1 import kbuild +import snapcraft_legacy +from snapcraft_legacy.internal import errors, meta +from snapcraft_legacy.plugins.v1 import kbuild from . import PluginsV1BaseTestCase @@ -299,7 +299,7 @@ def test_unsupported_base(self): @pytest.mark.parametrize("deb_arch", ["armhf", "arm64", "i386", "ppc64el"]) @mock.patch("subprocess.check_call") def test_cross_compile(mock_check_call, monkeypatch, mock_run, deb_arch): - monkeypatch.setattr(snapcraft.project.Project, "is_cross_compiling", True) + monkeypatch.setattr(snapcraft_legacy.project.Project, "is_cross_compiling", True) class Options: build_parameters = [] @@ -309,7 +309,7 @@ class Options: kconfigs = [] build_attributes = [] - project = snapcraft.project.Project(target_deb_arch=deb_arch) + project = snapcraft_legacy.project.Project(target_deb_arch=deb_arch) project._snap_meta = meta.snap.Snap(name="test-snap", base="core18") plugin = kbuild.KBuildPlugin("test-part", Options(), project) diff --git a/tests/unit/plugins/v1/test_kernel.py b/tests/unit/plugins/v1/test_kernel.py index 17835d16db..0a2ab8d69c 100644 --- a/tests/unit/plugins/v1/test_kernel.py +++ b/tests/unit/plugins/v1/test_kernel.py @@ -25,10 +25,10 @@ import pytest from testtools.matchers import Contains, Equals, FileContains, HasLength -import snapcraft -from snapcraft import storeapi -from snapcraft.internal import errors, meta -from snapcraft.plugins.v1 import kernel +import snapcraft_legacy +from snapcraft_legacy import storeapi +from snapcraft_legacy.internal import errors, meta +from snapcraft_legacy.plugins.v1 import kernel from . import PluginsV1BaseTestCase @@ -65,7 +65,7 @@ class Options: self.run_output_mock = patcher.start() self.addCleanup(patcher.stop) - patcher = mock.patch("snapcraft.BasePlugin.build") + patcher = mock.patch("snapcraft_legacy.BasePlugin.build") self.base_build_mock = patcher.start() self.addCleanup(patcher.stop) @@ -133,7 +133,7 @@ def test_get_build_properties(self): ] resulting_build_properties = kernel.KernelPlugin.get_build_properties() expected_build_properties.extend( - snapcraft.plugins.v1.kbuild.KBuildPlugin.get_build_properties() + snapcraft_legacy.plugins.v1.kbuild.KBuildPlugin.get_build_properties() ) self.assertThat( @@ -360,7 +360,7 @@ def test_pack_initrd_modules_return_same_deps(self): [mock.call(modprobe_cmd + ["vfat"], env=mock.ANY)] ) - @mock.patch.object(snapcraft.ProjectOptions, "kernel_arch", new="not_arm") + @mock.patch.object(snapcraft_legacy.ProjectOptions, "kernel_arch", new="not_arm") def test_build_with_kconfigfile(self): self.options.kconfigfile = "config" with open(self.options.kconfigfile, "w") as f: @@ -405,7 +405,7 @@ def test_build_with_kconfigfile(self): self.assertThat(config_contents, Equals("ACCEPT=y\n")) self._assert_common_assets(plugin.installdir) - @mock.patch.object(snapcraft.ProjectOptions, "kernel_arch", new="not_arm") + @mock.patch.object(snapcraft_legacy.ProjectOptions, "kernel_arch", new="not_arm") def test_build_verbose_with_kconfigfile(self): fake_logger = fixtures.FakeLogger(level=logging.DEBUG) self.useFixture(fake_logger) @@ -476,7 +476,7 @@ def test_build_verbose_with_kconfigfile(self): self.assertThat(config_contents, Equals("ACCEPT=y\n")) self._assert_common_assets(plugin.installdir) - @mock.patch.object(snapcraft.ProjectOptions, "kernel_arch", new="not_arm") + @mock.patch.object(snapcraft_legacy.ProjectOptions, "kernel_arch", new="not_arm") def test_check_config(self): fake_logger = fixtures.FakeLogger(level=logging.WARNING) self.useFixture(fake_logger) @@ -501,7 +501,7 @@ def test_check_config(self): for warn in required_opts: self.assertIn("CONFIG_{}".format(warn), fake_logger.output) - @mock.patch.object(snapcraft.ProjectOptions, "kernel_arch", new="not_arm") + @mock.patch.object(snapcraft_legacy.ProjectOptions, "kernel_arch", new="not_arm") def test_check_initrd(self): fake_logger = fixtures.FakeLogger(level=logging.WARNING) self.useFixture(fake_logger) @@ -521,7 +521,7 @@ def test_check_initrd(self): for module in kernel.required_boot: self.assertIn("CONFIG_{}".format(module.upper()), fake_logger.output) - @mock.patch.object(snapcraft.ProjectOptions, "kernel_arch", new="not_arm") + @mock.patch.object(snapcraft_legacy.ProjectOptions, "kernel_arch", new="not_arm") def test_build_with_kconfigfile_and_kconfigs(self): self.options.kconfigfile = "config" self.options.kconfigs = ["SOMETHING=y", "ACCEPT=n"] @@ -576,7 +576,7 @@ def test_build_with_kconfigfile_and_kconfigs(self): self.assertThat(config_contents, Equals(expected_config)) self._assert_common_assets(plugin.installdir) - @mock.patch.object(snapcraft.ProjectOptions, "kernel_arch", new="not_arm") + @mock.patch.object(snapcraft_legacy.ProjectOptions, "kernel_arch", new="not_arm") def test_build_with_defconfig_and_kconfigs(self): self.options.kdefconfig = ["defconfig"] self.options.kconfigs = ["SOMETHING=y", "ACCEPT=n"] @@ -638,7 +638,7 @@ def fake_defconfig(*args, **kwargs): self.assertThat(config_contents, Equals(expected_config)) self._assert_common_assets(plugin.installdir) - @mock.patch.object(snapcraft.ProjectOptions, "kernel_arch", new="not_arm") + @mock.patch.object(snapcraft_legacy.ProjectOptions, "kernel_arch", new="not_arm") def test_build_with_two_defconfigs(self): self.options.kdefconfig = ["defconfig", "defconfig2"] @@ -686,7 +686,7 @@ def fake_defconfig(*args, **kwargs): self.assertTrue(os.path.exists(config_file)) self._assert_common_assets(plugin.installdir) - @mock.patch.object(snapcraft.ProjectOptions, "kernel_arch", new="not_arm") + @mock.patch.object(snapcraft_legacy.ProjectOptions, "kernel_arch", new="not_arm") def test_build_with_kconfigfile_and_dtbs(self): self.options.kconfigfile = "config" with open(self.options.kconfigfile, "w") as f: @@ -753,7 +753,7 @@ def test_build_with_kconfigfile_and_dtbs_not_found(self): str(raised), Equals("No match for dtb 'fake-dtb.dtb' was found") ) - @mock.patch.object(snapcraft.ProjectOptions, "kernel_arch", new="not_arm") + @mock.patch.object(snapcraft_legacy.ProjectOptions, "kernel_arch", new="not_arm") def test_build_with_kconfigfile_and_modules(self): self.options.kconfigfile = "config" with open(self.options.kconfigfile, "w") as f: @@ -854,7 +854,7 @@ def __eq__(self, other): self.assertThat(config_contents, Equals("ACCEPT=y\n")) self._assert_common_assets(plugin.installdir) - @mock.patch.object(snapcraft.ProjectOptions, "kernel_arch", new="not_arm") + @mock.patch.object(snapcraft_legacy.ProjectOptions, "kernel_arch", new="not_arm") def test_build_with_kconfigfile_and_firmware(self): self.options.kconfigfile = "config" with open(self.options.kconfigfile, "w") as f: @@ -916,7 +916,7 @@ def fake_unpack(*args, **kwargs): os.path.exists(os.path.join(plugin.installdir, "firmware", "fake-fw-dir")) ) - @mock.patch.object(snapcraft.ProjectOptions, "kernel_arch", new="not_arm") + @mock.patch.object(snapcraft_legacy.ProjectOptions, "kernel_arch", new="not_arm") def test_build_with_kconfigfile_and_no_firmware(self): self.options.kconfigfile = "config" with open(self.options.kconfigfile, "w") as f: @@ -958,7 +958,7 @@ def test_build_with_kconfigfile_and_no_firmware(self): config_file = os.path.join(plugin.builddir, ".config") self.assertTrue(os.path.exists(config_file)) - @mock.patch.object(snapcraft.ProjectOptions, "kernel_arch", new="not_arm") + @mock.patch.object(snapcraft_legacy.ProjectOptions, "kernel_arch", new="not_arm") def test_build_with_kconfigflavour(self): arch = self.project.deb_arch branch = "master" @@ -1109,7 +1109,7 @@ def test_build_with_missing_system_map_fails(self): ) def test_enable_cross_compilation(self): - project = snapcraft.project.Project(target_deb_arch="arm64") + project = snapcraft_legacy.project.Project(target_deb_arch="arm64") project._snap_meta = meta.snap.Snap(name="test-snap", base="core18") plugin = kernel.KernelPlugin("test-part", self.options, project) @@ -1131,7 +1131,7 @@ def test_enable_cross_compilation(self): ) def test_override_cross_compile(self): - project = snapcraft.project.Project(target_deb_arch="arm64") + project = snapcraft_legacy.project.Project(target_deb_arch="arm64") project._snap_meta = meta.snap.Snap(name="test-snap", base="core18") plugin = kernel.KernelPlugin("test-part", self.options, project) @@ -1156,7 +1156,7 @@ def test_override_cross_compile(self): ) def test_override_cross_compile_empty(self): - project = snapcraft.project.Project(target_deb_arch="arm64") + project = snapcraft_legacy.project.Project(target_deb_arch="arm64") project._snap_meta = meta.snap.Snap(name="test-snap", base="core18") plugin = kernel.KernelPlugin("test-part", self.options, project) @@ -1180,7 +1180,7 @@ def test_override_cross_compile_empty(self): def test_kernel_image_target_as_map(self): self.options.kernel_image_target = {"arm64": "Image"} - project = snapcraft.project.Project(target_deb_arch="arm64") + project = snapcraft_legacy.project.Project(target_deb_arch="arm64") project._snap_meta = meta.snap.Snap(name="test-snap", base="core18") plugin = kernel.KernelPlugin("test-part", self.options, project) @@ -1189,7 +1189,7 @@ def test_kernel_image_target_as_map(self): def test_kernel_image_target_as_string(self): self.options.kernel_image_target = "Image" - project = snapcraft.project.Project(target_deb_arch="arm64") + project = snapcraft_legacy.project.Project(target_deb_arch="arm64") project._snap_meta = meta.snap.Snap(name="test-snap", base="core18") plugin = kernel.KernelPlugin("test-part", self.options, project) @@ -1208,7 +1208,7 @@ class Options: kernel_device_trees = [] kernel_initrd_compression = "gz" - project = snapcraft.project.Project(target_deb_arch="arm64") + project = snapcraft_legacy.project.Project(target_deb_arch="arm64") project._snap_meta = meta.snap.Snap(name="test-snap", base="core18") plugin = kernel.KernelPlugin("test-part", self.options, project) @@ -1266,7 +1266,7 @@ class Options: kernel_device_trees = [] kernel_initrd_compression = "gz" - project = snapcraft.project.Project(target_deb_arch=deb_arch) + project = snapcraft_legacy.project.Project(target_deb_arch=deb_arch) project._snap_meta = meta.snap.Snap(name="test-snap", base="core18") plugin = kernel.KernelPlugin("test-part", Options(), project) diff --git a/tests/unit/plugins/v1/test_make.py b/tests/unit/plugins/v1/test_make.py index a61036ff52..e600a5297e 100644 --- a/tests/unit/plugins/v1/test_make.py +++ b/tests/unit/plugins/v1/test_make.py @@ -19,8 +19,8 @@ from testtools.matchers import Equals, HasLength -from snapcraft.internal import errors -from snapcraft.plugins.v1 import make +from snapcraft_legacy.internal import errors +from snapcraft_legacy.plugins.v1 import make from . import PluginsV1BaseTestCase @@ -217,8 +217,8 @@ def test_build_empty_install_var(self, run_mock): ) @mock.patch.object(make.MakePlugin, "run") - @mock.patch("snapcraft.file_utils.link_or_copy_tree") - @mock.patch("snapcraft.file_utils.link_or_copy") + @mock.patch("snapcraft_legacy.file_utils.link_or_copy_tree") + @mock.patch("snapcraft_legacy.file_utils.link_or_copy") def test_build_artifacts(self, link_or_copy_mock, link_or_copy_tree_mock, run_mock): self.options.artifacts = ["dir_artifact", "file_artifact"] plugin = make.MakePlugin("test-part", self.options, self.project) diff --git a/tests/unit/plugins/v1/test_maven.py b/tests/unit/plugins/v1/test_maven.py index 0c08feaf87..f6efd00a6c 100644 --- a/tests/unit/plugins/v1/test_maven.py +++ b/tests/unit/plugins/v1/test_maven.py @@ -26,10 +26,10 @@ import pytest from testtools.matchers import Equals, FileExists, HasLength -from snapcraft.internal import errors -from snapcraft.internal.meta.snap import Snap -from snapcraft.plugins.v1 import maven -from snapcraft.project import Project +from snapcraft_legacy.internal import errors +from snapcraft_legacy.internal.meta.snap import Snap +from snapcraft_legacy.plugins.v1 import maven +from snapcraft_legacy.project import Project from tests import unit from . import PluginsV1BaseTestCase @@ -276,11 +276,11 @@ class Options: self.options = Options() - patcher = mock.patch("snapcraft.internal.common.run") + patcher = mock.patch("snapcraft_legacy.internal.common.run") self.run_mock = patcher.start() self.addCleanup(patcher.stop) - patcher = mock.patch("snapcraft.sources.Tar") + patcher = mock.patch("snapcraft_legacy.sources.Tar") self.tar_mock = patcher.start() self.addCleanup(patcher.stop) diff --git a/tests/unit/plugins/v1/test_meson.py b/tests/unit/plugins/v1/test_meson.py index a2c670b468..ed61c7d401 100644 --- a/tests/unit/plugins/v1/test_meson.py +++ b/tests/unit/plugins/v1/test_meson.py @@ -20,8 +20,8 @@ from testtools.matchers import Equals, HasLength -from snapcraft.internal import errors -from snapcraft.plugins.v1 import meson +from snapcraft_legacy.internal import errors +from snapcraft_legacy.plugins.v1 import meson from tests import unit from . import PluginsV1BaseTestCase diff --git a/tests/unit/plugins/v1/test_nil.py b/tests/unit/plugins/v1/test_nil.py index f11288b0b6..e58c656198 100644 --- a/tests/unit/plugins/v1/test_nil.py +++ b/tests/unit/plugins/v1/test_nil.py @@ -16,7 +16,7 @@ from testtools.matchers import Equals -from snapcraft.plugins.v1.nil import NilPlugin +from snapcraft_legacy.plugins.v1.nil import NilPlugin from tests import unit diff --git a/tests/unit/plugins/v1/test_nodejs.py b/tests/unit/plugins/v1/test_nodejs.py index 3fa83f7043..6a605b5ece 100644 --- a/tests/unit/plugins/v1/test_nodejs.py +++ b/tests/unit/plugins/v1/test_nodejs.py @@ -25,8 +25,8 @@ import pytest from testtools.matchers import Equals, FileExists, HasLength -from snapcraft.internal import errors -from snapcraft.plugins.v1 import nodejs +from snapcraft_legacy.internal import errors +from snapcraft_legacy.plugins.v1 import nodejs from tests import fixture_setup, unit from . import PluginsV1BaseTestCase @@ -48,16 +48,16 @@ class Options: # always have a package.json stub under source open("package.json", "w").close() - patcher = mock.patch("snapcraft.internal.common.run") + patcher = mock.patch("snapcraft_legacy.internal.common.run") self.run_mock = patcher.start() self.addCleanup(patcher.stop) - patcher = mock.patch("snapcraft.internal.common.run_output") + patcher = mock.patch("snapcraft_legacy.internal.common.run_output") self.run_output_mock = patcher.start() self.addCleanup(patcher.stop) self.run_output_mock.return_value = '{"dependencies": []}' - patcher = mock.patch("snapcraft.sources.Tar") + patcher = mock.patch("snapcraft_legacy.sources.Tar") self.tar_mock = patcher.start() self.addCleanup(patcher.stop) @@ -708,7 +708,7 @@ def test_get_nodejs_release(self, deb_arch, engine, expected_url): class NodePluginUnsupportedArchTest(NodePluginBaseTest): - @mock.patch("snapcraft.project.Project.deb_arch", "ppcel64") + @mock.patch("snapcraft_legacy.project.Project.deb_arch", "ppcel64") def test_unsupported_arch_raises_exception(self): raised = self.assertRaises( errors.SnapcraftEnvironmentError, diff --git a/tests/unit/plugins/v1/test_plainbox_provider.py b/tests/unit/plugins/v1/test_plainbox_provider.py index 948d61d881..81306db6f0 100644 --- a/tests/unit/plugins/v1/test_plainbox_provider.py +++ b/tests/unit/plugins/v1/test_plainbox_provider.py @@ -19,8 +19,8 @@ from testtools.matchers import Equals, HasLength -from snapcraft.internal import errors -from snapcraft.plugins.v1 import plainbox_provider +from snapcraft_legacy.internal import errors +from snapcraft_legacy.plugins.v1 import plainbox_provider from tests import fixture_setup, unit from . import PluginsV1BaseTestCase diff --git a/tests/unit/plugins/v1/test_python.py b/tests/unit/plugins/v1/test_python.py index d519518fd6..cf95d7a64c 100644 --- a/tests/unit/plugins/v1/test_python.py +++ b/tests/unit/plugins/v1/test_python.py @@ -21,8 +21,8 @@ import jsonschema from testtools.matchers import Equals, HasLength -from snapcraft.internal import errors -from snapcraft.plugins.v1 import python +from snapcraft_legacy.internal import errors +from snapcraft_legacy.plugins.v1 import python from tests import fixture_setup, unit from . import PluginsV1BaseTestCase @@ -71,7 +71,7 @@ class Options: self.options = Options() - patcher = mock.patch("snapcraft.plugins.v1._python.Pip") + patcher = mock.patch("snapcraft_legacy.plugins.v1._python.Pip") self.mock_pip = patcher.start() self.addCleanup(patcher.stop) diff --git a/tests/unit/plugins/v1/test_qmake.py b/tests/unit/plugins/v1/test_qmake.py index f8b0de522a..2f450c01a1 100644 --- a/tests/unit/plugins/v1/test_qmake.py +++ b/tests/unit/plugins/v1/test_qmake.py @@ -19,8 +19,8 @@ from testtools.matchers import Equals, HasLength -from snapcraft.internal import errors -from snapcraft.plugins.v1 import qmake +from snapcraft_legacy.internal import errors +from snapcraft_legacy.plugins.v1 import qmake from . import PluginsV1BaseTestCase @@ -29,7 +29,7 @@ class QMakeTestCase(PluginsV1BaseTestCase): def setUp(self): super().setUp() - patcher = mock.patch("snapcraft.internal.common.run") + patcher = mock.patch("snapcraft_legacy.internal.common.run") self.run_mock = patcher.start() self.addCleanup(patcher.stop) diff --git a/tests/unit/plugins/v1/test_ruby.py b/tests/unit/plugins/v1/test_ruby.py index 13ce7df700..82295aef01 100644 --- a/tests/unit/plugins/v1/test_ruby.py +++ b/tests/unit/plugins/v1/test_ruby.py @@ -19,9 +19,9 @@ from testtools.matchers import Equals, HasLength -import snapcraft -from snapcraft.internal import errors -from snapcraft.plugins.v1 import ruby +import snapcraft_legacy +from snapcraft_legacy.internal import errors +from snapcraft_legacy.plugins.v1 import ruby from . import PluginsV1BaseTestCase @@ -30,7 +30,7 @@ class RubyPluginTestCase(PluginsV1BaseTestCase): def setUp(self): super().setUp() - class Options(snapcraft.ProjectOptions): + class Options(snapcraft_legacy.ProjectOptions): source = "." ruby_version = "2.4.2" gems = [] @@ -126,7 +126,7 @@ def test_env_with_multiple_ruby(self): os.makedirs(os.path.join(part_dir, "lib", "ruby", "gems", "test-version2")) error = self.assertRaises( - snapcraft.internal.errors.SnapcraftEnvironmentError, + snapcraft_legacy.internal.errors.SnapcraftEnvironmentError, plugin.env, "test-part-path", ) @@ -149,7 +149,7 @@ def test_env_with_rbconfigs(self): open(os.path.join(real_arch_libdir1, "rbconfig.rb"), "w").close() error = self.assertRaises( - snapcraft.internal.errors.SnapcraftEnvironmentError, + snapcraft_legacy.internal.errors.SnapcraftEnvironmentError, plugin.env, "test-part-path", ) @@ -185,7 +185,7 @@ def test_pull_installs_ruby(self): with mock.patch.multiple( plugin, _ruby_tar=mock.DEFAULT, _gem_install=mock.DEFAULT ) as mocks: - with mock.patch("snapcraft.internal.common.run") as mock_run: + with mock.patch("snapcraft_legacy.internal.common.run") as mock_run: plugin.pull() ruby_expected_dir = os.path.join(self.path, "parts", "test-part", "ruby") @@ -218,7 +218,7 @@ def test_pull_installs_gems_without_bundler(self): with mock.patch.multiple( plugin, _ruby_tar=mock.DEFAULT, _ruby_install=mock.DEFAULT ): - with mock.patch("snapcraft.internal.common.run") as mock_run: + with mock.patch("snapcraft_legacy.internal.common.run") as mock_run: plugin.pull() test_part_dir = os.path.join(self.path, "parts", "test-part") @@ -243,7 +243,7 @@ def test_pull_with_bundler(self): with mock.patch.multiple( plugin, _ruby_tar=mock.DEFAULT, _ruby_install=mock.DEFAULT ): - with mock.patch("snapcraft.internal.common.run") as mock_run: + with mock.patch("snapcraft_legacy.internal.common.run") as mock_run: plugin.pull() test_part_dir = os.path.join(self.path, "parts", "test-part") mock_run.assert_has_calls( diff --git a/tests/unit/plugins/v1/test_rust.py b/tests/unit/plugins/v1/test_rust.py index c06da3734e..3e9da06475 100644 --- a/tests/unit/plugins/v1/test_rust.py +++ b/tests/unit/plugins/v1/test_rust.py @@ -25,9 +25,9 @@ import toml from testtools.matchers import Contains, Equals, FileExists, Not -import snapcraft -from snapcraft.internal import errors, meta -from snapcraft.plugins.v1 import rust +import snapcraft_legacy +from snapcraft_legacy.internal import errors, meta +from snapcraft_legacy.plugins.v1 import rust from tests import fixture_setup, unit from . import PluginsV1BaseTestCase @@ -49,11 +49,11 @@ class Options: self.options = Options() - patcher = mock.patch("snapcraft.internal.common.run") + patcher = mock.patch("snapcraft_legacy.internal.common.run") self.run_mock = patcher.start() self.addCleanup(patcher.stop) - patcher = mock.patch("snapcraft.internal.common.run_output") + patcher = mock.patch("snapcraft_legacy.internal.common.run_output") patcher.start() self.addCleanup(patcher.stop) @@ -150,7 +150,7 @@ class TestRustPluginCrossCompile: ("s390x", dict(deb_arch="s390x", target="s390x-unknown-linux-gnu")), ] - @mock.patch("snapcraft.internal.sources._script.Script.download") + @mock.patch("snapcraft_legacy.internal.sources._script.Script.download") def test_cross_compile( self, mock_download, @@ -162,8 +162,10 @@ def test_cross_compile( deb_arch, target, ): - monkeypatch.setattr(snapcraft.project.Project, "is_cross_compiling", True) - project = snapcraft.project.Project(target_deb_arch=deb_arch) + monkeypatch.setattr( + snapcraft_legacy.project.Project, "is_cross_compiling", True + ) + project = snapcraft_legacy.project.Project(target_deb_arch=deb_arch) project._snap_meta = meta.snap.Snap(name="test-snap", base="core18") plugin = rust.RustPlugin("test-part", options, project) @@ -493,7 +495,7 @@ def test_pull_with_source_and_source_subdir(self, script_mock): ] ) - @mock.patch("snapcraft.ProjectOptions.deb_arch", "fantasy-arch") + @mock.patch("snapcraft_legacy.ProjectOptions.deb_arch", "fantasy-arch") def test_unsupported_target_arch_raises_exception(self): self.assertRaises(errors.SnapcraftEnvironmentError, self.plugin._get_target) diff --git a/tests/unit/plugins/v1/test_scons.py b/tests/unit/plugins/v1/test_scons.py index 3c2fb8d231..add403075a 100644 --- a/tests/unit/plugins/v1/test_scons.py +++ b/tests/unit/plugins/v1/test_scons.py @@ -19,8 +19,8 @@ from testtools.matchers import Equals, HasLength -from snapcraft.internal import errors -from snapcraft.plugins.v1 import scons +from snapcraft_legacy.internal import errors +from snapcraft_legacy.plugins.v1 import scons from tests import unit from . import PluginsV1BaseTestCase diff --git a/tests/unit/plugins/v1/test_waf.py b/tests/unit/plugins/v1/test_waf.py index 4e8556f985..ef6c295a51 100644 --- a/tests/unit/plugins/v1/test_waf.py +++ b/tests/unit/plugins/v1/test_waf.py @@ -20,9 +20,9 @@ import pytest from testtools.matchers import Equals, HasLength -from snapcraft.internal import errors, meta -from snapcraft.plugins.v1 import waf -from snapcraft.project import Project +from snapcraft_legacy.internal import errors, meta +from snapcraft_legacy.plugins.v1 import waf +from snapcraft_legacy.project import Project from tests import unit from . import PluginsV1BaseTestCase diff --git a/tests/unit/plugins/v2/test_autotools.py b/tests/unit/plugins/v2/test_autotools.py index d0412cd52e..21a1f89132 100644 --- a/tests/unit/plugins/v2/test_autotools.py +++ b/tests/unit/plugins/v2/test_autotools.py @@ -13,7 +13,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -from snapcraft.plugins.v2.autotools import AutotoolsPlugin +from snapcraft_legacy.plugins.v2.autotools import AutotoolsPlugin def test_schema(): diff --git a/tests/unit/plugins/v2/test_catkin.py b/tests/unit/plugins/v2/test_catkin.py index d8a19765f0..c0d229867e 100644 --- a/tests/unit/plugins/v2/test_catkin.py +++ b/tests/unit/plugins/v2/test_catkin.py @@ -17,8 +17,8 @@ import os import sys -import snapcraft.plugins.v2._ros as _ros -import snapcraft.plugins.v2.catkin as catkin +import snapcraft_legacy.plugins.v2._ros as _ros +import snapcraft_legacy.plugins.v2.catkin as catkin def test_schema(): diff --git a/tests/unit/plugins/v2/test_catkin_tools.py b/tests/unit/plugins/v2/test_catkin_tools.py index 9fc3202ae3..f6ef5769f9 100644 --- a/tests/unit/plugins/v2/test_catkin_tools.py +++ b/tests/unit/plugins/v2/test_catkin_tools.py @@ -17,8 +17,8 @@ import os import sys -import snapcraft.plugins.v2._ros as _ros -import snapcraft.plugins.v2.catkin_tools as catkin_tools +import snapcraft_legacy.plugins.v2._ros as _ros +import snapcraft_legacy.plugins.v2.catkin_tools as catkin_tools def test_schema(): diff --git a/tests/unit/plugins/v2/test_cmake.py b/tests/unit/plugins/v2/test_cmake.py index 420be23386..16d35de844 100644 --- a/tests/unit/plugins/v2/test_cmake.py +++ b/tests/unit/plugins/v2/test_cmake.py @@ -14,7 +14,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -from snapcraft.plugins.v2.cmake import CMakePlugin +from snapcraft_legacy.plugins.v2.cmake import CMakePlugin def test_schema(): diff --git a/tests/unit/plugins/v2/test_colcon.py b/tests/unit/plugins/v2/test_colcon.py index 262f90bd68..605f186f2b 100644 --- a/tests/unit/plugins/v2/test_colcon.py +++ b/tests/unit/plugins/v2/test_colcon.py @@ -17,8 +17,8 @@ import os import sys -import snapcraft.plugins.v2._ros as _ros -import snapcraft.plugins.v2.colcon as colcon +import snapcraft_legacy.plugins.v2._ros as _ros +import snapcraft_legacy.plugins.v2.colcon as colcon def test_schema(): diff --git a/tests/unit/plugins/v2/test_conda.py b/tests/unit/plugins/v2/test_conda.py index d127f465d6..7b4108feee 100644 --- a/tests/unit/plugins/v2/test_conda.py +++ b/tests/unit/plugins/v2/test_conda.py @@ -18,7 +18,7 @@ import pytest -from snapcraft.plugins.v2.conda import ( +from snapcraft_legacy.plugins.v2.conda import ( CondaPlugin, ArchitectureMissing, _get_miniconda_source, diff --git a/tests/unit/plugins/v2/test_dump.py b/tests/unit/plugins/v2/test_dump.py index db22a56bad..e991eae4c9 100644 --- a/tests/unit/plugins/v2/test_dump.py +++ b/tests/unit/plugins/v2/test_dump.py @@ -17,7 +17,7 @@ from testtools import TestCase from testtools.matchers import Equals -from snapcraft.plugins.v2.dump import DumpPlugin +from snapcraft_legacy.plugins.v2.dump import DumpPlugin class DumpPluginTest(TestCase): diff --git a/tests/unit/plugins/v2/test_go.py b/tests/unit/plugins/v2/test_go.py index d804fe6809..8b8c3f151b 100644 --- a/tests/unit/plugins/v2/test_go.py +++ b/tests/unit/plugins/v2/test_go.py @@ -17,7 +17,7 @@ from testtools import TestCase from testtools.matchers import Equals -from snapcraft.plugins.v2.go import GoPlugin +from snapcraft_legacy.plugins.v2.go import GoPlugin class GoPluginTest(TestCase): diff --git a/tests/unit/plugins/v2/test_make.py b/tests/unit/plugins/v2/test_make.py index b3fc261f94..58998a26ef 100644 --- a/tests/unit/plugins/v2/test_make.py +++ b/tests/unit/plugins/v2/test_make.py @@ -17,7 +17,7 @@ from testtools import TestCase from testtools.matchers import Equals -from snapcraft.plugins.v2.make import MakePlugin +from snapcraft_legacy.plugins.v2.make import MakePlugin class MakePluginTest(TestCase): diff --git a/tests/unit/plugins/v2/test_meson.py b/tests/unit/plugins/v2/test_meson.py index f4737948ff..2eab5717c5 100644 --- a/tests/unit/plugins/v2/test_meson.py +++ b/tests/unit/plugins/v2/test_meson.py @@ -17,7 +17,7 @@ from testtools import TestCase from testtools.matchers import Equals -from snapcraft.plugins.v2.meson import MesonPlugin +from snapcraft_legacy.plugins.v2.meson import MesonPlugin class MesonPluginTest(TestCase): diff --git a/tests/unit/plugins/v2/test_nil.py b/tests/unit/plugins/v2/test_nil.py index 8fe0b18526..fd90dd6986 100644 --- a/tests/unit/plugins/v2/test_nil.py +++ b/tests/unit/plugins/v2/test_nil.py @@ -17,7 +17,7 @@ from testtools import TestCase from testtools.matchers import Equals -from snapcraft.plugins.v2.nil import NilPlugin +from snapcraft_legacy.plugins.v2.nil import NilPlugin class NilPluginTest(TestCase): diff --git a/tests/unit/plugins/v2/test_npm.py b/tests/unit/plugins/v2/test_npm.py index 80ce344412..ecada6cd45 100644 --- a/tests/unit/plugins/v2/test_npm.py +++ b/tests/unit/plugins/v2/test_npm.py @@ -20,7 +20,7 @@ from testtools import TestCase from testtools.matchers import Equals -from snapcraft.plugins.v2.npm import NpmPlugin +from snapcraft_legacy.plugins.v2.npm import NpmPlugin class NpmPluginTest(TestCase): diff --git a/tests/unit/plugins/v2/test_python.py b/tests/unit/plugins/v2/test_python.py index abe5f5f6e5..c56eae0987 100644 --- a/tests/unit/plugins/v2/test_python.py +++ b/tests/unit/plugins/v2/test_python.py @@ -16,7 +16,7 @@ from textwrap import dedent -from snapcraft.plugins.v2.python import PythonPlugin +from snapcraft_legacy.plugins.v2.python import PythonPlugin def test_schema(): diff --git a/tests/unit/plugins/v2/test_qmake.py b/tests/unit/plugins/v2/test_qmake.py index 5b13805abc..f7d298906a 100644 --- a/tests/unit/plugins/v2/test_qmake.py +++ b/tests/unit/plugins/v2/test_qmake.py @@ -14,7 +14,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -from snapcraft.plugins.v2.qmake import QMakePlugin +from snapcraft_legacy.plugins.v2.qmake import QMakePlugin def test_schema(): diff --git a/tests/unit/plugins/v2/test_rust.py b/tests/unit/plugins/v2/test_rust.py index cd06c9bfd1..d3ab8832b8 100644 --- a/tests/unit/plugins/v2/test_rust.py +++ b/tests/unit/plugins/v2/test_rust.py @@ -19,7 +19,7 @@ from testtools import TestCase from testtools.matchers import Equals -from snapcraft.plugins.v2.rust import RustPlugin +from snapcraft_legacy.plugins.v2.rust import RustPlugin class RustPluginTest(TestCase): diff --git a/tests/unit/project/__init__.py b/tests/unit/project/__init__.py index 48b4a2fc2c..9a9a50efc3 100644 --- a/tests/unit/project/__init__.py +++ b/tests/unit/project/__init__.py @@ -13,8 +13,8 @@ # # You should have received a copy of the GNU General Public License # along with this program. If not, see . -import snapcraft.yaml_utils.errors -from snapcraft.project import Project as _Project +import snapcraft_legacy.yaml_utils.errors +from snapcraft_legacy.project import Project as _Project from tests import unit @@ -37,6 +37,6 @@ def assertValidationRaises(self, snapcraft_yaml): project = self.make_snapcraft_project(snapcraft_yaml) return self.assertRaises( - snapcraft.yaml_utils.errors.YamlValidationError, + snapcraft_legacy.yaml_utils.errors.YamlValidationError, project.info.validate_raw_snapcraft, ) diff --git a/tests/unit/project/test_errors.py b/tests/unit/project/test_errors.py index 3066343d7d..0b664765b2 100644 --- a/tests/unit/project/test_errors.py +++ b/tests/unit/project/test_errors.py @@ -13,7 +13,7 @@ # # You should have received a copy of the GNU General Public License # along with this program. If not, see . -from snapcraft.project import errors +from snapcraft_legacy.project import errors class TestErrorFormatting: diff --git a/tests/unit/project/test_get_snapcraft.py b/tests/unit/project/test_get_snapcraft.py index 5e1abbedfa..9d0ce30bb0 100644 --- a/tests/unit/project/test_get_snapcraft.py +++ b/tests/unit/project/test_get_snapcraft.py @@ -18,7 +18,7 @@ import pytest -from snapcraft.project import errors, get_snapcraft_yaml +from snapcraft_legacy.project import errors, get_snapcraft_yaml @pytest.fixture( diff --git a/tests/unit/project/test_project.py b/tests/unit/project/test_project.py index 9d5ab54a09..28ca90b846 100644 --- a/tests/unit/project/test_project.py +++ b/tests/unit/project/test_project.py @@ -20,7 +20,7 @@ import pytest -from snapcraft.project import Project +from snapcraft_legacy.project import Project def test_project_with_arguments(): diff --git a/tests/unit/project/test_project_info.py b/tests/unit/project/test_project_info.py index c8252191a9..b34249d53d 100644 --- a/tests/unit/project/test_project_info.py +++ b/tests/unit/project/test_project_info.py @@ -19,8 +19,8 @@ import pytest from testtools.matchers import Equals, Is, MatchesRegex -import snapcraft.yaml_utils.errors -from snapcraft.project._project_info import ProjectInfo +import snapcraft_legacy.yaml_utils.errors +from snapcraft_legacy.project._project_info import ProjectInfo from tests import unit @@ -49,7 +49,7 @@ def test_empty_yaml(self): snapcraft_yaml_file_path = self.make_snapcraft_yaml("") raised = self.assertRaises( - snapcraft.yaml_utils.errors.YamlValidationError, + snapcraft_legacy.yaml_utils.errors.YamlValidationError, ProjectInfo, snapcraft_yaml_file_path=snapcraft_yaml_file_path, ) @@ -90,7 +90,7 @@ def test_name_is_required(self): ) raised = self.assertRaises( - snapcraft.yaml_utils.errors.YamlValidationError, + snapcraft_legacy.yaml_utils.errors.YamlValidationError, ProjectInfo, snapcraft_yaml_file_path=snapcraft_yaml_file_path, ) @@ -167,7 +167,7 @@ def test_tab_in_yaml(self): ) raised = self.assertRaises( - snapcraft.yaml_utils.errors.YamlValidationError, + snapcraft_legacy.yaml_utils.errors.YamlValidationError, ProjectInfo, snapcraft_yaml_file_path=snapcraft_yaml_file_path, ) @@ -195,7 +195,7 @@ def test_invalid_yaml_invalid_unicode_chars(self): ) raised = self.assertRaises( - snapcraft.yaml_utils.errors.YamlValidationError, + snapcraft_legacy.yaml_utils.errors.YamlValidationError, ProjectInfo, snapcraft_yaml_file_path=snapcraft_yaml_file_path, ) @@ -226,7 +226,7 @@ def test_invalid_yaml_unhashable(self): ) raised = self.assertRaises( - snapcraft.yaml_utils.errors.YamlValidationError, + snapcraft_legacy.yaml_utils.errors.YamlValidationError, ProjectInfo, snapcraft_yaml_file_path=snapcraft_yaml_file_path, ) @@ -247,7 +247,7 @@ def test_invalid_yaml_list_in_mapping(self): ) raised = self.assertRaises( - snapcraft.yaml_utils.errors.YamlValidationError, + snapcraft_legacy.yaml_utils.errors.YamlValidationError, ProjectInfo, snapcraft_yaml_file_path=snapcraft_yaml_file_path, ) diff --git a/tests/unit/project/test_sanity_checks.py b/tests/unit/project/test_sanity_checks.py index 2676d1071f..2658899fbf 100644 --- a/tests/unit/project/test_sanity_checks.py +++ b/tests/unit/project/test_sanity_checks.py @@ -20,9 +20,9 @@ import pytest -import snapcraft.internal.errors -from snapcraft.project import Project, errors -from snapcraft.project._sanity_checks import conduct_project_sanity_check +import snapcraft_legacy.internal.errors +from snapcraft_legacy.project import Project, errors +from snapcraft_legacy.project._sanity_checks import conduct_project_sanity_check @pytest.fixture @@ -104,7 +104,9 @@ def test_icon(tmp_work_path): ) # Test without icon raises error - with pytest.raises(snapcraft.internal.errors.SnapcraftEnvironmentError) as exc_info: + with pytest.raises( + snapcraft_legacy.internal.errors.SnapcraftEnvironmentError + ) as exc_info: conduct_project_sanity_check(project) assert exc_info.value.get_brief() == "Specified icon 'foo.png' does not exist." diff --git a/tests/unit/project/test_schema.py b/tests/unit/project/test_schema.py index 923889af4b..22338d3934 100644 --- a/tests/unit/project/test_schema.py +++ b/tests/unit/project/test_schema.py @@ -22,9 +22,9 @@ from testtools.matchers import Contains, Equals # required for schema format checkers -import snapcraft.internal.project_loader._config # noqa: F401 -import snapcraft.yaml_utils.errors -from snapcraft.project._schema import Validator +import snapcraft_legacy.internal.project_loader._config # noqa: F401 +import snapcraft_legacy.yaml_utils.errors +from snapcraft_legacy.project._schema import Validator from . import ProjectBaseTest @@ -58,7 +58,7 @@ class ValidationTest(ValidationBaseTest): def test_summary_too_long(self): self.data["summary"] = "a" * 80 raised = self.assertRaises( - snapcraft.yaml_utils.errors.YamlValidationError, + snapcraft_legacy.yaml_utils.errors.YamlValidationError, Validator(self.data).validate, ) @@ -72,7 +72,7 @@ def test_apps_required_properties(self): self.data["apps"] = {"service1": {}} raised = self.assertRaises( - snapcraft.yaml_utils.errors.YamlValidationError, + snapcraft_legacy.yaml_utils.errors.YamlValidationError, Validator(self.data).validate, ) @@ -87,9 +87,13 @@ def test_schema_file_not_found(self): mock_the_open = mock.mock_open() mock_the_open.side_effect = FileNotFoundError() - with mock.patch("snapcraft.project._schema.open", mock_the_open, create=True): + with mock.patch( + "snapcraft_legacy.project._schema.open", mock_the_open, create=True + ): raised = self.assertRaises( - snapcraft.yaml_utils.errors.YamlValidationError, Validator, self.data + snapcraft_legacy.yaml_utils.errors.YamlValidationError, + Validator, + self.data, ) expected_message = "snapcraft validation file is missing from installation path" @@ -180,7 +184,7 @@ def test_invalid_restart_condition(self): } raised = self.assertRaises( - snapcraft.yaml_utils.errors.YamlValidationError, + snapcraft_legacy.yaml_utils.errors.YamlValidationError, Validator(self.data).validate, ) @@ -199,7 +203,7 @@ def test_missing_required_property_and_missing_adopt_info(self): del self.data["adopt-info"] raised = self.assertRaises( - snapcraft.yaml_utils.errors.YamlValidationError, + snapcraft_legacy.yaml_utils.errors.YamlValidationError, Validator(self.data).validate, ) @@ -218,7 +222,7 @@ def test_invalid_install_mode(self): } raised = self.assertRaises( - snapcraft.yaml_utils.errors.YamlValidationError, + snapcraft_legacy.yaml_utils.errors.YamlValidationError, Validator(self.data).validate, ) @@ -241,7 +245,7 @@ def test_invalid_install_mode(self): def test_daemon_dependency(data, option, value): data["apps"] = {"service1": {"command": "binary1", option: value}} - with pytest.raises(snapcraft.yaml_utils.errors.YamlValidationError) as error: + with pytest.raises(snapcraft_legacy.yaml_utils.errors.YamlValidationError) as error: Validator(data).validate() assert str(error.value).endswith( @@ -254,7 +258,7 @@ def test_daemon_dependency(data, option, value): def test_required_properties(data, key): del data[key] - with pytest.raises(snapcraft.yaml_utils.errors.YamlValidationError) as error: + with pytest.raises(snapcraft_legacy.yaml_utils.errors.YamlValidationError) as error: Validator(data).validate() assert f"{key!r} is a required property" in str(error.value) @@ -309,7 +313,9 @@ class TestInvalidNames: def test(self, data, name, err): data["name"] = name - with pytest.raises(snapcraft.yaml_utils.errors.YamlValidationError) as error: + with pytest.raises( + snapcraft_legacy.yaml_utils.errors.YamlValidationError + ) as error: Validator(data).validate() assert str(error.value).endswith( @@ -341,7 +347,7 @@ def test_valid_types(data, snap_type): def test_invalid_types(data, snap_type): data["type"] = snap_type - with pytest.raises(snapcraft.yaml_utils.errors.YamlValidationError): + with pytest.raises(snapcraft_legacy.yaml_utils.errors.YamlValidationError): Validator(data).validate() @@ -355,7 +361,7 @@ def test_type_base_and_no_base(data): def test_type_base_and_base(data): data["type"] = "base" - with pytest.raises(snapcraft.yaml_utils.errors.YamlValidationError) as error: + with pytest.raises(snapcraft_legacy.yaml_utils.errors.YamlValidationError) as error: Validator(data).validate() assert _BASE_TYPE_MSG in str(error.value) @@ -440,7 +446,7 @@ def test_valid_app_names(data, name): def test_invalid_app_names(data, name): data["apps"] = {name: {"command": "1"}} - with pytest.raises(snapcraft.yaml_utils.errors.YamlValidationError) as error: + with pytest.raises(snapcraft_legacy.yaml_utils.errors.YamlValidationError) as error: Validator(data).validate() expected_message = ( @@ -486,7 +492,7 @@ def test_valid_refresh_modes(data, mode): def test_refresh_mode_daemon_missing_errors(data, mode): data["apps"] = {"service1": {"command": "binary1", "refresh-mode": mode}} - with pytest.raises(snapcraft.yaml_utils.errors.YamlValidationError): + with pytest.raises(snapcraft_legacy.yaml_utils.errors.YamlValidationError): Validator(data).validate() @@ -514,7 +520,7 @@ def test_valid_modes(data, mode): def test_daemon_missing_errors(data, mode): data["apps"] = {"service1": {"command": "binary1", "stop-mode": mode}} - with pytest.raises(snapcraft.yaml_utils.errors.YamlValidationError): + with pytest.raises(snapcraft_legacy.yaml_utils.errors.YamlValidationError): Validator(data).validate() @@ -549,7 +555,7 @@ def test_daemon_missing_errors(data, mode): def test_invalid_hook_names(data, name): data["hooks"] = {name: {"plugs": ["network"]}} - with pytest.raises(snapcraft.yaml_utils.errors.YamlValidationError) as error: + with pytest.raises(snapcraft_legacy.yaml_utils.errors.YamlValidationError) as error: Validator(data).validate() expected_message = ( @@ -567,7 +573,7 @@ def test_invalid_hook_names(data, name): def test_invalid_part_names(data, name): data["parts"] = {name: {"plugin": "nil"}} - with pytest.raises(snapcraft.yaml_utils.errors.YamlValidationError) as error: + with pytest.raises(snapcraft_legacy.yaml_utils.errors.YamlValidationError) as error: Validator(data).validate() expected_message = ( @@ -699,7 +705,9 @@ class TestInvalidArchitectures: def test(self, data, architectures, message): data["architectures"] = architectures - with pytest.raises(snapcraft.yaml_utils.errors.YamlValidationError) as error: + with pytest.raises( + snapcraft_legacy.yaml_utils.errors.YamlValidationError + ) as error: Validator(data).validate() assert message in str(error.value) @@ -758,7 +766,7 @@ def test_valid_title(data, title): def test_invalid_title(data, title, error_template): data["title"] = title - with pytest.raises(snapcraft.yaml_utils.errors.YamlValidationError) as error: + with pytest.raises(snapcraft_legacy.yaml_utils.errors.YamlValidationError) as error: Validator(data).validate() assert _EXPECTED_ERROR_TEMPLATE[error_template].format(title) in str(error.value) @@ -881,7 +889,7 @@ def test_valid_version(data, version): def test_invalid_version(data, version): data["version"] = version - with pytest.raises(snapcraft.yaml_utils.errors.YamlValidationError) as error: + with pytest.raises(snapcraft_legacy.yaml_utils.errors.YamlValidationError) as error: Validator(data).validate() expected_message = ( @@ -899,7 +907,7 @@ def test_invalid_version(data, version): def test_invalid_version_type(data): data["version"] = 0.1 - with pytest.raises(snapcraft.yaml_utils.errors.YamlValidationError) as error: + with pytest.raises(snapcraft_legacy.yaml_utils.errors.YamlValidationError) as error: Validator(data).validate() expected_message = ( @@ -916,7 +924,7 @@ def test_invalid_version_type(data): def test_invalid_version_length(data): data["version"] = "this.is.a.really.too.long.version" - with pytest.raises(snapcraft.yaml_utils.errors.YamlValidationError) as error: + with pytest.raises(snapcraft_legacy.yaml_utils.errors.YamlValidationError) as error: Validator(data).validate() expected_message = ( @@ -1018,7 +1026,7 @@ def test_valid_compression(data, compression): def test_invalid_compression(data, compression): data["compression"] = compression - with pytest.raises(snapcraft.yaml_utils.errors.YamlValidationError) as error: + with pytest.raises(snapcraft_legacy.yaml_utils.errors.YamlValidationError) as error: Validator(data).validate() expected_message = ( @@ -1039,7 +1047,7 @@ def test_valid_confinement(data, confinement): def test_invalid_confinement(data, confinement): data["confinement"] = confinement - with pytest.raises(snapcraft.yaml_utils.errors.YamlValidationError) as error: + with pytest.raises(snapcraft_legacy.yaml_utils.errors.YamlValidationError) as error: Validator(data).validate() expected_message = ( @@ -1060,7 +1068,7 @@ def test_valid_description(data, desc): def test_invalid_description(data, desc): data["description"] = desc - with pytest.raises(snapcraft.yaml_utils.errors.YamlValidationError) as error: + with pytest.raises(snapcraft_legacy.yaml_utils.errors.YamlValidationError) as error: Validator(data).validate() expected_message = ( @@ -1081,7 +1089,7 @@ def test_valid_grade(data, grade): def test_invalid_grade(data, grade): data["grade"] = grade - with pytest.raises(snapcraft.yaml_utils.errors.YamlValidationError) as error: + with pytest.raises(snapcraft_legacy.yaml_utils.errors.YamlValidationError) as error: Validator(data).validate() expected_message = ( @@ -1119,7 +1127,7 @@ def test_valid_epoch(data, epoch): def test_invalid_epoch(data, epoch): data["epoch"] = epoch - with pytest.raises(snapcraft.yaml_utils.errors.YamlValidationError) as error: + with pytest.raises(snapcraft_legacy.yaml_utils.errors.YamlValidationError) as error: Validator(data).validate() expected_message = ( @@ -1138,7 +1146,7 @@ def test_valid_license(data): def test_invalid_license(data): data["license"] = 1234 - with pytest.raises(snapcraft.yaml_utils.errors.YamlValidationError) as error: + with pytest.raises(snapcraft_legacy.yaml_utils.errors.YamlValidationError) as error: Validator(data).validate() expected_message = ( @@ -1159,7 +1167,7 @@ def test_valid_adapter(data, adapter): def test_invalid_adapter(data, adapter): data["apps"] = {"foo": {"command": "foo", "adapter": adapter}} - with pytest.raises(snapcraft.yaml_utils.errors.YamlValidationError) as error: + with pytest.raises(snapcraft_legacy.yaml_utils.errors.YamlValidationError) as error: Validator(data).validate() expected_message = "The 'apps/foo/adapter' property does not match" @@ -1173,7 +1181,7 @@ def test_invalid_adapter(data, adapter): def test_invalid_part_build_environment_key_type(data, build_environment): data["parts"]["part1"]["build-environment"] = build_environment - with pytest.raises(snapcraft.yaml_utils.errors.YamlValidationError): + with pytest.raises(snapcraft_legacy.yaml_utils.errors.YamlValidationError): Validator(data).validate() @@ -1183,7 +1191,7 @@ def test_invalid_part_build_environment_key_type(data, build_environment): def test_invalid_command_chain(data, command_chain): data["apps"] = {"foo": {"command": "foo", "command-chain": command_chain}} - with pytest.raises(snapcraft.yaml_utils.errors.YamlValidationError) as error: + with pytest.raises(snapcraft_legacy.yaml_utils.errors.YamlValidationError) as error: Validator(data).validate() expected_message = "The 'apps/foo/command-chain" @@ -1204,7 +1212,7 @@ def test_yaml_valid_system_usernames_short(data, username): def test_invalid_yaml_invalid_username(data): data["system-usernames"] = {"snap_user": "shared"} - with pytest.raises(snapcraft.yaml_utils.errors.YamlValidationError) as error: + with pytest.raises(snapcraft_legacy.yaml_utils.errors.YamlValidationError) as error: Validator(data).validate() expected_message = "The 'system-usernames' property does not match the required schema: 'snap_user' is not a valid system-username." @@ -1213,7 +1221,7 @@ def test_invalid_yaml_invalid_username(data): def test_invalid_yaml_invalid_short_scope(data): data["system-usernames"] = {"snap_daemon": "invalid-scope"} - with pytest.raises(snapcraft.yaml_utils.errors.YamlValidationError) as error: + with pytest.raises(snapcraft_legacy.yaml_utils.errors.YamlValidationError) as error: Validator(data).validate() expected_message = "The 'system-usernames/snap_daemon' property does not match the required schema: 'invalid-scope' is not valid under any of the given schemas" @@ -1222,7 +1230,7 @@ def test_invalid_yaml_invalid_short_scope(data): def test_invalid_yaml_invalid_long_scope(data): data["system-usernames"] = {"snap_daemon": {"scope": "invalid-scope"}} - with pytest.raises(snapcraft.yaml_utils.errors.YamlValidationError) as error: + with pytest.raises(snapcraft_legacy.yaml_utils.errors.YamlValidationError) as error: Validator(data).validate() expected_message = "The 'system-usernames/snap_daemon' property does not match the required schema: {'scope': 'invalid-scope'} is not valid under any of the given schemas" @@ -1419,7 +1427,9 @@ class TestInvalidAptConfigurations: def test_invalid(self, data, packages, message_contains): data["package-repositories"] = packages - with pytest.raises(snapcraft.yaml_utils.errors.YamlValidationError) as error: + with pytest.raises( + snapcraft_legacy.yaml_utils.errors.YamlValidationError + ) as error: Validator(data).validate() assert message_contains in str(error.value) @@ -1496,5 +1506,5 @@ def test_invalid_metadata_links(data, contact, donation, issues, source_code, we data["source-code"] = source_code data["website"] = website - with pytest.raises(snapcraft.yaml_utils.errors.YamlValidationError): + with pytest.raises(snapcraft_legacy.yaml_utils.errors.YamlValidationError): Validator(data).validate() diff --git a/tests/unit/project_loader/__init__.py b/tests/unit/project_loader/__init__.py index b609c0f7db..b2c4dd537d 100644 --- a/tests/unit/project_loader/__init__.py +++ b/tests/unit/project_loader/__init__.py @@ -16,8 +16,8 @@ from unittest import mock -from snapcraft.internal import project_loader -from snapcraft.project import Project as _Project +from snapcraft_legacy.internal import project_loader +from snapcraft_legacy.project import Project as _Project from tests import unit @@ -37,7 +37,7 @@ def setUp(self): super().setUp() patcher = mock.patch( - "snapcraft.internal.project_loader._parts_config.PartsConfig.load_part" + "snapcraft_legacy.internal.project_loader._parts_config.PartsConfig.load_part" ) self.mock_load_part = patcher.start() self.addCleanup(patcher.stop) diff --git a/tests/unit/project_loader/extensions/test_extensions.py b/tests/unit/project_loader/extensions/test_extensions.py index 3f2e353a84..10de3b6055 100644 --- a/tests/unit/project_loader/extensions/test_extensions.py +++ b/tests/unit/project_loader/extensions/test_extensions.py @@ -19,7 +19,10 @@ from testscenarios import multiply_scenarios -from snapcraft.internal.project_loader import find_extension, supported_extension_names +from snapcraft_legacy.internal.project_loader import ( + find_extension, + supported_extension_names, +) from .. import ProjectLoaderBaseTest diff --git a/tests/unit/project_loader/extensions/test_flutter.py b/tests/unit/project_loader/extensions/test_flutter.py index da297a3f0e..07998fbae2 100644 --- a/tests/unit/project_loader/extensions/test_flutter.py +++ b/tests/unit/project_loader/extensions/test_flutter.py @@ -18,10 +18,10 @@ import pytest -from snapcraft.internal.project_loader._extensions.flutter_dev import ( +from snapcraft_legacy.internal.project_loader._extensions.flutter_dev import ( ExtensionImpl as FlutterDevExtension, ) -from snapcraft.internal.project_loader._extensions.flutter_master import ( +from snapcraft_legacy.internal.project_loader._extensions.flutter_master import ( ExtensionImpl as FlutterMasterExtension, ) diff --git a/tests/unit/project_loader/extensions/test_gnome_3_28.py b/tests/unit/project_loader/extensions/test_gnome_3_28.py index d91c9452df..ef1430f676 100644 --- a/tests/unit/project_loader/extensions/test_gnome_3_28.py +++ b/tests/unit/project_loader/extensions/test_gnome_3_28.py @@ -16,7 +16,9 @@ from testtools.matchers import Equals -from snapcraft.internal.project_loader._extensions.gnome_3_28 import ExtensionImpl +from snapcraft_legacy.internal.project_loader._extensions.gnome_3_28 import ( + ExtensionImpl, +) from .. import ProjectLoaderBaseTest diff --git a/tests/unit/project_loader/extensions/test_gnome_3_34.py b/tests/unit/project_loader/extensions/test_gnome_3_34.py index 7d07472167..069595add9 100644 --- a/tests/unit/project_loader/extensions/test_gnome_3_34.py +++ b/tests/unit/project_loader/extensions/test_gnome_3_34.py @@ -16,7 +16,9 @@ from testtools.matchers import Equals -from snapcraft.internal.project_loader._extensions.gnome_3_34 import ExtensionImpl +from snapcraft_legacy.internal.project_loader._extensions.gnome_3_34 import ( + ExtensionImpl, +) from tests.unit.commands import CommandBaseTestCase from .. import ProjectLoaderBaseTest diff --git a/tests/unit/project_loader/extensions/test_gnome_3_38.py b/tests/unit/project_loader/extensions/test_gnome_3_38.py index eb86b5dc03..b32d3cacf1 100644 --- a/tests/unit/project_loader/extensions/test_gnome_3_38.py +++ b/tests/unit/project_loader/extensions/test_gnome_3_38.py @@ -16,7 +16,9 @@ from testtools.matchers import Equals -from snapcraft.internal.project_loader._extensions.gnome_3_38 import ExtensionImpl +from snapcraft_legacy.internal.project_loader._extensions.gnome_3_38 import ( + ExtensionImpl, +) from tests.unit.commands import CommandBaseTestCase from .. import ProjectLoaderBaseTest diff --git a/tests/unit/project_loader/extensions/test_kde_neon.py b/tests/unit/project_loader/extensions/test_kde_neon.py index daea0e8294..c150875d7e 100644 --- a/tests/unit/project_loader/extensions/test_kde_neon.py +++ b/tests/unit/project_loader/extensions/test_kde_neon.py @@ -14,7 +14,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -from snapcraft.internal.project_loader._extensions.kde_neon import ExtensionImpl +from snapcraft_legacy.internal.project_loader._extensions.kde_neon import ExtensionImpl def test_extension_core18(): diff --git a/tests/unit/project_loader/extensions/test_ros1_noetic.py b/tests/unit/project_loader/extensions/test_ros1_noetic.py index ab5cd90649..1e1b3bc96d 100644 --- a/tests/unit/project_loader/extensions/test_ros1_noetic.py +++ b/tests/unit/project_loader/extensions/test_ros1_noetic.py @@ -16,7 +16,7 @@ import pytest -from snapcraft.internal.project_loader._extensions.ros1_noetic import ( +from snapcraft_legacy.internal.project_loader._extensions.ros1_noetic import ( ExtensionImpl as Ros1NoeticExtension, ) diff --git a/tests/unit/project_loader/extensions/test_ros2_foxy.py b/tests/unit/project_loader/extensions/test_ros2_foxy.py index d4d30c6904..91a7dc40d6 100644 --- a/tests/unit/project_loader/extensions/test_ros2_foxy.py +++ b/tests/unit/project_loader/extensions/test_ros2_foxy.py @@ -16,7 +16,7 @@ import pytest -from snapcraft.internal.project_loader._extensions.ros2_foxy import ( +from snapcraft_legacy.internal.project_loader._extensions.ros2_foxy import ( ExtensionImpl as Ros2FoxyExtension, ) diff --git a/tests/unit/project_loader/extensions/test_utils.py b/tests/unit/project_loader/extensions/test_utils.py index c77a6f79ed..3aeff46478 100644 --- a/tests/unit/project_loader/extensions/test_utils.py +++ b/tests/unit/project_loader/extensions/test_utils.py @@ -19,9 +19,9 @@ from testtools.matchers import Contains, Equals, Not -import snapcraft.yaml_utils.errors -from snapcraft.internal.project_loader import errors -from snapcraft.internal.project_loader._extensions._extension import Extension +import snapcraft_legacy.yaml_utils.errors +from snapcraft_legacy.internal.project_loader import errors +from snapcraft_legacy.internal.project_loader._extensions._extension import Extension from tests import fixture_setup from .. import ProjectLoaderBaseTest @@ -498,7 +498,7 @@ def test_scalars_no_override(self): class InvalidExtensionTest(ExtensionTestBase): def test_invalid_app_extension_format(self): raised = self.assertRaises( - snapcraft.yaml_utils.errors.YamlValidationError, + snapcraft_legacy.yaml_utils.errors.YamlValidationError, self.make_snapcraft_project, textwrap.dedent( """\ @@ -532,7 +532,7 @@ def test_invalid_app_extension_format(self): def test_duplicate_extensions(self): raised = self.assertRaises( - snapcraft.yaml_utils.errors.YamlValidationError, + snapcraft_legacy.yaml_utils.errors.YamlValidationError, self.make_snapcraft_project, textwrap.dedent( """\ @@ -566,7 +566,7 @@ def test_duplicate_extensions(self): def test_invalid_extension_is_validated(self): raised = self.assertRaises( - snapcraft.yaml_utils.errors.YamlValidationError, + snapcraft_legacy.yaml_utils.errors.YamlValidationError, self.make_snapcraft_project, textwrap.dedent( """\ diff --git a/tests/unit/project_loader/grammar/test_compound_statement.py b/tests/unit/project_loader/grammar/test_compound_statement.py index c9b9df8c94..2350889a2a 100644 --- a/tests/unit/project_loader/grammar/test_compound_statement.py +++ b/tests/unit/project_loader/grammar/test_compound_statement.py @@ -19,11 +19,11 @@ import pytest -import snapcraft -import snapcraft.internal.project_loader.grammar._compound as compound -import snapcraft.internal.project_loader.grammar._on as on -import snapcraft.internal.project_loader.grammar._to as to -from snapcraft.internal.project_loader import grammar +import snapcraft_legacy +import snapcraft_legacy.internal.project_loader.grammar._compound as compound +import snapcraft_legacy.internal.project_loader.grammar._on as on +import snapcraft_legacy.internal.project_loader.grammar._to as to +from snapcraft_legacy.internal.project_loader import grammar class TestCompoundStatementGrammar: @@ -199,7 +199,9 @@ def test( monkeypatch.setattr(platform, "architecture", lambda: ("64bit", "ELF")) processor = grammar.GrammarProcessor( - None, snapcraft.ProjectOptions(target_deb_arch="armhf"), lambda x: True + None, + snapcraft_legacy.ProjectOptions(target_deb_arch="armhf"), + lambda x: True, ) statements = [ on.OnStatement(on=on_arch, body=None, processor=processor), @@ -250,7 +252,7 @@ def test( with pytest.raises(expected_exception) as error: processor = grammar.GrammarProcessor( None, - snapcraft.ProjectOptions(target_deb_arch="armhf"), + snapcraft_legacy.ProjectOptions(target_deb_arch="armhf"), lambda x: "invalid" not in x, ) statements = [ diff --git a/tests/unit/project_loader/grammar/test_on_statement.py b/tests/unit/project_loader/grammar/test_on_statement.py index 821af90c07..1769719704 100644 --- a/tests/unit/project_loader/grammar/test_on_statement.py +++ b/tests/unit/project_loader/grammar/test_on_statement.py @@ -20,9 +20,9 @@ import pytest -import snapcraft -import snapcraft.internal.project_loader.grammar._on as on -from snapcraft.internal.project_loader import grammar +import snapcraft_legacy +import snapcraft_legacy.internal.project_loader.grammar._on as on +from snapcraft_legacy.internal.project_loader import grammar def load_tests(loader, tests, ignore): @@ -162,7 +162,7 @@ def test( monkeypatch.setattr(platform, "architecture", lambda: ("64bit", "ELF")) processor = grammar.GrammarProcessor( - None, snapcraft.ProjectOptions(), lambda x: True + None, snapcraft_legacy.ProjectOptions(), lambda x: True ) statement = on.OnStatement(on=on_arch, body=body, processor=processor) @@ -235,7 +235,7 @@ class TestOnStatementInvalidGrammar: def test(self, on_arch, body, else_bodies, expected_exception): with pytest.raises(grammar.errors.OnStatementSyntaxError) as error: processor = grammar.GrammarProcessor( - None, snapcraft.ProjectOptions(), lambda x: "invalid" not in x + None, snapcraft_legacy.ProjectOptions(), lambda x: "invalid" not in x ) statement = on.OnStatement(on=on_arch, body=body, processor=processor) @@ -252,7 +252,7 @@ def test_else_fail(monkeypatch): monkeypatch.setattr(platform, "architecture", lambda: ("64bit", "ELF")) processor = grammar.GrammarProcessor( - None, snapcraft.ProjectOptions(), lambda x: True + None, snapcraft_legacy.ProjectOptions(), lambda x: True ) statement = on.OnStatement(on="on i386", body=["foo"], processor=processor) diff --git a/tests/unit/project_loader/grammar/test_processor.py b/tests/unit/project_loader/grammar/test_processor.py index 0a083c0649..0d132b6fd8 100644 --- a/tests/unit/project_loader/grammar/test_processor.py +++ b/tests/unit/project_loader/grammar/test_processor.py @@ -19,9 +19,9 @@ import pytest -import snapcraft -import snapcraft.internal.project_loader.grammar._to as _to -from snapcraft.internal.project_loader import grammar +import snapcraft_legacy +import snapcraft_legacy.internal.project_loader.grammar._to as _to +from snapcraft_legacy.internal.project_loader import grammar @pytest.mark.parametrize( @@ -35,7 +35,7 @@ def test_duplicates(entry): """Test that multiple identical selector sets is an error.""" processor = grammar.GrammarProcessor( - entry, snapcraft.ProjectOptions(), lambda x: True + entry, snapcraft_legacy.ProjectOptions(), lambda x: True ) with pytest.raises(grammar.errors.GrammarSyntaxError) as error: processor.process() @@ -314,7 +314,7 @@ def test_basic_grammar( monkeypatch.setattr(platform, "machine", lambda: host_arch) monkeypatch.setattr(platform, "architecture", lambda: ("64bit", "ELF")) - project = snapcraft.ProjectOptions(target_deb_arch=target_arch) + project = snapcraft_legacy.ProjectOptions(target_deb_arch=target_arch) processor = grammar.GrammarProcessor( grammar_entry, project, lambda x: "invalid" not in x @@ -391,7 +391,7 @@ def _transformer(call_stack, package_name, project_options): processor = grammar.GrammarProcessor( grammar_entry, - snapcraft.ProjectOptions(target_deb_arch="i386"), + snapcraft_legacy.ProjectOptions(target_deb_arch="i386"), lambda x: True, transformer=_transformer, ) @@ -427,7 +427,7 @@ class TestInvalidGrammar: def test_invalid_grammar(self, grammar_entry, expected_exception): processor = grammar.GrammarProcessor( - grammar_entry, snapcraft.ProjectOptions(), lambda x: True + grammar_entry, snapcraft_legacy.ProjectOptions(), lambda x: True ) with pytest.raises(grammar.errors.GrammarSyntaxError) as error: diff --git a/tests/unit/project_loader/grammar/test_to_statement.py b/tests/unit/project_loader/grammar/test_to_statement.py index a66a76832c..f3082c586e 100644 --- a/tests/unit/project_loader/grammar/test_to_statement.py +++ b/tests/unit/project_loader/grammar/test_to_statement.py @@ -20,9 +20,9 @@ import pytest -import snapcraft -import snapcraft.internal.project_loader.grammar._to as to -from snapcraft.internal.project_loader import grammar +import snapcraft_legacy +import snapcraft_legacy.internal.project_loader.grammar._to as to +from snapcraft_legacy.internal.project_loader import grammar def load_tests(loader, tests, ignore): @@ -191,7 +191,9 @@ def test( monkeypatch.setattr(platform, "machine", lambda: "x86_64") monkeypatch.setattr(platform, "architecture", lambda: ("64bit", "ELF")) processor = grammar.GrammarProcessor( - None, snapcraft.ProjectOptions(target_deb_arch=target_arch), lambda x: True + None, + snapcraft_legacy.ProjectOptions(target_deb_arch=target_arch), + lambda x: True, ) statement = to.ToStatement(to=to_arch, body=body, processor=processor) @@ -271,7 +273,7 @@ def test(self, to_arch, body, else_bodies, target_arch, expected_exception): with pytest.raises(grammar.errors.ToStatementSyntaxError) as error: processor = grammar.GrammarProcessor( None, - snapcraft.ProjectOptions(target_deb_arch=target_arch), + snapcraft_legacy.ProjectOptions(target_deb_arch=target_arch), lambda x: True, ) statement = to.ToStatement(to=to_arch, body=body, processor=processor) @@ -289,7 +291,7 @@ def test_else_fail(monkeypatch): monkeypatch.setattr(platform, "architecture", lambda: ("64bit", "ELF")) processor = grammar.GrammarProcessor( - None, snapcraft.ProjectOptions(target_deb_arch="i386"), lambda x: True + None, snapcraft_legacy.ProjectOptions(target_deb_arch="i386"), lambda x: True ) statement = to.ToStatement(to="to armhf", body=["foo"], processor=processor) diff --git a/tests/unit/project_loader/grammar/test_try_statement.py b/tests/unit/project_loader/grammar/test_try_statement.py index 32010fe0c9..cf2b76ee9d 100644 --- a/tests/unit/project_loader/grammar/test_try_statement.py +++ b/tests/unit/project_loader/grammar/test_try_statement.py @@ -18,9 +18,9 @@ import pytest -import snapcraft -import snapcraft.internal.project_loader.grammar._try as _try -from snapcraft.internal.project_loader import grammar +import snapcraft_legacy +import snapcraft_legacy.internal.project_loader.grammar._try as _try +from snapcraft_legacy.internal.project_loader import grammar def load_tests(loader, tests, ignore): @@ -111,7 +111,7 @@ class TestTryStatementGrammar: def test_try_statement_grammar(self, body, else_bodies, expected_packages): processor = grammar.GrammarProcessor( - None, snapcraft.ProjectOptions(), lambda x: "invalid" not in x + None, snapcraft_legacy.ProjectOptions(), lambda x: "invalid" not in x ) statement = _try.TryStatement(body=body, processor=processor) @@ -123,7 +123,7 @@ def test_try_statement_grammar(self, body, else_bodies, expected_packages): def test_else_fail(): processor = grammar.GrammarProcessor( - None, snapcraft.ProjectOptions(), lambda x: "invalid" not in x + None, snapcraft_legacy.ProjectOptions(), lambda x: "invalid" not in x ) statement = _try.TryStatement(body=["invalid"], processor=processor) diff --git a/tests/unit/project_loader/grammar_processing/test_global_grammar_processor.py b/tests/unit/project_loader/grammar_processing/test_global_grammar_processor.py index 603ed53257..097b3ae969 100644 --- a/tests/unit/project_loader/grammar_processing/test_global_grammar_processor.py +++ b/tests/unit/project_loader/grammar_processing/test_global_grammar_processor.py @@ -16,7 +16,7 @@ import doctest -from snapcraft.internal.project_loader.grammar_processing import ( +from snapcraft_legacy.internal.project_loader.grammar_processing import ( _global_grammar_processor as processor, ) diff --git a/tests/unit/project_loader/grammar_processing/test_part_grammar_processor.py b/tests/unit/project_loader/grammar_processing/test_part_grammar_processor.py index e85d90b39e..8cb994fab5 100644 --- a/tests/unit/project_loader/grammar_processing/test_part_grammar_processor.py +++ b/tests/unit/project_loader/grammar_processing/test_part_grammar_processor.py @@ -20,10 +20,12 @@ from testscenarios import multiply_scenarios -from snapcraft import project -from snapcraft.internal import repo as snapcraft_repo -from snapcraft.internal.project_loader.grammar_processing import PartGrammarProcessor -from snapcraft.internal.project_loader.grammar_processing import ( +from snapcraft_legacy import project +from snapcraft_legacy.internal import repo as snapcraft_repo +from snapcraft_legacy.internal.project_loader.grammar_processing import ( + PartGrammarProcessor, +) +from snapcraft_legacy.internal.project_loader.grammar_processing import ( _part_grammar_processor as processor, ) diff --git a/tests/unit/project_loader/inspection/test_latest_step.py b/tests/unit/project_loader/inspection/test_latest_step.py index 1930f1eaef..4e135bc7fb 100644 --- a/tests/unit/project_loader/inspection/test_latest_step.py +++ b/tests/unit/project_loader/inspection/test_latest_step.py @@ -18,9 +18,9 @@ from testtools.matchers import Equals -from snapcraft import project -from snapcraft.internal import steps -from snapcraft.internal.project_loader import inspection +from snapcraft_legacy import project +from snapcraft_legacy.internal import steps +from snapcraft_legacy.internal.project_loader import inspection from tests import unit diff --git a/tests/unit/project_loader/inspection/test_lifecycle_status.py b/tests/unit/project_loader/inspection/test_lifecycle_status.py index 1bb1387fb0..4c268d2e53 100644 --- a/tests/unit/project_loader/inspection/test_lifecycle_status.py +++ b/tests/unit/project_loader/inspection/test_lifecycle_status.py @@ -19,8 +19,8 @@ from testtools.matchers import Equals -from snapcraft.internal import steps -from snapcraft.internal.project_loader import inspection +from snapcraft_legacy.internal import steps +from snapcraft_legacy.internal.project_loader import inspection from .. import ProjectLoaderBaseTest diff --git a/tests/unit/project_loader/inspection/test_provides.py b/tests/unit/project_loader/inspection/test_provides.py index d7199f3e07..b9a9b62213 100644 --- a/tests/unit/project_loader/inspection/test_provides.py +++ b/tests/unit/project_loader/inspection/test_provides.py @@ -18,8 +18,8 @@ from testtools.matchers import Equals -from snapcraft import project -from snapcraft.internal.project_loader import inspection +from snapcraft_legacy import project +from snapcraft_legacy.internal.project_loader import inspection from tests import unit diff --git a/tests/unit/project_loader/test_build_packages.py b/tests/unit/project_loader/test_build_packages.py index fa0b128917..d1f918168c 100644 --- a/tests/unit/project_loader/test_build_packages.py +++ b/tests/unit/project_loader/test_build_packages.py @@ -19,8 +19,8 @@ import pytest -from snapcraft.internal import project_loader -from snapcraft.project import Project +from snapcraft_legacy.internal import project_loader +from snapcraft_legacy.project import Project def get_project_config(snapcraft_yaml_content): diff --git a/tests/unit/project_loader/test_config.py b/tests/unit/project_loader/test_config.py index 655b9f62cb..2f21b0d3ed 100644 --- a/tests/unit/project_loader/test_config.py +++ b/tests/unit/project_loader/test_config.py @@ -18,8 +18,8 @@ from testtools.matchers import Contains, Equals -import snapcraft.internal.project_loader._config as _config -from snapcraft.internal.project_loader import errors +import snapcraft_legacy.internal.project_loader._config as _config +from snapcraft_legacy.internal.project_loader import errors from tests import unit from . import LoadPartBaseTest, ProjectLoaderBaseTest diff --git a/tests/unit/project_loader/test_environment.py b/tests/unit/project_loader/test_environment.py index 4ca60e4078..162da861f8 100644 --- a/tests/unit/project_loader/test_environment.py +++ b/tests/unit/project_loader/test_environment.py @@ -24,8 +24,8 @@ import fixtures from testtools.matchers import Contains, Equals, GreaterThan, Not -import snapcraft -from snapcraft.internal import common +import snapcraft_legacy +from snapcraft_legacy.internal import common from tests.fixture_setup.os_release import FakeOsRelease from . import ProjectLoaderBaseTest @@ -95,7 +95,8 @@ def test_config_snap_environment_with_no_library_paths(self): ) @mock.patch.object( - snapcraft.internal.pluginhandler.PluginHandler, "get_primed_dependency_paths" + snapcraft_legacy.internal.pluginhandler.PluginHandler, + "get_primed_dependency_paths", ) def test_config_snap_environment_with_dependencies(self, mock_get_dependencies): library_paths = { @@ -120,7 +121,8 @@ def test_config_snap_environment_with_dependencies(self, mock_get_dependencies): ) @mock.patch.object( - snapcraft.internal.pluginhandler.PluginHandler, "get_primed_dependency_paths" + snapcraft_legacy.internal.pluginhandler.PluginHandler, + "get_primed_dependency_paths", ) def test_config_snap_environment_with_dependencies_but_no_paths( self, mock_get_dependencies @@ -304,7 +306,7 @@ def test_parts_build_env_ordering_with_deps(self): self.useFixture(fixtures.EnvironmentVariable("PATH", "/bin")) - arch_triplet = snapcraft.ProjectOptions().arch_triplet + arch_triplet = snapcraft_legacy.ProjectOptions().arch_triplet self.maxDiff = None paths = [ os.path.join(self.stage_dir, "lib"), diff --git a/tests/unit/project_loader/test_errors.py b/tests/unit/project_loader/test_errors.py index b3ee3a5daa..4c5bcbb5b4 100644 --- a/tests/unit/project_loader/test_errors.py +++ b/tests/unit/project_loader/test_errors.py @@ -16,7 +16,7 @@ import pathlib -from snapcraft.internal.project_loader import errors +from snapcraft_legacy.internal.project_loader import errors def test_SnapcraftProjectUnusedKeyAssetError(): diff --git a/tests/unit/project_loader/test_parts.py b/tests/unit/project_loader/test_parts.py index 4c4bdac3a1..56cc65db56 100644 --- a/tests/unit/project_loader/test_parts.py +++ b/tests/unit/project_loader/test_parts.py @@ -19,8 +19,8 @@ from testtools.matchers import Equals -from snapcraft.internal import project_loader -from snapcraft.project import Project +from snapcraft_legacy.internal import project_loader +from snapcraft_legacy.project import Project from tests import fixture_setup from . import LoadPartBaseTest, ProjectLoaderBaseTest diff --git a/tests/unit/project_loader/test_replace_attr.py b/tests/unit/project_loader/test_replace_attr.py index a6c81e8796..701f4cf650 100644 --- a/tests/unit/project_loader/test_replace_attr.py +++ b/tests/unit/project_loader/test_replace_attr.py @@ -16,7 +16,7 @@ from testtools.matchers import Equals -from snapcraft.internal import project_loader +from snapcraft_legacy.internal import project_loader from tests import unit diff --git a/tests/unit/project_loader/test_schema.py b/tests/unit/project_loader/test_schema.py index ff283bd1e5..2ae0c16db9 100644 --- a/tests/unit/project_loader/test_schema.py +++ b/tests/unit/project_loader/test_schema.py @@ -23,10 +23,10 @@ from testscenarios.scenarios import multiply_scenarios from testtools.matchers import Contains, Equals -import snapcraft.yaml_utils.errors -from snapcraft import project -from snapcraft.internal.errors import PluginError -from snapcraft.internal.project_loader import errors, load_config +import snapcraft_legacy.yaml_utils.errors +from snapcraft_legacy import project +from snapcraft_legacy.internal.errors import PluginError +from snapcraft_legacy.internal.project_loader import errors, load_config from tests import fixture_setup from . import ProjectLoaderBaseTest @@ -280,7 +280,7 @@ def test_duplicate_aliases(self): def test_invalid_alias(self): apps = [("test", dict(command="test", aliases=[".test"]))] raised = self.assertRaises( - snapcraft.yaml_utils.errors.YamlValidationError, + snapcraft_legacy.yaml_utils.errors.YamlValidationError, self.make_snapcraft_project, apps, ) diff --git a/tests/unit/remote_build/test_errors.py b/tests/unit/remote_build/test_errors.py index c4e47f7884..05865a6339 100644 --- a/tests/unit/remote_build/test_errors.py +++ b/tests/unit/remote_build/test_errors.py @@ -14,7 +14,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -from snapcraft.internal.remote_build import errors +from snapcraft_legacy.internal.remote_build import errors class TestSnapcraftException: diff --git a/tests/unit/remote_build/test_info_file.py b/tests/unit/remote_build/test_info_file.py index 924b474d47..2409a785a3 100644 --- a/tests/unit/remote_build/test_info_file.py +++ b/tests/unit/remote_build/test_info_file.py @@ -19,7 +19,7 @@ import fixtures from testtools.matchers import Equals, FileExists -from snapcraft.internal.remote_build import InfoFile +from snapcraft_legacy.internal.remote_build import InfoFile from tests import unit diff --git a/tests/unit/remote_build/test_launchpad.py b/tests/unit/remote_build/test_launchpad.py index 540d0c76ca..c6646d041d 100644 --- a/tests/unit/remote_build/test_launchpad.py +++ b/tests/unit/remote_build/test_launchpad.py @@ -20,10 +20,10 @@ import fixtures from testtools.matchers import Contains, Equals -import snapcraft -from snapcraft.internal.remote_build import LaunchpadClient, errors -from snapcraft.internal.sources._git import Git -from snapcraft.internal.sources.errors import SnapcraftPullError +import snapcraft_legacy +from snapcraft_legacy.internal.remote_build import LaunchpadClient, errors +from snapcraft_legacy.internal.sources._git import Git +from snapcraft_legacy.internal.sources.errors import SnapcraftPullError from tests import unit from . import TestDir @@ -217,7 +217,7 @@ def setUp(self): def test_login(self): self.assertThat(self.lpc.user, Equals("user")) self.fake_login_with.mock.assert_called_with( - "snapcraft remote-build {}".format(snapcraft.__version__), + "snapcraft remote-build {}".format(snapcraft_legacy.__version__), "production", mock.ANY, credentials_file=mock.ANY, @@ -322,7 +322,7 @@ def test_issue_build_request_defaults(self): ), ) - @mock.patch("snapcraft.internal.remote_build.LaunchpadClient._download_file") + @mock.patch("snapcraft_legacy.internal.remote_build.LaunchpadClient._download_file") def test_monitor_build(self, mock_download_file): open("test_i386.txt", "w").close() open("test_i386.1.txt", "w").close() @@ -356,7 +356,7 @@ def test_monitor_build(self, mock_download_file): ), ) - @mock.patch("snapcraft.internal.remote_build.LaunchpadClient._download_file") + @mock.patch("snapcraft_legacy.internal.remote_build.LaunchpadClient._download_file") @mock.patch( "tests.unit.remote_build.test_launchpad.BuildImpl.getFileUrls", return_value=[] ) @@ -384,7 +384,7 @@ def test_monitor_build_error(self, mock_log, mock_urls, mock_download_file): ), ) - @mock.patch("snapcraft.internal.remote_build.LaunchpadClient._download_file") + @mock.patch("snapcraft_legacy.internal.remote_build.LaunchpadClient._download_file") @mock.patch("time.time", return_value=500) def test_monitor_build_error_timeout(self, mock_time, mock_rb): self.lpc.deadline = 499 @@ -423,7 +423,7 @@ def _make_snapcraft_project(self): """ ) snapcraft_yaml_file_path = self.make_snapcraft_yaml(yaml) - project = snapcraft.project.Project( + project = snapcraft_legacy.project.Project( snapcraft_yaml_file_path=snapcraft_yaml_file_path ) return project diff --git a/tests/unit/remote_build/test_worktree.py b/tests/unit/remote_build/test_worktree.py index 90853260ff..fb38e62a5f 100644 --- a/tests/unit/remote_build/test_worktree.py +++ b/tests/unit/remote_build/test_worktree.py @@ -21,9 +21,9 @@ from testtools.matchers import Equals -from snapcraft import yaml_utils -from snapcraft.internal.remote_build import WorkTree -from snapcraft.project import Project +from snapcraft_legacy import yaml_utils +from snapcraft_legacy.internal.remote_build import WorkTree +from snapcraft_legacy.project import Project from tests import fixture_setup, unit from . import TestDir diff --git a/tests/unit/repo/test_apt_cache.py b/tests/unit/repo/test_apt_cache.py index 3025f82e97..ef077b6153 100644 --- a/tests/unit/repo/test_apt_cache.py +++ b/tests/unit/repo/test_apt_cache.py @@ -22,7 +22,7 @@ import fixtures from testtools.matchers import Equals -from snapcraft.internal.repo.apt_cache import AptCache +from snapcraft_legacy.internal.repo.apt_cache import AptCache from tests import unit @@ -67,7 +67,7 @@ def test_stage_cache(self): stage_cache = Path(self.path, "cache") stage_cache.mkdir(exist_ok=True, parents=True) self.fake_apt = self.useFixture( - fixtures.MockPatch("snapcraft.internal.repo.apt_cache.apt") + fixtures.MockPatch("snapcraft_legacy.internal.repo.apt_cache.apt") ).mock with AptCache(stage_cache=stage_cache): @@ -98,7 +98,7 @@ def test_stage_cache(self): def test_stage_cache_in_snap(self): self.fake_apt = self.useFixture( - fixtures.MockPatch("snapcraft.internal.repo.apt_cache.apt") + fixtures.MockPatch("snapcraft_legacy.internal.repo.apt_cache.apt") ).mock stage_cache = Path(self.path, "cache") @@ -108,7 +108,9 @@ def test_stage_cache_in_snap(self): snap.mkdir(exist_ok=True, parents=True) self.useFixture( - fixtures.MockPatch("snapcraft.internal.common.is_snap", return_value=True) + fixtures.MockPatch( + "snapcraft_legacy.internal.common.is_snap", return_value=True + ) ) self.useFixture(fixtures.EnvironmentVariable("SNAP", str(snap))) @@ -155,7 +157,7 @@ def test_stage_cache_in_snap(self): def test_host_cache_setup(self): self.fake_apt = self.useFixture( - fixtures.MockPatch("snapcraft.internal.repo.apt_cache.apt") + fixtures.MockPatch("snapcraft_legacy.internal.repo.apt_cache.apt") ).mock with AptCache() as _: diff --git a/tests/unit/repo/test_apt_key_manager.py b/tests/unit/repo/test_apt_key_manager.py index 2d38be19cf..dd25c2d8ea 100644 --- a/tests/unit/repo/test_apt_key_manager.py +++ b/tests/unit/repo/test_apt_key_manager.py @@ -22,12 +22,12 @@ import gnupg import pytest -from snapcraft.internal.meta.package_repository import ( +from snapcraft_legacy.internal.meta.package_repository import ( PackageRepositoryApt, PackageRepositoryAptPpa, ) -from snapcraft.internal.repo import apt_ppa, errors -from snapcraft.internal.repo.apt_key_manager import AptKeyManager +from snapcraft_legacy.internal.repo import apt_ppa, errors +from snapcraft_legacy.internal.repo.apt_key_manager import AptKeyManager @pytest.fixture(autouse=True) @@ -54,7 +54,7 @@ def mock_run(): @pytest.fixture(autouse=True) def mock_apt_ppa_get_signing_key(): with mock.patch( - "snapcraft.internal.repo.apt_ppa.get_launchpad_ppa_key_id", + "snapcraft_legacy.internal.repo.apt_ppa.get_launchpad_ppa_key_id", spec=apt_ppa.get_launchpad_ppa_key_id, return_value="FAKE-PPA-SIGNING-KEY", ) as m: @@ -220,7 +220,9 @@ def test_install_key_from_keyserver_with_apt_key_failure( assert exc_info.value._key_id == "fake-key-id" -@mock.patch("snapcraft.internal.repo.apt_key_manager.AptKeyManager.is_key_installed") +@mock.patch( + "snapcraft_legacy.internal.repo.apt_key_manager.AptKeyManager.is_key_installed" +) @pytest.mark.parametrize( "is_installed", [True, False], ) @@ -242,10 +244,10 @@ def test_install_package_repository_key_already_installed( @mock.patch( - "snapcraft.internal.repo.apt_key_manager.AptKeyManager.is_key_installed", + "snapcraft_legacy.internal.repo.apt_key_manager.AptKeyManager.is_key_installed", return_value=False, ) -@mock.patch("snapcraft.internal.repo.apt_key_manager.AptKeyManager.install_key") +@mock.patch("snapcraft_legacy.internal.repo.apt_key_manager.AptKeyManager.install_key") def test_install_package_repository_key_from_asset( mock_install_key, mock_is_key_installed, apt_gpg, key_assets, ): @@ -267,11 +269,11 @@ def test_install_package_repository_key_from_asset( @mock.patch( - "snapcraft.internal.repo.apt_key_manager.AptKeyManager.is_key_installed", + "snapcraft_legacy.internal.repo.apt_key_manager.AptKeyManager.is_key_installed", return_value=False, ) @mock.patch( - "snapcraft.internal.repo.apt_key_manager.AptKeyManager.install_key_from_keyserver" + "snapcraft_legacy.internal.repo.apt_key_manager.AptKeyManager.install_key_from_keyserver" ) def test_install_package_repository_key_apt_from_keyserver( mock_install_key_from_keyserver, mock_is_key_installed, apt_gpg, @@ -295,11 +297,11 @@ def test_install_package_repository_key_apt_from_keyserver( @mock.patch( - "snapcraft.internal.repo.apt_key_manager.AptKeyManager.is_key_installed", + "snapcraft_legacy.internal.repo.apt_key_manager.AptKeyManager.is_key_installed", return_value=False, ) @mock.patch( - "snapcraft.internal.repo.apt_key_manager.AptKeyManager.install_key_from_keyserver" + "snapcraft_legacy.internal.repo.apt_key_manager.AptKeyManager.install_key_from_keyserver" ) def test_install_package_repository_key_ppa_from_keyserver( mock_install_key_from_keyserver, mock_is_key_installed, apt_gpg, diff --git a/tests/unit/repo/test_apt_ppa.py b/tests/unit/repo/test_apt_ppa.py index ed773246ff..033ef83ed7 100644 --- a/tests/unit/repo/test_apt_ppa.py +++ b/tests/unit/repo/test_apt_ppa.py @@ -21,13 +21,13 @@ import launchpadlib import pytest -from snapcraft.internal.repo import apt_ppa, errors +from snapcraft_legacy.internal.repo import apt_ppa, errors @pytest.fixture def mock_launchpad(autouse=True): with mock.patch( - "snapcraft.internal.repo.apt_ppa.Launchpad", + "snapcraft_legacy.internal.repo.apt_ppa.Launchpad", spec=launchpadlib.launchpad.Launchpad, ) as m: m.login_anonymously.return_value.load.return_value.signing_key_fingerprint = ( diff --git a/tests/unit/repo/test_apt_sources_manager.py b/tests/unit/repo/test_apt_sources_manager.py index 71e6bf49cb..a955553fe1 100644 --- a/tests/unit/repo/test_apt_sources_manager.py +++ b/tests/unit/repo/test_apt_sources_manager.py @@ -23,17 +23,17 @@ import pytest -from snapcraft.internal.meta.package_repository import ( +from snapcraft_legacy.internal.meta.package_repository import ( PackageRepositoryApt, PackageRepositoryAptPpa, ) -from snapcraft.internal.repo import apt_ppa, apt_sources_manager, errors +from snapcraft_legacy.internal.repo import apt_ppa, apt_sources_manager, errors @pytest.fixture(autouse=True) def mock_apt_ppa_get_signing_key(): with mock.patch( - "snapcraft.internal.repo.apt_ppa.get_launchpad_ppa_key_id", + "snapcraft_legacy.internal.repo.apt_ppa.get_launchpad_ppa_key_id", spec=apt_ppa.get_launchpad_ppa_key_id, return_value="FAKE-PPA-SIGNING-KEY", ) as m: @@ -48,7 +48,9 @@ def mock_environ_copy(): @pytest.fixture(autouse=True) def mock_host_arch(): - with mock.patch("snapcraft.internal.repo.apt_sources_manager.ProjectOptions") as m: + with mock.patch( + "snapcraft_legacy.internal.repo.apt_sources_manager.ProjectOptions" + ) as m: m.return_value.deb_arch = "FAKE-HOST-ARCH" yield m @@ -65,7 +67,7 @@ def write_file(*, dst_path: pathlib.Path, content: bytes) -> None: dst_path.write_bytes(content) with mock.patch( - "snapcraft.internal.repo.apt_sources_manager._sudo_write_file" + "snapcraft_legacy.internal.repo.apt_sources_manager._sudo_write_file" ) as m: m.side_effect = write_file yield m @@ -74,7 +76,7 @@ def write_file(*, dst_path: pathlib.Path, content: bytes) -> None: @pytest.fixture(autouse=True) def mock_version_codename(): with mock.patch( - "snapcraft.internal.os_release.OsRelease.version_codename", + "snapcraft_legacy.internal.os_release.OsRelease.version_codename", return_value="FAKE-CODENAME", ) as m: yield m diff --git a/tests/unit/repo/test_base.py b/tests/unit/repo/test_base.py index a922af9104..4e48718008 100644 --- a/tests/unit/repo/test_base.py +++ b/tests/unit/repo/test_base.py @@ -20,7 +20,7 @@ from testtools.matchers import Equals, FileContains, FileExists, Not -from snapcraft.internal.repo._base import BaseRepo, get_pkg_name_parts +from snapcraft_legacy.internal.repo._base import BaseRepo, get_pkg_name_parts from tests import unit from . import RepoBaseTestCase diff --git a/tests/unit/repo/test_deb.py b/tests/unit/repo/test_deb.py index 4fc09f14e6..3e64a92af0 100644 --- a/tests/unit/repo/test_deb.py +++ b/tests/unit/repo/test_deb.py @@ -26,9 +26,9 @@ import testtools from testtools.matchers import Equals -from snapcraft.internal import repo -from snapcraft.internal.repo import errors -from snapcraft.internal.repo.deb_package import DebPackage +from snapcraft_legacy.internal import repo +from snapcraft_legacy.internal.repo import errors +from snapcraft_legacy.internal.repo.deb_package import DebPackage from tests import unit @@ -43,7 +43,7 @@ def setUp(self): super().setUp() self.fake_apt_cache = self.useFixture( - fixtures.MockPatch("snapcraft.internal.repo._deb.AptCache") + fixtures.MockPatch("snapcraft_legacy.internal.repo._deb.AptCache") ).mock self.fake_run = self.useFixture( @@ -65,7 +65,7 @@ def fake_tempdir(*, suffix: str, **kwargs): self.fake_tmp_mock = self.useFixture( fixtures.MockPatch( - "snapcraft.internal.repo._deb.tempfile.TemporaryDirectory", + "snapcraft_legacy.internal.repo._deb.tempfile.TemporaryDirectory", new=fake_tempdir, ) ).mock @@ -73,7 +73,7 @@ def fake_tempdir(*, suffix: str, **kwargs): self.stage_packages_path = Path(self.path) @mock.patch( - "snapcraft.internal.repo._deb._DEFAULT_FILTERED_STAGE_PACKAGES", + "snapcraft_legacy.internal.repo._deb._DEFAULT_FILTERED_STAGE_PACKAGES", {"filtered-pkg-1", "filtered-pkg-2"}, ) def test_fetch_stage_packages(self): @@ -105,7 +105,7 @@ def test_fetch_stage_packages(self): self.assertThat(fetched_packages, Equals(["fake-package=1.0"])) @mock.patch( - "snapcraft.internal.repo._deb._DEFAULT_FILTERED_STAGE_PACKAGES", + "snapcraft_legacy.internal.repo._deb._DEFAULT_FILTERED_STAGE_PACKAGES", {"filtered-pkg-1", "filtered-pkg-2", "filtered-pkg-3:amd64", "filtered-pkg-4"}, ) def test_fetch_stage_package_filtered_arch_version(self): @@ -214,7 +214,7 @@ def setUp(self): super().setUp() self.fake_apt_cache = self.useFixture( - fixtures.MockPatch("snapcraft.internal.repo._deb.AptCache") + fixtures.MockPatch("snapcraft_legacy.internal.repo._deb.AptCache") ).mock self.fake_run = self.useFixture( @@ -230,7 +230,7 @@ def get_installed_version(package_name, resolve_virtual_packages=False): self.fake_is_dumb_terminal = self.useFixture( fixtures.MockPatch( - "snapcraft.repo._deb.is_dumb_terminal", return_value=True + "snapcraft_legacy.repo._deb.is_dumb_terminal", return_value=True ) ).mock @@ -467,7 +467,7 @@ def test_broken_package_apt_install(self): ] self.useFixture( fixtures.MockPatch( - "snapcraft.internal.repo._deb.Ubuntu.refresh_build_packages" + "snapcraft_legacy.internal.repo._deb.Ubuntu.refresh_build_packages" ) ) diff --git a/tests/unit/repo/test_deb_package.py b/tests/unit/repo/test_deb_package.py index 097f45d838..3b9b9058ac 100644 --- a/tests/unit/repo/test_deb_package.py +++ b/tests/unit/repo/test_deb_package.py @@ -14,7 +14,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -from snapcraft.internal.repo.deb_package import DebPackage +from snapcraft_legacy.internal.repo.deb_package import DebPackage def test_basic(): diff --git a/tests/unit/repo/test_errors.py b/tests/unit/repo/test_errors.py index b41bbfe243..dc336d66eb 100644 --- a/tests/unit/repo/test_errors.py +++ b/tests/unit/repo/test_errors.py @@ -14,7 +14,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -from snapcraft.internal.repo import errors +from snapcraft_legacy.internal.repo import errors class TestErrorFormatting: diff --git a/tests/unit/repo/test_snaps.py b/tests/unit/repo/test_snaps.py index cfdac057f0..7eff2b58c7 100644 --- a/tests/unit/repo/test_snaps.py +++ b/tests/unit/repo/test_snaps.py @@ -19,7 +19,7 @@ import fixtures from testtools.matchers import Equals, FileContains, FileExists, Is -from snapcraft.internal.repo import errors, snaps +from snapcraft_legacy.internal.repo import errors, snaps from tests import unit @@ -311,7 +311,8 @@ def test_install_branch(self): def test_download_from_host(self): fake_get_assertion = fixtures.MockPatch( - "snapcraft.internal.repo.snaps.get_assertion", return_value=b"foo-assert" + "snapcraft_legacy.internal.repo.snaps.get_assertion", + return_value=b"foo-assert", ) self.useFixture(fake_get_assertion) @@ -350,7 +351,8 @@ def test_download_from_host(self): def test_download_from_host_dangerous(self): fake_get_assertion = fixtures.MockPatch( - "snapcraft.internal.repo.snaps.get_assertion", return_value=b"foo-assert" + "snapcraft_legacy.internal.repo.snaps.get_assertion", + return_value=b"foo-assert", ) self.useFixture(fake_get_assertion) self.fake_snapd.snaps_result = [ @@ -658,7 +660,7 @@ class SnapdNotInstalledTestCase(unit.TestCase): def setUp(self): super().setUp() socket_path_patcher = mock.patch( - "snapcraft.internal.repo.snaps.get_snapd_socket_path_template" + "snapcraft_legacy.internal.repo.snaps.get_snapd_socket_path_template" ) mock_socket_path = socket_path_patcher.start() mock_socket_path.return_value = "http+unix://nonexisting" diff --git a/tests/unit/repo/test_ua_manager.py b/tests/unit/repo/test_ua_manager.py index df7713323e..5bb314fa00 100644 --- a/tests/unit/repo/test_ua_manager.py +++ b/tests/unit/repo/test_ua_manager.py @@ -16,7 +16,7 @@ import pytest -from snapcraft.internal.repo import ua_manager +from snapcraft_legacy.internal.repo import ua_manager def test_ua_manager(fake_process): diff --git a/tests/unit/review_tools/test_errors.py b/tests/unit/review_tools/test_errors.py index d785b85c16..c59ffb5305 100644 --- a/tests/unit/review_tools/test_errors.py +++ b/tests/unit/review_tools/test_errors.py @@ -16,7 +16,7 @@ from textwrap import dedent -from snapcraft.internal.review_tools import errors +from snapcraft_legacy.internal.review_tools import errors class TestSnapcraftException: diff --git a/tests/unit/review_tools/test_runner.py b/tests/unit/review_tools/test_runner.py index aa22d0972d..a8409cf7ee 100644 --- a/tests/unit/review_tools/test_runner.py +++ b/tests/unit/review_tools/test_runner.py @@ -20,7 +20,7 @@ import fixtures -from snapcraft.internal import review_tools +from snapcraft_legacy.internal import review_tools from tests import unit @@ -37,7 +37,7 @@ def setUp(self): self.user_common_path = pathlib.Path(self.path) / "common" self.useFixture( fixtures.MockPatch( - "snapcraft.internal.review_tools._runner._get_review_tools_user_common", + "snapcraft_legacy.internal.review_tools._runner._get_review_tools_user_common", return_value=self.user_common_path, ) ) diff --git a/tests/unit/sources/test_7z.py b/tests/unit/sources/test_7z.py index 04cf445e8f..318b3b1d85 100644 --- a/tests/unit/sources/test_7z.py +++ b/tests/unit/sources/test_7z.py @@ -22,7 +22,7 @@ import fixtures from testtools.matchers import Equals, MatchesRegex -from snapcraft.internal import sources +from snapcraft_legacy.internal import sources from tests import unit diff --git a/tests/unit/sources/test_base.py b/tests/unit/sources/test_base.py index b80baaae3e..b91ffbac07 100644 --- a/tests/unit/sources/test_base.py +++ b/tests/unit/sources/test_base.py @@ -20,7 +20,7 @@ import requests from testtools.matchers import Contains, Equals -from snapcraft.internal.sources import _base, errors +from snapcraft_legacy.internal.sources import _base, errors from tests import unit @@ -30,7 +30,7 @@ def get_mock_file_base(self, source, dir): setattr(file_src, "provision", mock.Mock()) return file_src - @mock.patch("snapcraft.internal.sources._base.FileBase.download") + @mock.patch("snapcraft_legacy.internal.sources._base.FileBase.download") def test_pull_url(self, mock_download): mock_download.return_value = "dir" file_src = self.get_mock_file_base("http://snapcraft.io/snapcraft.yaml", "dir") @@ -62,9 +62,9 @@ def test_pull_copy_source_does_not_exist(self, mock_shutil_copy2): str(raised), Contains("Failed to pull source: 'does-not-exist.tar.gz'") ) - @mock.patch("snapcraft.internal.sources._base.requests") - @mock.patch("snapcraft.internal.sources._base.download_requests_stream") - @mock.patch("snapcraft.internal.sources._base.download_urllib_source") + @mock.patch("snapcraft_legacy.internal.sources._base.requests") + @mock.patch("snapcraft_legacy.internal.sources._base.download_requests_stream") + @mock.patch("snapcraft_legacy.internal.sources._base.download_urllib_source") def test_download_file_destination(self, dus, drs, req): file_src = self.get_mock_file_base("http://snapcraft.io/snapcraft.yaml", "dir") self.assertFalse(hasattr(file_src, "file")) @@ -78,7 +78,7 @@ def test_download_file_destination(self, dus, drs, req): ), ) - @mock.patch("snapcraft.internal.common.get_url_scheme", return_value=False) + @mock.patch("snapcraft_legacy.internal.common.get_url_scheme", return_value=False) @mock.patch("requests.get", side_effect=requests.exceptions.ConnectionError("foo")) def test_download_error(self, mock_get, mock_gus): base = self.get_mock_file_base("", "") @@ -88,8 +88,8 @@ def test_download_error(self, mock_get, mock_gus): self.assertThat(str(raised), Contains("Network request error")) - @mock.patch("snapcraft.internal.sources._base.download_requests_stream") - @mock.patch("snapcraft.internal.sources._base.requests") + @mock.patch("snapcraft_legacy.internal.sources._base.download_requests_stream") + @mock.patch("snapcraft_legacy.internal.sources._base.requests") def test_download_http(self, mock_requests, mock_download): file_src = self.get_mock_file_base("http://snapcraft.io/snapcraft.yaml", "dir") @@ -104,7 +104,7 @@ def test_download_http(self, mock_requests, mock_download): mock_request.raise_for_status.assert_called_once_with() mock_download.assert_called_once_with(mock_request, file_src.file) - @mock.patch("snapcraft.internal.sources._base.download_urllib_source") + @mock.patch("snapcraft_legacy.internal.sources._base.download_urllib_source") def test_download_ftp(self, mock_download): file_src = self.get_mock_file_base("ftp://snapcraft.io/snapcraft.yaml", "dir") @@ -112,7 +112,7 @@ def test_download_ftp(self, mock_download): mock_download.assert_called_once_with(file_src.source, file_src.file) - @mock.patch("snapcraft.internal.indicators.urlretrieve") + @mock.patch("snapcraft_legacy.internal.indicators.urlretrieve") def test_download_ftp_url_opener(self, mock_urlretrieve): file_src = self.get_mock_file_base("ftp://snapcraft.io/snapcraft.yaml", "dir") diff --git a/tests/unit/sources/test_bazaar.py b/tests/unit/sources/test_bazaar.py index 0f12c746ea..65ac8c0638 100644 --- a/tests/unit/sources/test_bazaar.py +++ b/tests/unit/sources/test_bazaar.py @@ -21,7 +21,7 @@ import fixtures from testtools.matchers import Equals -from snapcraft.internal import sources +from snapcraft_legacy.internal import sources from tests import unit @@ -32,7 +32,7 @@ def setUp(self): # Mock _get_source_details() since not all tests have a # full repo checkout - patcher = mock.patch("snapcraft.sources.Bazaar._get_source_details") + patcher = mock.patch("snapcraft_legacy.sources.Bazaar._get_source_details") self.mock_get_source_details = patcher.start() self.mock_get_source_details.return_value = "" self.addCleanup(patcher.stop) diff --git a/tests/unit/sources/test_checksum.py b/tests/unit/sources/test_checksum.py index 421e5d3516..e371f5442b 100644 --- a/tests/unit/sources/test_checksum.py +++ b/tests/unit/sources/test_checksum.py @@ -20,8 +20,8 @@ from testtools.matchers import Equals -from snapcraft.internal.sources import errors -from snapcraft.internal.sources._checksum import verify_checksum +from snapcraft_legacy.internal.sources import errors +from snapcraft_legacy.internal.sources._checksum import verify_checksum from tests import unit diff --git a/tests/unit/sources/test_deb.py b/tests/unit/sources/test_deb.py index 5a719223d2..b677957dc9 100644 --- a/tests/unit/sources/test_deb.py +++ b/tests/unit/sources/test_deb.py @@ -20,7 +20,7 @@ from testtools.matchers import Equals -from snapcraft.internal import sources +from snapcraft_legacy.internal import sources from tests import unit diff --git a/tests/unit/sources/test_errors.py b/tests/unit/sources/test_errors.py index c087f2c720..691de2a5f4 100644 --- a/tests/unit/sources/test_errors.py +++ b/tests/unit/sources/test_errors.py @@ -14,7 +14,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -from snapcraft.internal.sources import errors +from snapcraft_legacy.internal.sources import errors class TestErrorFormatting: diff --git a/tests/unit/sources/test_git.py b/tests/unit/sources/test_git.py index fe3bfd25d3..e08026d057 100644 --- a/tests/unit/sources/test_git.py +++ b/tests/unit/sources/test_git.py @@ -22,8 +22,8 @@ import fixtures from testtools.matchers import Equals -from snapcraft.internal import sources -from snapcraft.internal.sources import errors +from snapcraft_legacy.internal import sources +from snapcraft_legacy.internal.sources import errors from tests import unit from tests.subprocess_utils import call, call_with_output @@ -37,7 +37,7 @@ class TestGit(unit.sources.SourceTestCase): # type: ignore def setUp(self): super().setUp() - patcher = mock.patch("snapcraft.sources.Git._get_source_details") + patcher = mock.patch("snapcraft_legacy.sources.Git._get_source_details") self.mock_get_source_details = patcher.start() self.mock_get_source_details.return_value = "" self.addCleanup(patcher.stop) diff --git a/tests/unit/sources/test_local.py b/tests/unit/sources/test_local.py index 3a23ad7e1d..c4302a3b3f 100644 --- a/tests/unit/sources/test_local.py +++ b/tests/unit/sources/test_local.py @@ -21,12 +21,12 @@ from testtools.matchers import DirExists, Equals, FileContains, FileExists, Not -from snapcraft.internal import common, errors, sources +from snapcraft_legacy.internal import common, errors, sources from tests import unit class TestLocal(unit.TestCase): - @mock.patch("snapcraft.internal.sources._local.glob.glob") + @mock.patch("snapcraft_legacy.internal.sources._local.glob.glob") def test_pull_does_not_change_snapcraft_files_list(self, mock_glob): # Regression test for https://bugs.launchpad.net/snapcraft/+bug/1614913 # Verify that SNAPCRAFT_FILES was not modified by the pull when there diff --git a/tests/unit/sources/test_mercurial.py b/tests/unit/sources/test_mercurial.py index 91865937d9..5b6b4169b3 100644 --- a/tests/unit/sources/test_mercurial.py +++ b/tests/unit/sources/test_mercurial.py @@ -21,7 +21,7 @@ import fixtures from testtools.matchers import Equals -from snapcraft.internal import sources +from snapcraft_legacy.internal import sources from tests import unit @@ -29,7 +29,7 @@ class TestMercurial(unit.sources.SourceTestCase): # type: ignore def setUp(self): super().setUp() - patcher = mock.patch("snapcraft.sources.Mercurial._get_source_details") + patcher = mock.patch("snapcraft_legacy.sources.Mercurial._get_source_details") self.mock_get_source_details = patcher.start() self.mock_get_source_details.return_value = "" self.addCleanup(patcher.stop) diff --git a/tests/unit/sources/test_rpm.py b/tests/unit/sources/test_rpm.py index d86a121682..2d12abb94f 100644 --- a/tests/unit/sources/test_rpm.py +++ b/tests/unit/sources/test_rpm.py @@ -22,7 +22,7 @@ from testtools.matchers import Equals, MatchesRegex -from snapcraft.internal import sources +from snapcraft_legacy.internal import sources from tests import unit diff --git a/tests/unit/sources/test_script.py b/tests/unit/sources/test_script.py index 3486eed610..59480c2de0 100644 --- a/tests/unit/sources/test_script.py +++ b/tests/unit/sources/test_script.py @@ -19,7 +19,7 @@ from testtools.matchers import FileExists -from snapcraft.internal.sources import Script +from snapcraft_legacy.internal.sources import Script from tests import unit @@ -32,7 +32,7 @@ def setUp(self): self.source.file = os.path.join("destination", "file") open(self.source.file, "w").close() - @mock.patch("snapcraft.internal.sources._script.FileBase.download") + @mock.patch("snapcraft_legacy.internal.sources._script.FileBase.download") def test_download_makes_executable(self, mock_download): self.source.file = os.path.join("destination", "file") self.source.download() diff --git a/tests/unit/sources/test_snap.py b/tests/unit/sources/test_snap.py index 83f151470d..b45b27722c 100644 --- a/tests/unit/sources/test_snap.py +++ b/tests/unit/sources/test_snap.py @@ -21,7 +21,7 @@ from testtools.matchers import DirExists, Equals, FileExists, MatchesRegex -from snapcraft.internal import sources +from snapcraft_legacy.internal import sources from tests import unit diff --git a/tests/unit/sources/test_sources.py b/tests/unit/sources/test_sources.py index 130bf31b8b..a895b7e8b3 100644 --- a/tests/unit/sources/test_sources.py +++ b/tests/unit/sources/test_sources.py @@ -16,7 +16,7 @@ import pytest -from snapcraft.internal import sources +from snapcraft_legacy.internal import sources class TestUri: diff --git a/tests/unit/sources/test_subversion.py b/tests/unit/sources/test_subversion.py index f183d1a12a..457a164712 100644 --- a/tests/unit/sources/test_subversion.py +++ b/tests/unit/sources/test_subversion.py @@ -21,7 +21,7 @@ import fixtures from testtools.matchers import Equals -from snapcraft.internal import sources +from snapcraft_legacy.internal import sources from tests import unit @@ -30,7 +30,7 @@ class TestSubversion(unit.sources.SourceTestCase): # type: ignore def setUp(self): super().setUp() - patcher = mock.patch("snapcraft.sources.Subversion._get_source_details") + patcher = mock.patch("snapcraft_legacy.sources.Subversion._get_source_details") self.mock_get_source_details = patcher.start() self.mock_get_source_details.return_value = "" self.addCleanup(patcher.stop) diff --git a/tests/unit/sources/test_tar.py b/tests/unit/sources/test_tar.py index 31b8b93a84..41d8058485 100644 --- a/tests/unit/sources/test_tar.py +++ b/tests/unit/sources/test_tar.py @@ -21,12 +21,12 @@ import requests from testtools.matchers import Equals -from snapcraft.internal import sources +from snapcraft_legacy.internal import sources from tests import unit class TestTar(unit.FakeFileHTTPServerBasedTestCase): - @mock.patch("snapcraft.sources.Tar.provision") + @mock.patch("snapcraft_legacy.sources.Tar.provision") def test_pull_tarball_must_download_to_sourcedir(self, mock_prov): plugin_name = "test_plugin" dest_dir = os.path.join("parts", plugin_name, "src") @@ -44,7 +44,7 @@ def test_pull_tarball_must_download_to_sourcedir(self, mock_prov): with open(os.path.join(dest_dir, tar_file_name), "r") as tar_file: self.assertThat(tar_file.read(), Equals("Test fake file")) - @mock.patch("snapcraft.sources.Tar.provision") + @mock.patch("snapcraft_legacy.sources.Tar.provision") def test_pull_twice_downloads_once(self, mock_prov): """If a source checksum is defined, the cache should be tried first.""" source = "http://{}:{}/{file_name}".format( diff --git a/tests/unit/sources/test_zip.py b/tests/unit/sources/test_zip.py index dd0ecf1b84..e1df26010e 100644 --- a/tests/unit/sources/test_zip.py +++ b/tests/unit/sources/test_zip.py @@ -19,7 +19,7 @@ from testtools.matchers import Equals -from snapcraft.internal import sources +from snapcraft_legacy.internal import sources from tests import unit diff --git a/tests/unit/states/conftest.py b/tests/unit/states/conftest.py index fbce25210d..09bbd3db8c 100644 --- a/tests/unit/states/conftest.py +++ b/tests/unit/states/conftest.py @@ -2,7 +2,7 @@ import pytest -from snapcraft.internal import states +from snapcraft_legacy.internal import states class Project: diff --git a/tests/unit/states/test_build.py b/tests/unit/states/test_build.py index b068c7d209..a6da1ff7f7 100644 --- a/tests/unit/states/test_build.py +++ b/tests/unit/states/test_build.py @@ -18,8 +18,8 @@ from testtools.matchers import Equals -import snapcraft.internal -from snapcraft import yaml_utils +import snapcraft_legacy.internal +from snapcraft_legacy import yaml_utils from tests import unit from .conftest import Project @@ -33,16 +33,16 @@ def setUp(self): self.property_names = ["foo"] self.part_properties = {"foo": "bar"} - self.state = snapcraft.internal.states.BuildState( + self.state = snapcraft_legacy.internal.states.BuildState( self.property_names, self.part_properties, self.project ) class BuildStateTestCase(BuildStateBaseTestCase): @mock.patch.object( - snapcraft.internal.states.BuildState, + snapcraft_legacy.internal.states.BuildState, "__init__", - wraps=snapcraft.internal.states.BuildState.__init__, + wraps=snapcraft_legacy.internal.states.BuildState.__init__, ) def test_yaml_conversion(self, init_spy): state_string = yaml_utils.dump(self.state) @@ -58,7 +58,7 @@ def test_yaml_conversion(self, init_spy): init_spy.assert_not_called() def test_comparison(self): - other = snapcraft.internal.states.BuildState( + other = snapcraft_legacy.internal.states.BuildState( self.property_names, self.part_properties, self.project ) diff --git a/tests/unit/states/test_global_state.py b/tests/unit/states/test_global_state.py index 428909d369..d3541d490c 100644 --- a/tests/unit/states/test_global_state.py +++ b/tests/unit/states/test_global_state.py @@ -16,7 +16,7 @@ from textwrap import dedent -from snapcraft.internal.states import GlobalState +from snapcraft_legacy.internal.states import GlobalState _scenarios = [ ( diff --git a/tests/unit/states/test_prime.py b/tests/unit/states/test_prime.py index c524c242e5..ec06a84c40 100644 --- a/tests/unit/states/test_prime.py +++ b/tests/unit/states/test_prime.py @@ -18,8 +18,8 @@ from testtools.matchers import Equals -import snapcraft.internal -from snapcraft import yaml_utils +import snapcraft_legacy.internal +from snapcraft_legacy import yaml_utils from tests import unit from .conftest import Project @@ -38,7 +38,7 @@ def setUp(self): "prime": ["qux"], } - self.state = snapcraft.internal.states.PrimeState( + self.state = snapcraft_legacy.internal.states.PrimeState( self.files, self.directories, self.dependency_paths, @@ -49,9 +49,9 @@ def setUp(self): class PrimeStateTestCase(PrimeStateBaseTestCase): @mock.patch.object( - snapcraft.internal.states.PrimeState, + snapcraft_legacy.internal.states.PrimeState, "__init__", - wraps=snapcraft.internal.states.PrimeState.__init__, + wraps=snapcraft_legacy.internal.states.PrimeState.__init__, ) def test_yaml_conversion(self, init_spy): state_string = yaml_utils.dump(self.state) @@ -67,7 +67,7 @@ def test_yaml_conversion(self, init_spy): init_spy.assert_not_called() def test_comparison(self): - other = snapcraft.internal.states.PrimeState( + other = snapcraft_legacy.internal.states.PrimeState( self.files, self.directories, self.dependency_paths, diff --git a/tests/unit/states/test_pull.py b/tests/unit/states/test_pull.py index 5fe2064def..41d2b809a9 100644 --- a/tests/unit/states/test_pull.py +++ b/tests/unit/states/test_pull.py @@ -18,8 +18,8 @@ from testtools.matchers import Equals -import snapcraft.internal -from snapcraft import yaml_utils +import snapcraft_legacy.internal +from snapcraft_legacy import yaml_utils from tests import unit from .conftest import Project @@ -33,16 +33,16 @@ def setUp(self): self.property_names = ["foo"] self.part_properties = {"foo": "bar"} - self.state = snapcraft.internal.states.PullState( + self.state = snapcraft_legacy.internal.states.PullState( self.property_names, self.part_properties, self.project ) class PullStateTestCase(PullStateBaseTestCase): @mock.patch.object( - snapcraft.internal.states.PullState, + snapcraft_legacy.internal.states.PullState, "__init__", - wraps=snapcraft.internal.states.PullState.__init__, + wraps=snapcraft_legacy.internal.states.PullState.__init__, ) def test_yaml_conversion(self, init_spy): state_string = yaml_utils.dump(self.state) @@ -58,7 +58,7 @@ def test_yaml_conversion(self, init_spy): init_spy.assert_not_called() def test_comparison(self): - other = snapcraft.internal.states.PullState( + other = snapcraft_legacy.internal.states.PullState( self.property_names, self.part_properties, self.project ) diff --git a/tests/unit/states/test_stage.py b/tests/unit/states/test_stage.py index 483971419a..40704c3aba 100644 --- a/tests/unit/states/test_stage.py +++ b/tests/unit/states/test_stage.py @@ -18,8 +18,8 @@ from testtools.matchers import Equals -import snapcraft.internal -from snapcraft import yaml_utils +import snapcraft_legacy.internal +from snapcraft_legacy import yaml_utils from tests import unit from .conftest import Project @@ -38,16 +38,16 @@ def setUp(self): "stage": ["baz"], } - self.state = snapcraft.internal.states.StageState( + self.state = snapcraft_legacy.internal.states.StageState( self.files, self.directories, self.part_properties, self.project ) class StateStageTestCase(StageStateBaseTestCase): @mock.patch.object( - snapcraft.internal.states.StageState, + snapcraft_legacy.internal.states.StageState, "__init__", - wraps=snapcraft.internal.states.StageState.__init__, + wraps=snapcraft_legacy.internal.states.StageState.__init__, ) def test_yaml_conversion(self, init_spy): state_string = yaml_utils.dump(self.state) @@ -63,7 +63,7 @@ def test_yaml_conversion(self, init_spy): init_spy.assert_not_called() def test_comparison(self): - other = snapcraft.internal.states.StageState( + other = snapcraft_legacy.internal.states.StageState( self.files, self.directories, self.part_properties, self.project ) diff --git a/tests/unit/states/test_state.py b/tests/unit/states/test_state.py index 03c73448d7..833dc549cd 100644 --- a/tests/unit/states/test_state.py +++ b/tests/unit/states/test_state.py @@ -14,7 +14,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -from snapcraft.internal.states._state import PartState +from snapcraft_legacy.internal.states._state import PartState class _TestState(PartState): diff --git a/tests/unit/store/http_client/test_agent.py b/tests/unit/store/http_client/test_agent.py index 0cab921263..db78138603 100644 --- a/tests/unit/store/http_client/test_agent.py +++ b/tests/unit/store/http_client/test_agent.py @@ -19,9 +19,9 @@ import fixtures from testtools.matchers import Equals -from snapcraft import ProjectOptions -from snapcraft import __version__ as snapcraft_version -from snapcraft.storeapi.http_clients import agent +from snapcraft_legacy import ProjectOptions +from snapcraft_legacy import __version__ as snapcraft_version +from snapcraft_legacy.storeapi.http_clients import agent from tests import unit from tests.fixture_setup.os_release import FakeOsRelease diff --git a/tests/unit/store/http_client/test_candid_client.py b/tests/unit/store/http_client/test_candid_client.py index 76390322c6..88b5665e01 100644 --- a/tests/unit/store/http_client/test_candid_client.py +++ b/tests/unit/store/http_client/test_candid_client.py @@ -23,7 +23,7 @@ from macaroonbakery import bakery, httpbakery from pymacaroons.macaroon import Macaroon -from snapcraft.storeapi.http_clients._candid_client import ( +from snapcraft_legacy.storeapi.http_clients._candid_client import ( CandidClient, CandidConfig, WebBrowserWaitingInteractor, diff --git a/tests/unit/store/http_client/test_config.py b/tests/unit/store/http_client/test_config.py index 9bb641f858..253adc2d3a 100644 --- a/tests/unit/store/http_client/test_config.py +++ b/tests/unit/store/http_client/test_config.py @@ -18,8 +18,8 @@ import pytest -from snapcraft.storeapi.http_clients import errors -from snapcraft.storeapi.http_clients._config import Config +from snapcraft_legacy.storeapi.http_clients import errors +from snapcraft_legacy.storeapi.http_clients._config import Config class ConfigImpl(Config): diff --git a/tests/unit/store/http_client/test_errors.py b/tests/unit/store/http_client/test_errors.py index 69453870d9..a249418307 100644 --- a/tests/unit/store/http_client/test_errors.py +++ b/tests/unit/store/http_client/test_errors.py @@ -18,7 +18,7 @@ import urllib3 from unittest import mock -from snapcraft.storeapi.http_clients import errors +from snapcraft_legacy.storeapi.http_clients import errors def _fake_error_response(status_code, reason): diff --git a/tests/unit/store/http_client/test_ubuntu_one_auth_client.py b/tests/unit/store/http_client/test_ubuntu_one_auth_client.py index 1ea07805aa..ac8efb0dab 100644 --- a/tests/unit/store/http_client/test_ubuntu_one_auth_client.py +++ b/tests/unit/store/http_client/test_ubuntu_one_auth_client.py @@ -19,7 +19,7 @@ import pymacaroons import pytest -from snapcraft.storeapi import http_clients +from snapcraft_legacy.storeapi import http_clients def test_invalid_macaroon_root_raises_exception(tmp_work_path): diff --git a/tests/unit/store/test_channels.py b/tests/unit/store/test_channels.py index 14b5182277..9b674d4323 100644 --- a/tests/unit/store/test_channels.py +++ b/tests/unit/store/test_channels.py @@ -16,7 +16,7 @@ import pytest -from snapcraft.storeapi import channels +from snapcraft_legacy.storeapi import channels class TestChannel: diff --git a/tests/unit/store/test_errors.py b/tests/unit/store/test_errors.py index 403dc481aa..725356f187 100644 --- a/tests/unit/store/test_errors.py +++ b/tests/unit/store/test_errors.py @@ -16,7 +16,7 @@ from textwrap import dedent -from snapcraft.storeapi import errors +from snapcraft_legacy.storeapi import errors class TestSnapcraftException: diff --git a/tests/unit/store/test_metrics.py b/tests/unit/store/test_metrics.py index 508d242473..892d85eae9 100644 --- a/tests/unit/store/test_metrics.py +++ b/tests/unit/store/test_metrics.py @@ -18,7 +18,7 @@ import pytest -from snapcraft.storeapi.metrics import ( +from snapcraft_legacy.storeapi.metrics import ( MetricResults, MetricsFilter, MetricsNames, diff --git a/tests/unit/store/test_status.py b/tests/unit/store/test_status.py index b5740f626e..7993c2844c 100644 --- a/tests/unit/store/test_status.py +++ b/tests/unit/store/test_status.py @@ -16,7 +16,7 @@ from testtools.matchers import Equals, HasLength -from snapcraft.storeapi import channels, errors, status +from snapcraft_legacy.storeapi import channels, errors, status from tests import unit diff --git a/tests/unit/store/test_store_client.py b/tests/unit/store/test_store_client.py index 93fb7ad0fb..3ed4f6c172 100644 --- a/tests/unit/store/test_store_client.py +++ b/tests/unit/store/test_store_client.py @@ -35,9 +35,9 @@ ) import tests -from snapcraft import storeapi -from snapcraft.storeapi import errors, http_clients, metrics -from snapcraft.storeapi.v2 import channel_map, releases, validation_sets, whoami +from snapcraft_legacy import storeapi +from snapcraft_legacy.storeapi import errors, http_clients, metrics +from snapcraft_legacy.storeapi.v2 import channel_map, releases, validation_sets, whoami from tests import fixture_setup, unit @@ -958,8 +958,8 @@ def setUp(self): ) # These should eventually converge to the same module pbars = ( - "snapcraft.storeapi._upload.ProgressBar", - "snapcraft.storeapi._status_tracker.ProgressBar", + "snapcraft_legacy.storeapi._upload.ProgressBar", + "snapcraft_legacy.storeapi._status_tracker.ProgressBar", ) for pbar in pbars: patcher = mock.patch(pbar, new=unit.SilentProgressBar) diff --git a/tests/unit/store/v2/test_channel_map.py b/tests/unit/store/v2/test_channel_map.py index 22d8bb4c47..542c55f6e9 100644 --- a/tests/unit/store/v2/test_channel_map.py +++ b/tests/unit/store/v2/test_channel_map.py @@ -17,7 +17,7 @@ import pytest from testtools.matchers import Equals, HasLength, Is, IsInstance -from snapcraft.storeapi.v2 import channel_map +from snapcraft_legacy.storeapi.v2 import channel_map from tests import unit diff --git a/tests/unit/store/v2/test_releases.py b/tests/unit/store/v2/test_releases.py index ad692e38b0..28bbe5676d 100644 --- a/tests/unit/store/v2/test_releases.py +++ b/tests/unit/store/v2/test_releases.py @@ -16,7 +16,7 @@ import pytest -from snapcraft.storeapi.v2 import releases +from snapcraft_legacy.storeapi.v2 import releases @pytest.mark.parametrize( diff --git a/tests/unit/store/v2/test_validation_sets.py b/tests/unit/store/v2/test_validation_sets.py index a6598f8e63..e63db9188f 100644 --- a/tests/unit/store/v2/test_validation_sets.py +++ b/tests/unit/store/v2/test_validation_sets.py @@ -16,7 +16,7 @@ import pytest -from snapcraft.storeapi.v2 import validation_sets +from snapcraft_legacy.storeapi.v2 import validation_sets @pytest.mark.parametrize("snap_id", (None, "snap_id")) diff --git a/tests/unit/store/v2/test_whoami.py b/tests/unit/store/v2/test_whoami.py index 2e5bf7f884..11620e1549 100644 --- a/tests/unit/store/v2/test_whoami.py +++ b/tests/unit/store/v2/test_whoami.py @@ -17,7 +17,7 @@ import pytest from jsonschema.exceptions import ValidationError -from snapcraft.storeapi.v2 import whoami +from snapcraft_legacy.storeapi.v2 import whoami @pytest.fixture diff --git a/tests/unit/test_common.py b/tests/unit/test_common.py index 0be663d380..0d77511add 100644 --- a/tests/unit/test_common.py +++ b/tests/unit/test_common.py @@ -19,7 +19,7 @@ import pytest from testtools.matchers import Equals -from snapcraft.internal import common, errors +from snapcraft_legacy.internal import common, errors from tests import unit diff --git a/tests/unit/test_config.py b/tests/unit/test_config.py index 3e3db651e4..be93fd6bcf 100644 --- a/tests/unit/test_config.py +++ b/tests/unit/test_config.py @@ -19,8 +19,8 @@ import xdg from testtools.matchers import Equals -from snapcraft import config -from snapcraft.internal.errors import SnapcraftInvalidCLIConfigError +from snapcraft_legacy import config +from snapcraft_legacy.internal.errors import SnapcraftInvalidCLIConfigError from tests import unit diff --git a/tests/unit/test_elf.py b/tests/unit/test_elf.py index 03622bf291..ec0739fbb7 100644 --- a/tests/unit/test_elf.py +++ b/tests/unit/test_elf.py @@ -24,8 +24,8 @@ import pytest from testtools.matchers import Contains, EndsWith, Equals, NotEquals, StartsWith -from snapcraft import ProjectOptions -from snapcraft.internal import elf, errors +from snapcraft_legacy import ProjectOptions +from snapcraft_legacy.internal import elf, errors from tests import fixture_setup, unit @@ -262,7 +262,9 @@ def _is_valid_elf(self, resolved_path: str) -> bool: else: return super()._is_valid_elf(resolved_path) - with mock.patch("snapcraft.internal.elf.Library", side_effect=MooLibrary): + with mock.patch( + "snapcraft_legacy.internal.elf.Library", side_effect=MooLibrary + ): libs = elf_file.load_dependencies( root_path=self.fake_elf.root_path, core_base_path=self.fake_elf.core_base_path, @@ -290,7 +292,7 @@ def test_is_valid_elf_ignores_corrupt_files(self): self.useFixture( fixtures.MockPatch( - "snapcraft.internal.elf.ElfFile", + "snapcraft_legacy.internal.elf.ElfFile", side_effect=errors.CorruptedElfFileError( path=soname_path, error=RuntimeError() ), @@ -526,7 +528,9 @@ def _setup_libc6(self): def setUp(self): super().setUp() - patcher = mock.patch("snapcraft.internal.repo.Repo.get_package_libraries") + patcher = mock.patch( + "snapcraft_legacy.internal.repo.Repo.get_package_libraries" + ) self.get_packages_mock = patcher.start() self.get_packages_mock.return_value = self._setup_libc6() self.addCleanup(patcher.stop) diff --git a/tests/unit/test_errors.py b/tests/unit/test_errors.py index 542bfad347..801a8063d5 100644 --- a/tests/unit/test_errors.py +++ b/tests/unit/test_errors.py @@ -18,10 +18,12 @@ from subprocess import CalledProcessError from typing import List -from snapcraft.internal import errors, pluginhandler, steps -from snapcraft.internal.project_loader import errors as project_loader_errors -from snapcraft.internal.project_loader.inspection import errors as inspection_errors -from snapcraft.internal.repo import errors as repo_errors +from snapcraft_legacy.internal import errors, pluginhandler, steps +from snapcraft_legacy.internal.project_loader import errors as project_loader_errors +from snapcraft_legacy.internal.project_loader.inspection import ( + errors as inspection_errors, +) +from snapcraft_legacy.internal.repo import errors as repo_errors def test_details_from_called_process_error(): diff --git a/tests/unit/test_file_utils.py b/tests/unit/test_file_utils.py index 613a05a2e6..efaf7c7de0 100644 --- a/tests/unit/test_file_utils.py +++ b/tests/unit/test_file_utils.py @@ -25,8 +25,8 @@ import testtools from testtools.matchers import Equals -from snapcraft import file_utils -from snapcraft.internal import common, errors +from snapcraft_legacy import file_utils +from snapcraft_legacy.internal import common, errors from tests import fixture_setup, unit diff --git a/tests/unit/test_formatting_utils.py b/tests/unit/test_formatting_utils.py index 7269eeda4e..f068d94e7e 100644 --- a/tests/unit/test_formatting_utils.py +++ b/tests/unit/test_formatting_utils.py @@ -17,7 +17,7 @@ import pytest from testtools.matchers import Equals -from snapcraft import formatting_utils +from snapcraft_legacy import formatting_utils from tests import unit diff --git a/tests/unit/test_indicators.py b/tests/unit/test_indicators.py index ea9db69d1c..56ca33cdc5 100644 --- a/tests/unit/test_indicators.py +++ b/tests/unit/test_indicators.py @@ -21,7 +21,7 @@ import progressbar import requests -from snapcraft.internal import indicators +from snapcraft_legacy.internal import indicators from tests import unit diff --git a/tests/unit/test_init.py b/tests/unit/test_init.py index 88066c2baa..6807b3fcdb 100644 --- a/tests/unit/test_init.py +++ b/tests/unit/test_init.py @@ -17,7 +17,7 @@ import fixtures from testtools.matchers import Equals -import snapcraft +import snapcraft_legacy from tests import unit @@ -25,4 +25,4 @@ class VersionTestCase(unit.TestCase): def test_version_from_snap(self): self.useFixture(fixtures.EnvironmentVariable("SNAP_NAME", "snapcraft")) self.useFixture(fixtures.EnvironmentVariable("SNAP_VERSION", "3.14")) - self.assertThat(snapcraft._get_version(), Equals("3.14")) + self.assertThat(snapcraft_legacy._get_version(), Equals("3.14")) diff --git a/tests/unit/test_log.py b/tests/unit/test_log.py index b62a237f4c..78a9cb0752 100644 --- a/tests/unit/test_log.py +++ b/tests/unit/test_log.py @@ -18,7 +18,7 @@ from testtools.matchers import Contains, Equals, Not -from snapcraft.internal import log +from snapcraft_legacy.internal import log from tests import fixture_setup, unit diff --git a/tests/unit/test_mangling.py b/tests/unit/test_mangling.py index 942fe9634b..0933422373 100644 --- a/tests/unit/test_mangling.py +++ b/tests/unit/test_mangling.py @@ -19,7 +19,7 @@ from testtools.matchers import FileContains, FileExists, Not -from snapcraft.internal import mangling +from snapcraft_legacy.internal import mangling from tests import fixture_setup, unit diff --git a/tests/unit/test_mountinfo.py b/tests/unit/test_mountinfo.py index 209cc13408..86185b32c6 100644 --- a/tests/unit/test_mountinfo.py +++ b/tests/unit/test_mountinfo.py @@ -20,7 +20,7 @@ import fixtures from testtools.matchers import Equals, HasLength -from snapcraft.internal import errors, mountinfo +from snapcraft_legacy.internal import errors, mountinfo from tests import unit diff --git a/tests/unit/test_options.py b/tests/unit/test_options.py index 2002b44d27..11d6468804 100644 --- a/tests/unit/test_options.py +++ b/tests/unit/test_options.py @@ -21,10 +21,10 @@ import testtools from testtools.matchers import Equals -import snapcraft -from snapcraft.internal import common -from snapcraft.internal.errors import SnapcraftEnvironmentError -from snapcraft.project._project_options import ( +import snapcraft_legacy +from snapcraft_legacy.internal import common +from snapcraft_legacy.internal.errors import SnapcraftEnvironmentError +from snapcraft_legacy.project._project_options import ( _32BIT_USERSPACE_ARCHITECTURE, _get_platform_architecture, ) @@ -181,7 +181,7 @@ def test_architecture_options( monkeypatch.setattr(platform, "architecture", lambda: architecture) monkeypatch.setattr(platform, "machine", lambda: machine) - options = snapcraft.ProjectOptions() + options = snapcraft_legacy.ProjectOptions() assert options.arch_triplet == expected_arch_triplet assert options.deb_arch == expected_deb_arch @@ -221,7 +221,7 @@ def test_get_platform_architecture( class OptionsTestCase(unit.TestCase): def test_cross_compiler_prefix_missing(self): - options = snapcraft.ProjectOptions(target_deb_arch="x86_64") + options = snapcraft_legacy.ProjectOptions(target_deb_arch="x86_64") with testtools.ExpectedException( SnapcraftEnvironmentError, @@ -236,7 +236,7 @@ def test_cross_compiler_prefix_empty( ): mock_platform_machine.return_value = "x86_64" mock_platform_architecture.return_value = ("64bit", "ELF") - options = snapcraft.ProjectOptions(target_deb_arch="i386") + options = snapcraft_legacy.ProjectOptions(target_deb_arch="i386") self.assertThat(options.cross_compiler_prefix, Equals("")) @@ -263,13 +263,13 @@ class TestHostIsCompatibleWithTargetBase: def test_compatibility(self, monkeypatch, codename, base, is_compatible): monkeypatch.setattr( - snapcraft.internal.os_release.OsRelease, + snapcraft_legacy.internal.os_release.OsRelease, "version_codename", lambda x: codename, ) assert ( - snapcraft.ProjectOptions().is_host_compatible_with_base(base) + snapcraft_legacy.ProjectOptions().is_host_compatible_with_base(base) is is_compatible ) @@ -277,20 +277,20 @@ def test_compatibility(self, monkeypatch, codename, base, is_compatible): class TestLinkerVersionForBase(unit.TestCase): def setUp(self): super().setUp() - patcher = mock.patch("snapcraft.file_utils.get_linker_version_from_file") + patcher = mock.patch("snapcraft_legacy.file_utils.get_linker_version_from_file") self.get_linker_version_mock = patcher.start() self.addCleanup(patcher.stop) def test_get_linker_version_for_core20(self): self.assertThat( - snapcraft.ProjectOptions()._get_linker_version_for_base("core20"), + snapcraft_legacy.ProjectOptions()._get_linker_version_for_base("core20"), Equals("2.31"), ) self.get_linker_version_mock.assert_not_called() def test_get_linker_version_for_core18(self): self.assertThat( - snapcraft.ProjectOptions()._get_linker_version_for_base("core18"), + snapcraft_legacy.ProjectOptions()._get_linker_version_for_base("core18"), Equals("2.27"), ) self.get_linker_version_mock.assert_not_called() @@ -298,7 +298,7 @@ def test_get_linker_version_for_core18(self): def test_get_linker_version_for_random_core(self): self.get_linker_version_mock.return_value = "4.10" self.assertThat( - snapcraft.ProjectOptions()._get_linker_version_for_base("random"), + snapcraft_legacy.ProjectOptions()._get_linker_version_for_base("random"), Equals("4.10"), ) self.get_linker_version_mock.assert_called_once_with("ld-2.23.so") diff --git a/tests/unit/test_os_release.py b/tests/unit/test_os_release.py index da25cfa4cc..21389a0ad1 100644 --- a/tests/unit/test_os_release.py +++ b/tests/unit/test_os_release.py @@ -18,7 +18,7 @@ from testtools.matchers import Equals -from snapcraft.internal import errors, os_release +from snapcraft_legacy.internal import errors, os_release from tests import unit diff --git a/tests/unit/test_steps.py b/tests/unit/test_steps.py index 497f151d37..eb8f6df922 100644 --- a/tests/unit/test_steps.py +++ b/tests/unit/test_steps.py @@ -16,7 +16,7 @@ import pytest -from snapcraft.internal import steps +from snapcraft_legacy.internal import steps def test_step_order(): diff --git a/tests/unit/test_target_arch.py b/tests/unit/test_target_arch.py index 20ea037ae0..039fff1455 100644 --- a/tests/unit/test_target_arch.py +++ b/tests/unit/test_target_arch.py @@ -14,7 +14,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -from snapcraft.project._project_options import _find_machine +from snapcraft_legacy.project._project_options import _find_machine class TestFindMachine: diff --git a/tests/unit/test_xattrs.py b/tests/unit/test_xattrs.py index 4608508a73..431327e2d4 100644 --- a/tests/unit/test_xattrs.py +++ b/tests/unit/test_xattrs.py @@ -20,8 +20,8 @@ from testtools.matchers import Equals -from snapcraft.internal import xattrs -from snapcraft.internal.errors import XAttributeTooLongError +from snapcraft_legacy.internal import xattrs +from snapcraft_legacy.internal.errors import XAttributeTooLongError from tests import unit diff --git a/tests/unit/yaml_utils/test_errors.py b/tests/unit/yaml_utils/test_errors.py index 1e2f7c3b9e..7871057bdc 100644 --- a/tests/unit/yaml_utils/test_errors.py +++ b/tests/unit/yaml_utils/test_errors.py @@ -14,7 +14,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -from snapcraft.yaml_utils import errors +from snapcraft_legacy.yaml_utils import errors def test_YamlvalidationError(): diff --git a/tests/unit/yaml_utils/test_yaml_utils.py b/tests/unit/yaml_utils/test_yaml_utils.py index 35310dea7e..0815007a03 100644 --- a/tests/unit/yaml_utils/test_yaml_utils.py +++ b/tests/unit/yaml_utils/test_yaml_utils.py @@ -18,8 +18,8 @@ import pytest -from snapcraft import yaml_utils -from snapcraft.yaml_utils import YamlValidationError +from snapcraft_legacy import yaml_utils +from snapcraft_legacy.yaml_utils import YamlValidationError def test_load_yaml_file(caplog, tmp_path): diff --git a/tools/brew_install_from_source.py b/tools/brew_install_from_source.py index 644d457d19..22ca41d371 100755 --- a/tools/brew_install_from_source.py +++ b/tools/brew_install_from_source.py @@ -28,11 +28,11 @@ def main(): temp_dir = tempfile.mkdtemp() compressed_snapcraft_source = download_snapcraft_source(temp_dir) compressed_snapcraft_sha256 = sha256_checksum(compressed_snapcraft_source) - brew_formula_path = os.path.join(temp_dir, "snapcraft.rb") + brew_formula_path = os.path.join(temp_dir, "snapcraft_legacy.rb") download_brew_formula(brew_formula_path) patched_dir = os.path.join(temp_dir, "patched") os.mkdir(patched_dir) - brew_formula_from_source_path = os.path.join(patched_dir, "snapcraft.rb") + brew_formula_from_source_path = os.path.join(patched_dir, "snapcraft_legacy.rb") patch_brew_formula_source( brew_formula_path, brew_formula_from_source_path, diff --git a/units.py b/units.py index c3b19f15f1..16351b2fa9 100644 --- a/units.py +++ b/units.py @@ -1,5 +1,5 @@ import unittest unittest.main( - "snapcraft.tests.unit.commands.test_build", argv=["BuildCommandTestCase"] + "snapcraft_legacy.tests.unit.commands.test_build", argv=["BuildCommandTestCase"] ) # noqa From 2501399d41cf448f2e5d76c00a5633ebf10c45a9 Mon Sep 17 00:00:00 2001 From: Claudio Matsuoka Date: Thu, 6 Jan 2022 15:57:35 -0300 Subject: [PATCH 002/167] tests: move legacy unit tests The legacy snapcraft package was renamed to snapcraft_legacy, so move its unit tests to tests/legacy. Signed-off-by: Claudio Matsuoka --- Makefile | 8 +- .../lxd => legacy}/__init__.py | 0 tests/{ => legacy}/data/icon.png | Bin tests/{ => legacy}/data/invalid.snap | 0 .../test-snap-with-icon-license-title.snap | Bin .../data/test-snap-with-icon.snap | Bin .../data/test-snap-with-started-at.snap | Bin tests/{ => legacy}/data/test-snap.snap | Bin tests/{ => legacy}/data/test.desktop | 0 tests/{ => legacy}/fake_servers/__init__.py | 0 tests/{ => legacy}/fake_servers/api.py | 2 +- tests/{ => legacy}/fake_servers/base.py | 0 tests/{ => legacy}/fake_servers/search.py | 8 +- tests/{ => legacy}/fake_servers/snapd.py | 2 +- tests/{ => legacy}/fake_servers/upload.py | 2 +- tests/{ => legacy}/fixture_setup/__init__.py | 0 tests/{ => legacy}/fixture_setup/_fixtures.py | 4 +- .../{ => legacy}/fixture_setup/_unittests.py | 0 tests/{ => legacy}/fixture_setup/_unix.py | 2 +- .../{ => legacy}/fixture_setup/os_release.py | 0 tests/legacy/unit/__init__.py | 318 ++++++++++++++++++ .../unit/build_providers/__init__.py | 2 +- .../unit/build_providers/conftest.py | 0 .../unit/build_providers/lxd}/__init__.py | 0 .../unit/build_providers/lxd/test_lxd.py | 2 +- .../build_providers/multipass}/__init__.py | 0 .../multipass/test_instance_info.py | 2 +- .../multipass/test_multipass.py | 2 +- .../multipass/test_multipass_command.py | 2 +- .../build_providers/test_base_provider.py | 0 .../unit/build_providers/test_errors.py | 0 .../unit/build_providers/test_snap.py | 2 +- .../cli => legacy/unit/cache}/__init__.py | 0 tests/{ => legacy}/unit/cache/conftest.py | 0 tests/{ => legacy}/unit/cache/test_file.py | 0 tests/{ => legacy}/unit/cache/test_snap.py | 6 +- .../deltas => legacy/unit/cli}/__init__.py | 0 tests/{ => legacy}/unit/cli/conftest.py | 0 tests/{ => legacy}/unit/cli/test_echo.py | 2 +- tests/{ => legacy}/unit/cli/test_errors.py | 2 +- tests/{ => legacy}/unit/cli/test_lifecycle.py | 0 tests/{ => legacy}/unit/cli/test_metrics.py | 0 tests/{ => legacy}/unit/cli/test_options.py | 2 +- tests/{ => legacy}/unit/commands/__init__.py | 2 +- tests/{ => legacy}/unit/commands/conftest.py | 0 .../unit/commands/snapcraftctl/__init__.py | 2 +- .../unit/commands/snapcraftctl/test_build.py | 0 .../commands/snapcraftctl/test_set_grade.py | 0 .../commands/snapcraftctl/test_set_version.py | 0 .../unit/commands/test_build_providers.py | 6 +- .../{ => legacy}/unit/commands/test_clean.py | 0 .../{ => legacy}/unit/commands/test_close.py | 0 .../unit/commands/test_create_key.py | 0 .../commands/test_edit_validation_sets.py | 0 .../unit/commands/test_export_login.py | 0 .../unit/commands/test_extensions.py | 2 +- .../{ => legacy}/unit/commands/test_gated.py | 0 tests/{ => legacy}/unit/commands/test_help.py | 2 +- tests/{ => legacy}/unit/commands/test_init.py | 0 tests/{ => legacy}/unit/commands/test_list.py | 0 .../unit/commands/test_list_keys.py | 0 .../unit/commands/test_list_plugins.py | 2 +- .../unit/commands/test_list_revisions.py | 0 .../unit/commands/test_list_tracks.py | 0 .../commands/test_list_validation_sets.py | 0 .../{ => legacy}/unit/commands/test_login.py | 0 .../{ => legacy}/unit/commands/test_logout.py | 0 .../unit/commands/test_metrics.py | 0 tests/{ => legacy}/unit/commands/test_pack.py | 0 .../unit/commands/test_promote.py | 0 .../commands/test_pull_build_stage_prime.py | 0 .../unit/commands/test_refresh.py | 2 +- .../unit/commands/test_register.py | 0 .../unit/commands/test_register_key.py | 0 .../unit/commands/test_release.py | 0 .../{ => legacy}/unit/commands/test_remote.py | 2 +- .../unit/commands/test_set_default_track.py | 0 .../unit/commands/test_sign_build.py | 6 +- tests/{ => legacy}/unit/commands/test_snap.py | 0 .../{ => legacy}/unit/commands/test_status.py | 0 .../{ => legacy}/unit/commands/test_upload.py | 10 +- .../unit/commands/test_upload_metadata.py | 8 +- .../unit/commands/test_validate.py | 0 .../unit/commands/test_version.py | 0 .../{ => legacy}/unit/commands/test_whoami.py | 0 tests/{ => legacy}/unit/conftest.py | 0 tests/{ => legacy}/unit/db/test_datastore.py | 0 tests/{ => legacy}/unit/db/test_errors.py | 0 tests/{ => legacy}/unit/db/test_migration.py | 0 .../unit/deltas}/__init__.py | 0 tests/{ => legacy}/unit/deltas/test_deltas.py | 2 +- .../unit/deltas/test_deltas_xdelta3.py | 2 +- .../unit/extractors}/__init__.py | 0 .../unit/extractors/test_appstream.py | 2 +- .../unit/extractors/test_metadata.py | 2 +- .../unit/extractors/test_setuppy.py | 2 +- tests/{ => legacy}/unit/lifecycle/__init__.py | 2 +- .../unit/lifecycle/test_errors.py | 0 .../unit/lifecycle/test_global_state.py | 2 +- .../unit/lifecycle/test_lifecycle.py | 2 +- .../{ => legacy}/unit/lifecycle/test_order.py | 0 .../unit/lifecycle/test_snap_installation.py | 0 .../unit/lifecycle/test_status_cache.py | 0 .../unit/meta}/__init__.py | 0 .../unit/meta/test_application.py | 2 +- tests/{ => legacy}/unit/meta/test_command.py | 2 +- .../unit/meta/test_command_mangle.py | 0 tests/{ => legacy}/unit/meta/test_desktop.py | 0 tests/{ => legacy}/unit/meta/test_errors.py | 0 tests/{ => legacy}/unit/meta/test_hook.py | 2 +- tests/{ => legacy}/unit/meta/test_meta.py | 2 +- .../unit/meta/test_package_repository.py | 0 tests/{ => legacy}/unit/meta/test_plugs.py | 2 +- tests/{ => legacy}/unit/meta/test_slots.py | 2 +- tests/{ => legacy}/unit/meta/test_snap.py | 2 +- .../unit/meta/test_snap_packaging.py | 2 +- .../unit/meta/test_system_user.py | 2 +- tests/{ => legacy}/unit/part_loader.py | 0 .../unit/pluginhandler}/__init__.py | 0 .../{ => legacy}/unit/pluginhandler/mocks.py | 0 .../unit/pluginhandler/test_clean.py | 2 +- .../unit/pluginhandler/test_dirty_report.py | 0 .../pluginhandler/test_metadata_extraction.py | 2 +- .../pluginhandler/test_missing_dependency.py | 2 +- .../unit/pluginhandler/test_patcher.py | 2 +- .../unit/pluginhandler/test_plugin_loader.py | 2 +- .../unit/pluginhandler/test_pluginhandler.py | 2 +- .../unit/pluginhandler/test_runner.py | 2 +- .../unit/pluginhandler/test_scriptlets.py | 4 +- .../unit/pluginhandler/test_state.py | 2 +- .../unit/plugins}/__init__.py | 0 .../{ => legacy}/unit/plugins/v1/__init__.py | 2 +- .../{ => legacy}/unit/plugins/v1/conftest.py | 0 .../unit/plugins/v1/python}/__init__.py | 0 .../unit/plugins/v1/python/_basesuite.py | 2 +- .../unit/plugins/v1/python/test_errors.py | 0 .../unit/plugins/v1/python/test_pip.py | 0 .../plugins/v1/python/test_python_finder.py | 0 .../plugins/v1/python/test_sitecustomize.py | 0 .../unit/plugins/v1/ros}/__init__.py | 0 .../unit/plugins/v1/ros/test_rosdep.py | 2 +- .../unit/plugins/v1/ros/test_wstool.py | 2 +- .../{ => legacy}/unit/plugins/v1/test_ant.py | 2 +- .../unit/plugins/v1/test_autotools.py | 0 .../{ => legacy}/unit/plugins/v1/test_base.py | 2 +- .../unit/plugins/v1/test_catkin.py | 2 +- .../unit/plugins/v1/test_catkin_tools.py | 0 .../unit/plugins/v1/test_cmake.py | 2 +- .../unit/plugins/v1/test_colcon.py | 2 +- .../unit/plugins/v1/test_conda.py | 2 +- .../unit/plugins/v1/test_crystal.py | 2 +- .../unit/plugins/v1/test_dotnet.py | 2 +- .../{ => legacy}/unit/plugins/v1/test_dump.py | 2 +- .../unit/plugins/v1/test_flutter.py | 0 tests/{ => legacy}/unit/plugins/v1/test_go.py | 2 +- .../unit/plugins/v1/test_godeps.py | 2 +- .../unit/plugins/v1/test_gradle.py | 0 .../unit/plugins/v1/test_kbuild.py | 0 .../unit/plugins/v1/test_kernel.py | 0 .../{ => legacy}/unit/plugins/v1/test_make.py | 0 .../unit/plugins/v1/test_maven.py | 2 +- .../unit/plugins/v1/test_meson.py | 2 +- .../{ => legacy}/unit/plugins/v1/test_nil.py | 2 +- .../unit/plugins/v1/test_nodejs.py | 2 +- .../unit/plugins/v1/test_plainbox_provider.py | 2 +- .../unit/plugins/v1/test_python.py | 2 +- .../unit/plugins/v1/test_qmake.py | 0 .../{ => legacy}/unit/plugins/v1/test_ruby.py | 0 .../{ => legacy}/unit/plugins/v1/test_rust.py | 2 +- .../unit/plugins/v1/test_scons.py | 2 +- .../{ => legacy}/unit/plugins/v1/test_waf.py | 2 +- .../unit/plugins/v2/test_autotools.py | 0 .../unit/plugins/v2/test_catkin.py | 0 .../unit/plugins/v2/test_catkin_tools.py | 0 .../unit/plugins/v2/test_cmake.py | 0 .../unit/plugins/v2/test_colcon.py | 0 .../unit/plugins/v2/test_conda.py | 0 .../{ => legacy}/unit/plugins/v2/test_dump.py | 0 tests/{ => legacy}/unit/plugins/v2/test_go.py | 0 .../{ => legacy}/unit/plugins/v2/test_make.py | 0 .../unit/plugins/v2/test_meson.py | 0 .../{ => legacy}/unit/plugins/v2/test_nil.py | 0 .../{ => legacy}/unit/plugins/v2/test_npm.py | 0 .../unit/plugins/v2/test_python.py | 0 .../unit/plugins/v2/test_qmake.py | 0 .../{ => legacy}/unit/plugins/v2/test_rust.py | 0 tests/{ => legacy}/unit/project/__init__.py | 2 +- .../{ => legacy}/unit/project/test_errors.py | 0 .../unit/project/test_get_snapcraft.py | 0 .../{ => legacy}/unit/project/test_project.py | 0 .../unit/project/test_project_info.py | 2 +- .../unit/project/test_sanity_checks.py | 0 .../{ => legacy}/unit/project/test_schema.py | 0 .../unit/project_loader/__init__.py | 2 +- .../project_loader/extensions}/__init__.py | 0 .../extensions/test_extensions.py | 0 .../project_loader/extensions/test_flutter.py | 0 .../extensions/test_gnome_3_28.py | 0 .../extensions/test_gnome_3_34.py | 2 +- .../extensions/test_gnome_3_38.py | 2 +- .../extensions/test_kde_neon.py | 0 .../extensions/test_ros1_noetic.py | 0 .../extensions/test_ros2_foxy.py | 0 .../project_loader/extensions/test_utils.py | 2 +- .../unit/project_loader/grammar}/__init__.py | 0 .../grammar/test_compound_statement.py | 0 .../grammar/test_on_statement.py | 0 .../project_loader/grammar/test_processor.py | 0 .../grammar/test_to_statement.py | 0 .../grammar/test_try_statement.py | 0 .../grammar_processing}/__init__.py | 0 .../test_global_grammar_processor.py | 0 .../test_part_grammar_processor.py | 0 .../project_loader/inspection}/__init__.py | 0 .../inspection/test_latest_step.py | 2 +- .../inspection/test_lifecycle_status.py | 0 .../inspection/test_provides.py | 2 +- .../project_loader/test_build_packages.py | 0 .../unit/project_loader/test_build_snaps.py | 0 .../unit/project_loader/test_config.py | 2 +- .../unit/project_loader/test_environment.py | 2 +- .../unit/project_loader/test_errors.py | 0 .../unit/project_loader/test_parts.py | 2 +- .../unit/project_loader/test_replace_attr.py | 2 +- .../unit/project_loader/test_schema.py | 2 +- .../unit/remote_build/__init__.py | 0 .../unit/remote_build/test_errors.py | 0 .../unit/remote_build/test_info_file.py | 2 +- .../unit/remote_build/test_launchpad.py | 9 +- .../unit/remote_build/test_worktree.py | 2 +- tests/{ => legacy}/unit/repo/__init__.py | 2 +- .../{ => legacy}/unit/repo/test_apt_cache.py | 2 +- .../unit/repo/test_apt_key_manager.py | 0 tests/{ => legacy}/unit/repo/test_apt_ppa.py | 0 .../unit/repo/test_apt_sources_manager.py | 0 tests/{ => legacy}/unit/repo/test_base.py | 2 +- tests/{ => legacy}/unit/repo/test_deb.py | 2 +- .../unit/repo/test_deb_package.py | 0 tests/{ => legacy}/unit/repo/test_errors.py | 0 tests/{ => legacy}/unit/repo/test_snaps.py | 2 +- .../{ => legacy}/unit/repo/test_ua_manager.py | 0 .../unit/review_tools}/__init__.py | 0 .../unit/review_tools/test_errors.py | 0 .../unit/review_tools/test_runner.py | 2 +- tests/{ => legacy}/unit/sources/__init__.py | 2 +- tests/{ => legacy}/unit/sources/test_7z.py | 2 +- tests/{ => legacy}/unit/sources/test_base.py | 2 +- .../{ => legacy}/unit/sources/test_bazaar.py | 2 +- .../unit/sources/test_checksum.py | 2 +- tests/{ => legacy}/unit/sources/test_deb.py | 2 +- .../{ => legacy}/unit/sources/test_errors.py | 0 tests/{ => legacy}/unit/sources/test_git.py | 2 +- tests/{ => legacy}/unit/sources/test_local.py | 2 +- .../unit/sources/test_mercurial.py | 2 +- tests/{ => legacy}/unit/sources/test_rpm.py | 2 +- .../{ => legacy}/unit/sources/test_script.py | 2 +- tests/{ => legacy}/unit/sources/test_snap.py | 2 +- .../{ => legacy}/unit/sources/test_sources.py | 0 .../unit/sources/test_subversion.py | 2 +- tests/{ => legacy}/unit/sources/test_tar.py | 2 +- tests/{ => legacy}/unit/sources/test_zip.py | 2 +- .../store => legacy/unit/states}/__init__.py | 0 tests/{ => legacy}/unit/states/conftest.py | 0 tests/{ => legacy}/unit/states/test_build.py | 2 +- .../unit/states/test_global_state.py | 0 tests/{ => legacy}/unit/states/test_prime.py | 2 +- tests/{ => legacy}/unit/states/test_pull.py | 2 +- tests/{ => legacy}/unit/states/test_stage.py | 2 +- tests/{ => legacy}/unit/states/test_state.py | 0 .../unit/store}/__init__.py | 0 .../unit/store/http_client}/__init__.py | 0 .../unit/store/http_client/test_agent.py | 4 +- .../store/http_client/test_candid_client.py | 0 .../unit/store/http_client/test_config.py | 0 .../unit/store/http_client/test_errors.py | 0 .../test_ubuntu_one_auth_client.py | 0 .../{ => legacy}/unit/store/test_channels.py | 0 tests/{ => legacy}/unit/store/test_errors.py | 0 tests/{ => legacy}/unit/store/test_metrics.py | 0 tests/{ => legacy}/unit/store/test_status.py | 2 +- .../unit/store/test_store_client.py | 18 +- .../unit/store/v2}/__init__.py | 0 .../unit/store/v2/test_channel_map.py | 2 +- .../unit/store/v2/test_releases.py | 0 .../unit/store/v2/test_validation_sets.py | 0 .../{ => legacy}/unit/store/v2/test_whoami.py | 0 tests/{ => legacy}/unit/test_common.py | 2 +- tests/{ => legacy}/unit/test_config.py | 2 +- tests/{ => legacy}/unit/test_elf.py | 2 +- tests/{ => legacy}/unit/test_errors.py | 0 tests/{ => legacy}/unit/test_file_utils.py | 2 +- tests/{ => legacy}/unit/test_fixture_setup.py | 2 +- .../unit/test_formatting_utils.py | 2 +- tests/{ => legacy}/unit/test_indicators.py | 2 +- tests/{ => legacy}/unit/test_init.py | 2 +- tests/{ => legacy}/unit/test_log.py | 2 +- tests/{ => legacy}/unit/test_mangling.py | 2 +- tests/{ => legacy}/unit/test_mountinfo.py | 2 +- tests/{ => legacy}/unit/test_options.py | 2 +- tests/{ => legacy}/unit/test_os_release.py | 2 +- tests/{ => legacy}/unit/test_steps.py | 0 tests/{ => legacy}/unit/test_target_arch.py | 0 tests/{ => legacy}/unit/test_xattrs.py | 2 +- tests/legacy/unit/yaml_utils/__init__.py | 0 .../unit/yaml_utils/test_errors.py | 0 .../unit/yaml_utils/test_yaml_utils.py | 0 tests/unit/__init__.py | 318 ------------------ 307 files changed, 495 insertions(+), 482 deletions(-) rename tests/{unit/build_providers/lxd => legacy}/__init__.py (100%) rename tests/{ => legacy}/data/icon.png (100%) rename tests/{ => legacy}/data/invalid.snap (100%) rename tests/{ => legacy}/data/test-snap-with-icon-license-title.snap (100%) rename tests/{ => legacy}/data/test-snap-with-icon.snap (100%) rename tests/{ => legacy}/data/test-snap-with-started-at.snap (100%) rename tests/{ => legacy}/data/test-snap.snap (100%) rename tests/{ => legacy}/data/test.desktop (100%) rename tests/{ => legacy}/fake_servers/__init__.py (100%) rename tests/{ => legacy}/fake_servers/api.py (99%) rename tests/{ => legacy}/fake_servers/base.py (100%) rename tests/{ => legacy}/fake_servers/search.py (96%) rename tests/{ => legacy}/fake_servers/snapd.py (99%) rename tests/{ => legacy}/fake_servers/upload.py (97%) rename tests/{ => legacy}/fixture_setup/__init__.py (100%) rename tests/{ => legacy}/fixture_setup/_fixtures.py (99%) rename tests/{ => legacy}/fixture_setup/_unittests.py (100%) rename tests/{ => legacy}/fixture_setup/_unix.py (98%) rename tests/{ => legacy}/fixture_setup/os_release.py (100%) create mode 100644 tests/legacy/unit/__init__.py rename tests/{ => legacy}/unit/build_providers/__init__.py (99%) rename tests/{ => legacy}/unit/build_providers/conftest.py (100%) rename tests/{unit/build_providers/multipass => legacy/unit/build_providers/lxd}/__init__.py (100%) rename tests/{ => legacy}/unit/build_providers/lxd/test_lxd.py (99%) rename tests/{unit/cache => legacy/unit/build_providers/multipass}/__init__.py (100%) rename tests/{ => legacy}/unit/build_providers/multipass/test_instance_info.py (99%) rename tests/{ => legacy}/unit/build_providers/multipass/test_multipass.py (99%) rename tests/{ => legacy}/unit/build_providers/multipass/test_multipass_command.py (99%) rename tests/{ => legacy}/unit/build_providers/test_base_provider.py (100%) rename tests/{ => legacy}/unit/build_providers/test_errors.py (100%) rename tests/{ => legacy}/unit/build_providers/test_snap.py (99%) rename tests/{unit/cli => legacy/unit/cache}/__init__.py (100%) rename tests/{ => legacy}/unit/cache/conftest.py (100%) rename tests/{ => legacy}/unit/cache/test_file.py (100%) rename tests/{ => legacy}/unit/cache/test_snap.py (97%) rename tests/{unit/deltas => legacy/unit/cli}/__init__.py (100%) rename tests/{ => legacy}/unit/cli/conftest.py (100%) rename tests/{ => legacy}/unit/cli/test_echo.py (99%) rename tests/{ => legacy}/unit/cli/test_errors.py (99%) rename tests/{ => legacy}/unit/cli/test_lifecycle.py (100%) rename tests/{ => legacy}/unit/cli/test_metrics.py (100%) rename tests/{ => legacy}/unit/cli/test_options.py (99%) rename tests/{ => legacy}/unit/commands/__init__.py (99%) rename tests/{ => legacy}/unit/commands/conftest.py (100%) rename tests/{ => legacy}/unit/commands/snapcraftctl/__init__.py (98%) rename tests/{ => legacy}/unit/commands/snapcraftctl/test_build.py (100%) rename tests/{ => legacy}/unit/commands/snapcraftctl/test_set_grade.py (100%) rename tests/{ => legacy}/unit/commands/snapcraftctl/test_set_version.py (100%) rename tests/{ => legacy}/unit/commands/test_build_providers.py (99%) rename tests/{ => legacy}/unit/commands/test_clean.py (100%) rename tests/{ => legacy}/unit/commands/test_close.py (100%) rename tests/{ => legacy}/unit/commands/test_create_key.py (100%) rename tests/{ => legacy}/unit/commands/test_edit_validation_sets.py (100%) rename tests/{ => legacy}/unit/commands/test_export_login.py (100%) rename tests/{ => legacy}/unit/commands/test_extensions.py (99%) rename tests/{ => legacy}/unit/commands/test_gated.py (100%) rename tests/{ => legacy}/unit/commands/test_help.py (99%) rename tests/{ => legacy}/unit/commands/test_init.py (100%) rename tests/{ => legacy}/unit/commands/test_list.py (100%) rename tests/{ => legacy}/unit/commands/test_list_keys.py (100%) rename tests/{ => legacy}/unit/commands/test_list_plugins.py (99%) rename tests/{ => legacy}/unit/commands/test_list_revisions.py (100%) rename tests/{ => legacy}/unit/commands/test_list_tracks.py (100%) rename tests/{ => legacy}/unit/commands/test_list_validation_sets.py (100%) rename tests/{ => legacy}/unit/commands/test_login.py (100%) rename tests/{ => legacy}/unit/commands/test_logout.py (100%) rename tests/{ => legacy}/unit/commands/test_metrics.py (100%) rename tests/{ => legacy}/unit/commands/test_pack.py (100%) rename tests/{ => legacy}/unit/commands/test_promote.py (100%) rename tests/{ => legacy}/unit/commands/test_pull_build_stage_prime.py (100%) rename tests/{ => legacy}/unit/commands/test_refresh.py (97%) rename tests/{ => legacy}/unit/commands/test_register.py (100%) rename tests/{ => legacy}/unit/commands/test_register_key.py (100%) rename tests/{ => legacy}/unit/commands/test_release.py (100%) rename tests/{ => legacy}/unit/commands/test_remote.py (99%) rename tests/{ => legacy}/unit/commands/test_set_default_track.py (100%) rename tests/{ => legacy}/unit/commands/test_sign_build.py (98%) rename tests/{ => legacy}/unit/commands/test_snap.py (100%) rename tests/{ => legacy}/unit/commands/test_status.py (100%) rename tests/{ => legacy}/unit/commands/test_upload.py (98%) rename tests/{ => legacy}/unit/commands/test_upload_metadata.py (97%) rename tests/{ => legacy}/unit/commands/test_validate.py (100%) rename tests/{ => legacy}/unit/commands/test_version.py (100%) rename tests/{ => legacy}/unit/commands/test_whoami.py (100%) rename tests/{ => legacy}/unit/conftest.py (100%) rename tests/{ => legacy}/unit/db/test_datastore.py (100%) rename tests/{ => legacy}/unit/db/test_errors.py (100%) rename tests/{ => legacy}/unit/db/test_migration.py (100%) rename tests/{unit/extractors => legacy/unit/deltas}/__init__.py (100%) rename tests/{ => legacy}/unit/deltas/test_deltas.py (99%) rename tests/{ => legacy}/unit/deltas/test_deltas_xdelta3.py (99%) rename tests/{unit/meta => legacy/unit/extractors}/__init__.py (100%) rename tests/{ => legacy}/unit/extractors/test_appstream.py (99%) rename tests/{ => legacy}/unit/extractors/test_metadata.py (99%) rename tests/{ => legacy}/unit/extractors/test_setuppy.py (99%) rename tests/{ => legacy}/unit/lifecycle/__init__.py (98%) rename tests/{ => legacy}/unit/lifecycle/test_errors.py (100%) rename tests/{ => legacy}/unit/lifecycle/test_global_state.py (99%) rename tests/{ => legacy}/unit/lifecycle/test_lifecycle.py (99%) rename tests/{ => legacy}/unit/lifecycle/test_order.py (100%) rename tests/{ => legacy}/unit/lifecycle/test_snap_installation.py (100%) rename tests/{ => legacy}/unit/lifecycle/test_status_cache.py (100%) rename tests/{unit/pluginhandler => legacy/unit/meta}/__init__.py (100%) rename tests/{ => legacy}/unit/meta/test_application.py (99%) rename tests/{ => legacy}/unit/meta/test_command.py (99%) rename tests/{ => legacy}/unit/meta/test_command_mangle.py (100%) rename tests/{ => legacy}/unit/meta/test_desktop.py (100%) rename tests/{ => legacy}/unit/meta/test_errors.py (100%) rename tests/{ => legacy}/unit/meta/test_hook.py (99%) rename tests/{ => legacy}/unit/meta/test_meta.py (99%) rename tests/{ => legacy}/unit/meta/test_package_repository.py (100%) rename tests/{ => legacy}/unit/meta/test_plugs.py (99%) rename tests/{ => legacy}/unit/meta/test_slots.py (99%) rename tests/{ => legacy}/unit/meta/test_snap.py (99%) rename tests/{ => legacy}/unit/meta/test_snap_packaging.py (99%) rename tests/{ => legacy}/unit/meta/test_system_user.py (99%) rename tests/{ => legacy}/unit/part_loader.py (100%) rename tests/{unit/plugins => legacy/unit/pluginhandler}/__init__.py (100%) rename tests/{ => legacy}/unit/pluginhandler/mocks.py (100%) rename tests/{ => legacy}/unit/pluginhandler/test_clean.py (99%) rename tests/{ => legacy}/unit/pluginhandler/test_dirty_report.py (100%) rename tests/{ => legacy}/unit/pluginhandler/test_metadata_extraction.py (98%) rename tests/{ => legacy}/unit/pluginhandler/test_missing_dependency.py (99%) rename tests/{ => legacy}/unit/pluginhandler/test_patcher.py (99%) rename tests/{ => legacy}/unit/pluginhandler/test_plugin_loader.py (99%) rename tests/{ => legacy}/unit/pluginhandler/test_pluginhandler.py (99%) rename tests/{ => legacy}/unit/pluginhandler/test_runner.py (99%) rename tests/{ => legacy}/unit/pluginhandler/test_scriptlets.py (99%) rename tests/{ => legacy}/unit/pluginhandler/test_state.py (99%) rename tests/{unit/plugins/v1/python => legacy/unit/plugins}/__init__.py (100%) rename tests/{ => legacy}/unit/plugins/v1/__init__.py (97%) rename tests/{ => legacy}/unit/plugins/v1/conftest.py (100%) rename tests/{unit/plugins/v1/ros => legacy/unit/plugins/v1/python}/__init__.py (100%) rename tests/{ => legacy}/unit/plugins/v1/python/_basesuite.py (97%) rename tests/{ => legacy}/unit/plugins/v1/python/test_errors.py (100%) rename tests/{ => legacy}/unit/plugins/v1/python/test_pip.py (100%) rename tests/{ => legacy}/unit/plugins/v1/python/test_python_finder.py (100%) rename tests/{ => legacy}/unit/plugins/v1/python/test_sitecustomize.py (100%) rename tests/{unit/project_loader/extensions => legacy/unit/plugins/v1/ros}/__init__.py (100%) rename tests/{ => legacy}/unit/plugins/v1/ros/test_rosdep.py (99%) rename tests/{ => legacy}/unit/plugins/v1/ros/test_wstool.py (99%) rename tests/{ => legacy}/unit/plugins/v1/test_ant.py (99%) rename tests/{ => legacy}/unit/plugins/v1/test_autotools.py (100%) rename tests/{ => legacy}/unit/plugins/v1/test_base.py (99%) rename tests/{ => legacy}/unit/plugins/v1/test_catkin.py (99%) rename tests/{ => legacy}/unit/plugins/v1/test_catkin_tools.py (100%) rename tests/{ => legacy}/unit/plugins/v1/test_cmake.py (99%) rename tests/{ => legacy}/unit/plugins/v1/test_colcon.py (99%) rename tests/{ => legacy}/unit/plugins/v1/test_conda.py (99%) rename tests/{ => legacy}/unit/plugins/v1/test_crystal.py (99%) rename tests/{ => legacy}/unit/plugins/v1/test_dotnet.py (99%) rename tests/{ => legacy}/unit/plugins/v1/test_dump.py (99%) rename tests/{ => legacy}/unit/plugins/v1/test_flutter.py (100%) rename tests/{ => legacy}/unit/plugins/v1/test_go.py (99%) rename tests/{ => legacy}/unit/plugins/v1/test_godeps.py (99%) rename tests/{ => legacy}/unit/plugins/v1/test_gradle.py (100%) rename tests/{ => legacy}/unit/plugins/v1/test_kbuild.py (100%) rename tests/{ => legacy}/unit/plugins/v1/test_kernel.py (100%) rename tests/{ => legacy}/unit/plugins/v1/test_make.py (100%) rename tests/{ => legacy}/unit/plugins/v1/test_maven.py (99%) rename tests/{ => legacy}/unit/plugins/v1/test_meson.py (99%) rename tests/{ => legacy}/unit/plugins/v1/test_nil.py (97%) rename tests/{ => legacy}/unit/plugins/v1/test_nodejs.py (99%) rename tests/{ => legacy}/unit/plugins/v1/test_plainbox_provider.py (99%) rename tests/{ => legacy}/unit/plugins/v1/test_python.py (99%) rename tests/{ => legacy}/unit/plugins/v1/test_qmake.py (100%) rename tests/{ => legacy}/unit/plugins/v1/test_ruby.py (100%) rename tests/{ => legacy}/unit/plugins/v1/test_rust.py (99%) rename tests/{ => legacy}/unit/plugins/v1/test_scons.py (99%) rename tests/{ => legacy}/unit/plugins/v1/test_waf.py (99%) rename tests/{ => legacy}/unit/plugins/v2/test_autotools.py (100%) rename tests/{ => legacy}/unit/plugins/v2/test_catkin.py (100%) rename tests/{ => legacy}/unit/plugins/v2/test_catkin_tools.py (100%) rename tests/{ => legacy}/unit/plugins/v2/test_cmake.py (100%) rename tests/{ => legacy}/unit/plugins/v2/test_colcon.py (100%) rename tests/{ => legacy}/unit/plugins/v2/test_conda.py (100%) rename tests/{ => legacy}/unit/plugins/v2/test_dump.py (100%) rename tests/{ => legacy}/unit/plugins/v2/test_go.py (100%) rename tests/{ => legacy}/unit/plugins/v2/test_make.py (100%) rename tests/{ => legacy}/unit/plugins/v2/test_meson.py (100%) rename tests/{ => legacy}/unit/plugins/v2/test_nil.py (100%) rename tests/{ => legacy}/unit/plugins/v2/test_npm.py (100%) rename tests/{ => legacy}/unit/plugins/v2/test_python.py (100%) rename tests/{ => legacy}/unit/plugins/v2/test_qmake.py (100%) rename tests/{ => legacy}/unit/plugins/v2/test_rust.py (100%) rename tests/{ => legacy}/unit/project/__init__.py (98%) rename tests/{ => legacy}/unit/project/test_errors.py (100%) rename tests/{ => legacy}/unit/project/test_get_snapcraft.py (100%) rename tests/{ => legacy}/unit/project/test_project.py (100%) rename tests/{ => legacy}/unit/project/test_project_info.py (99%) rename tests/{ => legacy}/unit/project/test_sanity_checks.py (100%) rename tests/{ => legacy}/unit/project/test_schema.py (100%) rename tests/{ => legacy}/unit/project_loader/__init__.py (98%) rename tests/{unit/project_loader/grammar => legacy/unit/project_loader/extensions}/__init__.py (100%) rename tests/{ => legacy}/unit/project_loader/extensions/test_extensions.py (100%) rename tests/{ => legacy}/unit/project_loader/extensions/test_flutter.py (100%) rename tests/{ => legacy}/unit/project_loader/extensions/test_gnome_3_28.py (100%) rename tests/{ => legacy}/unit/project_loader/extensions/test_gnome_3_34.py (99%) rename tests/{ => legacy}/unit/project_loader/extensions/test_gnome_3_38.py (99%) rename tests/{ => legacy}/unit/project_loader/extensions/test_kde_neon.py (100%) rename tests/{ => legacy}/unit/project_loader/extensions/test_ros1_noetic.py (100%) rename tests/{ => legacy}/unit/project_loader/extensions/test_ros2_foxy.py (100%) rename tests/{ => legacy}/unit/project_loader/extensions/test_utils.py (99%) rename tests/{unit/project_loader/grammar_processing => legacy/unit/project_loader/grammar}/__init__.py (100%) rename tests/{ => legacy}/unit/project_loader/grammar/test_compound_statement.py (100%) rename tests/{ => legacy}/unit/project_loader/grammar/test_on_statement.py (100%) rename tests/{ => legacy}/unit/project_loader/grammar/test_processor.py (100%) rename tests/{ => legacy}/unit/project_loader/grammar/test_to_statement.py (100%) rename tests/{ => legacy}/unit/project_loader/grammar/test_try_statement.py (100%) rename tests/{unit/project_loader/inspection => legacy/unit/project_loader/grammar_processing}/__init__.py (100%) rename tests/{ => legacy}/unit/project_loader/grammar_processing/test_global_grammar_processor.py (100%) rename tests/{ => legacy}/unit/project_loader/grammar_processing/test_part_grammar_processor.py (100%) rename tests/{unit/review_tools => legacy/unit/project_loader/inspection}/__init__.py (100%) rename tests/{ => legacy}/unit/project_loader/inspection/test_latest_step.py (98%) rename tests/{ => legacy}/unit/project_loader/inspection/test_lifecycle_status.py (100%) rename tests/{ => legacy}/unit/project_loader/inspection/test_provides.py (99%) rename tests/{ => legacy}/unit/project_loader/test_build_packages.py (100%) rename tests/{ => legacy}/unit/project_loader/test_build_snaps.py (100%) rename tests/{ => legacy}/unit/project_loader/test_config.py (99%) rename tests/{ => legacy}/unit/project_loader/test_environment.py (99%) rename tests/{ => legacy}/unit/project_loader/test_errors.py (100%) rename tests/{ => legacy}/unit/project_loader/test_parts.py (99%) rename tests/{ => legacy}/unit/project_loader/test_replace_attr.py (99%) rename tests/{ => legacy}/unit/project_loader/test_schema.py (99%) rename tests/{ => legacy}/unit/remote_build/__init__.py (100%) rename tests/{ => legacy}/unit/remote_build/test_errors.py (100%) rename tests/{ => legacy}/unit/remote_build/test_info_file.py (98%) rename tests/{ => legacy}/unit/remote_build/test_launchpad.py (98%) rename tests/{ => legacy}/unit/remote_build/test_worktree.py (99%) rename tests/{ => legacy}/unit/repo/__init__.py (97%) rename tests/{ => legacy}/unit/repo/test_apt_cache.py (99%) rename tests/{ => legacy}/unit/repo/test_apt_key_manager.py (100%) rename tests/{ => legacy}/unit/repo/test_apt_ppa.py (100%) rename tests/{ => legacy}/unit/repo/test_apt_sources_manager.py (100%) rename tests/{ => legacy}/unit/repo/test_base.py (99%) rename tests/{ => legacy}/unit/repo/test_deb.py (99%) rename tests/{ => legacy}/unit/repo/test_deb_package.py (100%) rename tests/{ => legacy}/unit/repo/test_errors.py (100%) rename tests/{ => legacy}/unit/repo/test_snaps.py (99%) rename tests/{ => legacy}/unit/repo/test_ua_manager.py (100%) rename tests/{unit/states => legacy/unit/review_tools}/__init__.py (100%) rename tests/{ => legacy}/unit/review_tools/test_errors.py (100%) rename tests/{ => legacy}/unit/review_tools/test_runner.py (99%) rename tests/{ => legacy}/unit/sources/__init__.py (97%) rename tests/{ => legacy}/unit/sources/test_7z.py (98%) rename tests/{ => legacy}/unit/sources/test_base.py (99%) rename tests/{ => legacy}/unit/sources/test_bazaar.py (99%) rename tests/{ => legacy}/unit/sources/test_checksum.py (98%) rename tests/{ => legacy}/unit/sources/test_deb.py (99%) rename tests/{ => legacy}/unit/sources/test_errors.py (100%) rename tests/{ => legacy}/unit/sources/test_git.py (99%) rename tests/{ => legacy}/unit/sources/test_local.py (99%) rename tests/{ => legacy}/unit/sources/test_mercurial.py (99%) rename tests/{ => legacy}/unit/sources/test_rpm.py (98%) rename tests/{ => legacy}/unit/sources/test_script.py (97%) rename tests/{ => legacy}/unit/sources/test_snap.py (99%) rename tests/{ => legacy}/unit/sources/test_sources.py (100%) rename tests/{ => legacy}/unit/sources/test_subversion.py (99%) rename tests/{ => legacy}/unit/sources/test_tar.py (99%) rename tests/{ => legacy}/unit/sources/test_zip.py (98%) rename tests/{unit/store => legacy/unit/states}/__init__.py (100%) rename tests/{ => legacy}/unit/states/conftest.py (100%) rename tests/{ => legacy}/unit/states/test_build.py (99%) rename tests/{ => legacy}/unit/states/test_global_state.py (100%) rename tests/{ => legacy}/unit/states/test_prime.py (99%) rename tests/{ => legacy}/unit/states/test_pull.py (99%) rename tests/{ => legacy}/unit/states/test_stage.py (98%) rename tests/{ => legacy}/unit/states/test_state.py (100%) rename tests/{unit/store/http_client => legacy/unit/store}/__init__.py (100%) rename tests/{unit/store/v2 => legacy/unit/store/http_client}/__init__.py (100%) rename tests/{ => legacy}/unit/store/http_client/test_agent.py (96%) rename tests/{ => legacy}/unit/store/http_client/test_candid_client.py (100%) rename tests/{ => legacy}/unit/store/http_client/test_config.py (100%) rename tests/{ => legacy}/unit/store/http_client/test_errors.py (100%) rename tests/{ => legacy}/unit/store/http_client/test_ubuntu_one_auth_client.py (100%) rename tests/{ => legacy}/unit/store/test_channels.py (100%) rename tests/{ => legacy}/unit/store/test_errors.py (100%) rename tests/{ => legacy}/unit/store/test_metrics.py (100%) rename tests/{ => legacy}/unit/store/test_status.py (99%) rename tests/{ => legacy}/unit/store/test_store_client.py (99%) rename tests/{unit/yaml_utils => legacy/unit/store/v2}/__init__.py (100%) rename tests/{ => legacy}/unit/store/v2/test_channel_map.py (99%) rename tests/{ => legacy}/unit/store/v2/test_releases.py (100%) rename tests/{ => legacy}/unit/store/v2/test_validation_sets.py (100%) rename tests/{ => legacy}/unit/store/v2/test_whoami.py (100%) rename tests/{ => legacy}/unit/test_common.py (99%) rename tests/{ => legacy}/unit/test_config.py (99%) rename tests/{ => legacy}/unit/test_elf.py (99%) rename tests/{ => legacy}/unit/test_errors.py (100%) rename tests/{ => legacy}/unit/test_file_utils.py (99%) rename tests/{ => legacy}/unit/test_fixture_setup.py (98%) rename tests/{ => legacy}/unit/test_formatting_utils.py (98%) rename tests/{ => legacy}/unit/test_indicators.py (99%) rename tests/{ => legacy}/unit/test_init.py (97%) rename tests/{ => legacy}/unit/test_log.py (99%) rename tests/{ => legacy}/unit/test_mangling.py (99%) rename tests/{ => legacy}/unit/test_mountinfo.py (99%) rename tests/{ => legacy}/unit/test_options.py (99%) rename tests/{ => legacy}/unit/test_os_release.py (99%) rename tests/{ => legacy}/unit/test_steps.py (100%) rename tests/{ => legacy}/unit/test_target_arch.py (100%) rename tests/{ => legacy}/unit/test_xattrs.py (99%) create mode 100644 tests/legacy/unit/yaml_utils/__init__.py rename tests/{ => legacy}/unit/yaml_utils/test_errors.py (100%) rename tests/{ => legacy}/unit/yaml_utils/test_yaml_utils.py (100%) diff --git a/Makefile b/Makefile index f4d4920012..3ea8a2df8c 100644 --- a/Makefile +++ b/Makefile @@ -28,9 +28,13 @@ test-shellcheck: find . \( -name .git -o -name gradlew \) -prune -o -print0 | xargs -0 file -N | grep shell.script | cut -f1 -d: | xargs shellcheck ./tools/spread-shellcheck.py spread.yaml tests/spread/ +.PHONY: test-legacy-units +test-legacy-units: + pytest --cov-report=xml --cov=snapcraft tests/legacy/unit + .PHONY: test-units -test-units: - pytest --cov-report=xml --cov=snapcraft tests/unit +test-units: test-legacy-units + # pytest --cov-report=xml --cov=snapcraft tests/unit .PHONY: tests tests: tests-static test-units diff --git a/tests/unit/build_providers/lxd/__init__.py b/tests/legacy/__init__.py similarity index 100% rename from tests/unit/build_providers/lxd/__init__.py rename to tests/legacy/__init__.py diff --git a/tests/data/icon.png b/tests/legacy/data/icon.png similarity index 100% rename from tests/data/icon.png rename to tests/legacy/data/icon.png diff --git a/tests/data/invalid.snap b/tests/legacy/data/invalid.snap similarity index 100% rename from tests/data/invalid.snap rename to tests/legacy/data/invalid.snap diff --git a/tests/data/test-snap-with-icon-license-title.snap b/tests/legacy/data/test-snap-with-icon-license-title.snap similarity index 100% rename from tests/data/test-snap-with-icon-license-title.snap rename to tests/legacy/data/test-snap-with-icon-license-title.snap diff --git a/tests/data/test-snap-with-icon.snap b/tests/legacy/data/test-snap-with-icon.snap similarity index 100% rename from tests/data/test-snap-with-icon.snap rename to tests/legacy/data/test-snap-with-icon.snap diff --git a/tests/data/test-snap-with-started-at.snap b/tests/legacy/data/test-snap-with-started-at.snap similarity index 100% rename from tests/data/test-snap-with-started-at.snap rename to tests/legacy/data/test-snap-with-started-at.snap diff --git a/tests/data/test-snap.snap b/tests/legacy/data/test-snap.snap similarity index 100% rename from tests/data/test-snap.snap rename to tests/legacy/data/test-snap.snap diff --git a/tests/data/test.desktop b/tests/legacy/data/test.desktop similarity index 100% rename from tests/data/test.desktop rename to tests/legacy/data/test.desktop diff --git a/tests/fake_servers/__init__.py b/tests/legacy/fake_servers/__init__.py similarity index 100% rename from tests/fake_servers/__init__.py rename to tests/legacy/fake_servers/__init__.py diff --git a/tests/fake_servers/api.py b/tests/legacy/fake_servers/api.py similarity index 99% rename from tests/fake_servers/api.py rename to tests/legacy/fake_servers/api.py index 3a1468f9d3..653c44bffd 100644 --- a/tests/fake_servers/api.py +++ b/tests/legacy/fake_servers/api.py @@ -26,7 +26,7 @@ import pymacaroons from pyramid import response -from tests.fake_servers import base +from tests.legacy.fake_servers import base logger = logging.getLogger(__name__) diff --git a/tests/fake_servers/base.py b/tests/legacy/fake_servers/base.py similarity index 100% rename from tests/fake_servers/base.py rename to tests/legacy/fake_servers/base.py diff --git a/tests/fake_servers/search.py b/tests/legacy/fake_servers/search.py similarity index 96% rename from tests/fake_servers/search.py rename to tests/legacy/fake_servers/search.py index 813f8ec455..199010ce46 100644 --- a/tests/fake_servers/search.py +++ b/tests/legacy/fake_servers/search.py @@ -21,8 +21,8 @@ from pyramid import response -import tests -from tests.fake_servers import base +import tests.legacy +from tests.legacy.fake_servers import base logger = logging.getLogger(__name__) @@ -64,7 +64,7 @@ def info(self, request): def _get_info_payload(self, request): # core snap is used in integration tests with fake servers. snap = request.matchdict["snap"] - # tests/data/test-snap.snap + # tests/legacy/data/test-snap.snap test_sha3_384 = ( "8c0118831680a22090503ee5db98c88dd90ef551d80fc816" "dec968f60527216199dacc040cddfe5cec6870db836cb908" @@ -146,7 +146,7 @@ def download(self, request): # TODO create a test snap during the test instead of hardcoding it. # --elopio - 2016-05-01 snap_path = os.path.join( - os.path.dirname(tests.__file__), "data", "test-snap.snap" + os.path.dirname(tests.legacy.__file__), "data", "test-snap.snap" ) with open(snap_path, "rb") as snap_file: diff --git a/tests/fake_servers/snapd.py b/tests/legacy/fake_servers/snapd.py similarity index 99% rename from tests/fake_servers/snapd.py rename to tests/legacy/fake_servers/snapd.py index 6d60276558..bd0429ca05 100644 --- a/tests/fake_servers/snapd.py +++ b/tests/legacy/fake_servers/snapd.py @@ -17,7 +17,7 @@ from typing import Any, Dict, List # noqa from urllib import parse -from tests import fake_servers +from tests.legacy import fake_servers class FakeSnapdRequestHandler(fake_servers.BaseHTTPRequestHandler): diff --git a/tests/fake_servers/upload.py b/tests/legacy/fake_servers/upload.py similarity index 97% rename from tests/fake_servers/upload.py rename to tests/legacy/fake_servers/upload.py index 7229cd8b69..990620bb6b 100644 --- a/tests/fake_servers/upload.py +++ b/tests/legacy/fake_servers/upload.py @@ -20,7 +20,7 @@ from pyramid import response -from tests.fake_servers import base +from tests.legacy.fake_servers import base logger = logging.getLogger(__name__) diff --git a/tests/fixture_setup/__init__.py b/tests/legacy/fixture_setup/__init__.py similarity index 100% rename from tests/fixture_setup/__init__.py rename to tests/legacy/fixture_setup/__init__.py diff --git a/tests/fixture_setup/_fixtures.py b/tests/legacy/fixture_setup/_fixtures.py similarity index 99% rename from tests/fixture_setup/_fixtures.py rename to tests/legacy/fixture_setup/_fixtures.py index 0ada4f1041..8522644ca5 100644 --- a/tests/fixture_setup/_fixtures.py +++ b/tests/legacy/fixture_setup/_fixtures.py @@ -31,8 +31,8 @@ import fixtures import xdg -from tests import fake_servers -from tests.fake_servers import api, search, upload +from tests.legacy import fake_servers +from tests.legacy.fake_servers import api, search, upload from tests.subprocess_utils import call, call_with_output # we do not want snapcraft imports for the integration tests diff --git a/tests/fixture_setup/_unittests.py b/tests/legacy/fixture_setup/_unittests.py similarity index 100% rename from tests/fixture_setup/_unittests.py rename to tests/legacy/fixture_setup/_unittests.py diff --git a/tests/fixture_setup/_unix.py b/tests/legacy/fixture_setup/_unix.py similarity index 98% rename from tests/fixture_setup/_unix.py rename to tests/legacy/fixture_setup/_unix.py index 1bfbd13963..1584f116df 100644 --- a/tests/fixture_setup/_unix.py +++ b/tests/legacy/fixture_setup/_unix.py @@ -22,7 +22,7 @@ import fixtures -from tests.fake_servers import snapd +from tests.legacy.fake_servers import snapd class UnixHTTPServer(socketserver.UnixStreamServer): diff --git a/tests/fixture_setup/os_release.py b/tests/legacy/fixture_setup/os_release.py similarity index 100% rename from tests/fixture_setup/os_release.py rename to tests/legacy/fixture_setup/os_release.py diff --git a/tests/legacy/unit/__init__.py b/tests/legacy/unit/__init__.py new file mode 100644 index 0000000000..81192da000 --- /dev/null +++ b/tests/legacy/unit/__init__.py @@ -0,0 +1,318 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright (C) 2015-2020 Canonical Ltd +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import http.server +import logging +import os +import stat +import threading +from unittest import mock + +import apt +import fixtures +import progressbar +import testscenarios +import testtools + +from snapcraft_legacy.internal import common, steps +from tests.legacy import fake_servers, fixture_setup +from tests.file_utils import get_snapcraft_path +from tests.legacy.unit.part_loader import load_part + + +class ContainsList(list): + def __eq__(self, other): + return all([i[0] in i[1] for i in zip(self, other)]) + + +class MockOptions: + def __init__( + self, + source=None, + source_type=None, + source_branch=None, + source_tag=None, + source_subdir=None, + source_depth=None, + source_commit=None, + source_checksum=None, + disable_parallel=False, + ): + self.source = source + self.source_type = source_type + self.source_depth = source_depth + self.source_branch = source_branch + self.source_commit = source_commit + self.source_tag = source_tag + self.source_subdir = source_subdir + self.disable_parallel = disable_parallel + + +class IsExecutable: + """Match if a file path is executable.""" + + def __str__(self): + return "IsExecutable()" + + def match(self, file_path): + if not os.stat(file_path).st_mode & stat.S_IEXEC: + return testtools.matchers.Mismatch( + "Expected {!r} to be executable, but it was not".format(file_path) + ) + return None + + +class LinkExists: + """Match if a file path is a symlink.""" + + def __init__(self, expected_target=None): + self._expected_target = expected_target + + def __str__(self): + return "LinkExists()" + + def match(self, file_path): + if not os.path.exists(file_path): + return testtools.matchers.Mismatch( + "Expected {!r} to be a symlink, but it doesn't exist".format(file_path) + ) + + if not os.path.islink(file_path): + return testtools.matchers.Mismatch( + "Expected {!r} to be a symlink, but it was not".format(file_path) + ) + + target = os.readlink(file_path) + if target != self._expected_target: + return testtools.matchers.Mismatch( + "Expected {!r} to be a symlink pointing to {!r}, but it was " + "pointing to {!r}".format(file_path, self._expected_target, target) + ) + + return None + + +class TestCase(testscenarios.WithScenarios, testtools.TestCase): + @classmethod + def setUpClass(cls): + cls.fake_snapd = fixture_setup.FakeSnapd() + cls.fake_snapd.setUp() + + @classmethod + def tearDownClass(cls): + cls.fake_snapd.cleanUp() + + def setUp(self): + super().setUp() + temp_cwd_fixture = fixture_setup.TempCWD() + self.useFixture(temp_cwd_fixture) + self.path = temp_cwd_fixture.path + + # Use a separate path for XDG dirs, or changes there may be detected as + # source changes. + self.xdg_path = self.useFixture(fixtures.TempDir()).path + self.useFixture(fixture_setup.TempXDG(self.xdg_path)) + self.fake_terminal = fixture_setup.FakeTerminal() + self.useFixture(self.fake_terminal) + # Some tests will directly or indirectly change the plugindir, which + # is a module variable. Make sure that it is returned to the original + # value when a test ends. + self.addCleanup(common.set_plugindir, common.get_plugindir()) + self.addCleanup(common.set_schemadir, common.get_schemadir()) + self.addCleanup(common.set_extensionsdir, common.get_extensionsdir()) + self.addCleanup(common.set_keyringsdir, common.get_keyringsdir()) + self.addCleanup(common.reset_env) + common.set_schemadir(os.path.join(get_snapcraft_path(), "schema")) + self.fake_logger = fixtures.FakeLogger(level=logging.ERROR) + self.useFixture(self.fake_logger) + + # Some tests will change the apt Dir::Etc::Trusted and + # Dir::Etc::TrustedParts directories. Make sure they're properly reset. + self.addCleanup( + apt.apt_pkg.config.set, + "Dir::Etc::Trusted", + apt.apt_pkg.config.find_file("Dir::Etc::Trusted"), + ) + self.addCleanup( + apt.apt_pkg.config.set, + "Dir::Etc::TrustedParts", + apt.apt_pkg.config.find_file("Dir::Etc::TrustedParts"), + ) + + patcher = mock.patch("os.sched_getaffinity") + self.cpu_count = patcher.start() + self.cpu_count.return_value = {1, 2} + self.addCleanup(patcher.stop) + + # We do not want the paths to affect every test we have. + patcher = mock.patch( + "snapcraft_legacy.file_utils.get_snap_tool_path", side_effect=lambda x: x + ) + patcher.start() + self.addCleanup(patcher.stop) + + patcher = mock.patch( + "snapcraft_legacy.internal.indicators.ProgressBar", new=SilentProgressBar + ) + patcher.start() + self.addCleanup(patcher.stop) + + # These are what we expect by default + self.snap_dir = os.path.join(os.getcwd(), "snap") + self.prime_dir = os.path.join(os.getcwd(), "prime") + self.stage_dir = os.path.join(os.getcwd(), "stage") + self.parts_dir = os.path.join(os.getcwd(), "parts") + self.local_plugins_dir = os.path.join(self.snap_dir, "plugins") + + # Use this host to run through the lifecycle tests + self.useFixture( + fixtures.EnvironmentVariable("SNAPCRAFT_BUILD_ENVIRONMENT", "host") + ) + + # Make sure snap installation does the right thing + self.fake_snapd.installed_snaps = [ + dict(name="core20", channel="stable", revision="10"), + dict(name="core18", channel="stable", revision="10"), + ] + self.fake_snapd.snaps_result = [ + dict(name="core20", channel="stable", revision="10"), + dict(name="core18", channel="stable", revision="10"), + ] + self.fake_snapd.find_result = [ + dict( + core20=dict( + channel="stable", + channels={"latest/stable": dict(confinement="strict")}, + ) + ), + dict( + core18=dict( + channel="stable", + channels={"latest/stable": dict(confinement="strict")}, + ) + ), + ] + self.fake_snapd.snap_details_func = None + + self.fake_snap_command = fixture_setup.FakeSnapCommand() + self.useFixture(self.fake_snap_command) + + # Avoid installing patchelf in the tests + self.useFixture(fixtures.EnvironmentVariable("SNAPCRAFT_NO_PATCHELF", "1")) + + # Disable Sentry reporting for tests, otherwise they'll hang waiting + # for input + self.useFixture( + fixtures.EnvironmentVariable("SNAPCRAFT_ENABLE_ERROR_REPORTING", "false") + ) + + # Don't let the managed host variable leak into tests + self.useFixture(fixtures.EnvironmentVariable("SNAPCRAFT_MANAGED_HOST")) + + machine = os.environ.get("SNAPCRAFT_TEST_MOCK_MACHINE", None) + self.base_environment = fixture_setup.FakeBaseEnvironment(machine=machine) + self.useFixture(self.base_environment) + + # Make sure "SNAPCRAFT_ENABLE_DEVELOPER_DEBUG" is reset between tests + self.useFixture( + fixtures.EnvironmentVariable("SNAPCRAFT_ENABLE_DEVELOPER_DEBUG") + ) + self.useFixture(fixture_setup.FakeSnapcraftctl()) + + # Don't let host SNAPCRAFT_BUILD_INFO variable leak into tests + self.useFixture(fixtures.EnvironmentVariable("SNAPCRAFT_BUILD_INFO")) + + def make_snapcraft_yaml(self, content, encoding="utf-8", location=""): + snap_dir = os.path.join(location, "snap") + os.makedirs(snap_dir, exist_ok=True) + snapcraft_yaml = os.path.join(snap_dir, "snapcraft.yaml") + with open(snapcraft_yaml, "w", encoding=encoding) as fp: + fp.write(content) + return snapcraft_yaml + + def verify_state(self, part_name, state_dir, expected_step_name): + self.assertTrue( + os.path.isdir(state_dir), + "Expected state directory for {}".format(part_name), + ) + + # Expect every step up to and including the specified one to be run + step = steps.get_step_by_name(expected_step_name) + for step in step.previous_steps() + [step]: + self.assertTrue( + os.path.exists(os.path.join(state_dir, step.name)), + "Expected {!r} to be run for {}".format(step.name, part_name), + ) + + def load_part( + self, + part_name, + plugin_name=None, + part_properties=None, + project=None, + stage_packages_repo=None, + snap_name="test-snap", + base="core18", + build_base=None, + confinement="strict", + snap_type="app", + ): + return load_part( + part_name=part_name, + plugin_name=plugin_name, + part_properties=part_properties, + project=project, + stage_packages_repo=stage_packages_repo, + snap_name=snap_name, + base=base, + build_base=build_base, + confinement=confinement, + snap_type=snap_type, + ) + + +class TestWithFakeRemoteParts(TestCase): + def setUp(self): + super().setUp() + self.useFixture(fixture_setup.FakeParts()) + + +class FakeFileHTTPServerBasedTestCase(TestCase): + def setUp(self): + super().setUp() + + self.useFixture(fixtures.EnvironmentVariable("no_proxy", "localhost,127.0.0.1")) + self.server = http.server.HTTPServer( + ("127.0.0.1", 0), fake_servers.FakeFileHTTPRequestHandler + ) + server_thread = threading.Thread(target=self.server.serve_forever) + self.addCleanup(server_thread.join) + self.addCleanup(self.server.server_close) + self.addCleanup(self.server.shutdown) + server_thread.start() + + +class SilentProgressBar(progressbar.ProgressBar): + """A progress bar causing no spurious output during tests.""" + + def start(self): + pass + + def update(self, value=None): + pass + + def finish(self): + pass diff --git a/tests/unit/build_providers/__init__.py b/tests/legacy/unit/build_providers/__init__.py similarity index 99% rename from tests/unit/build_providers/__init__.py rename to tests/legacy/unit/build_providers/__init__.py index c10a42706b..681fd541ec 100644 --- a/tests/unit/build_providers/__init__.py +++ b/tests/legacy/unit/build_providers/__init__.py @@ -21,7 +21,7 @@ from snapcraft_legacy.internal.build_providers._base_provider import Provider from snapcraft_legacy.internal.meta.snap import Snap from snapcraft_legacy.project import Project -from tests import fixture_setup, unit +from tests.legacy import fixture_setup, unit class ProviderImpl(Provider): diff --git a/tests/unit/build_providers/conftest.py b/tests/legacy/unit/build_providers/conftest.py similarity index 100% rename from tests/unit/build_providers/conftest.py rename to tests/legacy/unit/build_providers/conftest.py diff --git a/tests/unit/build_providers/multipass/__init__.py b/tests/legacy/unit/build_providers/lxd/__init__.py similarity index 100% rename from tests/unit/build_providers/multipass/__init__.py rename to tests/legacy/unit/build_providers/lxd/__init__.py diff --git a/tests/unit/build_providers/lxd/test_lxd.py b/tests/legacy/unit/build_providers/lxd/test_lxd.py similarity index 99% rename from tests/unit/build_providers/lxd/test_lxd.py rename to tests/legacy/unit/build_providers/lxd/test_lxd.py index dda8efe63b..b062ddda14 100644 --- a/tests/unit/build_providers/lxd/test_lxd.py +++ b/tests/legacy/unit/build_providers/lxd/test_lxd.py @@ -26,7 +26,7 @@ from snapcraft_legacy.internal.build_providers._lxd import LXD from snapcraft_legacy.internal.errors import SnapcraftEnvironmentError from snapcraft_legacy.internal.repo.errors import SnapdConnectionError -from tests.unit.build_providers import BaseProviderBaseTest +from tests.legacy.unit.build_providers import BaseProviderBaseTest if sys.platform == "linux": import pylxd diff --git a/tests/unit/cache/__init__.py b/tests/legacy/unit/build_providers/multipass/__init__.py similarity index 100% rename from tests/unit/cache/__init__.py rename to tests/legacy/unit/build_providers/multipass/__init__.py diff --git a/tests/unit/build_providers/multipass/test_instance_info.py b/tests/legacy/unit/build_providers/multipass/test_instance_info.py similarity index 99% rename from tests/unit/build_providers/multipass/test_instance_info.py rename to tests/legacy/unit/build_providers/multipass/test_instance_info.py index a8b138fbde..fd6df8d098 100644 --- a/tests/unit/build_providers/multipass/test_instance_info.py +++ b/tests/legacy/unit/build_providers/multipass/test_instance_info.py @@ -22,7 +22,7 @@ from snapcraft_legacy.internal.build_providers._multipass._instance_info import ( # noqa: E501 InstanceInfo, ) -from tests import unit +from tests.legacy import unit class InstanceInfoGeneralTest(unit.TestCase): diff --git a/tests/unit/build_providers/multipass/test_multipass.py b/tests/legacy/unit/build_providers/multipass/test_multipass.py similarity index 99% rename from tests/unit/build_providers/multipass/test_multipass.py rename to tests/legacy/unit/build_providers/multipass/test_multipass.py index 24c136517a..3edb5c9d66 100644 --- a/tests/unit/build_providers/multipass/test_multipass.py +++ b/tests/legacy/unit/build_providers/multipass/test_multipass.py @@ -30,7 +30,7 @@ MultipassCommand, ) from snapcraft_legacy.internal.errors import SnapcraftEnvironmentError -from tests.unit.build_providers import BaseProviderBaseTest, get_project +from tests.legacy.unit.build_providers import BaseProviderBaseTest, get_project _DEFAULT_INSTANCE_INFO = dedent( """\ diff --git a/tests/unit/build_providers/multipass/test_multipass_command.py b/tests/legacy/unit/build_providers/multipass/test_multipass_command.py similarity index 99% rename from tests/unit/build_providers/multipass/test_multipass_command.py rename to tests/legacy/unit/build_providers/multipass/test_multipass_command.py index 69bca2ae3f..432ff72e01 100644 --- a/tests/unit/build_providers/multipass/test_multipass_command.py +++ b/tests/legacy/unit/build_providers/multipass/test_multipass_command.py @@ -25,7 +25,7 @@ from snapcraft_legacy.internal.build_providers import errors from snapcraft_legacy.internal.build_providers._multipass import MultipassCommand -from tests import unit +from tests.legacy import unit class MultipassCommandBaseTest(unit.TestCase): diff --git a/tests/unit/build_providers/test_base_provider.py b/tests/legacy/unit/build_providers/test_base_provider.py similarity index 100% rename from tests/unit/build_providers/test_base_provider.py rename to tests/legacy/unit/build_providers/test_base_provider.py diff --git a/tests/unit/build_providers/test_errors.py b/tests/legacy/unit/build_providers/test_errors.py similarity index 100% rename from tests/unit/build_providers/test_errors.py rename to tests/legacy/unit/build_providers/test_errors.py diff --git a/tests/unit/build_providers/test_snap.py b/tests/legacy/unit/build_providers/test_snap.py similarity index 99% rename from tests/unit/build_providers/test_snap.py rename to tests/legacy/unit/build_providers/test_snap.py index fa8b503e16..bd9a20e415 100644 --- a/tests/unit/build_providers/test_snap.py +++ b/tests/legacy/unit/build_providers/test_snap.py @@ -27,7 +27,7 @@ _get_snap_channel, repo, ) -from tests import fixture_setup, unit +from tests.legacy import fixture_setup, unit from . import ProviderImpl, get_project diff --git a/tests/unit/cli/__init__.py b/tests/legacy/unit/cache/__init__.py similarity index 100% rename from tests/unit/cli/__init__.py rename to tests/legacy/unit/cache/__init__.py diff --git a/tests/unit/cache/conftest.py b/tests/legacy/unit/cache/conftest.py similarity index 100% rename from tests/unit/cache/conftest.py rename to tests/legacy/unit/cache/conftest.py diff --git a/tests/unit/cache/test_file.py b/tests/legacy/unit/cache/test_file.py similarity index 100% rename from tests/unit/cache/test_file.py rename to tests/legacy/unit/cache/test_file.py diff --git a/tests/unit/cache/test_snap.py b/tests/legacy/unit/cache/test_snap.py similarity index 97% rename from tests/unit/cache/test_snap.py rename to tests/legacy/unit/cache/test_snap.py index b09c2d9d14..1c12df1e07 100644 --- a/tests/unit/cache/test_snap.py +++ b/tests/legacy/unit/cache/test_snap.py @@ -23,10 +23,10 @@ from testtools.matchers import Contains, Equals, Not import snapcraft_legacy -import tests +import tests.legacy from snapcraft_legacy import file_utils from snapcraft_legacy.internal import cache -from tests.unit.commands import CommandBaseTestCase +from tests.legacy.unit.commands import CommandBaseTestCase class SnapCacheBaseTestCase(CommandBaseTestCase): @@ -35,7 +35,7 @@ def setUp(self): self.deb_arch = snapcraft_legacy.ProjectOptions().deb_arch self.snap_path = os.path.join( - os.path.dirname(tests.__file__), "data", "test-snap.snap" + os.path.dirname(tests.legacy.__file__), "data", "test-snap.snap" ) diff --git a/tests/unit/deltas/__init__.py b/tests/legacy/unit/cli/__init__.py similarity index 100% rename from tests/unit/deltas/__init__.py rename to tests/legacy/unit/cli/__init__.py diff --git a/tests/unit/cli/conftest.py b/tests/legacy/unit/cli/conftest.py similarity index 100% rename from tests/unit/cli/conftest.py rename to tests/legacy/unit/cli/conftest.py diff --git a/tests/unit/cli/test_echo.py b/tests/legacy/unit/cli/test_echo.py similarity index 99% rename from tests/unit/cli/test_echo.py rename to tests/legacy/unit/cli/test_echo.py index 7b2bf5ebf7..157ae3d6ba 100644 --- a/tests/unit/cli/test_echo.py +++ b/tests/legacy/unit/cli/test_echo.py @@ -22,7 +22,7 @@ import pytest from snapcraft_legacy.cli import echo -from tests import unit +from tests.legacy import unit @pytest.fixture() diff --git a/tests/unit/cli/test_errors.py b/tests/legacy/unit/cli/test_errors.py similarity index 99% rename from tests/unit/cli/test_errors.py rename to tests/legacy/unit/cli/test_errors.py index 5b96f19c53..782715789d 100644 --- a/tests/unit/cli/test_errors.py +++ b/tests/legacy/unit/cli/test_errors.py @@ -34,7 +34,7 @@ exception_handler, ) from snapcraft_legacy.internal.build_providers.errors import ProviderExecError -from tests import fixture_setup, unit +from tests.legacy import fixture_setup, unit class SnapcraftTError(snapcraft_legacy.internal.errors.SnapcraftError): diff --git a/tests/unit/cli/test_lifecycle.py b/tests/legacy/unit/cli/test_lifecycle.py similarity index 100% rename from tests/unit/cli/test_lifecycle.py rename to tests/legacy/unit/cli/test_lifecycle.py diff --git a/tests/unit/cli/test_metrics.py b/tests/legacy/unit/cli/test_metrics.py similarity index 100% rename from tests/unit/cli/test_metrics.py rename to tests/legacy/unit/cli/test_metrics.py diff --git a/tests/unit/cli/test_options.py b/tests/legacy/unit/cli/test_options.py similarity index 99% rename from tests/unit/cli/test_options.py rename to tests/legacy/unit/cli/test_options.py index 3138512e6c..f998b5b836 100644 --- a/tests/unit/cli/test_options.py +++ b/tests/legacy/unit/cli/test_options.py @@ -21,7 +21,7 @@ from testtools.matchers import Equals import snapcraft_legacy.cli._options as options -from tests import unit +from tests.legacy import unit class TestProviderOptions: diff --git a/tests/unit/commands/__init__.py b/tests/legacy/unit/commands/__init__.py similarity index 99% rename from tests/unit/commands/__init__.py rename to tests/legacy/unit/commands/__init__.py index 3ec204f686..4dee118127 100644 --- a/tests/unit/commands/__init__.py +++ b/tests/legacy/unit/commands/__init__.py @@ -28,7 +28,7 @@ from snapcraft_legacy.storeapi import metrics from snapcraft_legacy.storeapi.v2.channel_map import ChannelMap from snapcraft_legacy.storeapi.v2.releases import Releases -from tests import fixture_setup, unit +from tests.legacy import fixture_setup, unit _sample_keys = [ { diff --git a/tests/unit/commands/conftest.py b/tests/legacy/unit/commands/conftest.py similarity index 100% rename from tests/unit/commands/conftest.py rename to tests/legacy/unit/commands/conftest.py diff --git a/tests/unit/commands/snapcraftctl/__init__.py b/tests/legacy/unit/commands/snapcraftctl/__init__.py similarity index 98% rename from tests/unit/commands/snapcraftctl/__init__.py rename to tests/legacy/unit/commands/snapcraftctl/__init__.py index 890b71472e..358915bfd6 100644 --- a/tests/unit/commands/snapcraftctl/__init__.py +++ b/tests/legacy/unit/commands/snapcraftctl/__init__.py @@ -20,7 +20,7 @@ from click.testing import CliRunner from snapcraft_legacy.cli.snapcraftctl._runner import run -from tests import unit +from tests.legacy import unit class CommandBaseNoFifoTestCase(unit.TestCase): diff --git a/tests/unit/commands/snapcraftctl/test_build.py b/tests/legacy/unit/commands/snapcraftctl/test_build.py similarity index 100% rename from tests/unit/commands/snapcraftctl/test_build.py rename to tests/legacy/unit/commands/snapcraftctl/test_build.py diff --git a/tests/unit/commands/snapcraftctl/test_set_grade.py b/tests/legacy/unit/commands/snapcraftctl/test_set_grade.py similarity index 100% rename from tests/unit/commands/snapcraftctl/test_set_grade.py rename to tests/legacy/unit/commands/snapcraftctl/test_set_grade.py diff --git a/tests/unit/commands/snapcraftctl/test_set_version.py b/tests/legacy/unit/commands/snapcraftctl/test_set_version.py similarity index 100% rename from tests/unit/commands/snapcraftctl/test_set_version.py rename to tests/legacy/unit/commands/snapcraftctl/test_set_version.py diff --git a/tests/unit/commands/test_build_providers.py b/tests/legacy/unit/commands/test_build_providers.py similarity index 99% rename from tests/unit/commands/test_build_providers.py rename to tests/legacy/unit/commands/test_build_providers.py index 454d4771f0..15dbb90b0b 100644 --- a/tests/unit/commands/test_build_providers.py +++ b/tests/legacy/unit/commands/test_build_providers.py @@ -24,8 +24,8 @@ import snapcraft_legacy.yaml_utils.errors from snapcraft_legacy.internal import steps from snapcraft_legacy.internal.build_providers.errors import ProviderExecError -from tests import fixture_setup -from tests.unit.build_providers import ProviderImpl +from tests.legacy import fixture_setup +from tests.legacy.unit.build_providers import ProviderImpl from . import CommandBaseTestCase @@ -77,7 +77,7 @@ def setUp(self): # line interface flag parsing. self.useFixture(fixtures.MockPatch("snapcraft_legacy.internal.lifecycle.clean")) - # tests.unit.TestCase sets SNAPCRAFT_BUILD_ENVIRONMENT to host. + # tests.legacy.unit.TestCase sets SNAPCRAFT_BUILD_ENVIRONMENT to host. # These build provider tests will want to set this explicitly. self.useFixture( fixtures.EnvironmentVariable("SNAPCRAFT_BUILD_ENVIRONMENT", None) diff --git a/tests/unit/commands/test_clean.py b/tests/legacy/unit/commands/test_clean.py similarity index 100% rename from tests/unit/commands/test_clean.py rename to tests/legacy/unit/commands/test_clean.py diff --git a/tests/unit/commands/test_close.py b/tests/legacy/unit/commands/test_close.py similarity index 100% rename from tests/unit/commands/test_close.py rename to tests/legacy/unit/commands/test_close.py diff --git a/tests/unit/commands/test_create_key.py b/tests/legacy/unit/commands/test_create_key.py similarity index 100% rename from tests/unit/commands/test_create_key.py rename to tests/legacy/unit/commands/test_create_key.py diff --git a/tests/unit/commands/test_edit_validation_sets.py b/tests/legacy/unit/commands/test_edit_validation_sets.py similarity index 100% rename from tests/unit/commands/test_edit_validation_sets.py rename to tests/legacy/unit/commands/test_edit_validation_sets.py diff --git a/tests/unit/commands/test_export_login.py b/tests/legacy/unit/commands/test_export_login.py similarity index 100% rename from tests/unit/commands/test_export_login.py rename to tests/legacy/unit/commands/test_export_login.py diff --git a/tests/unit/commands/test_extensions.py b/tests/legacy/unit/commands/test_extensions.py similarity index 99% rename from tests/unit/commands/test_extensions.py rename to tests/legacy/unit/commands/test_extensions.py index 2a4ab75b52..b8b1610ada 100644 --- a/tests/unit/commands/test_extensions.py +++ b/tests/legacy/unit/commands/test_extensions.py @@ -22,7 +22,7 @@ from snapcraft_legacy.internal.project_loader import errors, supported_extension_names from snapcraft_legacy.internal.project_loader._extensions._extension import Extension -from tests import fixture_setup +from tests.legacy import fixture_setup from . import CommandBaseTestCase diff --git a/tests/unit/commands/test_gated.py b/tests/legacy/unit/commands/test_gated.py similarity index 100% rename from tests/unit/commands/test_gated.py rename to tests/legacy/unit/commands/test_gated.py diff --git a/tests/unit/commands/test_help.py b/tests/legacy/unit/commands/test_help.py similarity index 99% rename from tests/unit/commands/test_help.py rename to tests/legacy/unit/commands/test_help.py index ff813fa681..9a2e532533 100644 --- a/tests/unit/commands/test_help.py +++ b/tests/legacy/unit/commands/test_help.py @@ -23,7 +23,7 @@ from snapcraft_legacy.cli._runner import run from snapcraft_legacy.cli.help import _TOPICS -from tests import fixture_setup +from tests.legacy import fixture_setup from . import CommandBaseTestCase diff --git a/tests/unit/commands/test_init.py b/tests/legacy/unit/commands/test_init.py similarity index 100% rename from tests/unit/commands/test_init.py rename to tests/legacy/unit/commands/test_init.py diff --git a/tests/unit/commands/test_list.py b/tests/legacy/unit/commands/test_list.py similarity index 100% rename from tests/unit/commands/test_list.py rename to tests/legacy/unit/commands/test_list.py diff --git a/tests/unit/commands/test_list_keys.py b/tests/legacy/unit/commands/test_list_keys.py similarity index 100% rename from tests/unit/commands/test_list_keys.py rename to tests/legacy/unit/commands/test_list_keys.py diff --git a/tests/unit/commands/test_list_plugins.py b/tests/legacy/unit/commands/test_list_plugins.py similarity index 99% rename from tests/unit/commands/test_list_plugins.py rename to tests/legacy/unit/commands/test_list_plugins.py index 90257a9b46..6d0ebfad82 100644 --- a/tests/unit/commands/test_list_plugins.py +++ b/tests/legacy/unit/commands/test_list_plugins.py @@ -18,7 +18,7 @@ from testtools.matchers import Contains, Equals import snapcraft_legacy -from tests import fixture_setup +from tests.legacy import fixture_setup from . import CommandBaseTestCase diff --git a/tests/unit/commands/test_list_revisions.py b/tests/legacy/unit/commands/test_list_revisions.py similarity index 100% rename from tests/unit/commands/test_list_revisions.py rename to tests/legacy/unit/commands/test_list_revisions.py diff --git a/tests/unit/commands/test_list_tracks.py b/tests/legacy/unit/commands/test_list_tracks.py similarity index 100% rename from tests/unit/commands/test_list_tracks.py rename to tests/legacy/unit/commands/test_list_tracks.py diff --git a/tests/unit/commands/test_list_validation_sets.py b/tests/legacy/unit/commands/test_list_validation_sets.py similarity index 100% rename from tests/unit/commands/test_list_validation_sets.py rename to tests/legacy/unit/commands/test_list_validation_sets.py diff --git a/tests/unit/commands/test_login.py b/tests/legacy/unit/commands/test_login.py similarity index 100% rename from tests/unit/commands/test_login.py rename to tests/legacy/unit/commands/test_login.py diff --git a/tests/unit/commands/test_logout.py b/tests/legacy/unit/commands/test_logout.py similarity index 100% rename from tests/unit/commands/test_logout.py rename to tests/legacy/unit/commands/test_logout.py diff --git a/tests/unit/commands/test_metrics.py b/tests/legacy/unit/commands/test_metrics.py similarity index 100% rename from tests/unit/commands/test_metrics.py rename to tests/legacy/unit/commands/test_metrics.py diff --git a/tests/unit/commands/test_pack.py b/tests/legacy/unit/commands/test_pack.py similarity index 100% rename from tests/unit/commands/test_pack.py rename to tests/legacy/unit/commands/test_pack.py diff --git a/tests/unit/commands/test_promote.py b/tests/legacy/unit/commands/test_promote.py similarity index 100% rename from tests/unit/commands/test_promote.py rename to tests/legacy/unit/commands/test_promote.py diff --git a/tests/unit/commands/test_pull_build_stage_prime.py b/tests/legacy/unit/commands/test_pull_build_stage_prime.py similarity index 100% rename from tests/unit/commands/test_pull_build_stage_prime.py rename to tests/legacy/unit/commands/test_pull_build_stage_prime.py diff --git a/tests/unit/commands/test_refresh.py b/tests/legacy/unit/commands/test_refresh.py similarity index 97% rename from tests/unit/commands/test_refresh.py rename to tests/legacy/unit/commands/test_refresh.py index b967a28e58..6d8dee82a7 100644 --- a/tests/unit/commands/test_refresh.py +++ b/tests/legacy/unit/commands/test_refresh.py @@ -19,7 +19,7 @@ from testtools.matchers import Equals -from tests.unit import TestWithFakeRemoteParts +from tests.legacy.unit import TestWithFakeRemoteParts from . import CommandBaseTestCase diff --git a/tests/unit/commands/test_register.py b/tests/legacy/unit/commands/test_register.py similarity index 100% rename from tests/unit/commands/test_register.py rename to tests/legacy/unit/commands/test_register.py diff --git a/tests/unit/commands/test_register_key.py b/tests/legacy/unit/commands/test_register_key.py similarity index 100% rename from tests/unit/commands/test_register_key.py rename to tests/legacy/unit/commands/test_register_key.py diff --git a/tests/unit/commands/test_release.py b/tests/legacy/unit/commands/test_release.py similarity index 100% rename from tests/unit/commands/test_release.py rename to tests/legacy/unit/commands/test_release.py diff --git a/tests/unit/commands/test_remote.py b/tests/legacy/unit/commands/test_remote.py similarity index 99% rename from tests/unit/commands/test_remote.py rename to tests/legacy/unit/commands/test_remote.py index e60cf4e161..47f785822f 100644 --- a/tests/unit/commands/test_remote.py +++ b/tests/legacy/unit/commands/test_remote.py @@ -21,7 +21,7 @@ import snapcraft_legacy.internal.remote_build.errors as errors import snapcraft_legacy.project -from tests import fixture_setup +from tests.legacy import fixture_setup from . import CommandBaseTestCase diff --git a/tests/unit/commands/test_set_default_track.py b/tests/legacy/unit/commands/test_set_default_track.py similarity index 100% rename from tests/unit/commands/test_set_default_track.py rename to tests/legacy/unit/commands/test_set_default_track.py diff --git a/tests/unit/commands/test_sign_build.py b/tests/legacy/unit/commands/test_sign_build.py similarity index 98% rename from tests/unit/commands/test_sign_build.py rename to tests/legacy/unit/commands/test_sign_build.py index cda53dd7ff..b6c751c2f2 100644 --- a/tests/unit/commands/test_sign_build.py +++ b/tests/legacy/unit/commands/test_sign_build.py @@ -21,7 +21,7 @@ import fixtures from testtools.matchers import Contains, Equals, FileExists, Not -import tests +import tests.legacy from snapcraft_legacy import internal, storeapi from . import CommandBaseTestCase @@ -34,7 +34,7 @@ class SnapTest(fixtures.TempDir): gets cleaned up automatically. """ - data_dir = os.path.join(os.path.dirname(tests.__file__), "data") + data_dir = os.path.join(os.path.dirname(tests.legacy.__file__), "data") def __init__(self, test_snap_name): super(SnapTest, self).__init__() @@ -66,7 +66,7 @@ def test_sign_build_nonexisting_snap(self): def test_sign_build_invalid_snap(self): snap_path = os.path.join( - os.path.dirname(tests.__file__), "data", "invalid.snap" + os.path.dirname(tests.legacy.__file__), "data", "invalid.snap" ) raised = self.assertRaises( diff --git a/tests/unit/commands/test_snap.py b/tests/legacy/unit/commands/test_snap.py similarity index 100% rename from tests/unit/commands/test_snap.py rename to tests/legacy/unit/commands/test_snap.py diff --git a/tests/unit/commands/test_status.py b/tests/legacy/unit/commands/test_status.py similarity index 100% rename from tests/unit/commands/test_status.py rename to tests/legacy/unit/commands/test_status.py diff --git a/tests/unit/commands/test_upload.py b/tests/legacy/unit/commands/test_upload.py similarity index 98% rename from tests/unit/commands/test_upload.py rename to tests/legacy/unit/commands/test_upload.py index 2a9582cb69..be971680dc 100644 --- a/tests/unit/commands/test_upload.py +++ b/tests/legacy/unit/commands/test_upload.py @@ -22,7 +22,7 @@ from testtools.matchers import Contains, Equals, FileExists, Not from xdg import BaseDirectory -import tests +import tests.legacy from snapcraft_legacy import file_utils, internal, storeapi from snapcraft_legacy.internal import review_tools from snapcraft_legacy.storeapi.errors import ( @@ -39,7 +39,7 @@ def setUp(self): super().setUp() self.snap_file = os.path.join( - os.path.dirname(tests.__file__), "data", "test-snap.snap" + os.path.dirname(tests.legacy.__file__), "data", "test-snap.snap" ) self.fake_review_tools_run = fixtures.MockPatch( @@ -136,7 +136,9 @@ def test_upload_a_snap_review_tools_run_fail(self): def test_upload_with_started_at(self): snap_file = os.path.join( - os.path.dirname(tests.__file__), "data", "test-snap-with-started-at.snap" + os.path.dirname(tests.legacy.__file__), + "data", + "test-snap-with-started-at.snap", ) # Upload @@ -176,7 +178,7 @@ def test_upload_nonexisting_snap_must_raise_exception(self): def test_upload_invalid_snap_must_raise_exception(self): snap_path = os.path.join( - os.path.dirname(tests.__file__), "data", "invalid.snap" + os.path.dirname(tests.legacy.__file__), "data", "invalid.snap" ) raised = self.assertRaises( diff --git a/tests/unit/commands/test_upload_metadata.py b/tests/legacy/unit/commands/test_upload_metadata.py similarity index 97% rename from tests/unit/commands/test_upload_metadata.py rename to tests/legacy/unit/commands/test_upload_metadata.py index 71e81afdb1..e2a34c55b8 100644 --- a/tests/unit/commands/test_upload_metadata.py +++ b/tests/legacy/unit/commands/test_upload_metadata.py @@ -21,7 +21,7 @@ import fixtures from testtools.matchers import Contains, Equals, Not -import tests +import tests.legacy from snapcraft_legacy import storeapi from snapcraft_legacy.storeapi.errors import StoreUploadError @@ -55,7 +55,7 @@ def _save_updated_icon(snap_name, metadata, force): self.useFixture(self.fake_binary_metadata) self.snap_file = os.path.join( - os.path.dirname(tests.__file__), "data", "test-snap-with-icon.snap" + os.path.dirname(tests.legacy.__file__), "data", "test-snap-with-icon.snap" ) def assert_expected_metadata_calls(self, force=False, optional_text_metadata=None): @@ -102,7 +102,7 @@ def test_simple(self): def test_with_license_and_title(self): self.snap_file = os.path.join( - os.path.dirname(tests.__file__), + os.path.dirname(tests.legacy.__file__), "data", "test-snap-with-icon-license-title.snap", ) @@ -219,7 +219,7 @@ def test_forced(self): def test_snap_without_icon(self): snap_file = os.path.join( - os.path.dirname(tests.__file__), "data", "test-snap.snap" + os.path.dirname(tests.legacy.__file__), "data", "test-snap.snap" ) # upload metadata diff --git a/tests/unit/commands/test_validate.py b/tests/legacy/unit/commands/test_validate.py similarity index 100% rename from tests/unit/commands/test_validate.py rename to tests/legacy/unit/commands/test_validate.py diff --git a/tests/unit/commands/test_version.py b/tests/legacy/unit/commands/test_version.py similarity index 100% rename from tests/unit/commands/test_version.py rename to tests/legacy/unit/commands/test_version.py diff --git a/tests/unit/commands/test_whoami.py b/tests/legacy/unit/commands/test_whoami.py similarity index 100% rename from tests/unit/commands/test_whoami.py rename to tests/legacy/unit/commands/test_whoami.py diff --git a/tests/unit/conftest.py b/tests/legacy/unit/conftest.py similarity index 100% rename from tests/unit/conftest.py rename to tests/legacy/unit/conftest.py diff --git a/tests/unit/db/test_datastore.py b/tests/legacy/unit/db/test_datastore.py similarity index 100% rename from tests/unit/db/test_datastore.py rename to tests/legacy/unit/db/test_datastore.py diff --git a/tests/unit/db/test_errors.py b/tests/legacy/unit/db/test_errors.py similarity index 100% rename from tests/unit/db/test_errors.py rename to tests/legacy/unit/db/test_errors.py diff --git a/tests/unit/db/test_migration.py b/tests/legacy/unit/db/test_migration.py similarity index 100% rename from tests/unit/db/test_migration.py rename to tests/legacy/unit/db/test_migration.py diff --git a/tests/unit/extractors/__init__.py b/tests/legacy/unit/deltas/__init__.py similarity index 100% rename from tests/unit/extractors/__init__.py rename to tests/legacy/unit/deltas/__init__.py diff --git a/tests/unit/deltas/test_deltas.py b/tests/legacy/unit/deltas/test_deltas.py similarity index 99% rename from tests/unit/deltas/test_deltas.py rename to tests/legacy/unit/deltas/test_deltas.py index 3ebd1b9e96..9c72578b69 100644 --- a/tests/unit/deltas/test_deltas.py +++ b/tests/legacy/unit/deltas/test_deltas.py @@ -22,7 +22,7 @@ from testtools import matchers as m from snapcraft_legacy.internal import deltas -from tests import fixture_setup +from tests.legacy import fixture_setup class BaseDeltaGenerationTestCase(TestCase): diff --git a/tests/unit/deltas/test_deltas_xdelta3.py b/tests/legacy/unit/deltas/test_deltas_xdelta3.py similarity index 99% rename from tests/unit/deltas/test_deltas_xdelta3.py rename to tests/legacy/unit/deltas/test_deltas_xdelta3.py index 89ad01d559..be695352c7 100644 --- a/tests/unit/deltas/test_deltas_xdelta3.py +++ b/tests/legacy/unit/deltas/test_deltas_xdelta3.py @@ -24,7 +24,7 @@ from testtools import matchers as m from snapcraft_legacy.internal import deltas -from tests import fixture_setup, unit +from tests.legacy import fixture_setup, unit class XDelta3TestCase(unit.TestCase): diff --git a/tests/unit/meta/__init__.py b/tests/legacy/unit/extractors/__init__.py similarity index 100% rename from tests/unit/meta/__init__.py rename to tests/legacy/unit/extractors/__init__.py diff --git a/tests/unit/extractors/test_appstream.py b/tests/legacy/unit/extractors/test_appstream.py similarity index 99% rename from tests/unit/extractors/test_appstream.py rename to tests/legacy/unit/extractors/test_appstream.py index 89c832ad2a..e45f178b07 100644 --- a/tests/unit/extractors/test_appstream.py +++ b/tests/legacy/unit/extractors/test_appstream.py @@ -21,7 +21,7 @@ from testtools.matchers import Equals from snapcraft_legacy.extractors import ExtractedMetadata, _errors, appstream -from tests import unit +from tests.legacy import unit def _create_desktop_file(desktop_file_path, icon: str = None) -> None: diff --git a/tests/unit/extractors/test_metadata.py b/tests/legacy/unit/extractors/test_metadata.py similarity index 99% rename from tests/unit/extractors/test_metadata.py rename to tests/legacy/unit/extractors/test_metadata.py index f6e4a0f82a..d2bbd65170 100644 --- a/tests/unit/extractors/test_metadata.py +++ b/tests/legacy/unit/extractors/test_metadata.py @@ -17,7 +17,7 @@ from testtools.matchers import Equals, Not from snapcraft_legacy.extractors._metadata import ExtractedMetadata -from tests import unit +from tests.legacy import unit class ExtractedMetadataTestCase(unit.TestCase): diff --git a/tests/unit/extractors/test_setuppy.py b/tests/legacy/unit/extractors/test_setuppy.py similarity index 99% rename from tests/unit/extractors/test_setuppy.py rename to tests/legacy/unit/extractors/test_setuppy.py index 6ea6bdb5ad..ab362d5fd0 100644 --- a/tests/unit/extractors/test_setuppy.py +++ b/tests/legacy/unit/extractors/test_setuppy.py @@ -20,7 +20,7 @@ from testtools.matchers import Equals from snapcraft_legacy.extractors import ExtractedMetadata, _errors, setuppy -from tests import unit +from tests.legacy import unit class TestSetupPy: diff --git a/tests/unit/lifecycle/__init__.py b/tests/legacy/unit/lifecycle/__init__.py similarity index 98% rename from tests/unit/lifecycle/__init__.py rename to tests/legacy/unit/lifecycle/__init__.py index 0c3f9c09f4..2f4dfc53bc 100644 --- a/tests/unit/lifecycle/__init__.py +++ b/tests/legacy/unit/lifecycle/__init__.py @@ -21,7 +21,7 @@ import snapcraft_legacy from snapcraft_legacy.internal import project_loader -from tests import unit +from tests.legacy import unit class LifecycleTestBase(unit.TestCase): diff --git a/tests/unit/lifecycle/test_errors.py b/tests/legacy/unit/lifecycle/test_errors.py similarity index 100% rename from tests/unit/lifecycle/test_errors.py rename to tests/legacy/unit/lifecycle/test_errors.py diff --git a/tests/unit/lifecycle/test_global_state.py b/tests/legacy/unit/lifecycle/test_global_state.py similarity index 99% rename from tests/unit/lifecycle/test_global_state.py rename to tests/legacy/unit/lifecycle/test_global_state.py index ad071103e8..1f91b2c3fb 100644 --- a/tests/unit/lifecycle/test_global_state.py +++ b/tests/legacy/unit/lifecycle/test_global_state.py @@ -22,7 +22,7 @@ from snapcraft_legacy.project import Project from snapcraft_legacy.storeapi.errors import SnapNotFoundError from snapcraft_legacy.storeapi.info import SnapInfo -from tests import fixture_setup +from tests.legacy import fixture_setup class TestGlobalState(TestCase): diff --git a/tests/unit/lifecycle/test_lifecycle.py b/tests/legacy/unit/lifecycle/test_lifecycle.py similarity index 99% rename from tests/unit/lifecycle/test_lifecycle.py rename to tests/legacy/unit/lifecycle/test_lifecycle.py index d09811c9d6..1ae3978a8f 100644 --- a/tests/unit/lifecycle/test_lifecycle.py +++ b/tests/legacy/unit/lifecycle/test_lifecycle.py @@ -39,7 +39,7 @@ ) from snapcraft_legacy.internal.lifecycle._runner import _replace_in_part from snapcraft_legacy.project import Project -from tests import fixture_setup, unit +from tests.legacy import fixture_setup, unit from . import LifecycleTestBase diff --git a/tests/unit/lifecycle/test_order.py b/tests/legacy/unit/lifecycle/test_order.py similarity index 100% rename from tests/unit/lifecycle/test_order.py rename to tests/legacy/unit/lifecycle/test_order.py diff --git a/tests/unit/lifecycle/test_snap_installation.py b/tests/legacy/unit/lifecycle/test_snap_installation.py similarity index 100% rename from tests/unit/lifecycle/test_snap_installation.py rename to tests/legacy/unit/lifecycle/test_snap_installation.py diff --git a/tests/unit/lifecycle/test_status_cache.py b/tests/legacy/unit/lifecycle/test_status_cache.py similarity index 100% rename from tests/unit/lifecycle/test_status_cache.py rename to tests/legacy/unit/lifecycle/test_status_cache.py diff --git a/tests/unit/pluginhandler/__init__.py b/tests/legacy/unit/meta/__init__.py similarity index 100% rename from tests/unit/pluginhandler/__init__.py rename to tests/legacy/unit/meta/__init__.py diff --git a/tests/unit/meta/test_application.py b/tests/legacy/unit/meta/test_application.py similarity index 99% rename from tests/unit/meta/test_application.py rename to tests/legacy/unit/meta/test_application.py index 24220aa531..58f30a571b 100644 --- a/tests/unit/meta/test_application.py +++ b/tests/legacy/unit/meta/test_application.py @@ -21,7 +21,7 @@ from snapcraft_legacy import yaml_utils from snapcraft_legacy.internal.meta import application, desktop, errors -from tests import unit +from tests.legacy import unit class AppCommandTest(unit.TestCase): diff --git a/tests/unit/meta/test_command.py b/tests/legacy/unit/meta/test_command.py similarity index 99% rename from tests/unit/meta/test_command.py rename to tests/legacy/unit/meta/test_command.py index 296dcfa29e..7a25ea8e4d 100644 --- a/tests/unit/meta/test_command.py +++ b/tests/legacy/unit/meta/test_command.py @@ -22,7 +22,7 @@ from testtools.matchers import Equals, FileContains, FileExists, Is from snapcraft_legacy.internal.meta import command, errors -from tests import unit +from tests.legacy import unit def _create_file(file_path: str, *, mode=0o755, contents="") -> None: diff --git a/tests/unit/meta/test_command_mangle.py b/tests/legacy/unit/meta/test_command_mangle.py similarity index 100% rename from tests/unit/meta/test_command_mangle.py rename to tests/legacy/unit/meta/test_command_mangle.py diff --git a/tests/unit/meta/test_desktop.py b/tests/legacy/unit/meta/test_desktop.py similarity index 100% rename from tests/unit/meta/test_desktop.py rename to tests/legacy/unit/meta/test_desktop.py diff --git a/tests/unit/meta/test_errors.py b/tests/legacy/unit/meta/test_errors.py similarity index 100% rename from tests/unit/meta/test_errors.py rename to tests/legacy/unit/meta/test_errors.py diff --git a/tests/unit/meta/test_hook.py b/tests/legacy/unit/meta/test_hook.py similarity index 99% rename from tests/unit/meta/test_hook.py rename to tests/legacy/unit/meta/test_hook.py index 183a3cdaca..279dec6763 100644 --- a/tests/unit/meta/test_hook.py +++ b/tests/legacy/unit/meta/test_hook.py @@ -20,7 +20,7 @@ from snapcraft_legacy.internal.meta import errors from snapcraft_legacy.internal.meta.hooks import Hook -from tests import unit +from tests.legacy import unit class GenericHookTests(unit.TestCase): diff --git a/tests/unit/meta/test_meta.py b/tests/legacy/unit/meta/test_meta.py similarity index 99% rename from tests/unit/meta/test_meta.py rename to tests/legacy/unit/meta/test_meta.py index 92ad5c187a..04c027139f 100644 --- a/tests/unit/meta/test_meta.py +++ b/tests/legacy/unit/meta/test_meta.py @@ -39,7 +39,7 @@ from snapcraft_legacy.internal.meta import _snap_packaging from snapcraft_legacy.internal.meta import errors as meta_errors from snapcraft_legacy.project import Project -from tests import fixture_setup, unit +from tests.legacy import fixture_setup, unit class CreateBaseTestCase(unit.TestCase): diff --git a/tests/unit/meta/test_package_repository.py b/tests/legacy/unit/meta/test_package_repository.py similarity index 100% rename from tests/unit/meta/test_package_repository.py rename to tests/legacy/unit/meta/test_package_repository.py diff --git a/tests/unit/meta/test_plugs.py b/tests/legacy/unit/meta/test_plugs.py similarity index 99% rename from tests/unit/meta/test_plugs.py rename to tests/legacy/unit/meta/test_plugs.py index 06243b800e..5db7dda980 100644 --- a/tests/unit/meta/test_plugs.py +++ b/tests/legacy/unit/meta/test_plugs.py @@ -20,7 +20,7 @@ from snapcraft_legacy.internal.meta import errors from snapcraft_legacy.internal.meta.plugs import ContentPlug, Plug -from tests import unit +from tests.legacy import unit class GenericPlugTests(unit.TestCase): diff --git a/tests/unit/meta/test_slots.py b/tests/legacy/unit/meta/test_slots.py similarity index 99% rename from tests/unit/meta/test_slots.py rename to tests/legacy/unit/meta/test_slots.py index 618f42bed2..a44496075a 100644 --- a/tests/unit/meta/test_slots.py +++ b/tests/legacy/unit/meta/test_slots.py @@ -20,7 +20,7 @@ from snapcraft_legacy.internal.meta import errors from snapcraft_legacy.internal.meta.slots import ContentSlot, DbusSlot, Slot -from tests import unit +from tests.legacy import unit class GenericSlotTests(unit.TestCase): diff --git a/tests/unit/meta/test_snap.py b/tests/legacy/unit/meta/test_snap.py similarity index 99% rename from tests/unit/meta/test_snap.py rename to tests/legacy/unit/meta/test_snap.py index 5aa48eab69..e957ec4a8a 100644 --- a/tests/unit/meta/test_snap.py +++ b/tests/legacy/unit/meta/test_snap.py @@ -24,7 +24,7 @@ from snapcraft_legacy.internal.meta import errors from snapcraft_legacy.internal.meta.snap import Snap from snapcraft_legacy.internal.meta.system_user import SystemUserScope -from tests import unit +from tests.legacy import unit class SnapTests(unit.TestCase): diff --git a/tests/unit/meta/test_snap_packaging.py b/tests/legacy/unit/meta/test_snap_packaging.py similarity index 99% rename from tests/unit/meta/test_snap_packaging.py rename to tests/legacy/unit/meta/test_snap_packaging.py index c342150cfe..c071752cb0 100644 --- a/tests/unit/meta/test_snap_packaging.py +++ b/tests/legacy/unit/meta/test_snap_packaging.py @@ -22,7 +22,7 @@ from snapcraft_legacy.internal.meta._snap_packaging import _SnapPackaging from snapcraft_legacy.internal.project_loader import load_config from snapcraft_legacy.project import Project -from tests import fixture_setup, unit +from tests.legacy import fixture_setup, unit class SnapPackagingRunnerTests(unit.TestCase): diff --git a/tests/unit/meta/test_system_user.py b/tests/legacy/unit/meta/test_system_user.py similarity index 99% rename from tests/unit/meta/test_system_user.py rename to tests/legacy/unit/meta/test_system_user.py index 943a0b7cd5..ddca88c2cf 100644 --- a/tests/unit/meta/test_system_user.py +++ b/tests/legacy/unit/meta/test_system_user.py @@ -18,7 +18,7 @@ from snapcraft_legacy.internal.meta import errors from snapcraft_legacy.internal.meta.system_user import SystemUser, SystemUserScope -from tests import unit +from tests.legacy import unit class SystemUserTests(unit.TestCase): diff --git a/tests/unit/part_loader.py b/tests/legacy/unit/part_loader.py similarity index 100% rename from tests/unit/part_loader.py rename to tests/legacy/unit/part_loader.py diff --git a/tests/unit/plugins/__init__.py b/tests/legacy/unit/pluginhandler/__init__.py similarity index 100% rename from tests/unit/plugins/__init__.py rename to tests/legacy/unit/pluginhandler/__init__.py diff --git a/tests/unit/pluginhandler/mocks.py b/tests/legacy/unit/pluginhandler/mocks.py similarity index 100% rename from tests/unit/pluginhandler/mocks.py rename to tests/legacy/unit/pluginhandler/mocks.py diff --git a/tests/unit/pluginhandler/test_clean.py b/tests/legacy/unit/pluginhandler/test_clean.py similarity index 99% rename from tests/unit/pluginhandler/test_clean.py rename to tests/legacy/unit/pluginhandler/test_clean.py index 34130bb546..f701cf2761 100644 --- a/tests/unit/pluginhandler/test_clean.py +++ b/tests/legacy/unit/pluginhandler/test_clean.py @@ -20,7 +20,7 @@ from snapcraft_legacy import file_utils from snapcraft_legacy.internal import errors, pluginhandler, steps -from tests.unit import TestCase, load_part +from tests.legacy.unit import TestCase, load_part class CleanTestCase(TestCase): diff --git a/tests/unit/pluginhandler/test_dirty_report.py b/tests/legacy/unit/pluginhandler/test_dirty_report.py similarity index 100% rename from tests/unit/pluginhandler/test_dirty_report.py rename to tests/legacy/unit/pluginhandler/test_dirty_report.py diff --git a/tests/unit/pluginhandler/test_metadata_extraction.py b/tests/legacy/unit/pluginhandler/test_metadata_extraction.py similarity index 98% rename from tests/unit/pluginhandler/test_metadata_extraction.py rename to tests/legacy/unit/pluginhandler/test_metadata_extraction.py index 5226e6bc17..6b809bddd2 100644 --- a/tests/unit/pluginhandler/test_metadata_extraction.py +++ b/tests/legacy/unit/pluginhandler/test_metadata_extraction.py @@ -22,7 +22,7 @@ from snapcraft_legacy import extractors from snapcraft_legacy.internal import errors from snapcraft_legacy.internal.pluginhandler import extract_metadata -from tests import fixture_setup, unit +from tests.legacy import fixture_setup, unit class MetadataExtractionTestCase(unit.TestCase): diff --git a/tests/unit/pluginhandler/test_missing_dependency.py b/tests/legacy/unit/pluginhandler/test_missing_dependency.py similarity index 99% rename from tests/unit/pluginhandler/test_missing_dependency.py rename to tests/legacy/unit/pluginhandler/test_missing_dependency.py index 47861658ee..72c3a16af3 100644 --- a/tests/unit/pluginhandler/test_missing_dependency.py +++ b/tests/legacy/unit/pluginhandler/test_missing_dependency.py @@ -23,7 +23,7 @@ from snapcraft_legacy.internal.pluginhandler._dependencies import ( MissingDependencyResolver, ) -from tests import unit +from tests.legacy import unit class MissingDependencyTest(unit.TestCase): diff --git a/tests/unit/pluginhandler/test_patcher.py b/tests/legacy/unit/pluginhandler/test_patcher.py similarity index 99% rename from tests/unit/pluginhandler/test_patcher.py rename to tests/legacy/unit/pluginhandler/test_patcher.py index 21c7ed3482..e22d3b0b64 100644 --- a/tests/unit/pluginhandler/test_patcher.py +++ b/tests/legacy/unit/pluginhandler/test_patcher.py @@ -21,7 +21,7 @@ from snapcraft_legacy import file_utils from snapcraft_legacy.internal import errors from snapcraft_legacy.internal.pluginhandler import PartPatcher -from tests.unit import load_part +from tests.legacy.unit import load_part @pytest.fixture diff --git a/tests/unit/pluginhandler/test_plugin_loader.py b/tests/legacy/unit/pluginhandler/test_plugin_loader.py similarity index 99% rename from tests/unit/pluginhandler/test_plugin_loader.py rename to tests/legacy/unit/pluginhandler/test_plugin_loader.py index e91040c727..92cc460ac1 100644 --- a/tests/unit/pluginhandler/test_plugin_loader.py +++ b/tests/legacy/unit/pluginhandler/test_plugin_loader.py @@ -26,7 +26,7 @@ from snapcraft_legacy.plugins._plugin_finder import _PLUGINS from snapcraft_legacy.plugins.v1 import PluginV1 from snapcraft_legacy.plugins.v2 import PluginV2 -from tests import unit +from tests.legacy import unit class NonLocalTest(unit.TestCase): diff --git a/tests/unit/pluginhandler/test_pluginhandler.py b/tests/legacy/unit/pluginhandler/test_pluginhandler.py similarity index 99% rename from tests/unit/pluginhandler/test_pluginhandler.py rename to tests/legacy/unit/pluginhandler/test_pluginhandler.py index c6480ca8b7..78ed9f02b6 100644 --- a/tests/unit/pluginhandler/test_pluginhandler.py +++ b/tests/legacy/unit/pluginhandler/test_pluginhandler.py @@ -39,7 +39,7 @@ ) from snapcraft_legacy.internal.sources.errors import SnapcraftSourceUnhandledError from snapcraft_legacy.project import Project -from tests import fixture_setup, unit +from tests.legacy import fixture_setup, unit from . import mocks diff --git a/tests/unit/pluginhandler/test_runner.py b/tests/legacy/unit/pluginhandler/test_runner.py similarity index 99% rename from tests/unit/pluginhandler/test_runner.py rename to tests/legacy/unit/pluginhandler/test_runner.py index 9262266600..c10751450f 100644 --- a/tests/unit/pluginhandler/test_runner.py +++ b/tests/legacy/unit/pluginhandler/test_runner.py @@ -24,7 +24,7 @@ from snapcraft_legacy.internal import errors from snapcraft_legacy.internal.pluginhandler import _runner -from tests import fixture_setup, unit +from tests.legacy import fixture_setup, unit def _fake_pull(): diff --git a/tests/unit/pluginhandler/test_scriptlets.py b/tests/legacy/unit/pluginhandler/test_scriptlets.py similarity index 99% rename from tests/unit/pluginhandler/test_scriptlets.py rename to tests/legacy/unit/pluginhandler/test_scriptlets.py index 546c13b333..d5ac61606e 100644 --- a/tests/unit/pluginhandler/test_scriptlets.py +++ b/tests/legacy/unit/pluginhandler/test_scriptlets.py @@ -27,8 +27,8 @@ from snapcraft_legacy import yaml_utils from snapcraft_legacy.internal import errors -from tests import unit -from tests.unit.commands import CommandBaseTestCase +from tests.legacy import unit +from tests.legacy.unit.commands import CommandBaseTestCase class ScriptletCommandsTestCase(CommandBaseTestCase): diff --git a/tests/unit/pluginhandler/test_state.py b/tests/legacy/unit/pluginhandler/test_state.py similarity index 99% rename from tests/unit/pluginhandler/test_state.py rename to tests/legacy/unit/pluginhandler/test_state.py index 8257f7f210..e1a4584c6d 100644 --- a/tests/unit/pluginhandler/test_state.py +++ b/tests/legacy/unit/pluginhandler/test_state.py @@ -23,7 +23,7 @@ from snapcraft_legacy import extractors, plugins from snapcraft_legacy.internal import elf, errors, states, steps -from tests import fixture_setup, unit +from tests.legacy import fixture_setup, unit class StateBaseTestCase(unit.TestCase): diff --git a/tests/unit/plugins/v1/python/__init__.py b/tests/legacy/unit/plugins/__init__.py similarity index 100% rename from tests/unit/plugins/v1/python/__init__.py rename to tests/legacy/unit/plugins/__init__.py diff --git a/tests/unit/plugins/v1/__init__.py b/tests/legacy/unit/plugins/v1/__init__.py similarity index 97% rename from tests/unit/plugins/v1/__init__.py rename to tests/legacy/unit/plugins/v1/__init__.py index 8d0fb5ecc5..d33ccf5213 100644 --- a/tests/unit/plugins/v1/__init__.py +++ b/tests/legacy/unit/plugins/v1/__init__.py @@ -16,7 +16,7 @@ from snapcraft_legacy.internal.meta.snap import Snap from snapcraft_legacy.project import Project -from tests import unit +from tests.legacy import unit class PluginsV1BaseTestCase(unit.TestCase): diff --git a/tests/unit/plugins/v1/conftest.py b/tests/legacy/unit/plugins/v1/conftest.py similarity index 100% rename from tests/unit/plugins/v1/conftest.py rename to tests/legacy/unit/plugins/v1/conftest.py diff --git a/tests/unit/plugins/v1/ros/__init__.py b/tests/legacy/unit/plugins/v1/python/__init__.py similarity index 100% rename from tests/unit/plugins/v1/ros/__init__.py rename to tests/legacy/unit/plugins/v1/python/__init__.py diff --git a/tests/unit/plugins/v1/python/_basesuite.py b/tests/legacy/unit/plugins/v1/python/_basesuite.py similarity index 97% rename from tests/unit/plugins/v1/python/_basesuite.py rename to tests/legacy/unit/plugins/v1/python/_basesuite.py index f153add58b..6ce4a483f8 100644 --- a/tests/unit/plugins/v1/python/_basesuite.py +++ b/tests/legacy/unit/plugins/v1/python/_basesuite.py @@ -15,7 +15,7 @@ # along with this program. If not, see . import os -from tests import unit +from tests.legacy import unit # LP: #1733584 diff --git a/tests/unit/plugins/v1/python/test_errors.py b/tests/legacy/unit/plugins/v1/python/test_errors.py similarity index 100% rename from tests/unit/plugins/v1/python/test_errors.py rename to tests/legacy/unit/plugins/v1/python/test_errors.py diff --git a/tests/unit/plugins/v1/python/test_pip.py b/tests/legacy/unit/plugins/v1/python/test_pip.py similarity index 100% rename from tests/unit/plugins/v1/python/test_pip.py rename to tests/legacy/unit/plugins/v1/python/test_pip.py diff --git a/tests/unit/plugins/v1/python/test_python_finder.py b/tests/legacy/unit/plugins/v1/python/test_python_finder.py similarity index 100% rename from tests/unit/plugins/v1/python/test_python_finder.py rename to tests/legacy/unit/plugins/v1/python/test_python_finder.py diff --git a/tests/unit/plugins/v1/python/test_sitecustomize.py b/tests/legacy/unit/plugins/v1/python/test_sitecustomize.py similarity index 100% rename from tests/unit/plugins/v1/python/test_sitecustomize.py rename to tests/legacy/unit/plugins/v1/python/test_sitecustomize.py diff --git a/tests/unit/project_loader/extensions/__init__.py b/tests/legacy/unit/plugins/v1/ros/__init__.py similarity index 100% rename from tests/unit/project_loader/extensions/__init__.py rename to tests/legacy/unit/plugins/v1/ros/__init__.py diff --git a/tests/unit/plugins/v1/ros/test_rosdep.py b/tests/legacy/unit/plugins/v1/ros/test_rosdep.py similarity index 99% rename from tests/unit/plugins/v1/ros/test_rosdep.py rename to tests/legacy/unit/plugins/v1/ros/test_rosdep.py index 7310e8353f..4fbf3b1589 100644 --- a/tests/unit/plugins/v1/ros/test_rosdep.py +++ b/tests/legacy/unit/plugins/v1/ros/test_rosdep.py @@ -23,7 +23,7 @@ import snapcraft_legacy from snapcraft_legacy.plugins.v1._ros import rosdep -from tests import unit +from tests.legacy import unit class RosdepTestCase(unit.TestCase): diff --git a/tests/unit/plugins/v1/ros/test_wstool.py b/tests/legacy/unit/plugins/v1/ros/test_wstool.py similarity index 99% rename from tests/unit/plugins/v1/ros/test_wstool.py rename to tests/legacy/unit/plugins/v1/ros/test_wstool.py index 99c4bb2277..9263000bdb 100644 --- a/tests/unit/plugins/v1/ros/test_wstool.py +++ b/tests/legacy/unit/plugins/v1/ros/test_wstool.py @@ -23,7 +23,7 @@ import snapcraft_legacy from snapcraft_legacy.plugins.v1._ros import wstool -from tests import unit +from tests.legacy import unit class WstoolTestCase(unit.TestCase): diff --git a/tests/unit/plugins/v1/test_ant.py b/tests/legacy/unit/plugins/v1/test_ant.py similarity index 99% rename from tests/unit/plugins/v1/test_ant.py rename to tests/legacy/unit/plugins/v1/test_ant.py index 5a1e4ab293..26848997c8 100644 --- a/tests/unit/plugins/v1/test_ant.py +++ b/tests/legacy/unit/plugins/v1/test_ant.py @@ -26,7 +26,7 @@ from snapcraft_legacy.internal.meta.snap import Snap from snapcraft_legacy.plugins.v1 import ant from snapcraft_legacy.project import Project -from tests import unit +from tests.legacy import unit from . import PluginsV1BaseTestCase diff --git a/tests/unit/plugins/v1/test_autotools.py b/tests/legacy/unit/plugins/v1/test_autotools.py similarity index 100% rename from tests/unit/plugins/v1/test_autotools.py rename to tests/legacy/unit/plugins/v1/test_autotools.py diff --git a/tests/unit/plugins/v1/test_base.py b/tests/legacy/unit/plugins/v1/test_base.py similarity index 99% rename from tests/unit/plugins/v1/test_base.py rename to tests/legacy/unit/plugins/v1/test_base.py index 630a84cc45..890d30670e 100644 --- a/tests/unit/plugins/v1/test_base.py +++ b/tests/legacy/unit/plugins/v1/test_base.py @@ -20,7 +20,7 @@ import snapcraft_legacy from snapcraft_legacy.internal import errors -from tests import unit +from tests.legacy import unit class TestBasePlugin(unit.TestCase): diff --git a/tests/unit/plugins/v1/test_catkin.py b/tests/legacy/unit/plugins/v1/test_catkin.py similarity index 99% rename from tests/unit/plugins/v1/test_catkin.py rename to tests/legacy/unit/plugins/v1/test_catkin.py index f01a19c35f..3409c763a8 100644 --- a/tests/unit/plugins/v1/test_catkin.py +++ b/tests/legacy/unit/plugins/v1/test_catkin.py @@ -40,7 +40,7 @@ from snapcraft_legacy import repo from snapcraft_legacy.internal import errors from snapcraft_legacy.plugins.v1 import _ros, catkin -from tests import unit +from tests.legacy import unit from . import PluginsV1BaseTestCase diff --git a/tests/unit/plugins/v1/test_catkin_tools.py b/tests/legacy/unit/plugins/v1/test_catkin_tools.py similarity index 100% rename from tests/unit/plugins/v1/test_catkin_tools.py rename to tests/legacy/unit/plugins/v1/test_catkin_tools.py diff --git a/tests/unit/plugins/v1/test_cmake.py b/tests/legacy/unit/plugins/v1/test_cmake.py similarity index 99% rename from tests/unit/plugins/v1/test_cmake.py rename to tests/legacy/unit/plugins/v1/test_cmake.py index 192f4fb3b1..b026a07e3c 100644 --- a/tests/unit/plugins/v1/test_cmake.py +++ b/tests/legacy/unit/plugins/v1/test_cmake.py @@ -21,7 +21,7 @@ from snapcraft_legacy.internal import errors from snapcraft_legacy.plugins.v1 import cmake -from tests import fixture_setup, unit +from tests.legacy import fixture_setup, unit from . import PluginsV1BaseTestCase diff --git a/tests/unit/plugins/v1/test_colcon.py b/tests/legacy/unit/plugins/v1/test_colcon.py similarity index 99% rename from tests/unit/plugins/v1/test_colcon.py rename to tests/legacy/unit/plugins/v1/test_colcon.py index fec0540512..9ef91f460f 100644 --- a/tests/unit/plugins/v1/test_colcon.py +++ b/tests/legacy/unit/plugins/v1/test_colcon.py @@ -27,7 +27,7 @@ from snapcraft_legacy import repo from snapcraft_legacy.internal import errors from snapcraft_legacy.plugins.v1 import _ros, colcon -from tests import unit +from tests.legacy import unit from . import PluginsV1BaseTestCase diff --git a/tests/unit/plugins/v1/test_conda.py b/tests/legacy/unit/plugins/v1/test_conda.py similarity index 99% rename from tests/unit/plugins/v1/test_conda.py rename to tests/legacy/unit/plugins/v1/test_conda.py index aabbd0be3c..870ecb2c36 100644 --- a/tests/unit/plugins/v1/test_conda.py +++ b/tests/legacy/unit/plugins/v1/test_conda.py @@ -23,7 +23,7 @@ from snapcraft_legacy.internal import errors from snapcraft_legacy.plugins.v1 import conda -from tests import unit +from tests.legacy import unit from . import PluginsV1BaseTestCase diff --git a/tests/unit/plugins/v1/test_crystal.py b/tests/legacy/unit/plugins/v1/test_crystal.py similarity index 99% rename from tests/unit/plugins/v1/test_crystal.py rename to tests/legacy/unit/plugins/v1/test_crystal.py index 36ffab746a..f60f188e97 100644 --- a/tests/unit/plugins/v1/test_crystal.py +++ b/tests/legacy/unit/plugins/v1/test_crystal.py @@ -22,7 +22,7 @@ from snapcraft_legacy.internal import errors from snapcraft_legacy.plugins.v1 import crystal -from tests import unit +from tests.legacy import unit from . import PluginsV1BaseTestCase diff --git a/tests/unit/plugins/v1/test_dotnet.py b/tests/legacy/unit/plugins/v1/test_dotnet.py similarity index 99% rename from tests/unit/plugins/v1/test_dotnet.py rename to tests/legacy/unit/plugins/v1/test_dotnet.py index 256e476526..f60407c775 100644 --- a/tests/unit/plugins/v1/test_dotnet.py +++ b/tests/legacy/unit/plugins/v1/test_dotnet.py @@ -25,7 +25,7 @@ from snapcraft_legacy import file_utils from snapcraft_legacy.internal import sources from snapcraft_legacy.plugins.v1 import dotnet -from tests import unit +from tests.legacy import unit from . import PluginsV1BaseTestCase diff --git a/tests/unit/plugins/v1/test_dump.py b/tests/legacy/unit/plugins/v1/test_dump.py similarity index 99% rename from tests/unit/plugins/v1/test_dump.py rename to tests/legacy/unit/plugins/v1/test_dump.py index b70fd05198..4b9d8fbd28 100644 --- a/tests/unit/plugins/v1/test_dump.py +++ b/tests/legacy/unit/plugins/v1/test_dump.py @@ -20,7 +20,7 @@ import snapcraft_legacy from snapcraft_legacy.plugins.v1.dump import DumpInvalidSymlinkError, DumpPlugin -from tests import unit +from tests.legacy import unit class DumpPluginTestCase(unit.TestCase): diff --git a/tests/unit/plugins/v1/test_flutter.py b/tests/legacy/unit/plugins/v1/test_flutter.py similarity index 100% rename from tests/unit/plugins/v1/test_flutter.py rename to tests/legacy/unit/plugins/v1/test_flutter.py diff --git a/tests/unit/plugins/v1/test_go.py b/tests/legacy/unit/plugins/v1/test_go.py similarity index 99% rename from tests/unit/plugins/v1/test_go.py rename to tests/legacy/unit/plugins/v1/test_go.py index 7c91ed2483..0786f654a4 100644 --- a/tests/unit/plugins/v1/test_go.py +++ b/tests/legacy/unit/plugins/v1/test_go.py @@ -26,7 +26,7 @@ from snapcraft_legacy.internal import errors, meta from snapcraft_legacy.plugins.v1 import go from snapcraft_legacy.project import Project -from tests import fixture_setup, unit +from tests.legacy import fixture_setup, unit from . import PluginsV1BaseTestCase diff --git a/tests/unit/plugins/v1/test_godeps.py b/tests/legacy/unit/plugins/v1/test_godeps.py similarity index 99% rename from tests/unit/plugins/v1/test_godeps.py rename to tests/legacy/unit/plugins/v1/test_godeps.py index 65af1f2136..a443f0bf4a 100644 --- a/tests/unit/plugins/v1/test_godeps.py +++ b/tests/legacy/unit/plugins/v1/test_godeps.py @@ -21,7 +21,7 @@ from snapcraft_legacy.internal import errors from snapcraft_legacy.plugins.v1 import godeps -from tests import unit +from tests.legacy import unit from . import PluginsV1BaseTestCase diff --git a/tests/unit/plugins/v1/test_gradle.py b/tests/legacy/unit/plugins/v1/test_gradle.py similarity index 100% rename from tests/unit/plugins/v1/test_gradle.py rename to tests/legacy/unit/plugins/v1/test_gradle.py diff --git a/tests/unit/plugins/v1/test_kbuild.py b/tests/legacy/unit/plugins/v1/test_kbuild.py similarity index 100% rename from tests/unit/plugins/v1/test_kbuild.py rename to tests/legacy/unit/plugins/v1/test_kbuild.py diff --git a/tests/unit/plugins/v1/test_kernel.py b/tests/legacy/unit/plugins/v1/test_kernel.py similarity index 100% rename from tests/unit/plugins/v1/test_kernel.py rename to tests/legacy/unit/plugins/v1/test_kernel.py diff --git a/tests/unit/plugins/v1/test_make.py b/tests/legacy/unit/plugins/v1/test_make.py similarity index 100% rename from tests/unit/plugins/v1/test_make.py rename to tests/legacy/unit/plugins/v1/test_make.py diff --git a/tests/unit/plugins/v1/test_maven.py b/tests/legacy/unit/plugins/v1/test_maven.py similarity index 99% rename from tests/unit/plugins/v1/test_maven.py rename to tests/legacy/unit/plugins/v1/test_maven.py index f6efd00a6c..f126a15caf 100644 --- a/tests/unit/plugins/v1/test_maven.py +++ b/tests/legacy/unit/plugins/v1/test_maven.py @@ -30,7 +30,7 @@ from snapcraft_legacy.internal.meta.snap import Snap from snapcraft_legacy.plugins.v1 import maven from snapcraft_legacy.project import Project -from tests import unit +from tests.legacy import unit from . import PluginsV1BaseTestCase diff --git a/tests/unit/plugins/v1/test_meson.py b/tests/legacy/unit/plugins/v1/test_meson.py similarity index 99% rename from tests/unit/plugins/v1/test_meson.py rename to tests/legacy/unit/plugins/v1/test_meson.py index ed61c7d401..7ddd9742fc 100644 --- a/tests/unit/plugins/v1/test_meson.py +++ b/tests/legacy/unit/plugins/v1/test_meson.py @@ -22,7 +22,7 @@ from snapcraft_legacy.internal import errors from snapcraft_legacy.plugins.v1 import meson -from tests import unit +from tests.legacy import unit from . import PluginsV1BaseTestCase diff --git a/tests/unit/plugins/v1/test_nil.py b/tests/legacy/unit/plugins/v1/test_nil.py similarity index 97% rename from tests/unit/plugins/v1/test_nil.py rename to tests/legacy/unit/plugins/v1/test_nil.py index e58c656198..1633d12e6b 100644 --- a/tests/unit/plugins/v1/test_nil.py +++ b/tests/legacy/unit/plugins/v1/test_nil.py @@ -17,7 +17,7 @@ from testtools.matchers import Equals from snapcraft_legacy.plugins.v1.nil import NilPlugin -from tests import unit +from tests.legacy import unit class TestNilPlugin(unit.TestCase): diff --git a/tests/unit/plugins/v1/test_nodejs.py b/tests/legacy/unit/plugins/v1/test_nodejs.py similarity index 99% rename from tests/unit/plugins/v1/test_nodejs.py rename to tests/legacy/unit/plugins/v1/test_nodejs.py index 6a605b5ece..d14fb0fcde 100644 --- a/tests/unit/plugins/v1/test_nodejs.py +++ b/tests/legacy/unit/plugins/v1/test_nodejs.py @@ -27,7 +27,7 @@ from snapcraft_legacy.internal import errors from snapcraft_legacy.plugins.v1 import nodejs -from tests import fixture_setup, unit +from tests.legacy import fixture_setup, unit from . import PluginsV1BaseTestCase diff --git a/tests/unit/plugins/v1/test_plainbox_provider.py b/tests/legacy/unit/plugins/v1/test_plainbox_provider.py similarity index 99% rename from tests/unit/plugins/v1/test_plainbox_provider.py rename to tests/legacy/unit/plugins/v1/test_plainbox_provider.py index 81306db6f0..09309705b1 100644 --- a/tests/unit/plugins/v1/test_plainbox_provider.py +++ b/tests/legacy/unit/plugins/v1/test_plainbox_provider.py @@ -21,7 +21,7 @@ from snapcraft_legacy.internal import errors from snapcraft_legacy.plugins.v1 import plainbox_provider -from tests import fixture_setup, unit +from tests.legacy import fixture_setup, unit from . import PluginsV1BaseTestCase diff --git a/tests/unit/plugins/v1/test_python.py b/tests/legacy/unit/plugins/v1/test_python.py similarity index 99% rename from tests/unit/plugins/v1/test_python.py rename to tests/legacy/unit/plugins/v1/test_python.py index cf95d7a64c..df3ffd2663 100644 --- a/tests/unit/plugins/v1/test_python.py +++ b/tests/legacy/unit/plugins/v1/test_python.py @@ -23,7 +23,7 @@ from snapcraft_legacy.internal import errors from snapcraft_legacy.plugins.v1 import python -from tests import fixture_setup, unit +from tests.legacy import fixture_setup, unit from . import PluginsV1BaseTestCase diff --git a/tests/unit/plugins/v1/test_qmake.py b/tests/legacy/unit/plugins/v1/test_qmake.py similarity index 100% rename from tests/unit/plugins/v1/test_qmake.py rename to tests/legacy/unit/plugins/v1/test_qmake.py diff --git a/tests/unit/plugins/v1/test_ruby.py b/tests/legacy/unit/plugins/v1/test_ruby.py similarity index 100% rename from tests/unit/plugins/v1/test_ruby.py rename to tests/legacy/unit/plugins/v1/test_ruby.py diff --git a/tests/unit/plugins/v1/test_rust.py b/tests/legacy/unit/plugins/v1/test_rust.py similarity index 99% rename from tests/unit/plugins/v1/test_rust.py rename to tests/legacy/unit/plugins/v1/test_rust.py index 3e9da06475..5ecab1b939 100644 --- a/tests/unit/plugins/v1/test_rust.py +++ b/tests/legacy/unit/plugins/v1/test_rust.py @@ -28,7 +28,7 @@ import snapcraft_legacy from snapcraft_legacy.internal import errors, meta from snapcraft_legacy.plugins.v1 import rust -from tests import fixture_setup, unit +from tests.legacy import fixture_setup, unit from . import PluginsV1BaseTestCase diff --git a/tests/unit/plugins/v1/test_scons.py b/tests/legacy/unit/plugins/v1/test_scons.py similarity index 99% rename from tests/unit/plugins/v1/test_scons.py rename to tests/legacy/unit/plugins/v1/test_scons.py index add403075a..30a513e373 100644 --- a/tests/unit/plugins/v1/test_scons.py +++ b/tests/legacy/unit/plugins/v1/test_scons.py @@ -21,7 +21,7 @@ from snapcraft_legacy.internal import errors from snapcraft_legacy.plugins.v1 import scons -from tests import unit +from tests.legacy import unit from . import PluginsV1BaseTestCase diff --git a/tests/unit/plugins/v1/test_waf.py b/tests/legacy/unit/plugins/v1/test_waf.py similarity index 99% rename from tests/unit/plugins/v1/test_waf.py rename to tests/legacy/unit/plugins/v1/test_waf.py index ef6c295a51..15ac64b4e5 100644 --- a/tests/unit/plugins/v1/test_waf.py +++ b/tests/legacy/unit/plugins/v1/test_waf.py @@ -23,7 +23,7 @@ from snapcraft_legacy.internal import errors, meta from snapcraft_legacy.plugins.v1 import waf from snapcraft_legacy.project import Project -from tests import unit +from tests.legacy import unit from . import PluginsV1BaseTestCase diff --git a/tests/unit/plugins/v2/test_autotools.py b/tests/legacy/unit/plugins/v2/test_autotools.py similarity index 100% rename from tests/unit/plugins/v2/test_autotools.py rename to tests/legacy/unit/plugins/v2/test_autotools.py diff --git a/tests/unit/plugins/v2/test_catkin.py b/tests/legacy/unit/plugins/v2/test_catkin.py similarity index 100% rename from tests/unit/plugins/v2/test_catkin.py rename to tests/legacy/unit/plugins/v2/test_catkin.py diff --git a/tests/unit/plugins/v2/test_catkin_tools.py b/tests/legacy/unit/plugins/v2/test_catkin_tools.py similarity index 100% rename from tests/unit/plugins/v2/test_catkin_tools.py rename to tests/legacy/unit/plugins/v2/test_catkin_tools.py diff --git a/tests/unit/plugins/v2/test_cmake.py b/tests/legacy/unit/plugins/v2/test_cmake.py similarity index 100% rename from tests/unit/plugins/v2/test_cmake.py rename to tests/legacy/unit/plugins/v2/test_cmake.py diff --git a/tests/unit/plugins/v2/test_colcon.py b/tests/legacy/unit/plugins/v2/test_colcon.py similarity index 100% rename from tests/unit/plugins/v2/test_colcon.py rename to tests/legacy/unit/plugins/v2/test_colcon.py diff --git a/tests/unit/plugins/v2/test_conda.py b/tests/legacy/unit/plugins/v2/test_conda.py similarity index 100% rename from tests/unit/plugins/v2/test_conda.py rename to tests/legacy/unit/plugins/v2/test_conda.py diff --git a/tests/unit/plugins/v2/test_dump.py b/tests/legacy/unit/plugins/v2/test_dump.py similarity index 100% rename from tests/unit/plugins/v2/test_dump.py rename to tests/legacy/unit/plugins/v2/test_dump.py diff --git a/tests/unit/plugins/v2/test_go.py b/tests/legacy/unit/plugins/v2/test_go.py similarity index 100% rename from tests/unit/plugins/v2/test_go.py rename to tests/legacy/unit/plugins/v2/test_go.py diff --git a/tests/unit/plugins/v2/test_make.py b/tests/legacy/unit/plugins/v2/test_make.py similarity index 100% rename from tests/unit/plugins/v2/test_make.py rename to tests/legacy/unit/plugins/v2/test_make.py diff --git a/tests/unit/plugins/v2/test_meson.py b/tests/legacy/unit/plugins/v2/test_meson.py similarity index 100% rename from tests/unit/plugins/v2/test_meson.py rename to tests/legacy/unit/plugins/v2/test_meson.py diff --git a/tests/unit/plugins/v2/test_nil.py b/tests/legacy/unit/plugins/v2/test_nil.py similarity index 100% rename from tests/unit/plugins/v2/test_nil.py rename to tests/legacy/unit/plugins/v2/test_nil.py diff --git a/tests/unit/plugins/v2/test_npm.py b/tests/legacy/unit/plugins/v2/test_npm.py similarity index 100% rename from tests/unit/plugins/v2/test_npm.py rename to tests/legacy/unit/plugins/v2/test_npm.py diff --git a/tests/unit/plugins/v2/test_python.py b/tests/legacy/unit/plugins/v2/test_python.py similarity index 100% rename from tests/unit/plugins/v2/test_python.py rename to tests/legacy/unit/plugins/v2/test_python.py diff --git a/tests/unit/plugins/v2/test_qmake.py b/tests/legacy/unit/plugins/v2/test_qmake.py similarity index 100% rename from tests/unit/plugins/v2/test_qmake.py rename to tests/legacy/unit/plugins/v2/test_qmake.py diff --git a/tests/unit/plugins/v2/test_rust.py b/tests/legacy/unit/plugins/v2/test_rust.py similarity index 100% rename from tests/unit/plugins/v2/test_rust.py rename to tests/legacy/unit/plugins/v2/test_rust.py diff --git a/tests/unit/project/__init__.py b/tests/legacy/unit/project/__init__.py similarity index 98% rename from tests/unit/project/__init__.py rename to tests/legacy/unit/project/__init__.py index 9a9a50efc3..0381a942b5 100644 --- a/tests/unit/project/__init__.py +++ b/tests/legacy/unit/project/__init__.py @@ -15,7 +15,7 @@ # along with this program. If not, see . import snapcraft_legacy.yaml_utils.errors from snapcraft_legacy.project import Project as _Project -from tests import unit +from tests.legacy import unit class ProjectBaseTest(unit.TestCase): diff --git a/tests/unit/project/test_errors.py b/tests/legacy/unit/project/test_errors.py similarity index 100% rename from tests/unit/project/test_errors.py rename to tests/legacy/unit/project/test_errors.py diff --git a/tests/unit/project/test_get_snapcraft.py b/tests/legacy/unit/project/test_get_snapcraft.py similarity index 100% rename from tests/unit/project/test_get_snapcraft.py rename to tests/legacy/unit/project/test_get_snapcraft.py diff --git a/tests/unit/project/test_project.py b/tests/legacy/unit/project/test_project.py similarity index 100% rename from tests/unit/project/test_project.py rename to tests/legacy/unit/project/test_project.py diff --git a/tests/unit/project/test_project_info.py b/tests/legacy/unit/project/test_project_info.py similarity index 99% rename from tests/unit/project/test_project_info.py rename to tests/legacy/unit/project/test_project_info.py index b34249d53d..29d279c6ee 100644 --- a/tests/unit/project/test_project_info.py +++ b/tests/legacy/unit/project/test_project_info.py @@ -21,7 +21,7 @@ import snapcraft_legacy.yaml_utils.errors from snapcraft_legacy.project._project_info import ProjectInfo -from tests import unit +from tests.legacy import unit class ProjectInfoTest(unit.TestCase): diff --git a/tests/unit/project/test_sanity_checks.py b/tests/legacy/unit/project/test_sanity_checks.py similarity index 100% rename from tests/unit/project/test_sanity_checks.py rename to tests/legacy/unit/project/test_sanity_checks.py diff --git a/tests/unit/project/test_schema.py b/tests/legacy/unit/project/test_schema.py similarity index 100% rename from tests/unit/project/test_schema.py rename to tests/legacy/unit/project/test_schema.py diff --git a/tests/unit/project_loader/__init__.py b/tests/legacy/unit/project_loader/__init__.py similarity index 98% rename from tests/unit/project_loader/__init__.py rename to tests/legacy/unit/project_loader/__init__.py index b2c4dd537d..7ddf3769b9 100644 --- a/tests/unit/project_loader/__init__.py +++ b/tests/legacy/unit/project_loader/__init__.py @@ -18,7 +18,7 @@ from snapcraft_legacy.internal import project_loader from snapcraft_legacy.project import Project as _Project -from tests import unit +from tests.legacy import unit class ProjectLoaderBaseTest(unit.TestCase): diff --git a/tests/unit/project_loader/grammar/__init__.py b/tests/legacy/unit/project_loader/extensions/__init__.py similarity index 100% rename from tests/unit/project_loader/grammar/__init__.py rename to tests/legacy/unit/project_loader/extensions/__init__.py diff --git a/tests/unit/project_loader/extensions/test_extensions.py b/tests/legacy/unit/project_loader/extensions/test_extensions.py similarity index 100% rename from tests/unit/project_loader/extensions/test_extensions.py rename to tests/legacy/unit/project_loader/extensions/test_extensions.py diff --git a/tests/unit/project_loader/extensions/test_flutter.py b/tests/legacy/unit/project_loader/extensions/test_flutter.py similarity index 100% rename from tests/unit/project_loader/extensions/test_flutter.py rename to tests/legacy/unit/project_loader/extensions/test_flutter.py diff --git a/tests/unit/project_loader/extensions/test_gnome_3_28.py b/tests/legacy/unit/project_loader/extensions/test_gnome_3_28.py similarity index 100% rename from tests/unit/project_loader/extensions/test_gnome_3_28.py rename to tests/legacy/unit/project_loader/extensions/test_gnome_3_28.py diff --git a/tests/unit/project_loader/extensions/test_gnome_3_34.py b/tests/legacy/unit/project_loader/extensions/test_gnome_3_34.py similarity index 99% rename from tests/unit/project_loader/extensions/test_gnome_3_34.py rename to tests/legacy/unit/project_loader/extensions/test_gnome_3_34.py index 069595add9..03f89ad9de 100644 --- a/tests/unit/project_loader/extensions/test_gnome_3_34.py +++ b/tests/legacy/unit/project_loader/extensions/test_gnome_3_34.py @@ -19,7 +19,7 @@ from snapcraft_legacy.internal.project_loader._extensions.gnome_3_34 import ( ExtensionImpl, ) -from tests.unit.commands import CommandBaseTestCase +from tests.legacy.unit.commands import CommandBaseTestCase from .. import ProjectLoaderBaseTest diff --git a/tests/unit/project_loader/extensions/test_gnome_3_38.py b/tests/legacy/unit/project_loader/extensions/test_gnome_3_38.py similarity index 99% rename from tests/unit/project_loader/extensions/test_gnome_3_38.py rename to tests/legacy/unit/project_loader/extensions/test_gnome_3_38.py index b32d3cacf1..d2a4b9a18e 100644 --- a/tests/unit/project_loader/extensions/test_gnome_3_38.py +++ b/tests/legacy/unit/project_loader/extensions/test_gnome_3_38.py @@ -19,7 +19,7 @@ from snapcraft_legacy.internal.project_loader._extensions.gnome_3_38 import ( ExtensionImpl, ) -from tests.unit.commands import CommandBaseTestCase +from tests.legacy.unit.commands import CommandBaseTestCase from .. import ProjectLoaderBaseTest diff --git a/tests/unit/project_loader/extensions/test_kde_neon.py b/tests/legacy/unit/project_loader/extensions/test_kde_neon.py similarity index 100% rename from tests/unit/project_loader/extensions/test_kde_neon.py rename to tests/legacy/unit/project_loader/extensions/test_kde_neon.py diff --git a/tests/unit/project_loader/extensions/test_ros1_noetic.py b/tests/legacy/unit/project_loader/extensions/test_ros1_noetic.py similarity index 100% rename from tests/unit/project_loader/extensions/test_ros1_noetic.py rename to tests/legacy/unit/project_loader/extensions/test_ros1_noetic.py diff --git a/tests/unit/project_loader/extensions/test_ros2_foxy.py b/tests/legacy/unit/project_loader/extensions/test_ros2_foxy.py similarity index 100% rename from tests/unit/project_loader/extensions/test_ros2_foxy.py rename to tests/legacy/unit/project_loader/extensions/test_ros2_foxy.py diff --git a/tests/unit/project_loader/extensions/test_utils.py b/tests/legacy/unit/project_loader/extensions/test_utils.py similarity index 99% rename from tests/unit/project_loader/extensions/test_utils.py rename to tests/legacy/unit/project_loader/extensions/test_utils.py index 3aeff46478..cfd2d51bc4 100644 --- a/tests/unit/project_loader/extensions/test_utils.py +++ b/tests/legacy/unit/project_loader/extensions/test_utils.py @@ -22,7 +22,7 @@ import snapcraft_legacy.yaml_utils.errors from snapcraft_legacy.internal.project_loader import errors from snapcraft_legacy.internal.project_loader._extensions._extension import Extension -from tests import fixture_setup +from tests.legacy import fixture_setup from .. import ProjectLoaderBaseTest diff --git a/tests/unit/project_loader/grammar_processing/__init__.py b/tests/legacy/unit/project_loader/grammar/__init__.py similarity index 100% rename from tests/unit/project_loader/grammar_processing/__init__.py rename to tests/legacy/unit/project_loader/grammar/__init__.py diff --git a/tests/unit/project_loader/grammar/test_compound_statement.py b/tests/legacy/unit/project_loader/grammar/test_compound_statement.py similarity index 100% rename from tests/unit/project_loader/grammar/test_compound_statement.py rename to tests/legacy/unit/project_loader/grammar/test_compound_statement.py diff --git a/tests/unit/project_loader/grammar/test_on_statement.py b/tests/legacy/unit/project_loader/grammar/test_on_statement.py similarity index 100% rename from tests/unit/project_loader/grammar/test_on_statement.py rename to tests/legacy/unit/project_loader/grammar/test_on_statement.py diff --git a/tests/unit/project_loader/grammar/test_processor.py b/tests/legacy/unit/project_loader/grammar/test_processor.py similarity index 100% rename from tests/unit/project_loader/grammar/test_processor.py rename to tests/legacy/unit/project_loader/grammar/test_processor.py diff --git a/tests/unit/project_loader/grammar/test_to_statement.py b/tests/legacy/unit/project_loader/grammar/test_to_statement.py similarity index 100% rename from tests/unit/project_loader/grammar/test_to_statement.py rename to tests/legacy/unit/project_loader/grammar/test_to_statement.py diff --git a/tests/unit/project_loader/grammar/test_try_statement.py b/tests/legacy/unit/project_loader/grammar/test_try_statement.py similarity index 100% rename from tests/unit/project_loader/grammar/test_try_statement.py rename to tests/legacy/unit/project_loader/grammar/test_try_statement.py diff --git a/tests/unit/project_loader/inspection/__init__.py b/tests/legacy/unit/project_loader/grammar_processing/__init__.py similarity index 100% rename from tests/unit/project_loader/inspection/__init__.py rename to tests/legacy/unit/project_loader/grammar_processing/__init__.py diff --git a/tests/unit/project_loader/grammar_processing/test_global_grammar_processor.py b/tests/legacy/unit/project_loader/grammar_processing/test_global_grammar_processor.py similarity index 100% rename from tests/unit/project_loader/grammar_processing/test_global_grammar_processor.py rename to tests/legacy/unit/project_loader/grammar_processing/test_global_grammar_processor.py diff --git a/tests/unit/project_loader/grammar_processing/test_part_grammar_processor.py b/tests/legacy/unit/project_loader/grammar_processing/test_part_grammar_processor.py similarity index 100% rename from tests/unit/project_loader/grammar_processing/test_part_grammar_processor.py rename to tests/legacy/unit/project_loader/grammar_processing/test_part_grammar_processor.py diff --git a/tests/unit/review_tools/__init__.py b/tests/legacy/unit/project_loader/inspection/__init__.py similarity index 100% rename from tests/unit/review_tools/__init__.py rename to tests/legacy/unit/project_loader/inspection/__init__.py diff --git a/tests/unit/project_loader/inspection/test_latest_step.py b/tests/legacy/unit/project_loader/inspection/test_latest_step.py similarity index 98% rename from tests/unit/project_loader/inspection/test_latest_step.py rename to tests/legacy/unit/project_loader/inspection/test_latest_step.py index 4e135bc7fb..a174987bcf 100644 --- a/tests/unit/project_loader/inspection/test_latest_step.py +++ b/tests/legacy/unit/project_loader/inspection/test_latest_step.py @@ -21,7 +21,7 @@ from snapcraft_legacy import project from snapcraft_legacy.internal import steps from snapcraft_legacy.internal.project_loader import inspection -from tests import unit +from tests.legacy import unit class LatestStepTest(unit.TestCase): diff --git a/tests/unit/project_loader/inspection/test_lifecycle_status.py b/tests/legacy/unit/project_loader/inspection/test_lifecycle_status.py similarity index 100% rename from tests/unit/project_loader/inspection/test_lifecycle_status.py rename to tests/legacy/unit/project_loader/inspection/test_lifecycle_status.py diff --git a/tests/unit/project_loader/inspection/test_provides.py b/tests/legacy/unit/project_loader/inspection/test_provides.py similarity index 99% rename from tests/unit/project_loader/inspection/test_provides.py rename to tests/legacy/unit/project_loader/inspection/test_provides.py index b9a9b62213..546497ea45 100644 --- a/tests/unit/project_loader/inspection/test_provides.py +++ b/tests/legacy/unit/project_loader/inspection/test_provides.py @@ -20,7 +20,7 @@ from snapcraft_legacy import project from snapcraft_legacy.internal.project_loader import inspection -from tests import unit +from tests.legacy import unit class ProvidesTest(unit.TestCase): diff --git a/tests/unit/project_loader/test_build_packages.py b/tests/legacy/unit/project_loader/test_build_packages.py similarity index 100% rename from tests/unit/project_loader/test_build_packages.py rename to tests/legacy/unit/project_loader/test_build_packages.py diff --git a/tests/unit/project_loader/test_build_snaps.py b/tests/legacy/unit/project_loader/test_build_snaps.py similarity index 100% rename from tests/unit/project_loader/test_build_snaps.py rename to tests/legacy/unit/project_loader/test_build_snaps.py diff --git a/tests/unit/project_loader/test_config.py b/tests/legacy/unit/project_loader/test_config.py similarity index 99% rename from tests/unit/project_loader/test_config.py rename to tests/legacy/unit/project_loader/test_config.py index 2f21b0d3ed..5b32509c3f 100644 --- a/tests/unit/project_loader/test_config.py +++ b/tests/legacy/unit/project_loader/test_config.py @@ -20,7 +20,7 @@ import snapcraft_legacy.internal.project_loader._config as _config from snapcraft_legacy.internal.project_loader import errors -from tests import unit +from tests.legacy import unit from . import LoadPartBaseTest, ProjectLoaderBaseTest diff --git a/tests/unit/project_loader/test_environment.py b/tests/legacy/unit/project_loader/test_environment.py similarity index 99% rename from tests/unit/project_loader/test_environment.py rename to tests/legacy/unit/project_loader/test_environment.py index 162da861f8..e2aefbff83 100644 --- a/tests/unit/project_loader/test_environment.py +++ b/tests/legacy/unit/project_loader/test_environment.py @@ -26,7 +26,7 @@ import snapcraft_legacy from snapcraft_legacy.internal import common -from tests.fixture_setup.os_release import FakeOsRelease +from tests.legacy.fixture_setup.os_release import FakeOsRelease from . import ProjectLoaderBaseTest diff --git a/tests/unit/project_loader/test_errors.py b/tests/legacy/unit/project_loader/test_errors.py similarity index 100% rename from tests/unit/project_loader/test_errors.py rename to tests/legacy/unit/project_loader/test_errors.py diff --git a/tests/unit/project_loader/test_parts.py b/tests/legacy/unit/project_loader/test_parts.py similarity index 99% rename from tests/unit/project_loader/test_parts.py rename to tests/legacy/unit/project_loader/test_parts.py index 56cc65db56..e2e9aaf06f 100644 --- a/tests/unit/project_loader/test_parts.py +++ b/tests/legacy/unit/project_loader/test_parts.py @@ -21,7 +21,7 @@ from snapcraft_legacy.internal import project_loader from snapcraft_legacy.project import Project -from tests import fixture_setup +from tests.legacy import fixture_setup from . import LoadPartBaseTest, ProjectLoaderBaseTest diff --git a/tests/unit/project_loader/test_replace_attr.py b/tests/legacy/unit/project_loader/test_replace_attr.py similarity index 99% rename from tests/unit/project_loader/test_replace_attr.py rename to tests/legacy/unit/project_loader/test_replace_attr.py index 701f4cf650..e3af832387 100644 --- a/tests/unit/project_loader/test_replace_attr.py +++ b/tests/legacy/unit/project_loader/test_replace_attr.py @@ -17,7 +17,7 @@ from testtools.matchers import Equals from snapcraft_legacy.internal import project_loader -from tests import unit +from tests.legacy import unit class VariableReplacementsTest(unit.TestCase): diff --git a/tests/unit/project_loader/test_schema.py b/tests/legacy/unit/project_loader/test_schema.py similarity index 99% rename from tests/unit/project_loader/test_schema.py rename to tests/legacy/unit/project_loader/test_schema.py index 2ae0c16db9..1030015d7a 100644 --- a/tests/unit/project_loader/test_schema.py +++ b/tests/legacy/unit/project_loader/test_schema.py @@ -27,7 +27,7 @@ from snapcraft_legacy import project from snapcraft_legacy.internal.errors import PluginError from snapcraft_legacy.internal.project_loader import errors, load_config -from tests import fixture_setup +from tests.legacy import fixture_setup from . import ProjectLoaderBaseTest diff --git a/tests/unit/remote_build/__init__.py b/tests/legacy/unit/remote_build/__init__.py similarity index 100% rename from tests/unit/remote_build/__init__.py rename to tests/legacy/unit/remote_build/__init__.py diff --git a/tests/unit/remote_build/test_errors.py b/tests/legacy/unit/remote_build/test_errors.py similarity index 100% rename from tests/unit/remote_build/test_errors.py rename to tests/legacy/unit/remote_build/test_errors.py diff --git a/tests/unit/remote_build/test_info_file.py b/tests/legacy/unit/remote_build/test_info_file.py similarity index 98% rename from tests/unit/remote_build/test_info_file.py rename to tests/legacy/unit/remote_build/test_info_file.py index 2409a785a3..d442955e8d 100644 --- a/tests/unit/remote_build/test_info_file.py +++ b/tests/legacy/unit/remote_build/test_info_file.py @@ -20,7 +20,7 @@ from testtools.matchers import Equals, FileExists from snapcraft_legacy.internal.remote_build import InfoFile -from tests import unit +from tests.legacy import unit class TestInfoFile(unit.TestCase): diff --git a/tests/unit/remote_build/test_launchpad.py b/tests/legacy/unit/remote_build/test_launchpad.py similarity index 98% rename from tests/unit/remote_build/test_launchpad.py rename to tests/legacy/unit/remote_build/test_launchpad.py index c6646d041d..7c33c0d994 100644 --- a/tests/unit/remote_build/test_launchpad.py +++ b/tests/legacy/unit/remote_build/test_launchpad.py @@ -24,7 +24,7 @@ from snapcraft_legacy.internal.remote_build import LaunchpadClient, errors from snapcraft_legacy.internal.sources._git import Git from snapcraft_legacy.internal.sources.errors import SnapcraftPullError -from tests import unit +from tests.legacy import unit from . import TestDir @@ -289,7 +289,7 @@ def test_start_build(self): self.lpc.start_build() @mock.patch( - "tests.unit.remote_build.test_launchpad.SnapImpl.requestBuilds", + "tests.legacy.unit.remote_build.test_launchpad.SnapImpl.requestBuilds", return_value=SnapBuildReqImpl( status="Failed", error_message="snapcraft.yaml not found..." ), @@ -299,7 +299,7 @@ def test_start_build_error(self, mock_rb): self.assertThat(str(raised), Contains("snapcraft.yaml not found...")) @mock.patch( - "tests.unit.remote_build.test_launchpad.SnapImpl.requestBuilds", + "tests.legacy.unit.remote_build.test_launchpad.SnapImpl.requestBuilds", return_value=SnapBuildReqImpl(status="Pending", error_message=""), ) @mock.patch("time.time", return_value=500) @@ -358,7 +358,8 @@ def test_monitor_build(self, mock_download_file): @mock.patch("snapcraft_legacy.internal.remote_build.LaunchpadClient._download_file") @mock.patch( - "tests.unit.remote_build.test_launchpad.BuildImpl.getFileUrls", return_value=[] + "tests.legacy.unit.remote_build.test_launchpad.BuildImpl.getFileUrls", + return_value=[], ) @mock.patch("logging.Logger.error") def test_monitor_build_error(self, mock_log, mock_urls, mock_download_file): diff --git a/tests/unit/remote_build/test_worktree.py b/tests/legacy/unit/remote_build/test_worktree.py similarity index 99% rename from tests/unit/remote_build/test_worktree.py rename to tests/legacy/unit/remote_build/test_worktree.py index fb38e62a5f..634aa62e10 100644 --- a/tests/unit/remote_build/test_worktree.py +++ b/tests/legacy/unit/remote_build/test_worktree.py @@ -24,7 +24,7 @@ from snapcraft_legacy import yaml_utils from snapcraft_legacy.internal.remote_build import WorkTree from snapcraft_legacy.project import Project -from tests import fixture_setup, unit +from tests.legacy import fixture_setup, unit from . import TestDir diff --git a/tests/unit/repo/__init__.py b/tests/legacy/unit/repo/__init__.py similarity index 97% rename from tests/unit/repo/__init__.py rename to tests/legacy/unit/repo/__init__.py index 5148651223..2777051ccb 100644 --- a/tests/unit/repo/__init__.py +++ b/tests/legacy/unit/repo/__init__.py @@ -19,7 +19,7 @@ import fixtures -from tests import unit +from tests.legacy import unit class RepoBaseTestCase(unit.TestCase): diff --git a/tests/unit/repo/test_apt_cache.py b/tests/legacy/unit/repo/test_apt_cache.py similarity index 99% rename from tests/unit/repo/test_apt_cache.py rename to tests/legacy/unit/repo/test_apt_cache.py index ef077b6153..676154fa02 100644 --- a/tests/unit/repo/test_apt_cache.py +++ b/tests/legacy/unit/repo/test_apt_cache.py @@ -23,7 +23,7 @@ from testtools.matchers import Equals from snapcraft_legacy.internal.repo.apt_cache import AptCache -from tests import unit +from tests.legacy import unit class TestAptStageCache(unit.TestCase): diff --git a/tests/unit/repo/test_apt_key_manager.py b/tests/legacy/unit/repo/test_apt_key_manager.py similarity index 100% rename from tests/unit/repo/test_apt_key_manager.py rename to tests/legacy/unit/repo/test_apt_key_manager.py diff --git a/tests/unit/repo/test_apt_ppa.py b/tests/legacy/unit/repo/test_apt_ppa.py similarity index 100% rename from tests/unit/repo/test_apt_ppa.py rename to tests/legacy/unit/repo/test_apt_ppa.py diff --git a/tests/unit/repo/test_apt_sources_manager.py b/tests/legacy/unit/repo/test_apt_sources_manager.py similarity index 100% rename from tests/unit/repo/test_apt_sources_manager.py rename to tests/legacy/unit/repo/test_apt_sources_manager.py diff --git a/tests/unit/repo/test_base.py b/tests/legacy/unit/repo/test_base.py similarity index 99% rename from tests/unit/repo/test_base.py rename to tests/legacy/unit/repo/test_base.py index 4e48718008..3aaa820990 100644 --- a/tests/unit/repo/test_base.py +++ b/tests/legacy/unit/repo/test_base.py @@ -21,7 +21,7 @@ from testtools.matchers import Equals, FileContains, FileExists, Not from snapcraft_legacy.internal.repo._base import BaseRepo, get_pkg_name_parts -from tests import unit +from tests.legacy import unit from . import RepoBaseTestCase diff --git a/tests/unit/repo/test_deb.py b/tests/legacy/unit/repo/test_deb.py similarity index 99% rename from tests/unit/repo/test_deb.py rename to tests/legacy/unit/repo/test_deb.py index 3e64a92af0..c75aeaad00 100644 --- a/tests/unit/repo/test_deb.py +++ b/tests/legacy/unit/repo/test_deb.py @@ -29,7 +29,7 @@ from snapcraft_legacy.internal import repo from snapcraft_legacy.internal.repo import errors from snapcraft_legacy.internal.repo.deb_package import DebPackage -from tests import unit +from tests.legacy import unit @pytest.fixture(autouse=True) diff --git a/tests/unit/repo/test_deb_package.py b/tests/legacy/unit/repo/test_deb_package.py similarity index 100% rename from tests/unit/repo/test_deb_package.py rename to tests/legacy/unit/repo/test_deb_package.py diff --git a/tests/unit/repo/test_errors.py b/tests/legacy/unit/repo/test_errors.py similarity index 100% rename from tests/unit/repo/test_errors.py rename to tests/legacy/unit/repo/test_errors.py diff --git a/tests/unit/repo/test_snaps.py b/tests/legacy/unit/repo/test_snaps.py similarity index 99% rename from tests/unit/repo/test_snaps.py rename to tests/legacy/unit/repo/test_snaps.py index 7eff2b58c7..46b2ab1c80 100644 --- a/tests/unit/repo/test_snaps.py +++ b/tests/legacy/unit/repo/test_snaps.py @@ -20,7 +20,7 @@ from testtools.matchers import Equals, FileContains, FileExists, Is from snapcraft_legacy.internal.repo import errors, snaps -from tests import unit +from tests.legacy import unit class SnapPackageCurrentChannelTest(unit.TestCase): diff --git a/tests/unit/repo/test_ua_manager.py b/tests/legacy/unit/repo/test_ua_manager.py similarity index 100% rename from tests/unit/repo/test_ua_manager.py rename to tests/legacy/unit/repo/test_ua_manager.py diff --git a/tests/unit/states/__init__.py b/tests/legacy/unit/review_tools/__init__.py similarity index 100% rename from tests/unit/states/__init__.py rename to tests/legacy/unit/review_tools/__init__.py diff --git a/tests/unit/review_tools/test_errors.py b/tests/legacy/unit/review_tools/test_errors.py similarity index 100% rename from tests/unit/review_tools/test_errors.py rename to tests/legacy/unit/review_tools/test_errors.py diff --git a/tests/unit/review_tools/test_runner.py b/tests/legacy/unit/review_tools/test_runner.py similarity index 99% rename from tests/unit/review_tools/test_runner.py rename to tests/legacy/unit/review_tools/test_runner.py index a8409cf7ee..67ccc14b51 100644 --- a/tests/unit/review_tools/test_runner.py +++ b/tests/legacy/unit/review_tools/test_runner.py @@ -21,7 +21,7 @@ import fixtures from snapcraft_legacy.internal import review_tools -from tests import unit +from tests.legacy import unit class RunTest(unit.TestCase): diff --git a/tests/unit/sources/__init__.py b/tests/legacy/unit/sources/__init__.py similarity index 97% rename from tests/unit/sources/__init__.py rename to tests/legacy/unit/sources/__init__.py index 4f513b6389..674767f54e 100644 --- a/tests/unit/sources/__init__.py +++ b/tests/legacy/unit/sources/__init__.py @@ -16,7 +16,7 @@ from unittest import mock -from tests import unit +from tests.legacy import unit class SourceTestCase(unit.TestCase): diff --git a/tests/unit/sources/test_7z.py b/tests/legacy/unit/sources/test_7z.py similarity index 98% rename from tests/unit/sources/test_7z.py rename to tests/legacy/unit/sources/test_7z.py index 318b3b1d85..067e793390 100644 --- a/tests/unit/sources/test_7z.py +++ b/tests/legacy/unit/sources/test_7z.py @@ -23,7 +23,7 @@ from testtools.matchers import Equals, MatchesRegex from snapcraft_legacy.internal import sources -from tests import unit +from tests.legacy import unit def get_side_effect(original_call): diff --git a/tests/unit/sources/test_base.py b/tests/legacy/unit/sources/test_base.py similarity index 99% rename from tests/unit/sources/test_base.py rename to tests/legacy/unit/sources/test_base.py index b91ffbac07..bb04171d77 100644 --- a/tests/unit/sources/test_base.py +++ b/tests/legacy/unit/sources/test_base.py @@ -21,7 +21,7 @@ from testtools.matchers import Contains, Equals from snapcraft_legacy.internal.sources import _base, errors -from tests import unit +from tests.legacy import unit class TestFileBase(unit.TestCase): diff --git a/tests/unit/sources/test_bazaar.py b/tests/legacy/unit/sources/test_bazaar.py similarity index 99% rename from tests/unit/sources/test_bazaar.py rename to tests/legacy/unit/sources/test_bazaar.py index 65ac8c0638..afc4c43c58 100644 --- a/tests/unit/sources/test_bazaar.py +++ b/tests/legacy/unit/sources/test_bazaar.py @@ -22,7 +22,7 @@ from testtools.matchers import Equals from snapcraft_legacy.internal import sources -from tests import unit +from tests.legacy import unit # LP: #1733584 diff --git a/tests/unit/sources/test_checksum.py b/tests/legacy/unit/sources/test_checksum.py similarity index 98% rename from tests/unit/sources/test_checksum.py rename to tests/legacy/unit/sources/test_checksum.py index e371f5442b..ccf47d12e0 100644 --- a/tests/unit/sources/test_checksum.py +++ b/tests/legacy/unit/sources/test_checksum.py @@ -22,7 +22,7 @@ from snapcraft_legacy.internal.sources import errors from snapcraft_legacy.internal.sources._checksum import verify_checksum -from tests import unit +from tests.legacy import unit class TestChecksum(unit.TestCase): diff --git a/tests/unit/sources/test_deb.py b/tests/legacy/unit/sources/test_deb.py similarity index 99% rename from tests/unit/sources/test_deb.py rename to tests/legacy/unit/sources/test_deb.py index b677957dc9..a55faf3db2 100644 --- a/tests/unit/sources/test_deb.py +++ b/tests/legacy/unit/sources/test_deb.py @@ -21,7 +21,7 @@ from testtools.matchers import Equals from snapcraft_legacy.internal import sources -from tests import unit +from tests.legacy import unit class TestDeb(unit.FakeFileHTTPServerBasedTestCase): diff --git a/tests/unit/sources/test_errors.py b/tests/legacy/unit/sources/test_errors.py similarity index 100% rename from tests/unit/sources/test_errors.py rename to tests/legacy/unit/sources/test_errors.py diff --git a/tests/unit/sources/test_git.py b/tests/legacy/unit/sources/test_git.py similarity index 99% rename from tests/unit/sources/test_git.py rename to tests/legacy/unit/sources/test_git.py index e08026d057..b7789df168 100644 --- a/tests/unit/sources/test_git.py +++ b/tests/legacy/unit/sources/test_git.py @@ -24,7 +24,7 @@ from snapcraft_legacy.internal import sources from snapcraft_legacy.internal.sources import errors -from tests import unit +from tests.legacy import unit from tests.subprocess_utils import call, call_with_output diff --git a/tests/unit/sources/test_local.py b/tests/legacy/unit/sources/test_local.py similarity index 99% rename from tests/unit/sources/test_local.py rename to tests/legacy/unit/sources/test_local.py index c4302a3b3f..43c444bf90 100644 --- a/tests/unit/sources/test_local.py +++ b/tests/legacy/unit/sources/test_local.py @@ -22,7 +22,7 @@ from testtools.matchers import DirExists, Equals, FileContains, FileExists, Not from snapcraft_legacy.internal import common, errors, sources -from tests import unit +from tests.legacy import unit class TestLocal(unit.TestCase): diff --git a/tests/unit/sources/test_mercurial.py b/tests/legacy/unit/sources/test_mercurial.py similarity index 99% rename from tests/unit/sources/test_mercurial.py rename to tests/legacy/unit/sources/test_mercurial.py index 5b6b4169b3..e4dc4f2718 100644 --- a/tests/unit/sources/test_mercurial.py +++ b/tests/legacy/unit/sources/test_mercurial.py @@ -22,7 +22,7 @@ from testtools.matchers import Equals from snapcraft_legacy.internal import sources -from tests import unit +from tests.legacy import unit # LP: #1733584 diff --git a/tests/unit/sources/test_rpm.py b/tests/legacy/unit/sources/test_rpm.py similarity index 98% rename from tests/unit/sources/test_rpm.py rename to tests/legacy/unit/sources/test_rpm.py index 2d12abb94f..ac47427a2a 100644 --- a/tests/unit/sources/test_rpm.py +++ b/tests/legacy/unit/sources/test_rpm.py @@ -23,7 +23,7 @@ from testtools.matchers import Equals, MatchesRegex from snapcraft_legacy.internal import sources -from tests import unit +from tests.legacy import unit class TestRpm(unit.TestCase): diff --git a/tests/unit/sources/test_script.py b/tests/legacy/unit/sources/test_script.py similarity index 97% rename from tests/unit/sources/test_script.py rename to tests/legacy/unit/sources/test_script.py index 59480c2de0..7cb01c405c 100644 --- a/tests/unit/sources/test_script.py +++ b/tests/legacy/unit/sources/test_script.py @@ -20,7 +20,7 @@ from testtools.matchers import FileExists from snapcraft_legacy.internal.sources import Script -from tests import unit +from tests.legacy import unit class TestScript(unit.TestCase): diff --git a/tests/unit/sources/test_snap.py b/tests/legacy/unit/sources/test_snap.py similarity index 99% rename from tests/unit/sources/test_snap.py rename to tests/legacy/unit/sources/test_snap.py index b45b27722c..b7e7177201 100644 --- a/tests/unit/sources/test_snap.py +++ b/tests/legacy/unit/sources/test_snap.py @@ -22,7 +22,7 @@ from testtools.matchers import DirExists, Equals, FileExists, MatchesRegex from snapcraft_legacy.internal import sources -from tests import unit +from tests.legacy import unit class TestSnap(unit.TestCase): diff --git a/tests/unit/sources/test_sources.py b/tests/legacy/unit/sources/test_sources.py similarity index 100% rename from tests/unit/sources/test_sources.py rename to tests/legacy/unit/sources/test_sources.py diff --git a/tests/unit/sources/test_subversion.py b/tests/legacy/unit/sources/test_subversion.py similarity index 99% rename from tests/unit/sources/test_subversion.py rename to tests/legacy/unit/sources/test_subversion.py index 457a164712..c62f63ade2 100644 --- a/tests/unit/sources/test_subversion.py +++ b/tests/legacy/unit/sources/test_subversion.py @@ -22,7 +22,7 @@ from testtools.matchers import Equals from snapcraft_legacy.internal import sources -from tests import unit +from tests.legacy import unit # LP: #1733584 diff --git a/tests/unit/sources/test_tar.py b/tests/legacy/unit/sources/test_tar.py similarity index 99% rename from tests/unit/sources/test_tar.py rename to tests/legacy/unit/sources/test_tar.py index 41d8058485..67c07bdead 100644 --- a/tests/unit/sources/test_tar.py +++ b/tests/legacy/unit/sources/test_tar.py @@ -22,7 +22,7 @@ from testtools.matchers import Equals from snapcraft_legacy.internal import sources -from tests import unit +from tests.legacy import unit class TestTar(unit.FakeFileHTTPServerBasedTestCase): diff --git a/tests/unit/sources/test_zip.py b/tests/legacy/unit/sources/test_zip.py similarity index 98% rename from tests/unit/sources/test_zip.py rename to tests/legacy/unit/sources/test_zip.py index e1df26010e..1237c83eab 100644 --- a/tests/unit/sources/test_zip.py +++ b/tests/legacy/unit/sources/test_zip.py @@ -20,7 +20,7 @@ from testtools.matchers import Equals from snapcraft_legacy.internal import sources -from tests import unit +from tests.legacy import unit class TestZip(unit.FakeFileHTTPServerBasedTestCase): diff --git a/tests/unit/store/__init__.py b/tests/legacy/unit/states/__init__.py similarity index 100% rename from tests/unit/store/__init__.py rename to tests/legacy/unit/states/__init__.py diff --git a/tests/unit/states/conftest.py b/tests/legacy/unit/states/conftest.py similarity index 100% rename from tests/unit/states/conftest.py rename to tests/legacy/unit/states/conftest.py diff --git a/tests/unit/states/test_build.py b/tests/legacy/unit/states/test_build.py similarity index 99% rename from tests/unit/states/test_build.py rename to tests/legacy/unit/states/test_build.py index a6da1ff7f7..837185447c 100644 --- a/tests/unit/states/test_build.py +++ b/tests/legacy/unit/states/test_build.py @@ -20,7 +20,7 @@ import snapcraft_legacy.internal from snapcraft_legacy import yaml_utils -from tests import unit +from tests.legacy import unit from .conftest import Project diff --git a/tests/unit/states/test_global_state.py b/tests/legacy/unit/states/test_global_state.py similarity index 100% rename from tests/unit/states/test_global_state.py rename to tests/legacy/unit/states/test_global_state.py diff --git a/tests/unit/states/test_prime.py b/tests/legacy/unit/states/test_prime.py similarity index 99% rename from tests/unit/states/test_prime.py rename to tests/legacy/unit/states/test_prime.py index ec06a84c40..d53deee0cb 100644 --- a/tests/unit/states/test_prime.py +++ b/tests/legacy/unit/states/test_prime.py @@ -20,7 +20,7 @@ import snapcraft_legacy.internal from snapcraft_legacy import yaml_utils -from tests import unit +from tests.legacy import unit from .conftest import Project diff --git a/tests/unit/states/test_pull.py b/tests/legacy/unit/states/test_pull.py similarity index 99% rename from tests/unit/states/test_pull.py rename to tests/legacy/unit/states/test_pull.py index 41d2b809a9..59dabc523d 100644 --- a/tests/unit/states/test_pull.py +++ b/tests/legacy/unit/states/test_pull.py @@ -20,7 +20,7 @@ import snapcraft_legacy.internal from snapcraft_legacy import yaml_utils -from tests import unit +from tests.legacy import unit from .conftest import Project diff --git a/tests/unit/states/test_stage.py b/tests/legacy/unit/states/test_stage.py similarity index 98% rename from tests/unit/states/test_stage.py rename to tests/legacy/unit/states/test_stage.py index 40704c3aba..138350e3a7 100644 --- a/tests/unit/states/test_stage.py +++ b/tests/legacy/unit/states/test_stage.py @@ -20,7 +20,7 @@ import snapcraft_legacy.internal from snapcraft_legacy import yaml_utils -from tests import unit +from tests.legacy import unit from .conftest import Project diff --git a/tests/unit/states/test_state.py b/tests/legacy/unit/states/test_state.py similarity index 100% rename from tests/unit/states/test_state.py rename to tests/legacy/unit/states/test_state.py diff --git a/tests/unit/store/http_client/__init__.py b/tests/legacy/unit/store/__init__.py similarity index 100% rename from tests/unit/store/http_client/__init__.py rename to tests/legacy/unit/store/__init__.py diff --git a/tests/unit/store/v2/__init__.py b/tests/legacy/unit/store/http_client/__init__.py similarity index 100% rename from tests/unit/store/v2/__init__.py rename to tests/legacy/unit/store/http_client/__init__.py diff --git a/tests/unit/store/http_client/test_agent.py b/tests/legacy/unit/store/http_client/test_agent.py similarity index 96% rename from tests/unit/store/http_client/test_agent.py rename to tests/legacy/unit/store/http_client/test_agent.py index db78138603..a2382e696d 100644 --- a/tests/unit/store/http_client/test_agent.py +++ b/tests/legacy/unit/store/http_client/test_agent.py @@ -22,8 +22,8 @@ from snapcraft_legacy import ProjectOptions from snapcraft_legacy import __version__ as snapcraft_version from snapcraft_legacy.storeapi.http_clients import agent -from tests import unit -from tests.fixture_setup.os_release import FakeOsRelease +from tests.legacy import unit +from tests.legacy.fixture_setup.os_release import FakeOsRelease class UserAgentTestCase(unit.TestCase): diff --git a/tests/unit/store/http_client/test_candid_client.py b/tests/legacy/unit/store/http_client/test_candid_client.py similarity index 100% rename from tests/unit/store/http_client/test_candid_client.py rename to tests/legacy/unit/store/http_client/test_candid_client.py diff --git a/tests/unit/store/http_client/test_config.py b/tests/legacy/unit/store/http_client/test_config.py similarity index 100% rename from tests/unit/store/http_client/test_config.py rename to tests/legacy/unit/store/http_client/test_config.py diff --git a/tests/unit/store/http_client/test_errors.py b/tests/legacy/unit/store/http_client/test_errors.py similarity index 100% rename from tests/unit/store/http_client/test_errors.py rename to tests/legacy/unit/store/http_client/test_errors.py diff --git a/tests/unit/store/http_client/test_ubuntu_one_auth_client.py b/tests/legacy/unit/store/http_client/test_ubuntu_one_auth_client.py similarity index 100% rename from tests/unit/store/http_client/test_ubuntu_one_auth_client.py rename to tests/legacy/unit/store/http_client/test_ubuntu_one_auth_client.py diff --git a/tests/unit/store/test_channels.py b/tests/legacy/unit/store/test_channels.py similarity index 100% rename from tests/unit/store/test_channels.py rename to tests/legacy/unit/store/test_channels.py diff --git a/tests/unit/store/test_errors.py b/tests/legacy/unit/store/test_errors.py similarity index 100% rename from tests/unit/store/test_errors.py rename to tests/legacy/unit/store/test_errors.py diff --git a/tests/unit/store/test_metrics.py b/tests/legacy/unit/store/test_metrics.py similarity index 100% rename from tests/unit/store/test_metrics.py rename to tests/legacy/unit/store/test_metrics.py diff --git a/tests/unit/store/test_status.py b/tests/legacy/unit/store/test_status.py similarity index 99% rename from tests/unit/store/test_status.py rename to tests/legacy/unit/store/test_status.py index 7993c2844c..f815ada664 100644 --- a/tests/unit/store/test_status.py +++ b/tests/legacy/unit/store/test_status.py @@ -17,7 +17,7 @@ from testtools.matchers import Equals, HasLength from snapcraft_legacy.storeapi import channels, errors, status -from tests import unit +from tests.legacy import unit class TestSnapStatusChannelDetails: diff --git a/tests/unit/store/test_store_client.py b/tests/legacy/unit/store/test_store_client.py similarity index 99% rename from tests/unit/store/test_store_client.py rename to tests/legacy/unit/store/test_store_client.py index 3ed4f6c172..3322c5442b 100644 --- a/tests/unit/store/test_store_client.py +++ b/tests/legacy/unit/store/test_store_client.py @@ -34,11 +34,11 @@ Not, ) -import tests +import tests.legacy from snapcraft_legacy import storeapi from snapcraft_legacy.storeapi import errors, http_clients, metrics from snapcraft_legacy.storeapi.v2 import channel_map, releases, validation_sets, whoami -from tests import fixture_setup, unit +from tests.legacy import fixture_setup, unit class StoreTestCase(unit.TestCase): @@ -954,7 +954,7 @@ class UploadTestCase(StoreTestCase): def setUp(self): super().setUp() self.snap_path = os.path.join( - os.path.dirname(tests.__file__), "data", "test-snap.snap" + os.path.dirname(tests.legacy.__file__), "data", "test-snap.snap" ) # These should eventually converge to the same module pbars = ( @@ -1672,7 +1672,9 @@ def _setup_snap(self): """ self.client.login(email="dummy", password="test correct password") self.client.register("basic") - path = os.path.join(os.path.dirname(tests.__file__), "data", "test-snap.snap") + path = os.path.join( + os.path.dirname(tests.legacy.__file__), "data", "test-snap.snap" + ) tracker = self.client.upload("basic", path) tracker.track() @@ -1782,7 +1784,9 @@ def _setup_snap(self): """ self.client.login(email="dummy", password="test correct password") self.client.register("basic") - path = os.path.join(os.path.dirname(tests.__file__), "data", "test-snap.snap") + path = os.path.join( + os.path.dirname(tests.legacy.__file__), "data", "test-snap.snap" + ) tracker = self.client.upload("basic", path) tracker.track() @@ -1877,7 +1881,9 @@ def _setup_snap(self): """ self.client.login(email="dummy", password="test correct password") self.client.register("basic") - path = os.path.join(os.path.dirname(tests.__file__), "data", "test-snap.snap") + path = os.path.join( + os.path.dirname(tests.legacy.__file__), "data", "test-snap.snap" + ) tracker = self.client.upload("basic", path) tracker.track() diff --git a/tests/unit/yaml_utils/__init__.py b/tests/legacy/unit/store/v2/__init__.py similarity index 100% rename from tests/unit/yaml_utils/__init__.py rename to tests/legacy/unit/store/v2/__init__.py diff --git a/tests/unit/store/v2/test_channel_map.py b/tests/legacy/unit/store/v2/test_channel_map.py similarity index 99% rename from tests/unit/store/v2/test_channel_map.py rename to tests/legacy/unit/store/v2/test_channel_map.py index 542c55f6e9..239c897a74 100644 --- a/tests/unit/store/v2/test_channel_map.py +++ b/tests/legacy/unit/store/v2/test_channel_map.py @@ -18,7 +18,7 @@ from testtools.matchers import Equals, HasLength, Is, IsInstance from snapcraft_legacy.storeapi.v2 import channel_map -from tests import unit +from tests.legacy import unit class ProgressiveTest(unit.TestCase): diff --git a/tests/unit/store/v2/test_releases.py b/tests/legacy/unit/store/v2/test_releases.py similarity index 100% rename from tests/unit/store/v2/test_releases.py rename to tests/legacy/unit/store/v2/test_releases.py diff --git a/tests/unit/store/v2/test_validation_sets.py b/tests/legacy/unit/store/v2/test_validation_sets.py similarity index 100% rename from tests/unit/store/v2/test_validation_sets.py rename to tests/legacy/unit/store/v2/test_validation_sets.py diff --git a/tests/unit/store/v2/test_whoami.py b/tests/legacy/unit/store/v2/test_whoami.py similarity index 100% rename from tests/unit/store/v2/test_whoami.py rename to tests/legacy/unit/store/v2/test_whoami.py diff --git a/tests/unit/test_common.py b/tests/legacy/unit/test_common.py similarity index 99% rename from tests/unit/test_common.py rename to tests/legacy/unit/test_common.py index 0d77511add..0ecd312254 100644 --- a/tests/unit/test_common.py +++ b/tests/legacy/unit/test_common.py @@ -20,7 +20,7 @@ from testtools.matchers import Equals from snapcraft_legacy.internal import common, errors -from tests import unit +from tests.legacy import unit class CommonTestCase(unit.TestCase): diff --git a/tests/unit/test_config.py b/tests/legacy/unit/test_config.py similarity index 99% rename from tests/unit/test_config.py rename to tests/legacy/unit/test_config.py index be93fd6bcf..960bc22a38 100644 --- a/tests/unit/test_config.py +++ b/tests/legacy/unit/test_config.py @@ -21,7 +21,7 @@ from snapcraft_legacy import config from snapcraft_legacy.internal.errors import SnapcraftInvalidCLIConfigError -from tests import unit +from tests.legacy import unit class TestCLIConfig(unit.TestCase): diff --git a/tests/unit/test_elf.py b/tests/legacy/unit/test_elf.py similarity index 99% rename from tests/unit/test_elf.py rename to tests/legacy/unit/test_elf.py index ec0739fbb7..83e2774d89 100644 --- a/tests/unit/test_elf.py +++ b/tests/legacy/unit/test_elf.py @@ -26,7 +26,7 @@ from snapcraft_legacy import ProjectOptions from snapcraft_legacy.internal import elf, errors -from tests import fixture_setup, unit +from tests.legacy import fixture_setup, unit class TestElfBase(unit.TestCase): diff --git a/tests/unit/test_errors.py b/tests/legacy/unit/test_errors.py similarity index 100% rename from tests/unit/test_errors.py rename to tests/legacy/unit/test_errors.py diff --git a/tests/unit/test_file_utils.py b/tests/legacy/unit/test_file_utils.py similarity index 99% rename from tests/unit/test_file_utils.py rename to tests/legacy/unit/test_file_utils.py index efaf7c7de0..f5c1c479e7 100644 --- a/tests/unit/test_file_utils.py +++ b/tests/legacy/unit/test_file_utils.py @@ -27,7 +27,7 @@ from snapcraft_legacy import file_utils from snapcraft_legacy.internal import common, errors -from tests import fixture_setup, unit +from tests.legacy import fixture_setup, unit class TestReplaceInFile: diff --git a/tests/unit/test_fixture_setup.py b/tests/legacy/unit/test_fixture_setup.py similarity index 98% rename from tests/unit/test_fixture_setup.py rename to tests/legacy/unit/test_fixture_setup.py index ffe39667d3..a95c4f8d91 100644 --- a/tests/unit/test_fixture_setup.py +++ b/tests/legacy/unit/test_fixture_setup.py @@ -24,7 +24,7 @@ import fixtures from testtools.matchers import Equals -from tests import fixture_setup, unit +from tests.legacy import fixture_setup, unit class TempCWDTestCase(unit.TestCase): diff --git a/tests/unit/test_formatting_utils.py b/tests/legacy/unit/test_formatting_utils.py similarity index 98% rename from tests/unit/test_formatting_utils.py rename to tests/legacy/unit/test_formatting_utils.py index f068d94e7e..f5607b2b56 100644 --- a/tests/unit/test_formatting_utils.py +++ b/tests/legacy/unit/test_formatting_utils.py @@ -18,7 +18,7 @@ from testtools.matchers import Equals from snapcraft_legacy import formatting_utils -from tests import unit +from tests.legacy import unit class HumanizeListTestCases(unit.TestCase): diff --git a/tests/unit/test_indicators.py b/tests/legacy/unit/test_indicators.py similarity index 99% rename from tests/unit/test_indicators.py rename to tests/legacy/unit/test_indicators.py index 56ca33cdc5..7243b12e03 100644 --- a/tests/unit/test_indicators.py +++ b/tests/legacy/unit/test_indicators.py @@ -22,7 +22,7 @@ import requests from snapcraft_legacy.internal import indicators -from tests import unit +from tests.legacy import unit class DumbTerminalTests(unit.TestCase): diff --git a/tests/unit/test_init.py b/tests/legacy/unit/test_init.py similarity index 97% rename from tests/unit/test_init.py rename to tests/legacy/unit/test_init.py index 6807b3fcdb..26bf7605f8 100644 --- a/tests/unit/test_init.py +++ b/tests/legacy/unit/test_init.py @@ -18,7 +18,7 @@ from testtools.matchers import Equals import snapcraft_legacy -from tests import unit +from tests.legacy import unit class VersionTestCase(unit.TestCase): diff --git a/tests/unit/test_log.py b/tests/legacy/unit/test_log.py similarity index 99% rename from tests/unit/test_log.py rename to tests/legacy/unit/test_log.py index 78a9cb0752..fb7dcad8b7 100644 --- a/tests/unit/test_log.py +++ b/tests/legacy/unit/test_log.py @@ -19,7 +19,7 @@ from testtools.matchers import Contains, Equals, Not from snapcraft_legacy.internal import log -from tests import fixture_setup, unit +from tests.legacy import fixture_setup, unit class LogTestCase(unit.TestCase): diff --git a/tests/unit/test_mangling.py b/tests/legacy/unit/test_mangling.py similarity index 99% rename from tests/unit/test_mangling.py rename to tests/legacy/unit/test_mangling.py index 0933422373..e60b7f8f36 100644 --- a/tests/unit/test_mangling.py +++ b/tests/legacy/unit/test_mangling.py @@ -20,7 +20,7 @@ from testtools.matchers import FileContains, FileExists, Not from snapcraft_legacy.internal import mangling -from tests import fixture_setup, unit +from tests.legacy import fixture_setup, unit def _create_file(filename, contents): diff --git a/tests/unit/test_mountinfo.py b/tests/legacy/unit/test_mountinfo.py similarity index 99% rename from tests/unit/test_mountinfo.py rename to tests/legacy/unit/test_mountinfo.py index 86185b32c6..1390270180 100644 --- a/tests/unit/test_mountinfo.py +++ b/tests/legacy/unit/test_mountinfo.py @@ -21,7 +21,7 @@ from testtools.matchers import Equals, HasLength from snapcraft_legacy.internal import errors, mountinfo -from tests import unit +from tests.legacy import unit class MountInfoTestCase(unit.TestCase): diff --git a/tests/unit/test_options.py b/tests/legacy/unit/test_options.py similarity index 99% rename from tests/unit/test_options.py rename to tests/legacy/unit/test_options.py index 11d6468804..c8f558ada6 100644 --- a/tests/unit/test_options.py +++ b/tests/legacy/unit/test_options.py @@ -28,7 +28,7 @@ _32BIT_USERSPACE_ARCHITECTURE, _get_platform_architecture, ) -from tests import unit +from tests.legacy import unit class TestNativeOptions: diff --git a/tests/unit/test_os_release.py b/tests/legacy/unit/test_os_release.py similarity index 99% rename from tests/unit/test_os_release.py rename to tests/legacy/unit/test_os_release.py index 21389a0ad1..853ed9fee8 100644 --- a/tests/unit/test_os_release.py +++ b/tests/legacy/unit/test_os_release.py @@ -19,7 +19,7 @@ from testtools.matchers import Equals from snapcraft_legacy.internal import errors, os_release -from tests import unit +from tests.legacy import unit class OsReleaseTestCase(unit.TestCase): diff --git a/tests/unit/test_steps.py b/tests/legacy/unit/test_steps.py similarity index 100% rename from tests/unit/test_steps.py rename to tests/legacy/unit/test_steps.py diff --git a/tests/unit/test_target_arch.py b/tests/legacy/unit/test_target_arch.py similarity index 100% rename from tests/unit/test_target_arch.py rename to tests/legacy/unit/test_target_arch.py diff --git a/tests/unit/test_xattrs.py b/tests/legacy/unit/test_xattrs.py similarity index 99% rename from tests/unit/test_xattrs.py rename to tests/legacy/unit/test_xattrs.py index 431327e2d4..9d15fe95f0 100644 --- a/tests/unit/test_xattrs.py +++ b/tests/legacy/unit/test_xattrs.py @@ -22,7 +22,7 @@ from snapcraft_legacy.internal import xattrs from snapcraft_legacy.internal.errors import XAttributeTooLongError -from tests import unit +from tests.legacy import unit class TestXattrs(unit.TestCase): diff --git a/tests/legacy/unit/yaml_utils/__init__.py b/tests/legacy/unit/yaml_utils/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/unit/yaml_utils/test_errors.py b/tests/legacy/unit/yaml_utils/test_errors.py similarity index 100% rename from tests/unit/yaml_utils/test_errors.py rename to tests/legacy/unit/yaml_utils/test_errors.py diff --git a/tests/unit/yaml_utils/test_yaml_utils.py b/tests/legacy/unit/yaml_utils/test_yaml_utils.py similarity index 100% rename from tests/unit/yaml_utils/test_yaml_utils.py rename to tests/legacy/unit/yaml_utils/test_yaml_utils.py diff --git a/tests/unit/__init__.py b/tests/unit/__init__.py index 331f195e30..e69de29bb2 100644 --- a/tests/unit/__init__.py +++ b/tests/unit/__init__.py @@ -1,318 +0,0 @@ -# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- -# -# Copyright (C) 2015-2020 Canonical Ltd -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License version 3 as -# published by the Free Software Foundation. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -import http.server -import logging -import os -import stat -import threading -from unittest import mock - -import apt -import fixtures -import progressbar -import testscenarios -import testtools - -from snapcraft_legacy.internal import common, steps -from tests import fake_servers, fixture_setup -from tests.file_utils import get_snapcraft_path -from tests.unit.part_loader import load_part - - -class ContainsList(list): - def __eq__(self, other): - return all([i[0] in i[1] for i in zip(self, other)]) - - -class MockOptions: - def __init__( - self, - source=None, - source_type=None, - source_branch=None, - source_tag=None, - source_subdir=None, - source_depth=None, - source_commit=None, - source_checksum=None, - disable_parallel=False, - ): - self.source = source - self.source_type = source_type - self.source_depth = source_depth - self.source_branch = source_branch - self.source_commit = source_commit - self.source_tag = source_tag - self.source_subdir = source_subdir - self.disable_parallel = disable_parallel - - -class IsExecutable: - """Match if a file path is executable.""" - - def __str__(self): - return "IsExecutable()" - - def match(self, file_path): - if not os.stat(file_path).st_mode & stat.S_IEXEC: - return testtools.matchers.Mismatch( - "Expected {!r} to be executable, but it was not".format(file_path) - ) - return None - - -class LinkExists: - """Match if a file path is a symlink.""" - - def __init__(self, expected_target=None): - self._expected_target = expected_target - - def __str__(self): - return "LinkExists()" - - def match(self, file_path): - if not os.path.exists(file_path): - return testtools.matchers.Mismatch( - "Expected {!r} to be a symlink, but it doesn't exist".format(file_path) - ) - - if not os.path.islink(file_path): - return testtools.matchers.Mismatch( - "Expected {!r} to be a symlink, but it was not".format(file_path) - ) - - target = os.readlink(file_path) - if target != self._expected_target: - return testtools.matchers.Mismatch( - "Expected {!r} to be a symlink pointing to {!r}, but it was " - "pointing to {!r}".format(file_path, self._expected_target, target) - ) - - return None - - -class TestCase(testscenarios.WithScenarios, testtools.TestCase): - @classmethod - def setUpClass(cls): - cls.fake_snapd = fixture_setup.FakeSnapd() - cls.fake_snapd.setUp() - - @classmethod - def tearDownClass(cls): - cls.fake_snapd.cleanUp() - - def setUp(self): - super().setUp() - temp_cwd_fixture = fixture_setup.TempCWD() - self.useFixture(temp_cwd_fixture) - self.path = temp_cwd_fixture.path - - # Use a separate path for XDG dirs, or changes there may be detected as - # source changes. - self.xdg_path = self.useFixture(fixtures.TempDir()).path - self.useFixture(fixture_setup.TempXDG(self.xdg_path)) - self.fake_terminal = fixture_setup.FakeTerminal() - self.useFixture(self.fake_terminal) - # Some tests will directly or indirectly change the plugindir, which - # is a module variable. Make sure that it is returned to the original - # value when a test ends. - self.addCleanup(common.set_plugindir, common.get_plugindir()) - self.addCleanup(common.set_schemadir, common.get_schemadir()) - self.addCleanup(common.set_extensionsdir, common.get_extensionsdir()) - self.addCleanup(common.set_keyringsdir, common.get_keyringsdir()) - self.addCleanup(common.reset_env) - common.set_schemadir(os.path.join(get_snapcraft_path(), "schema")) - self.fake_logger = fixtures.FakeLogger(level=logging.ERROR) - self.useFixture(self.fake_logger) - - # Some tests will change the apt Dir::Etc::Trusted and - # Dir::Etc::TrustedParts directories. Make sure they're properly reset. - self.addCleanup( - apt.apt_pkg.config.set, - "Dir::Etc::Trusted", - apt.apt_pkg.config.find_file("Dir::Etc::Trusted"), - ) - self.addCleanup( - apt.apt_pkg.config.set, - "Dir::Etc::TrustedParts", - apt.apt_pkg.config.find_file("Dir::Etc::TrustedParts"), - ) - - patcher = mock.patch("os.sched_getaffinity") - self.cpu_count = patcher.start() - self.cpu_count.return_value = {1, 2} - self.addCleanup(patcher.stop) - - # We do not want the paths to affect every test we have. - patcher = mock.patch( - "snapcraft_legacy.file_utils.get_snap_tool_path", side_effect=lambda x: x - ) - patcher.start() - self.addCleanup(patcher.stop) - - patcher = mock.patch( - "snapcraft_legacy.internal.indicators.ProgressBar", new=SilentProgressBar - ) - patcher.start() - self.addCleanup(patcher.stop) - - # These are what we expect by default - self.snap_dir = os.path.join(os.getcwd(), "snap") - self.prime_dir = os.path.join(os.getcwd(), "prime") - self.stage_dir = os.path.join(os.getcwd(), "stage") - self.parts_dir = os.path.join(os.getcwd(), "parts") - self.local_plugins_dir = os.path.join(self.snap_dir, "plugins") - - # Use this host to run through the lifecycle tests - self.useFixture( - fixtures.EnvironmentVariable("SNAPCRAFT_BUILD_ENVIRONMENT", "host") - ) - - # Make sure snap installation does the right thing - self.fake_snapd.installed_snaps = [ - dict(name="core20", channel="stable", revision="10"), - dict(name="core18", channel="stable", revision="10"), - ] - self.fake_snapd.snaps_result = [ - dict(name="core20", channel="stable", revision="10"), - dict(name="core18", channel="stable", revision="10"), - ] - self.fake_snapd.find_result = [ - dict( - core20=dict( - channel="stable", - channels={"latest/stable": dict(confinement="strict")}, - ) - ), - dict( - core18=dict( - channel="stable", - channels={"latest/stable": dict(confinement="strict")}, - ) - ), - ] - self.fake_snapd.snap_details_func = None - - self.fake_snap_command = fixture_setup.FakeSnapCommand() - self.useFixture(self.fake_snap_command) - - # Avoid installing patchelf in the tests - self.useFixture(fixtures.EnvironmentVariable("SNAPCRAFT_NO_PATCHELF", "1")) - - # Disable Sentry reporting for tests, otherwise they'll hang waiting - # for input - self.useFixture( - fixtures.EnvironmentVariable("SNAPCRAFT_ENABLE_ERROR_REPORTING", "false") - ) - - # Don't let the managed host variable leak into tests - self.useFixture(fixtures.EnvironmentVariable("SNAPCRAFT_MANAGED_HOST")) - - machine = os.environ.get("SNAPCRAFT_TEST_MOCK_MACHINE", None) - self.base_environment = fixture_setup.FakeBaseEnvironment(machine=machine) - self.useFixture(self.base_environment) - - # Make sure "SNAPCRAFT_ENABLE_DEVELOPER_DEBUG" is reset between tests - self.useFixture( - fixtures.EnvironmentVariable("SNAPCRAFT_ENABLE_DEVELOPER_DEBUG") - ) - self.useFixture(fixture_setup.FakeSnapcraftctl()) - - # Don't let host SNAPCRAFT_BUILD_INFO variable leak into tests - self.useFixture(fixtures.EnvironmentVariable("SNAPCRAFT_BUILD_INFO")) - - def make_snapcraft_yaml(self, content, encoding="utf-8", location=""): - snap_dir = os.path.join(location, "snap") - os.makedirs(snap_dir, exist_ok=True) - snapcraft_yaml = os.path.join(snap_dir, "snapcraft.yaml") - with open(snapcraft_yaml, "w", encoding=encoding) as fp: - fp.write(content) - return snapcraft_yaml - - def verify_state(self, part_name, state_dir, expected_step_name): - self.assertTrue( - os.path.isdir(state_dir), - "Expected state directory for {}".format(part_name), - ) - - # Expect every step up to and including the specified one to be run - step = steps.get_step_by_name(expected_step_name) - for step in step.previous_steps() + [step]: - self.assertTrue( - os.path.exists(os.path.join(state_dir, step.name)), - "Expected {!r} to be run for {}".format(step.name, part_name), - ) - - def load_part( - self, - part_name, - plugin_name=None, - part_properties=None, - project=None, - stage_packages_repo=None, - snap_name="test-snap", - base="core18", - build_base=None, - confinement="strict", - snap_type="app", - ): - return load_part( - part_name=part_name, - plugin_name=plugin_name, - part_properties=part_properties, - project=project, - stage_packages_repo=stage_packages_repo, - snap_name=snap_name, - base=base, - build_base=build_base, - confinement=confinement, - snap_type=snap_type, - ) - - -class TestWithFakeRemoteParts(TestCase): - def setUp(self): - super().setUp() - self.useFixture(fixture_setup.FakeParts()) - - -class FakeFileHTTPServerBasedTestCase(TestCase): - def setUp(self): - super().setUp() - - self.useFixture(fixtures.EnvironmentVariable("no_proxy", "localhost,127.0.0.1")) - self.server = http.server.HTTPServer( - ("127.0.0.1", 0), fake_servers.FakeFileHTTPRequestHandler - ) - server_thread = threading.Thread(target=self.server.serve_forever) - self.addCleanup(server_thread.join) - self.addCleanup(self.server.server_close) - self.addCleanup(self.server.shutdown) - server_thread.start() - - -class SilentProgressBar(progressbar.ProgressBar): - """A progress bar causing no spurious output during tests.""" - - def start(self): - pass - - def update(self, value=None): - pass - - def finish(self): - pass From f5eacacc313af0b51d4ec3ccf3a0196a9e598fd1 Mon Sep 17 00:00:00 2001 From: Claudio Matsuoka Date: Mon, 10 Jan 2022 10:02:23 -0300 Subject: [PATCH 003/167] requirements: update dependencies Signed-off-by: Claudio Matsuoka --- requirements-devel.txt | 84 ++++++++++++++++++------------------ requirements.txt | 68 ++++++++++++++--------------- setup.py | 1 + tools/freeze-requirements.sh | 5 +-- 4 files changed, 78 insertions(+), 80 deletions(-) diff --git a/requirements-devel.txt b/requirements-devel.txt index 72f4a649d2..8882002264 100644 --- a/requirements-devel.txt +++ b/requirements-devel.txt @@ -1,91 +1,91 @@ -attrs==21.2.0 -catkin-pkg==0.4.23 -certifi==2021.5.30 -cffi==1.14.6 +attrs==21.4.0 +catkin-pkg==0.4.24 +certifi==2021.10.8 +cffi==1.15.0 chardet==4.0.0 -charset-normalizer==2.0.2 -click==8.0.1 +charset-normalizer==2.0.10 +click==8.0.3 codespell==2.1.0 -coverage==5.5 +coverage==6.2 cryptography==3.4 -distro==1.5.0 -docutils==0.17.1 +distro==1.6.0 +docutils==0.18.1 entrypoints==0.3 extras==1.0.0 fixtures==3.0.0 flake8==3.7.9 gnupg==2.3.1 -httplib2==0.19.1 +httplib2==0.20.2 hupper==1.10.3 -idna==3.2 -importlib-metadata==4.6.1 +idna==3.3 +importlib-metadata==4.10.0 iniconfig==1.1.1 -isort==5.9.2 -jeepney==0.7.0 +isort==5.10.1 +jeepney==0.7.1 jsonschema==2.5.1 -keyring==23.0.1 -launchpadlib==1.10.13 -lazr.restfulclient==0.14.3 -lazr.uri==1.0.5 -lxml==4.6.3 +keyring==23.5.0 +launchpadlib==1.10.15.1 +lazr.restfulclient==0.14.4 +lazr.uri==1.0.6 +lxml==4.7.1 macaroonbakery==1.3.1 mccabe==0.6.1 mypy==0.770 mypy-extensions==0.4.3 oauthlib==3.1.1 -packaging==21.0 +packaging==21.3 PasteDeploy==2.1.1 -pbr==5.6.0 +pbr==5.8.0 pexpect==4.8.0 plaster==1.0 plaster-pastedeploy==0.7 -pluggy==0.13.1 +pluggy==1.0.0 progressbar==2.5 -protobuf==3.17.3 -psutil==5.8.0 +protobuf==3.19.1 +psutil==5.9.0 ptyprocess==0.7.0 -py==1.10.0 +py==1.11.0 pycodestyle==2.5.0 -pycparser==2.20 +pycparser==2.21 pyelftools==0.27 pyflakes==2.1.1 pyftpdlib==1.5.6 -pylxd==2.3.0 +pylxd==2.3.1 pymacaroons==0.13.0 -pyparsing==2.4.7 +pyparsing==3.0.6 pyramid==2.0 pyRFC3339==1.1 -pytest==6.2.4 -pytest-cov==2.12.1 -pytest-subprocess==1.1.1 +pytest==6.2.5 +pytest-cov==3.0.0 +pytest-subprocess==1.3.2 python-dateutil==2.8.2 -python-debian==0.1.40 -pytz==2021.1 +python-debian==0.1.42 +pytz==2021.3 pyxdg==0.27 PyYAML==5.3 raven==6.10.0 -requests==2.26.0 +requests==2.27.1 requests-toolbelt==0.9.1 -requests-unixsocket==0.2.0 +requests-unixsocket==0.3.0 SecretStorage==3.3.1 semantic-version==2.8.5 -simplejson==3.17.3 +simplejson==3.17.6 six==1.16.0 tabulate==0.8.9 -testresources==2.0.1 testscenarios==0.5.0 testtools==2.5.0 -tinydb==4.5.0 +tinydb==4.5.2 toml==0.10.2 +tomli==2.0.0 translationstring==1.4 typed-ast==1.4.3 -typing-extensions==3.10.0.0 -urllib3==1.26.6 +typing_extensions==4.0.1 +urllib3==1.26.8 venusian==3.0.0 -wadllib==1.3.5 +wadllib==1.3.6 WebOb==1.8.7 ws4py==0.5.1 -zipp==3.5.0 +zipp==3.7.0 zope.deprecation==4.4.0 zope.interface==5.4.0 python-apt @ https://launchpad.net/ubuntu/+archive/primary/+sourcefiles/python-apt/2.0.0ubuntu0.20.04.6/python-apt_2.0.0ubuntu0.20.04.6.tar.xz; sys.platform == "linux" diff --git a/requirements.txt b/requirements.txt index 4d2a83cc38..3cda19086c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,60 +1,58 @@ -attrs==21.2.0 -catkin-pkg==0.4.23 -certifi==2021.5.30 -cffi==1.14.6 +attrs==21.4.0 +catkin-pkg==0.4.24 +certifi==2021.10.8 +cffi==1.15.0 chardet==4.0.0 -charset-normalizer==2.0.2 -click==8.0.1 +charset-normalizer==2.0.10 +click==8.0.3 cryptography==3.4 -distro==1.5.0 -docutils==0.17.1 +distro==1.6.0 +docutils==0.18.1 gnupg==2.3.1 -httplib2==0.19.1 -idna==3.2 -importlib-metadata==4.6.1 -jeepney==0.7.0 +httplib2==0.20.2 +idna==3.3 +importlib-metadata==4.10.0 +jeepney==0.7.1 jsonschema==2.5.1 -keyring==23.0.1 -launchpadlib==1.10.13 -lazr.restfulclient==0.14.3 -lazr.uri==1.0.5 -lxml==4.6.3 +keyring==23.5.0 +launchpadlib==1.10.15.1 +lazr.restfulclient==0.14.4 +lazr.uri==1.0.6 +lxml==4.7.1 macaroonbakery==1.3.1 mypy-extensions==0.4.3 oauthlib==3.1.1 -pbr==5.6.0 progressbar==2.5 -protobuf==3.17.3 -psutil==5.8.0 -pycparser==2.20 +protobuf==3.19.1 +psutil==5.9.0 +pycparser==2.21 pyelftools==0.27 -pylxd==2.3.0 +pylxd==2.3.1 pymacaroons==0.13.0 -pyparsing==2.4.7 +pyparsing==3.0.6 pyRFC3339==1.1 python-dateutil==2.8.2 -python-debian==0.1.40 -pytz==2021.1 +python-debian==0.1.42 +pytz==2021.3 pyxdg==0.27 PyYAML==5.3 raven==6.10.0 -requests==2.26.0 +requests==2.27.1 requests-toolbelt==0.9.1 -requests-unixsocket==0.2.0 +requests-unixsocket==0.3.0 SecretStorage==3.3.1 semantic-version==2.8.5 -simplejson==3.17.3 +simplejson==3.17.6 six==1.16.0 tabulate==0.8.9 -testresources==2.0.1 -tinydb==4.5.0 +tinydb==4.5.2 toml==0.10.2 -typing-extensions==3.10.0.0 -urllib3==1.26.6 -wadllib==1.3.5 +typing_extensions==4.0.1 +urllib3==1.26.8 +wadllib==1.3.6 ws4py==0.5.1 -zipp==3.5.0 +zipp==3.7.0 python-apt @ https://launchpad.net/ubuntu/+archive/primary/+sourcefiles/python-apt/2.0.0ubuntu0.20.04.6/python-apt_2.0.0ubuntu0.20.04.6.tar.xz; sys.platform == "linux" PyNaCl==1.4.0; sys.platform != "linux" PyNaCl @ https://files.pythonhosted.org/packages/61/ab/2ac6dea8489fa713e2b4c6c5b549cc962dd4a842b5998d9e80cf8440b7cd/PyNaCl-1.3.0.tar.gz; sys.platform == "linux" - +setuptools==49.6.0 diff --git a/setup.py b/setup.py index 39d83ed76d..a6e9b23ab1 100755 --- a/setup.py +++ b/setup.py @@ -103,6 +103,7 @@ def recursive_data_files(directory, install_directory): "requests", "simplejson", "tabulate", + "toml", "tinydb", "typing-extensions", ] diff --git a/tools/freeze-requirements.sh b/tools/freeze-requirements.sh index b88d7d6cb0..cbcc662691 100755 --- a/tools/freeze-requirements.sh +++ b/tools/freeze-requirements.sh @@ -5,8 +5,7 @@ requirements_fixups() { # Python apt library pinned to source. sed -i '/python-apt=*/d' "$req_file" - echo 'python-apt @ http://archive.ubuntu.com/ubuntu/pool/main/p/python-apt/python-apt_1.6.5ubuntu0.5.tar.xz; sys.platform == "linux"' >> "$req_file" - echo 'python-distutils-extra @ https://launchpad.net/python-distutils-extra/trunk/2.39/+download/python-distutils-extra-2.39.tar.gz; sys_platform == "linux"' >> "$req_file" + echo 'python-apt @ https://launchpad.net/ubuntu/+archive/primary/+sourcefiles/python-apt/2.0.0ubuntu0.20.04.6/python-apt_2.0.0ubuntu0.20.04.6.tar.xz; sys.platform == "linux"' >> "$req_file" # PyNaCl 1.4.0 has crypto related symbol issues when using the system # provided sodium. Ensure it is compiled on linux by pointing to source. @@ -15,7 +14,7 @@ requirements_fixups() { echo 'PyNaCl @ https://files.pythonhosted.org/packages/61/ab/2ac6dea8489fa713e2b4c6c5b549cc962dd4a842b5998d9e80cf8440b7cd/PyNaCl-1.3.0.tar.gz; sys.platform == "linux"' >> "$req_file" # https://bugs.launchpad.net/ubuntu/+source/python-pip/+bug/1635463 - sed -i '/pkg-resources==0.0.0/d' "$req_file" + sed -i '/pkg[-_]resources==0.0.0/d' "$req_file" # We updated setuptools in venv, forget it. sed -i '/setuptools/d' "$req_file" From 7f80c18c9d5173d20c888d06c53e1a799d78c7ec Mon Sep 17 00:00:00 2001 From: Claudio Matsuoka Date: Mon, 10 Jan 2022 14:21:56 -0300 Subject: [PATCH 004/167] appveyor: add missing package metadata Signed-off-by: Claudio Matsuoka --- appveyor.yml | 2 +- snapcraft.spec | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index eb0c1691e6..b8c1a0e54f 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -27,7 +27,7 @@ build_script: - cmd: | echo "Building snapcraft.exe..." venv\Scripts\activate.bat - pyinstaller.exe --onefile snapcraft.spec + pyinstaller.exe --copy-metadata lazr.restfulclient --onefile snapcraft.spec venv\Scripts\deactivate.bat echo "Test signing snapcraft.exe..." diff --git a/snapcraft.spec b/snapcraft.spec index 903a33291c..5ff5639fbe 100644 --- a/snapcraft.spec +++ b/snapcraft.spec @@ -1,5 +1,5 @@ # -*- mode: python ; coding: utf-8 -*- -from PyInstaller.utils.hooks import collect_data_files +from PyInstaller.utils.hooks import collect_data_files, copy_metadata block_cipher = None @@ -12,6 +12,10 @@ data += collect_data_files("launchpadlib") data += collect_data_files("lazr.restfulclient") data += collect_data_files("lazr.uri") data += collect_data_files("wadllib") +data += copy_metadata("launchpadlib") +data += copy_metadata("lazr.restfulclient") +data += copy_metadata("lazr.uri") +data += copy_metadata("wadllib") a = Analysis( ["snapcraft_legacy\\cli\\__main__.py"], From b7c08a876e75a921fead24da7aa8ff11fd1db6f1 Mon Sep 17 00:00:00 2001 From: Sergio Schvezov Date: Tue, 11 Jan 2022 16:03:50 -0300 Subject: [PATCH 005/167] snap: do not refresh already installed snaps Signed-off-by: Sergio Schvezov --- snapcraft_legacy/internal/repo/snaps.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/snapcraft_legacy/internal/repo/snaps.py b/snapcraft_legacy/internal/repo/snaps.py index dfcf4db691..75e286c5fc 100644 --- a/snapcraft_legacy/internal/repo/snaps.py +++ b/snapcraft_legacy/internal/repo/snaps.py @@ -303,8 +303,6 @@ def install_snaps(snaps_list: Union[Sequence[str], Set[str]]) -> List[str]: if not snap_pkg.installed: snap_pkg.install() - elif snap_pkg.get_current_channel() != snap_pkg.channel: - snap_pkg.refresh() snaps_installed.append( "{}={}".format(snap_pkg.name, snap_pkg.get_local_snap_info()["revision"]) From f96915bf98c5fdf79ee29459b9a70abef8a43851 Mon Sep 17 00:00:00 2001 From: Claudio Matsuoka Date: Tue, 11 Jan 2022 13:40:37 -0300 Subject: [PATCH 006/167] cli: create new snapcraft entry point Set a new entry point to the snapcraft application. This currently invokes the legacy implementation in all cases (future PRs will add logic to decide between new and legacy code execution). Signed-off-by: Claudio Matsuoka --- setup.py | 5 ++++- snapcraft/cli.py | 26 ++++++++++++++++++++++++++ snapcraft_legacy/cli/legacy.py | 23 +++++++++++++++++++++++ 3 files changed, 53 insertions(+), 1 deletion(-) create mode 100644 snapcraft/cli.py create mode 100644 snapcraft_legacy/cli/legacy.py diff --git a/setup.py b/setup.py index a6e9b23ab1..6c94a3de98 100755 --- a/setup.py +++ b/setup.py @@ -141,7 +141,10 @@ def recursive_data_files(directory, install_directory): classifiers=classifiers, scripts=scripts, entry_points=dict( - console_scripts=["snapcraft = snapcraft_legacy.cli.__main__:run"] + console_scripts=[ + "snapcraft_legacy = snapcraft_legacy.cli.__main__:run", + "snapcraft = snapcraft.cli:run", + ] ), data_files=( recursive_data_files("schema", "share/snapcraft") diff --git a/snapcraft/cli.py b/snapcraft/cli.py new file mode 100644 index 0000000000..5ff33e5c2c --- /dev/null +++ b/snapcraft/cli.py @@ -0,0 +1,26 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright (C) 2022 Canonical Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +"""Command-line application entry point.""" + +from typing import Optional, Sequence + +from snapcraft_legacy.cli import legacy + + +def run(argv: Optional[Sequence] = None): + """Run the CLI.""" + legacy.legacy_run() diff --git a/snapcraft_legacy/cli/legacy.py b/snapcraft_legacy/cli/legacy.py new file mode 100644 index 0000000000..ca2c3b4698 --- /dev/null +++ b/snapcraft_legacy/cli/legacy.py @@ -0,0 +1,23 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright (C) 2022 Canonical Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +"""Legacy execution entry points.""" + +from ._runner import run # noqa: F401 + + +def legacy_run(): + run() From 546d1473e3ae0931d7acaddc33ebe0fcf3c567fc Mon Sep 17 00:00:00 2001 From: Claudio Matsuoka Date: Wed, 12 Jan 2022 14:21:01 -0300 Subject: [PATCH 007/167] cli: remove unused parameter Signed-off-by: Claudio Matsuoka --- snapcraft/cli.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/snapcraft/cli.py b/snapcraft/cli.py index 5ff33e5c2c..c06a87be7d 100644 --- a/snapcraft/cli.py +++ b/snapcraft/cli.py @@ -16,11 +16,9 @@ """Command-line application entry point.""" -from typing import Optional, Sequence - from snapcraft_legacy.cli import legacy -def run(argv: Optional[Sequence] = None): +def run(): """Run the CLI.""" legacy.legacy_run() From 6c010ebc28d7490f094501c6f499c2ee5ebf49ab Mon Sep 17 00:00:00 2001 From: Claudio Matsuoka Date: Wed, 12 Jan 2022 14:20:16 -0300 Subject: [PATCH 008/167] setup: unpin linters and add additional linting tools Legacy snapcraft code requires old versions of linting tools such as mypy and flake8. Unpin the existing pinned linting tools and add new linters such as pyright and isort, pointing them to the new snapcraft code base. Signed-off-by: Claudio Matsuoka --- .github/workflows/tests.yaml | 22 +++++++++++++++++++++- Makefile | 32 +++++++++++++++++++++++++++----- requirements-devel.txt | 18 ++++++++++++++---- requirements.txt | 2 +- setup.cfg | 13 +++++-------- setup.py | 13 +++++++++---- tests/{ => legacy}/matchers.py | 0 tests/skip.py | 2 +- 8 files changed, 78 insertions(+), 24 deletions(-) rename tests/{ => legacy}/matchers.py (100%) diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 430639925d..3be16a5f76 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -11,9 +11,15 @@ jobs: fetch-depth: 0 - name: Install dependencies run: | - ./tools/environment-setup-local.sh + sudo apt update + sudo apt install -y python3-pip python3-venv libapt-pkg-dev libyaml-dev xdelta3 shellcheck + python3 -m venv ${HOME}/.venv/snapcraft + source ${HOME}/.venv/snapcraft/bin/activate + pip install -U -r requirements.txt -r requirements-devel.txt + pip install . - name: Run black run: | + source ${HOME}/.venv/snapcraft/bin/activate make test-black - name: Run codespell run: | @@ -23,6 +29,10 @@ jobs: run: | source ${HOME}/.venv/snapcraft/bin/activate make test-flake8 + - name: Run isort + run: | + source ${HOME}/.venv/snapcraft/bin/activate + make test-isort - name: Run mypy run: | source ${HOME}/.venv/snapcraft/bin/activate @@ -30,6 +40,16 @@ jobs: - name: Run shellcheck run: | make test-shellcheck + - name: Run pydocstyle + run: | + source ${HOME}/.venv/snapcraft/bin/activate + make test-pydocstyle + - name: Run pyright + run: | + source ${HOME}/.venv/snapcraft/bin/activate + sudo snap install --classic node + sudo snap install --classic pyright + make test-pyright - name: Run unit tests run: | source ${HOME}/.venv/snapcraft/bin/activate diff --git a/Makefile b/Makefile index 3ea8a2df8c..33df30b6f3 100644 --- a/Makefile +++ b/Makefile @@ -1,3 +1,5 @@ +SOURCES=setup.py snapcraft tests/*.py tests/unit + .PHONY: autoformat-black autoformat-black: black . @@ -8,19 +10,36 @@ freeze-requirements: .PHONY: test-black test-black: - black --check --diff . + black --check --diff $(SOURCES) .PHONY: test-codespell test-codespell: - codespell --quiet-level 4 --ignore-words-list crate,keyserver --skip '*.tar,*.xz,*.zip,*.bz2,*.7z,*.gz,*.deb,*.rpm,*.snap,*.gpg,*.pyc,*.png,*.ico,*.jar,changelog,.git,.hg,.mypy_cache,.tox,.venv,_build,buck-out,__pycache__,build,dist,.vscode,parts,stage,prime,test_appstream.py,./snapcraft.spec,./.direnv,./.pytest_cache' + codespell --quiet-level 4 --ignore-words-list crate,keyserver --skip '*.tar,*.xz,*.zip,*.bz2,*.7z,*.gz,*.deb,*.rpm,*.snap,*.gpg,*.pyc,*.png,*.ico,*.jar,*.so,changelog,.git,.hg,.mypy_cache,.tox,.venv,_build,buck-out,__pycache__,build,dist,.vscode,parts,stage,prime,test_appstream.py,./snapcraft.spec,./.direnv,./.pytest_cache' .PHONY: test-flake8 test-flake8: - python3 -m flake8 . + python3 -m flake8 $(SOURCES) + +.PHONY: test-isort +test-isort: + isort --check $(SOURCES) .PHONY: test-mypy test-mypy: - mypy . + mypy $(SOURCES) + +.PHONY: test-pydocstyle +test-pydocstyle: + pydocstyle snapcraft + +.PHONY: test-pylint +test-pylint: + pylint snapcraft + pylint tests/*.py tests/unit --disable=invalid-name,missing-module-docstring,missing-function-docstring,no-self-use,duplicate-code,protected-access,unspecified-encoding + +.PHONY: test-pyright +test-pyright: + pyright $(SOURCES) .PHONY: test-shellcheck test-shellcheck: @@ -40,4 +59,7 @@ test-units: test-legacy-units tests: tests-static test-units .PHONY: tests-static -tests-static: test-black test-codespell test-flake8 test-mypy test-shellcheck +tests-static: test-black test-codespell test-flake8 test-isort test-mypy test-pydocstyle test-pyright test-pylint test-shellcheck + +.PHONY: lint +lint: tests-static diff --git a/requirements-devel.txt b/requirements-devel.txt index 8882002264..c83c21bebd 100644 --- a/requirements-devel.txt +++ b/requirements-devel.txt @@ -1,4 +1,6 @@ +astroid==2.8.6 attrs==21.4.0 +black==21.12b0 catkin-pkg==0.4.24 certifi==2021.10.8 cffi==1.15.0 @@ -27,29 +29,36 @@ keyring==23.5.0 launchpadlib==1.10.15.1 lazr.restfulclient==0.14.4 lazr.uri==1.0.6 +lazy-object-proxy==1.7.1 lxml==4.7.1 macaroonbakery==1.3.1 mccabe==0.6.1 -mypy==0.770 +mypy==0.931 mypy-extensions==0.4.3 oauthlib==3.1.1 packaging==21.3 PasteDeploy==2.1.1 +pathspec==0.9.0 pbr==5.8.0 pexpect==4.8.0 plaster==1.0 plaster-pastedeploy==0.7 +platformdirs==2.4.1 pluggy==1.0.0 progressbar==2.5 -protobuf==3.19.1 +protobuf==3.19.3 psutil==5.9.0 ptyprocess==0.7.0 py==1.11.0 pycodestyle==2.5.0 pycparser==2.21 +pydocstyle==6.1.1 pyelftools==0.27 pyflakes==2.1.1 pyftpdlib==1.5.6 +pylint==2.11.1 +pylint-fixme-info==1.0.3 +pylint-pytest==1.1.2 pylxd==2.3.1 pymacaroons==0.13.0 pyparsing==3.0.6 @@ -71,19 +80,20 @@ SecretStorage==3.3.1 semantic-version==2.8.5 simplejson==3.17.6 six==1.16.0 +snowballstemmer==2.2.0 tabulate==0.8.9 testscenarios==0.5.0 testtools==2.5.0 tinydb==4.5.2 toml==0.10.2 -tomli==2.0.0 +tomli==1.2.3 translationstring==1.4 -typed-ast==1.4.3 typing_extensions==4.0.1 urllib3==1.26.8 venusian==3.0.0 wadllib==1.3.6 WebOb==1.8.7 +wrapt==1.13.3 ws4py==0.5.1 zipp==3.7.0 zope.deprecation==4.4.0 diff --git a/requirements.txt b/requirements.txt index 3cda19086c..34bf26ab3e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -23,7 +23,7 @@ macaroonbakery==1.3.1 mypy-extensions==0.4.3 oauthlib==3.1.1 progressbar==2.5 -protobuf==3.19.1 +protobuf==3.19.3 psutil==5.9.0 pycparser==2.21 pyelftools==0.27 diff --git a/setup.cfg b/setup.cfg index 599afeeb6e..f927778db3 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,12 +1,9 @@ [flake8] -ignore = - # let black handle this - E501, - # http://hexbyteinc.com/ambv-black/#line-breaks--binary-operators - W503, - # http://hexbyteinc.com/ambv-black/#slices - E203 +# E501 line too long +# E203 whitespace before ':' +extend-ignore = E203, E501 max-complexity = 10 +max-line-length = 88 exclude = # No need to traverse our git directory .direnv, @@ -35,5 +32,5 @@ follow_imports = silent [pycodestyle] max-line-length = 88 -ignore = E501,W503,E203 +ignore = E203,E501 diff --git a/setup.py b/setup.py index 6c94a3de98..6617ff2746 100755 --- a/setup.py +++ b/setup.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- # -# Copyright (C) 2015-2021 Canonical Ltd +# Copyright 2015-2022 Canonical Ltd. # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License version 3 as @@ -59,19 +59,24 @@ def recursive_data_files(directory, install_directory): scripts = [] dev_requires = [ + "black", "codespell", "coverage", - "flake8==3.7.9", + "flake8", "pyflakes==2.1.1", "fixtures", "isort", "mccabe", - "mypy==0.770", + "mypy", "testscenarios", "pexpect", "pip", - "pycodestyle==2.5.0", + "pycodestyle", + "pydocstyle", "pyftpdlib", + "pylint<2.12.0", + "pylint-fixme-info", + "pylint-pytest", "pyramid", "pytest", "pytest-cov", diff --git a/tests/matchers.py b/tests/legacy/matchers.py similarity index 100% rename from tests/matchers.py rename to tests/legacy/matchers.py diff --git a/tests/skip.py b/tests/skip.py index a1e94a7f6d..3a1de86222 100644 --- a/tests/skip.py +++ b/tests/skip.py @@ -22,7 +22,7 @@ def skip_unless_codename(codename, message: str) -> Callable[..., Callable[..., None]]: - if type(codename) is str: + if isinstance(codename, str): codename = [codename] def _wrap(func: Callable[..., None]) -> Callable[..., None]: From 1bbe09e88b37ddcc811a261657ea21684f287f3f Mon Sep 17 00:00:00 2001 From: Claudio Matsuoka Date: Mon, 17 Jan 2022 11:01:23 -0300 Subject: [PATCH 009/167] cli: integrate craft-cli and add version command Handle message output and command line arguments parsing using craft-cli. Implement the version command and fall back to the legacy snapcraft implementation to handle all other functionality. Signed-off-by: Claudio Matsuoka --- snapcraft/__init__.py | 33 +++++++++++++++++++++++++++ snapcraft/__main__.py | 21 +++++++++++++++++ snapcraft/cli.py | 40 +++++++++++++++++++++++++++++++-- snapcraft/commands/__init__.py | 19 ++++++++++++++++ snapcraft/commands/version.py | 34 ++++++++++++++++++++++++++++ snapcraft_legacy/cli/version.py | 2 +- 6 files changed, 146 insertions(+), 3 deletions(-) create mode 100644 snapcraft/__init__.py create mode 100644 snapcraft/__main__.py create mode 100644 snapcraft/commands/__init__.py create mode 100644 snapcraft/commands/version.py diff --git a/snapcraft/__init__.py b/snapcraft/__init__.py new file mode 100644 index 0000000000..65136265f5 --- /dev/null +++ b/snapcraft/__init__.py @@ -0,0 +1,33 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright 2022 Canonical Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +"""Publish your app for Linux users for desktop, cloud, and IoT.""" + +import os + +import pkg_resources # type: ignore + + +def _get_version(): + if os.environ.get("SNAP_NAME") == "snapcraft": + return os.environ["SNAP_VERSION"] + try: + return pkg_resources.require("snapcraft")[0].version + except pkg_resources.DistributionNotFound: + return "devel" + + +__version__ = _get_version() diff --git a/snapcraft/__main__.py b/snapcraft/__main__.py new file mode 100644 index 0000000000..cfc0de29be --- /dev/null +++ b/snapcraft/__main__.py @@ -0,0 +1,21 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright 2022 Canonical Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +"""Main entry point.""" + +from snapcraft import cli + +cli.run() diff --git a/snapcraft/cli.py b/snapcraft/cli.py index c06a87be7d..d7b81d3d55 100644 --- a/snapcraft/cli.py +++ b/snapcraft/cli.py @@ -1,6 +1,6 @@ # -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- # -# Copyright (C) 2022 Canonical Ltd. +# Copyright 2022 Canonical Ltd. # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License version 3 as @@ -16,9 +16,45 @@ """Command-line application entry point.""" +import sys + +import craft_cli +from craft_cli import EmitterMode, emit + +from snapcraft import __version__ from snapcraft_legacy.cli import legacy +from . import commands + +COMMAND_GROUPS = [ + craft_cli.CommandGroup("Basic", [commands.VersionCommand]), +] + +GLOBAL_ARGS = [ + craft_cli.GlobalArgument( + "version", "flag", "-V", "--version", "Show the application version and exit" + ) +] + def run(): """Run the CLI.""" - legacy.legacy_run() + emit.init(EmitterMode.NORMAL, "snapcraft", f"Starting Snapcraft {__version__}") + dispatcher = craft_cli.Dispatcher( + "snapcraft", + COMMAND_GROUPS, + summary="What's the app about", + extra_global_args=GLOBAL_ARGS, + ) + + try: + global_args = dispatcher.pre_parse_args(sys.argv[1:]) + if global_args.get("version"): + emit.message(f"snapcraft {__version__}") + else: + dispatcher.load_command(None) + dispatcher.run() + emit.ended_ok() + except craft_cli.ArgumentParsingError: + emit.ended_ok() + legacy.legacy_run() diff --git a/snapcraft/commands/__init__.py b/snapcraft/commands/__init__.py new file mode 100644 index 0000000000..da62bf2850 --- /dev/null +++ b/snapcraft/commands/__init__.py @@ -0,0 +1,19 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright 2022 Canonical Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +"""Snapcraft commands.""" + +from .version import VersionCommand # noqa: F401 diff --git a/snapcraft/commands/version.py b/snapcraft/commands/version.py new file mode 100644 index 0000000000..79bbb30247 --- /dev/null +++ b/snapcraft/commands/version.py @@ -0,0 +1,34 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright 2022 Canonical Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +"""Snapcraft version command.""" + +from craft_cli import BaseCommand, emit + +from snapcraft import __version__ + + +class VersionCommand(BaseCommand): + """Show the snapcraft version.""" + + name = "version" + help_msg = "Show the application version and exit" + overview = "overview" + common = True + + def run(self, parsed_args): + """Run the command.""" + emit.message(f"snapcraft {__version__}") diff --git a/snapcraft_legacy/cli/version.py b/snapcraft_legacy/cli/version.py index e39e497b92..054445490d 100644 --- a/snapcraft_legacy/cli/version.py +++ b/snapcraft_legacy/cli/version.py @@ -18,7 +18,7 @@ import snapcraft_legacy -SNAPCRAFT_VERSION_TEMPLATE = "snapcraft, version %(version)s" +SNAPCRAFT_VERSION_TEMPLATE = "snapcraft %(version)s" @click.group() From 268a7c35a4ab8d419b68a8d4661551ca7f22963f Mon Sep 17 00:00:00 2001 From: Claudio Matsuoka Date: Wed, 19 Jan 2022 16:03:16 -0300 Subject: [PATCH 010/167] requirements: update dependencies Signed-off-by: Claudio Matsuoka --- requirements-devel.txt | 13 ++++++++----- requirements.txt | 13 ++++++++----- setup.py | 1 + 3 files changed, 17 insertions(+), 10 deletions(-) diff --git a/requirements-devel.txt b/requirements-devel.txt index c83c21bebd..19e7fa8da3 100644 --- a/requirements-devel.txt +++ b/requirements-devel.txt @@ -9,6 +9,7 @@ charset-normalizer==2.0.10 click==8.0.3 codespell==2.1.0 coverage==6.2 +craft-cli==0.1.0 cryptography==3.4 distro==1.6.0 docutils==0.18.1 @@ -20,13 +21,13 @@ gnupg==2.3.1 httplib2==0.20.2 hupper==1.10.3 idna==3.3 -importlib-metadata==4.10.0 +importlib-metadata==4.10.1 iniconfig==1.1.1 isort==5.10.1 jeepney==0.7.1 jsonschema==2.5.1 keyring==23.5.0 -launchpadlib==1.10.15.1 +launchpadlib==1.10.16 lazr.restfulclient==0.14.4 lazr.uri==1.0.6 lazy-object-proxy==1.7.1 @@ -52,6 +53,7 @@ ptyprocess==0.7.0 py==1.11.0 pycodestyle==2.5.0 pycparser==2.21 +pydantic==1.9.0 pydocstyle==6.1.1 pyelftools==0.27 pyflakes==2.1.1 @@ -61,14 +63,15 @@ pylint-fixme-info==1.0.3 pylint-pytest==1.1.2 pylxd==2.3.1 pymacaroons==0.13.0 -pyparsing==3.0.6 +pyparsing==3.0.7 pyramid==2.0 pyRFC3339==1.1 pytest==6.2.5 pytest-cov==3.0.0 +pytest-mock==3.6.1 pytest-subprocess==1.3.2 python-dateutil==2.8.2 -python-debian==0.1.42 +python-debian==0.1.43 pytz==2021.3 pyxdg==0.27 PyYAML==5.3 @@ -84,7 +87,7 @@ snowballstemmer==2.2.0 tabulate==0.8.9 testscenarios==0.5.0 testtools==2.5.0 -tinydb==4.5.2 +tinydb==4.6.1 toml==0.10.2 tomli==1.2.3 translationstring==1.4 diff --git a/requirements.txt b/requirements.txt index 34bf26ab3e..af13f23390 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,34 +5,37 @@ cffi==1.15.0 chardet==4.0.0 charset-normalizer==2.0.10 click==8.0.3 +craft-cli==0.1.0 cryptography==3.4 distro==1.6.0 docutils==0.18.1 gnupg==2.3.1 httplib2==0.20.2 idna==3.3 -importlib-metadata==4.10.0 +importlib-metadata==4.10.1 jeepney==0.7.1 jsonschema==2.5.1 keyring==23.5.0 -launchpadlib==1.10.15.1 +launchpadlib==1.10.16 lazr.restfulclient==0.14.4 lazr.uri==1.0.6 lxml==4.7.1 macaroonbakery==1.3.1 mypy-extensions==0.4.3 oauthlib==3.1.1 +platformdirs==2.4.1 progressbar==2.5 protobuf==3.19.3 psutil==5.9.0 pycparser==2.21 +pydantic==1.9.0 pyelftools==0.27 pylxd==2.3.1 pymacaroons==0.13.0 -pyparsing==3.0.6 +pyparsing==3.0.7 pyRFC3339==1.1 python-dateutil==2.8.2 -python-debian==0.1.42 +python-debian==0.1.43 pytz==2021.3 pyxdg==0.27 PyYAML==5.3 @@ -45,7 +48,7 @@ semantic-version==2.8.5 simplejson==3.17.6 six==1.16.0 tabulate==0.8.9 -tinydb==4.5.2 +tinydb==4.6.1 toml==0.10.2 typing_extensions==4.0.1 urllib3==1.26.8 diff --git a/setup.py b/setup.py index 6617ff2746..526b6c30a4 100755 --- a/setup.py +++ b/setup.py @@ -89,6 +89,7 @@ def recursive_data_files(directory, install_directory): install_requires = [ "attrs", "click", + "craft-cli", "cryptography==3.4", "gnupg", "jsonschema==2.5.1", From 6b86de0c6d568404a1c46973b5df6839046573ed Mon Sep 17 00:00:00 2001 From: Claudio Matsuoka Date: Wed, 19 Jan 2022 16:52:52 -0300 Subject: [PATCH 011/167] appveyor: update python version to 3.8 This is required by craft-cli (and craft-providers later). Signed-off-by: Claudio Matsuoka --- appveyor.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index b8c1a0e54f..bc0ec1dc5c 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -7,7 +7,7 @@ environment: TIMESTAMP_SERVICE: http://timestamp.digicert.com matrix: - - PYTHON: C:\Python37-x64 + - PYTHON: C:\Python38-x64 cache: - '%LOCALAPPDATA%\pip\Cache\http' From 5bb34d6f9e5d185e36f54438ca23c096cedecccc Mon Sep 17 00:00:00 2001 From: Claudio Matsuoka Date: Wed, 19 Jan 2022 18:07:13 -0300 Subject: [PATCH 012/167] cli: add unit tests Signed-off-by: Claudio Matsuoka --- pyproject.toml | 6 ++ setup.py | 1 + tests/unit/commands/__init__.py | 0 tests/unit/commands/test_version.py | 24 +++++++ tests/unit/conftest.py | 99 +++++++++++++++++++++++++++++ tests/unit/test_cli.py | 44 +++++++++++++ 6 files changed, 174 insertions(+) create mode 100644 tests/unit/commands/__init__.py create mode 100644 tests/unit/commands/test_version.py create mode 100644 tests/unit/conftest.py create mode 100644 tests/unit/test_cli.py diff --git a/pyproject.toml b/pyproject.toml index 3f03696340..d195478a13 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,3 +29,9 @@ force_grid_wrap = 0 use_parentheses = true ensure_newline_before_comments = true line_length = 88 + +[tool.pylint.messages_control] +disable = "fixme" + +[tool.pylint.MASTER] +load-plugins = "pylint_fixme_info,pylint_pytest" diff --git a/setup.py b/setup.py index 526b6c30a4..61944db8d2 100755 --- a/setup.py +++ b/setup.py @@ -80,6 +80,7 @@ def recursive_data_files(directory, install_directory): "pyramid", "pytest", "pytest-cov", + "pytest-mock", "pytest-subprocess", ] diff --git a/tests/unit/commands/__init__.py b/tests/unit/commands/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/unit/commands/test_version.py b/tests/unit/commands/test_version.py new file mode 100644 index 0000000000..7af1f59a5c --- /dev/null +++ b/tests/unit/commands/test_version.py @@ -0,0 +1,24 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright 2022 Canonical Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from snapcraft import __version__ +from snapcraft.commands.version import VersionCommand + + +def test_version_command(emitter): + cmd = VersionCommand(None) + cmd.run([]) + emitter.assert_recorded([f"snapcraft {__version__}"]) diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py new file mode 100644 index 0000000000..88c52c2bab --- /dev/null +++ b/tests/unit/conftest.py @@ -0,0 +1,99 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright 2022 Canonical Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import os +import tempfile +from pathlib import Path + +import pytest +from craft_cli import messages + + +class RecordingEmitter: + """Record what is shown using the emitter and provide a nice API for tests.""" + + def __init__(self): + self.progress = [] + self.message = [] + self.trace = [] + self.emitted = [] + self.raw = [] + + def record(self, level, text): + """Record the text for the specific level and in the general storages.""" + getattr(self, level).append(text) + self.emitted.append(text) + self.raw.append((level, text)) + + def _check(self, expected, storage): + """Really verify messages.""" + for pos, recorded_msg in enumerate(storage): + if recorded_msg == expected[0]: + break + else: + raise AssertionError(f"Initial test message not found in {storage}") + + recorded = storage[pos : pos + len(expected)] # pylint: disable=W0631 + assert recorded == expected + + def assert_recorded(self, expected): + """Verify that the given messages were recorded consecutively.""" + self._check(expected, self.emitted) + + def assert_recorded_raw(self, expected): + """Verify that the given messages (with specific level) were recorded consecutively.""" + self._check(expected, self.raw) + + +@pytest.fixture(autouse=True) +def init_emitter(): + """Ensure emit is always clean, and initted (in test mode). + Note that the `init` is done in the current instance that all modules already + acquired. + """ + # init with a custom log filepath so user directories are not involved here; note that + # we're not using pytest's standard tmp_path as Emitter would write logs there, and in + # effect we would be polluting that temporary directory (potentially messing with + # tests, that may need that empty), so we use another one. + temp_fd, temp_logfile = tempfile.mkstemp(prefix="emitter-logs") + os.close(temp_fd) + temp_logfile = Path(temp_logfile) + + messages.TESTMODE = True + messages.emit.init( + messages.EmitterMode.QUIET, + "test-emitter", + "Hello world", + log_filepath=temp_logfile, + ) + yield + # end machinery (just in case it was not ended before; note it's ok to "double end") + messages.emit.ended_ok() + + +@pytest.fixture +def emitter(monkeypatch): + """Helper to test everything that was shown using craft-cli Emitter.""" + rec = RecordingEmitter() + monkeypatch.setattr( + messages.emit, "message", lambda text, **k: rec.record("message", text) + ) + monkeypatch.setattr( + messages.emit, "progress", lambda text: rec.record("progress", text) + ) + monkeypatch.setattr(messages.emit, "trace", lambda text: rec.record("trace", text)) + + return rec diff --git a/tests/unit/test_cli.py b/tests/unit/test_cli.py new file mode 100644 index 0000000000..70cd908d89 --- /dev/null +++ b/tests/unit/test_cli.py @@ -0,0 +1,44 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright 2022 Canonical Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import sys + +import pytest + +from snapcraft import cli + + +def test_version_command(mocker): + mocker.patch.object(sys, "argv", ["cmd", "version"]) + mock_version_cmd = mocker.patch("snapcraft.commands.VersionCommand") + cli.run() + assert mock_version_cmd.mock_calls == [] + + +def test_version_argument(mocker): + mocker.patch.object(sys, "argv", ["cmd", "--version"]) + # FIXME: handled by legacy, change after craft-cli handles default command + mock_version_cmd = mocker.patch("snapcraft_legacy.cli.version.version") + with pytest.raises(SystemExit): + cli.run() + assert mock_version_cmd.mock_calls == [] + + +def test_version_argment_with_command(mocker): + mocker.patch.object(sys, "argv", ["cmd", "--version", "version"]) + mock_version_cmd = mocker.patch("snapcraft.commands.VersionCommand") + cli.run() + assert mock_version_cmd.mock_calls == [] From ab9f976f9950211bac0a34d24c3b65d907836741 Mon Sep 17 00:00:00 2001 From: Claudio Matsuoka Date: Mon, 24 Jan 2022 09:39:37 -0300 Subject: [PATCH 013/167] cli: move version to the "other" command group Signed-off-by: Claudio Matsuoka --- snapcraft/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/snapcraft/cli.py b/snapcraft/cli.py index d7b81d3d55..b2d1f4db38 100644 --- a/snapcraft/cli.py +++ b/snapcraft/cli.py @@ -27,7 +27,7 @@ from . import commands COMMAND_GROUPS = [ - craft_cli.CommandGroup("Basic", [commands.VersionCommand]), + craft_cli.CommandGroup("Other", [commands.VersionCommand]), ] GLOBAL_ARGS = [ From 79e53d8a4aa13b48c1e821a4b9ed8c767019e5a3 Mon Sep 17 00:00:00 2001 From: Claudio Matsuoka Date: Mon, 24 Jan 2022 09:53:34 -0300 Subject: [PATCH 014/167] requirements: add types-setuptools Signed-off-by: Claudio Matsuoka --- requirements-devel.txt | 1 + setup.py | 1 + snapcraft/__init__.py | 2 +- tools/freeze-requirements.sh | 2 +- 4 files changed, 4 insertions(+), 2 deletions(-) diff --git a/requirements-devel.txt b/requirements-devel.txt index 19e7fa8da3..0d25662cbd 100644 --- a/requirements-devel.txt +++ b/requirements-devel.txt @@ -91,6 +91,7 @@ tinydb==4.6.1 toml==0.10.2 tomli==1.2.3 translationstring==1.4 +types-setuptools==57.4.7 typing_extensions==4.0.1 urllib3==1.26.8 venusian==3.0.0 diff --git a/setup.py b/setup.py index 61944db8d2..3455092531 100755 --- a/setup.py +++ b/setup.py @@ -82,6 +82,7 @@ def recursive_data_files(directory, install_directory): "pytest-cov", "pytest-mock", "pytest-subprocess", + "types-setuptools", ] if sys.platform == "win32": diff --git a/snapcraft/__init__.py b/snapcraft/__init__.py index 65136265f5..6c9a0a516e 100644 --- a/snapcraft/__init__.py +++ b/snapcraft/__init__.py @@ -18,7 +18,7 @@ import os -import pkg_resources # type: ignore +import pkg_resources def _get_version(): diff --git a/tools/freeze-requirements.sh b/tools/freeze-requirements.sh index cbcc662691..5f0279c9af 100755 --- a/tools/freeze-requirements.sh +++ b/tools/freeze-requirements.sh @@ -17,7 +17,7 @@ requirements_fixups() { sed -i '/pkg[-_]resources==0.0.0/d' "$req_file" # We updated setuptools in venv, forget it. - sed -i '/setuptools/d' "$req_file" + sed -i '/^setuptools/d' "$req_file" echo 'setuptools==49.6.0' >> "$req_file" # Pinned pyinstaller for windows. From aac15016bb14520ddf3b9f33ace90a0cfbe23390 Mon Sep 17 00:00:00 2001 From: Claudio Matsuoka Date: Mon, 24 Jan 2022 09:56:52 -0300 Subject: [PATCH 015/167] tests: add note to use craft-cli fixtures once available Signed-off-by: Claudio Matsuoka --- tests/unit/conftest.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py index 88c52c2bab..d320059d6c 100644 --- a/tests/unit/conftest.py +++ b/tests/unit/conftest.py @@ -22,6 +22,7 @@ from craft_cli import messages +# XXX: This can be removed once testing fixtures are provided by craft-cli. class RecordingEmitter: """Record what is shown using the emitter and provide a nice API for tests.""" From 4ed04d04c1fb58aa36c9445e0618257d2a4ba1b5 Mon Sep 17 00:00:00 2001 From: Claudio Matsuoka Date: Mon, 24 Jan 2022 09:36:28 -0300 Subject: [PATCH 016/167] cli: let help be handled by legacy The help message will eventually be handled by the new implementation, however it's still incomplete and provides very little information about valid command line arguments. Signed-off-by: Claudio Matsuoka --- snapcraft/cli.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/snapcraft/cli.py b/snapcraft/cli.py index b2d1f4db38..93be0b6eae 100644 --- a/snapcraft/cli.py +++ b/snapcraft/cli.py @@ -39,6 +39,11 @@ def run(): """Run the CLI.""" + # Let legacy snapcraft handle --help until we have all command stubs registered + # in craft-cli. + if "-h" in sys.argv or "--help" in sys.argv: + legacy.legacy_run() + emit.init(EmitterMode.NORMAL, "snapcraft", f"Starting Snapcraft {__version__}") dispatcher = craft_cli.Dispatcher( "snapcraft", From 2759ad4afe5b85cb1393c417320358a33c332f40 Mon Sep 17 00:00:00 2001 From: Claudio Matsuoka Date: Thu, 27 Jan 2022 17:56:42 -0300 Subject: [PATCH 017/167] cli: add lifecycle command stubs Add handlers for the pull, build, stage, and prime commands. These will be later used do run the parts lifecycle. All other commands fall back to their legacy implementations. Also moved CLI tests to a separate package for better organization, and fix existing tests for the version command. Signed-off-by: Claudio Matsuoka --- setup.cfg | 5 + setup.py | 1 + snapcraft/cli.py | 13 ++- snapcraft/commands/__init__.py | 6 + snapcraft/commands/lifecycle.py | 103 ++++++++++++++++++ snapcraft/commands/version.py | 2 +- snapcraft/errors.py | 30 +++++ tests/unit/cli/test_lifecycle.py | 57 ++++++++++ .../unit/{test_cli.py => cli/test_version.py} | 18 +-- 9 files changed, 224 insertions(+), 11 deletions(-) create mode 100644 snapcraft/commands/lifecycle.py create mode 100644 snapcraft/errors.py create mode 100644 tests/unit/cli/test_lifecycle.py rename tests/unit/{test_cli.py => cli/test_version.py} (69%) diff --git a/setup.cfg b/setup.cfg index f927778db3..13ca4fb041 100644 --- a/setup.cfg +++ b/setup.cfg @@ -34,3 +34,8 @@ follow_imports = silent max-line-length = 88 ignore = E203,E501 +[pydocstyle] +# D107 Missing docstring in __init__ (reason: documented in class docstring) +# D203 1 blank line required before class docstring (reason: pep257 default) +ignore = D107, D203 + diff --git a/setup.py b/setup.py index 3455092531..d35c055335 100755 --- a/setup.py +++ b/setup.py @@ -100,6 +100,7 @@ def recursive_data_files(directory, install_directory): "lxml", "macaroonbakery", "mypy-extensions", + "overrides", "progressbar", "pyelftools", "pymacaroons", diff --git a/snapcraft/cli.py b/snapcraft/cli.py index 93be0b6eae..0a7d559eee 100644 --- a/snapcraft/cli.py +++ b/snapcraft/cli.py @@ -21,12 +21,21 @@ import craft_cli from craft_cli import EmitterMode, emit -from snapcraft import __version__ +from snapcraft import __version__, errors from snapcraft_legacy.cli import legacy from . import commands COMMAND_GROUPS = [ + craft_cli.CommandGroup( + "Lifecycle", + [ + commands.PullCommand, + commands.BuildCommand, + commands.StageCommand, + commands.PrimeCommand, + ], + ), craft_cli.CommandGroup("Other", [commands.VersionCommand]), ] @@ -60,6 +69,8 @@ def run(): dispatcher.load_command(None) dispatcher.run() emit.ended_ok() + except errors.SnapcraftError as err: + emit.error(err) except craft_cli.ArgumentParsingError: emit.ended_ok() legacy.legacy_run() diff --git a/snapcraft/commands/__init__.py b/snapcraft/commands/__init__.py index da62bf2850..b4d2ca060c 100644 --- a/snapcraft/commands/__init__.py +++ b/snapcraft/commands/__init__.py @@ -16,4 +16,10 @@ """Snapcraft commands.""" +from .lifecycle import ( # noqa: F401 + BuildCommand, + PrimeCommand, + PullCommand, + StageCommand, +) from .version import VersionCommand # noqa: F401 diff --git a/snapcraft/commands/lifecycle.py b/snapcraft/commands/lifecycle.py new file mode 100644 index 0000000000..802ccf794d --- /dev/null +++ b/snapcraft/commands/lifecycle.py @@ -0,0 +1,103 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright 2022 Canonical Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +"""Snapcraft lifecycle commands.""" + +import abc +import textwrap +from typing import TYPE_CHECKING + +from craft_cli import BaseCommand, emit +from overrides import overrides + +from snapcraft_legacy.cli import legacy + +if TYPE_CHECKING: + import argparse + + +class _LifecycleCommand(BaseCommand, abc.ABC): + """Run lifecycle commands.""" + + @overrides + def fill_parser(self, parser: "argparse.ArgumentParser") -> None: + parser.add_argument( + "parts", metavar="parts", type=str, nargs="*", help="Parts to process" + ) + + @overrides + def run(self, parsed_args): + """Run the command.""" + if not self.name: + raise RuntimeError("command name not specified") + + emit.trace(f"lifecycle command: {self.name!r}, arguments: {parsed_args!r}") + legacy.legacy_run() + + +class PullCommand(_LifecycleCommand): + """Run the lifecycle up to the pull step.""" + + name = "pull" + help_msg = "Download or retrieve artifacts defined for a part" + overview = textwrap.dedent( + """ + Download or retrieve artifacts defined for a part. If part names + are specified only those parts will be pulled, otherwise all parts + will be pulled. + """ + ) + + +class BuildCommand(_LifecycleCommand): + """Run the lifecycle up to the build step.""" + + name = "build" + help_msg = "Build artifacts defined for a part" + overview = textwrap.dedent( + """ + Build artifacts defined for a part. If part names are specified only + those parts will be built, otherwise all parts will be built. + """ + ) + + +class StageCommand(_LifecycleCommand): + """Run the lifecycle up to the stage step.""" + + name = "stage" + help_msg = "Stage built artifacts into a common staging area" + overview = textwrap.dedent( + """ + Stage built artifacts into a common staging area. If part names are + specified only those parts will be staged. The default is to stage + all parts. + """ + ) + + +class PrimeCommand(_LifecycleCommand): + """Prepare the final payload for packing.""" + + name = "prime" + help_msg = "Build artifacts defined for a part" + overview = textwrap.dedent( + """ + Prepare the final payload to be packed as a snap. If part names are + specified only those parts will be primed. The default is to prime + all parts. + """ + ) diff --git a/snapcraft/commands/version.py b/snapcraft/commands/version.py index 79bbb30247..74e7ac04f7 100644 --- a/snapcraft/commands/version.py +++ b/snapcraft/commands/version.py @@ -26,7 +26,7 @@ class VersionCommand(BaseCommand): name = "version" help_msg = "Show the application version and exit" - overview = "overview" + overview = "Show the application version and exit" common = True def run(self, parsed_args): diff --git a/snapcraft/errors.py b/snapcraft/errors.py new file mode 100644 index 0000000000..28eb501ec8 --- /dev/null +++ b/snapcraft/errors.py @@ -0,0 +1,30 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright 2022 Canonical Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +"""Snapcraft error definitions.""" + +from craft_cli import CraftError + + +class SnapcraftError(CraftError): + """Failure in a Snapcraft operation.""" + + +class FeatureNotImplemented(SnapcraftError): + """Attempt to execute an unimplemented feature.""" + + def __init__(self): + super().__init__("This command or feature is not implemented in this release.") diff --git a/tests/unit/cli/test_lifecycle.py b/tests/unit/cli/test_lifecycle.py new file mode 100644 index 0000000000..73191a75ca --- /dev/null +++ b/tests/unit/cli/test_lifecycle.py @@ -0,0 +1,57 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright 2022 Canonical Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import argparse +import sys +from unittest.mock import call + +import pytest + +from snapcraft import cli + + +@pytest.mark.parametrize( + "cmd,run_method", + [ + ("pull", "snapcraft.commands.lifecycle.PullCommand.run"), + ("build", "snapcraft.commands.lifecycle.BuildCommand.run"), + ("stage", "snapcraft.commands.lifecycle.StageCommand.run"), + ("prime", "snapcraft.commands.lifecycle.PrimeCommand.run"), + ], +) +def test_lifecycle_command(cmd, run_method, mocker): + mocker.patch.object(sys, "argv", ["cmd", cmd]) + mock_lifecycle_cmd = mocker.patch(run_method) + cli.run() + assert mock_lifecycle_cmd.mock_calls == [call(argparse.Namespace(parts=[]))] + + +@pytest.mark.parametrize( + "cmd,run_method", + [ + ("pull", "snapcraft.commands.lifecycle.PullCommand.run"), + ("build", "snapcraft.commands.lifecycle.BuildCommand.run"), + ("stage", "snapcraft.commands.lifecycle.StageCommand.run"), + ("prime", "snapcraft.commands.lifecycle.PrimeCommand.run"), + ], +) +def test_lifecycle_command_arguments(cmd, run_method, mocker): + mocker.patch.object(sys, "argv", ["cmd", cmd, "part1", "part2"]) + mock_lifecycle_cmd = mocker.patch(run_method) + cli.run() + assert mock_lifecycle_cmd.mock_calls == [ + call(argparse.Namespace(parts=["part1", "part2"])) + ] diff --git a/tests/unit/test_cli.py b/tests/unit/cli/test_version.py similarity index 69% rename from tests/unit/test_cli.py rename to tests/unit/cli/test_version.py index 70cd908d89..96556e64e8 100644 --- a/tests/unit/test_cli.py +++ b/tests/unit/cli/test_version.py @@ -14,31 +14,31 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +import argparse import sys +from unittest.mock import call import pytest -from snapcraft import cli +from snapcraft import __version__, cli def test_version_command(mocker): mocker.patch.object(sys, "argv", ["cmd", "version"]) - mock_version_cmd = mocker.patch("snapcraft.commands.VersionCommand") + mock_version_cmd = mocker.patch("snapcraft.commands.version.VersionCommand.run") cli.run() - assert mock_version_cmd.mock_calls == [] + assert mock_version_cmd.mock_calls == [call(argparse.Namespace())] -def test_version_argument(mocker): +def test_version_argument(mocker, capsys): mocker.patch.object(sys, "argv", ["cmd", "--version"]) # FIXME: handled by legacy, change after craft-cli handles default command - mock_version_cmd = mocker.patch("snapcraft_legacy.cli.version.version") with pytest.raises(SystemExit): cli.run() - assert mock_version_cmd.mock_calls == [] + assert capsys.readouterr().out == f"snapcraft {__version__}\n" -def test_version_argment_with_command(mocker): +def test_version_argument_with_command(mocker, emitter): mocker.patch.object(sys, "argv", ["cmd", "--version", "version"]) - mock_version_cmd = mocker.patch("snapcraft.commands.VersionCommand") cli.run() - assert mock_version_cmd.mock_calls == [] + emitter.assert_recorded([f"snapcraft {__version__}"]) From 2913a1f37c090039153be6fbe6b293172eabf535 Mon Sep 17 00:00:00 2001 From: Claudio Matsuoka Date: Mon, 31 Jan 2022 19:21:13 -0300 Subject: [PATCH 018/167] makefile: enable new unit tests Signed-off-by: Claudio Matsuoka --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 33df30b6f3..a782a9c4b1 100644 --- a/Makefile +++ b/Makefile @@ -53,7 +53,7 @@ test-legacy-units: .PHONY: test-units test-units: test-legacy-units - # pytest --cov-report=xml --cov=snapcraft tests/unit + pytest --cov-report=xml --cov=snapcraft tests/unit .PHONY: tests tests: tests-static test-units From 467b12749da5cacfdf845113d1bd7f7ee3f552c2 Mon Sep 17 00:00:00 2001 From: Claudio Matsuoka Date: Mon, 31 Jan 2022 19:24:39 -0300 Subject: [PATCH 019/167] errors: add description of unimplemented feature Signed-off-by: Claudio Matsuoka --- snapcraft/errors.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/snapcraft/errors.py b/snapcraft/errors.py index 28eb501ec8..5ad7103025 100644 --- a/snapcraft/errors.py +++ b/snapcraft/errors.py @@ -24,7 +24,7 @@ class SnapcraftError(CraftError): class FeatureNotImplemented(SnapcraftError): - """Attempt to execute an unimplemented feature.""" + """Attempt to use an unimplemented feature.""" - def __init__(self): - super().__init__("This command or feature is not implemented in this release.") + def __init__(self, msg: str) -> None: + super().__init__(f"Command or feature not implemented: {msg}") From e6b2228a8f9e8f2ea2a03a3432a191c47d0011e6 Mon Sep 17 00:00:00 2001 From: Claudio Matsuoka Date: Mon, 31 Jan 2022 19:45:16 -0300 Subject: [PATCH 020/167] cmd/lifecycle: update docstrings Signed-off-by: Claudio Matsuoka --- snapcraft/commands/lifecycle.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/snapcraft/commands/lifecycle.py b/snapcraft/commands/lifecycle.py index 802ccf794d..75e80b1e98 100644 --- a/snapcraft/commands/lifecycle.py +++ b/snapcraft/commands/lifecycle.py @@ -93,11 +93,11 @@ class PrimeCommand(_LifecycleCommand): """Prepare the final payload for packing.""" name = "prime" - help_msg = "Build artifacts defined for a part" + help_msg = "Prime artifacts defined for a part" overview = textwrap.dedent( """ - Prepare the final payload to be packed as a snap. If part names are - specified only those parts will be primed. The default is to prime - all parts. + Prepare the final payload to be packed as a snap, performing additional + processing and adding metadata files. If part names are specified only + those parts will be primed. The default is to prime all parts. """ ) From c1e88db9bbf53d4b723f17affed4b30a6064f4b3 Mon Sep 17 00:00:00 2001 From: Claudio Matsuoka Date: Fri, 28 Jan 2022 16:17:03 -0300 Subject: [PATCH 021/167] parts: add early base parsing Load the project YAML file to verify the base entry when a lifecycle command is used. If it's not core22, fall back to the legacy handlers. Signed-off-by: Claudio Matsuoka --- setup.cfg | 3 +- setup.py | 1 + snapcraft/cli.py | 9 +- snapcraft/commands/lifecycle.py | 4 +- snapcraft/errors.py | 4 + snapcraft/parts/__init__.py | 19 +++++ snapcraft/parts/lifecycle.py | 87 +++++++++++++++++++ tests/conftest.py | 31 +++++++ tests/unit/commands/test_lifecycle.py | 45 ++++++++++ tests/unit/parts/__init__.py | 0 tests/unit/parts/test_lifecycle.py | 118 ++++++++++++++++++++++++++ 11 files changed, 314 insertions(+), 7 deletions(-) create mode 100644 snapcraft/parts/__init__.py create mode 100644 snapcraft/parts/lifecycle.py create mode 100644 tests/conftest.py create mode 100644 tests/unit/commands/test_lifecycle.py create mode 100644 tests/unit/parts/__init__.py create mode 100644 tests/unit/parts/test_lifecycle.py diff --git a/setup.cfg b/setup.cfg index 13ca4fb041..8bd336269e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -37,5 +37,6 @@ ignore = E203,E501 [pydocstyle] # D107 Missing docstring in __init__ (reason: documented in class docstring) # D203 1 blank line required before class docstring (reason: pep257 default) -ignore = D107, D203 +# D213 Multi-line docstring summary should start at the second line (reason: pep257 default) +ignore = D107, D203, D213 diff --git a/setup.py b/setup.py index d35c055335..0651e5d373 100755 --- a/setup.py +++ b/setup.py @@ -82,6 +82,7 @@ def recursive_data_files(directory, install_directory): "pytest-cov", "pytest-mock", "pytest-subprocess", + "types-PyYAML", "types-setuptools", ] diff --git a/snapcraft/cli.py b/snapcraft/cli.py index 0a7d559eee..c8a130d6dd 100644 --- a/snapcraft/cli.py +++ b/snapcraft/cli.py @@ -19,7 +19,7 @@ import sys import craft_cli -from craft_cli import EmitterMode, emit +from craft_cli import ArgumentParsingError, EmitterMode, emit from snapcraft import __version__, errors from snapcraft_legacy.cli import legacy @@ -69,8 +69,9 @@ def run(): dispatcher.load_command(None) dispatcher.run() emit.ended_ok() - except errors.SnapcraftError as err: - emit.error(err) - except craft_cli.ArgumentParsingError: + except (errors.LegacyFallback, ArgumentParsingError) as err: + emit.trace(f"run legacy implementation: {err!s}") emit.ended_ok() legacy.legacy_run() + except errors.SnapcraftError as err: + emit.error(err) diff --git a/snapcraft/commands/lifecycle.py b/snapcraft/commands/lifecycle.py index 75e80b1e98..cd607acb66 100644 --- a/snapcraft/commands/lifecycle.py +++ b/snapcraft/commands/lifecycle.py @@ -23,7 +23,7 @@ from craft_cli import BaseCommand, emit from overrides import overrides -from snapcraft_legacy.cli import legacy +from snapcraft import parts if TYPE_CHECKING: import argparse @@ -45,7 +45,7 @@ def run(self, parsed_args): raise RuntimeError("command name not specified") emit.trace(f"lifecycle command: {self.name!r}, arguments: {parsed_args!r}") - legacy.legacy_run() + parts.run_lifecycle(self.name, parsed_args) class PullCommand(_LifecycleCommand): diff --git a/snapcraft/errors.py b/snapcraft/errors.py index 5ad7103025..eb888d4919 100644 --- a/snapcraft/errors.py +++ b/snapcraft/errors.py @@ -28,3 +28,7 @@ class FeatureNotImplemented(SnapcraftError): def __init__(self, msg: str) -> None: super().__init__(f"Command or feature not implemented: {msg}") + + +class LegacyFallback(Exception): + """Fall back to legacy snapcraft implementation.""" diff --git a/snapcraft/parts/__init__.py b/snapcraft/parts/__init__.py new file mode 100644 index 0000000000..2a485f3a48 --- /dev/null +++ b/snapcraft/parts/__init__.py @@ -0,0 +1,19 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright 2022 Canonical Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +"""Parts lifecycle processing.""" + +from .lifecycle import run_lifecycle # noqa: F401 diff --git a/snapcraft/parts/lifecycle.py b/snapcraft/parts/lifecycle.py new file mode 100644 index 0000000000..92eddf30b0 --- /dev/null +++ b/snapcraft/parts/lifecycle.py @@ -0,0 +1,87 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright 2022 Canonical Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +"""Parts lifecycle preparation and execution.""" + +from pathlib import Path +from typing import TYPE_CHECKING, Any, Dict + +import yaml +import yaml.error +from craft_cli import emit + +from snapcraft import errors + +if TYPE_CHECKING: + import argparse + + +_PROJECT_FILES = [ + Path("snapcraft.yaml"), + Path("snap/snapcraft.yaml"), + Path("build-aux/snap/snapcraft.yaml"), + Path(".snapcraft.yaml"), +] + + +def run_lifecycle(step_name: str, parsed_args: "argparse.Namespace") -> None: + """Run the parts lifecycle. + + :raises SnapcraftError: if the step name is invalid, or the project + yaml file cannot be loaded. + :raises LegacyFallback: if the project's base is not core22. + """ + emit.trace(f"{step_name} parts, arguments: {parsed_args}") + yaml_data = {} + for project_file in _PROJECT_FILES: + if project_file.is_file(): + yaml_data = _load_yaml(project_file) + break + else: + raise errors.SnapcraftError( + "Could not find snap/snapcraft.yaml. Are you sure you are in the " + "right directory?\nTo start a new project, use `snapcraft init`" + ) + + if yaml_data.get("base") != "core22": + raise errors.LegacyFallback("base is not core22") + + _run_step(step_name, parsed_args, yaml_data) + + +def _run_step( + step_name: str, parsed_args: "argparse.Namespace", yaml_data: Dict[str, Any] +) -> None: + raise errors.FeatureNotImplemented(f"core22 {step_name} handler") + + +def _load_yaml(filename: Path) -> Dict[str, Any]: + """Load and parse a YAML-formatted file. + + :param filename: The YAML file to load. + + :raises SnapcraftError: if loading didn't succeed. + """ + try: + with open(filename, encoding="utf-8") as yaml_file: + return yaml.safe_load(yaml_file) + except OSError as err: + msg = err.strerror + if err.filename: + msg = f"{msg}: {err.filename!r}." + raise errors.SnapcraftError(msg) from err + except yaml.error.YAMLError as err: + raise errors.SnapcraftError(f"YAML parsing error: {err!s}") from err diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000000..516dcd2572 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,31 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright 2022 Canonical Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import os + +import pytest + + +@pytest.fixture +def new_dir(tmpdir): + """Change to a new temporary directory.""" + + cwd = os.getcwd() + os.chdir(tmpdir) + + yield tmpdir + + os.chdir(cwd) diff --git a/tests/unit/commands/test_lifecycle.py b/tests/unit/commands/test_lifecycle.py new file mode 100644 index 0000000000..fa30f108d3 --- /dev/null +++ b/tests/unit/commands/test_lifecycle.py @@ -0,0 +1,45 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright 2022 Canonical Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import argparse +from unittest.mock import call + +import pytest + +from snapcraft.commands.lifecycle import ( + BuildCommand, + PrimeCommand, + PullCommand, + StageCommand, +) + + +@pytest.mark.parametrize( + "step_name,cmd_class", + [ + ("pull", PullCommand), + ("build", BuildCommand), + ("stage", StageCommand), + ("prime", PrimeCommand), + ], +) +def test_lifecycle_command(step_name, cmd_class, mocker): + lifecycle_run_mock = mocker.patch("snapcraft.parts.run_lifecycle") + cmd = cmd_class(None) + cmd.run(argparse.Namespace(parts=["part1", "part2"])) + assert lifecycle_run_mock.mock_calls == [ + call(step_name, argparse.Namespace(parts=["part1", "part2"])) + ] diff --git a/tests/unit/parts/__init__.py b/tests/unit/parts/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/unit/parts/test_lifecycle.py b/tests/unit/parts/test_lifecycle.py new file mode 100644 index 0000000000..4f9383d4ea --- /dev/null +++ b/tests/unit/parts/test_lifecycle.py @@ -0,0 +1,118 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright 2022 Canonical Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import argparse +import textwrap +from pathlib import Path +from typing import Any, Dict +from unittest.mock import call + +import pytest + +from snapcraft import errors, parts + +_SNAPCRAFT_YAML_FILENAMES = [ + "snap/snapcraft.yaml", + "build-aux/snap/snapcraft.yaml", + "snapcraft.yaml", + ".snapcraft.yaml", +] + + +@pytest.fixture +def snapcraft_yaml(): + def write_file( + *, base: str, filename: str = "snap/snapcraft.yaml" + ) -> Dict[str, Any]: + content = textwrap.dedent( + f""" + name: mytest + version: 0.1.1 + base: {base} + summary: Just some test data + description: This is just some test data. + grade: stable + confinement: strict + + parts: + part1: + plugin: nil + """ + ) + yaml_path = Path(filename) + yaml_path.parent.mkdir(parents=True, exist_ok=True) + yaml_path.write_text(content) + + return { + "name": "mytest", + "version": "0.1.1", + "base": base, + "summary": "Just some test data", + "description": "This is just some test data.", + "grade": "stable", + "confinement": "strict", + "parts": {"part1": {"plugin": "nil"}}, + } + + yield write_file + + +def test_config_not_found(new_dir): + """If snapcraft.yaml is not found, raise an error.""" + with pytest.raises(errors.SnapcraftError) as raised: + parts.run_lifecycle("pull", argparse.Namespace()) + + assert str(raised.value) == ( + "Could not find snap/snapcraft.yaml. Are you sure you are in the right " + "directory?\nTo start a new project, use `snapcraft init`" + ) + + +@pytest.mark.parametrize("filename", _SNAPCRAFT_YAML_FILENAMES) +def test_snapcraft_yaml_load(new_dir, snapcraft_yaml, filename, mocker): + """Snapcraft.yaml should be parsed as a valid yaml file.""" + yaml_data = snapcraft_yaml(base="core22", filename=filename) + run_step_mock = mocker.patch("snapcraft.parts.lifecycle._run_step") + + parts.run_lifecycle("pull", argparse.Namespace(parts=["part1"])) + + assert run_step_mock.mock_calls == [ + call("pull", argparse.Namespace(parts=["part1"]), yaml_data) + ] + + +def test_snapcraft_yaml_parse_error(new_dir, snapcraft_yaml, mocker): + """If snapcraft.yaml is not a valid yaml, raise an error.""" + snapcraft_yaml(base="invalid: true") + run_step_mock = mocker.patch("snapcraft.parts.lifecycle._run_step") + + with pytest.raises(errors.SnapcraftError) as raised: + parts.run_lifecycle("pull", argparse.Namespace(parts=["part1"])) + + assert str(raised.value) == ( + "YAML parsing error: mapping values are not allowed here\n" + ' in "snap/snapcraft.yaml", line 4, column 14' + ) + assert run_step_mock.mock_calls == [] + + +def test_legacy_base_not_core22(new_dir, snapcraft_yaml): + """Only core22 is processed by the new code, use legacy otherwise.""" + snapcraft_yaml(base="core20") + with pytest.raises(errors.LegacyFallback) as raised: + parts.run_lifecycle("pull", argparse.Namespace()) + + assert str(raised.value) == "base is not core22" From 62b7568715eba2ec069343bf984bd41787ff6f7f Mon Sep 17 00:00:00 2001 From: Claudio Matsuoka Date: Wed, 2 Feb 2022 11:08:06 -0300 Subject: [PATCH 022/167] requirements: update dependencies Signed-off-by: Claudio Matsuoka --- requirements-devel.txt | 21 ++++++++++++--------- requirements.txt | 8 +++++--- 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/requirements-devel.txt b/requirements-devel.txt index 0d25662cbd..d5a5496bb0 100644 --- a/requirements-devel.txt +++ b/requirements-devel.txt @@ -1,14 +1,14 @@ astroid==2.8.6 attrs==21.4.0 -black==21.12b0 +black==22.1.0 catkin-pkg==0.4.24 certifi==2021.10.8 cffi==1.15.0 chardet==4.0.0 -charset-normalizer==2.0.10 +charset-normalizer==2.0.11 click==8.0.3 codespell==2.1.0 -coverage==6.2 +coverage==6.3.1 craft-cli==0.1.0 cryptography==3.4 distro==1.6.0 @@ -36,7 +36,8 @@ macaroonbakery==1.3.1 mccabe==0.6.1 mypy==0.931 mypy-extensions==0.4.3 -oauthlib==3.1.1 +oauthlib==3.2.0 +overrides==6.1.0 packaging==21.3 PasteDeploy==2.1.1 pathspec==0.9.0 @@ -47,7 +48,7 @@ plaster-pastedeploy==0.7 platformdirs==2.4.1 pluggy==1.0.0 progressbar==2.5 -protobuf==3.19.3 +protobuf==3.19.4 psutil==5.9.0 ptyprocess==0.7.0 py==1.11.0 @@ -68,8 +69,8 @@ pyramid==2.0 pyRFC3339==1.1 pytest==6.2.5 pytest-cov==3.0.0 -pytest-mock==3.6.1 -pytest-subprocess==1.3.2 +pytest-mock==3.7.0 +pytest-subprocess==1.4.0 python-dateutil==2.8.2 python-debian==0.1.43 pytz==2021.3 @@ -89,9 +90,11 @@ testscenarios==0.5.0 testtools==2.5.0 tinydb==4.6.1 toml==0.10.2 -tomli==1.2.3 +tomli==2.0.0 translationstring==1.4 -types-setuptools==57.4.7 +types-PyYAML==6.0.4 +types-setuptools==57.4.8 +typing-utils==0.1.0 typing_extensions==4.0.1 urllib3==1.26.8 venusian==3.0.0 diff --git a/requirements.txt b/requirements.txt index af13f23390..bf47f26df1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,7 +3,7 @@ catkin-pkg==0.4.24 certifi==2021.10.8 cffi==1.15.0 chardet==4.0.0 -charset-normalizer==2.0.10 +charset-normalizer==2.0.11 click==8.0.3 craft-cli==0.1.0 cryptography==3.4 @@ -22,10 +22,11 @@ lazr.uri==1.0.6 lxml==4.7.1 macaroonbakery==1.3.1 mypy-extensions==0.4.3 -oauthlib==3.1.1 +oauthlib==3.2.0 +overrides==6.1.0 platformdirs==2.4.1 progressbar==2.5 -protobuf==3.19.3 +protobuf==3.19.4 psutil==5.9.0 pycparser==2.21 pydantic==1.9.0 @@ -50,6 +51,7 @@ six==1.16.0 tabulate==0.8.9 tinydb==4.6.1 toml==0.10.2 +typing-utils==0.1.0 typing_extensions==4.0.1 urllib3==1.26.8 wadllib==1.3.6 From 1da2329cfe8d02cea7853e7cd5f7fe7c0291435f Mon Sep 17 00:00:00 2001 From: Claudio Matsuoka Date: Wed, 2 Feb 2022 16:27:46 -0300 Subject: [PATCH 023/167] cli: run legacy in managed mode created by legacy Signed-off-by: Claudio Matsuoka --- snapcraft/cli.py | 5 +++++ snapcraft_legacy/cli/legacy.py | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/snapcraft/cli.py b/snapcraft/cli.py index c8a130d6dd..d6db3e5a97 100644 --- a/snapcraft/cli.py +++ b/snapcraft/cli.py @@ -16,6 +16,7 @@ """Command-line application entry point.""" +import os import sys import craft_cli @@ -48,6 +49,10 @@ def run(): """Run the CLI.""" + # Run the legacy implementation if inside a legacy managed environment. + if os.getenv("SNAPCRAFT_BUILD_ENVIRONMENT") == "managed-host": + legacy.legacy_run() + # Let legacy snapcraft handle --help until we have all command stubs registered # in craft-cli. if "-h" in sys.argv or "--help" in sys.argv: diff --git a/snapcraft_legacy/cli/legacy.py b/snapcraft_legacy/cli/legacy.py index ca2c3b4698..9fb533b6f2 100644 --- a/snapcraft_legacy/cli/legacy.py +++ b/snapcraft_legacy/cli/legacy.py @@ -16,8 +16,13 @@ """Legacy execution entry points.""" +import sys + from ._runner import run # noqa: F401 def legacy_run(): run() + + # ensure this call never returns + sys.exit() From 8b405998b4dff3977cfe3919f755aaf349154c59 Mon Sep 17 00:00:00 2001 From: Claudio Matsuoka Date: Thu, 3 Feb 2022 11:39:18 -0300 Subject: [PATCH 024/167] parts/lifecycle: declare error resolution separately Signed-off-by: Claudio Matsuoka --- snapcraft/parts/lifecycle.py | 3 ++- tests/unit/parts/test_lifecycle.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/snapcraft/parts/lifecycle.py b/snapcraft/parts/lifecycle.py index 92eddf30b0..61f974690c 100644 --- a/snapcraft/parts/lifecycle.py +++ b/snapcraft/parts/lifecycle.py @@ -53,7 +53,8 @@ def run_lifecycle(step_name: str, parsed_args: "argparse.Namespace") -> None: else: raise errors.SnapcraftError( "Could not find snap/snapcraft.yaml. Are you sure you are in the " - "right directory?\nTo start a new project, use `snapcraft init`" + "right directory?", + resolution="To start a new project, use `snapcraft init`", ) if yaml_data.get("base") != "core22": diff --git a/tests/unit/parts/test_lifecycle.py b/tests/unit/parts/test_lifecycle.py index 4f9383d4ea..4e1b2c0827 100644 --- a/tests/unit/parts/test_lifecycle.py +++ b/tests/unit/parts/test_lifecycle.py @@ -77,8 +77,9 @@ def test_config_not_found(new_dir): assert str(raised.value) == ( "Could not find snap/snapcraft.yaml. Are you sure you are in the right " - "directory?\nTo start a new project, use `snapcraft init`" + "directory?" ) + assert raised.value.resolution == "To start a new project, use `snapcraft init`" @pytest.mark.parametrize("filename", _SNAPCRAFT_YAML_FILENAMES) From bbfe951e72b9220d295bb9db3da64c0899e78df8 Mon Sep 17 00:00:00 2001 From: Claudio Matsuoka Date: Wed, 2 Feb 2022 16:18:19 -0300 Subject: [PATCH 025/167] projects: add project model and validation Add pydantic model and validators for the snapcraft.yaml project file, based on legacy schema and public documentation. Signed-off-by: Claudio Matsuoka --- .gitignore | 3 +- pyproject.toml | 5 +- snapcraft/commands/lifecycle.py | 4 +- snapcraft/errors.py | 4 + snapcraft/parts/__init__.py | 2 - snapcraft/parts/lifecycle.py | 16 +- snapcraft/parts/validation.py | 24 ++ snapcraft/projects.py | 374 ++++++++++++++++++++++++ tests/unit/commands/test_lifecycle.py | 2 +- tests/unit/parts/test_lifecycle.py | 42 ++- tests/unit/test_projects.py | 401 ++++++++++++++++++++++++++ 11 files changed, 857 insertions(+), 20 deletions(-) create mode 100644 snapcraft/parts/validation.py create mode 100644 snapcraft/projects.py create mode 100644 tests/unit/test_projects.py diff --git a/.gitignore b/.gitignore index 494dd067b4..3338ef47ad 100644 --- a/.gitignore +++ b/.gitignore @@ -16,7 +16,7 @@ docs/reference.md htmlcov .idea .mypy_cache -parts +/parts pip-wheel-metadata/ prime *.pyc @@ -27,7 +27,6 @@ snap/.snapcraft/ stage *.swp target -tests/unit/parts/ tests/unit/snap/ tests/unit/stage/ .vscode diff --git a/pyproject.toml b/pyproject.toml index d195478a13..64f647c93a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,7 +31,10 @@ ensure_newline_before_comments = true line_length = 88 [tool.pylint.messages_control] -disable = "fixme" +disable = "too-few-public-methods,fixme" [tool.pylint.MASTER] +extension-pkg-allow-list = [ + "pydantic" +] load-plugins = "pylint_fixme_info,pylint_pytest" diff --git a/snapcraft/commands/lifecycle.py b/snapcraft/commands/lifecycle.py index cd607acb66..1888165300 100644 --- a/snapcraft/commands/lifecycle.py +++ b/snapcraft/commands/lifecycle.py @@ -23,7 +23,7 @@ from craft_cli import BaseCommand, emit from overrides import overrides -from snapcraft import parts +from snapcraft.parts import lifecycle as parts_lifecycle if TYPE_CHECKING: import argparse @@ -45,7 +45,7 @@ def run(self, parsed_args): raise RuntimeError("command name not specified") emit.trace(f"lifecycle command: {self.name!r}, arguments: {parsed_args!r}") - parts.run_lifecycle(self.name, parsed_args) + parts_lifecycle.run(self.name, parsed_args) class PullCommand(_LifecycleCommand): diff --git a/snapcraft/errors.py b/snapcraft/errors.py index eb888d4919..66e8d5ecf7 100644 --- a/snapcraft/errors.py +++ b/snapcraft/errors.py @@ -30,5 +30,9 @@ def __init__(self, msg: str) -> None: super().__init__(f"Command or feature not implemented: {msg}") +class ProjectValidationError(SnapcraftError): + """Error validatiing snapcraft.yaml.""" + + class LegacyFallback(Exception): """Fall back to legacy snapcraft implementation.""" diff --git a/snapcraft/parts/__init__.py b/snapcraft/parts/__init__.py index 2a485f3a48..6148bd3efc 100644 --- a/snapcraft/parts/__init__.py +++ b/snapcraft/parts/__init__.py @@ -15,5 +15,3 @@ # along with this program. If not, see . """Parts lifecycle processing.""" - -from .lifecycle import run_lifecycle # noqa: F401 diff --git a/snapcraft/parts/lifecycle.py b/snapcraft/parts/lifecycle.py index 61f974690c..6f02de927f 100644 --- a/snapcraft/parts/lifecycle.py +++ b/snapcraft/parts/lifecycle.py @@ -24,6 +24,7 @@ from craft_cli import emit from snapcraft import errors +from snapcraft.projects import Project if TYPE_CHECKING: import argparse @@ -37,7 +38,7 @@ ] -def run_lifecycle(step_name: str, parsed_args: "argparse.Namespace") -> None: +def run(step_name: str, parsed_args: "argparse.Namespace") -> None: """Run the parts lifecycle. :raises SnapcraftError: if the step name is invalid, or the project @@ -57,14 +58,23 @@ def run_lifecycle(step_name: str, parsed_args: "argparse.Namespace") -> None: resolution="To start a new project, use `snapcraft init`", ) + # only execute the new codebase from core22 onwards if yaml_data.get("base") != "core22": raise errors.LegacyFallback("base is not core22") - _run_step(step_name, parsed_args, yaml_data) + # TODO: apply extensions + # yaml_data = apply_extensions(yaml_data) + + # TODO: process grammar + # yaml_data = process_grammar(yaml_data) + + project = Project.unmarshal(yaml_data) + + _run_step(step_name, project, parsed_args) def _run_step( - step_name: str, parsed_args: "argparse.Namespace", yaml_data: Dict[str, Any] + step_name: str, project: Project, parsed_args: "argparse.Namespace", ) -> None: raise errors.FeatureNotImplemented(f"core22 {step_name} handler") diff --git a/snapcraft/parts/validation.py b/snapcraft/parts/validation.py new file mode 100644 index 0000000000..77840809a2 --- /dev/null +++ b/snapcraft/parts/validation.py @@ -0,0 +1,24 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright 2022 Canonical Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +"""Part schema validation.""" + +from typing import Any, Dict + + +def validate_part(data: Dict[str, Any]) -> None: # pylint: disable=unused-argument + """Validate the given part data against common and plugin models.""" + # TODO: implement after craft-parts integration diff --git a/snapcraft/projects.py b/snapcraft/projects.py new file mode 100644 index 0000000000..c77bfd895b --- /dev/null +++ b/snapcraft/projects.py @@ -0,0 +1,374 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright 2022 Canonical Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +"""Project file definition and helpers.""" + +import re + +# XXX: mypy doesn't like Literal +from typing import Literal # type: ignore +from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Union + +import pydantic +from pydantic import conlist, constr + +from snapcraft.errors import ProjectValidationError +from snapcraft.parts import validation as parts_validation + + +class ProjectModel(pydantic.BaseModel): + """Base model for snapcraft project classes.""" + + class Config: # pylint: disable=too-few-public-methods + """Pydantic model configuration.""" + + validate_assignment = True + # extra = "forbid" + allow_mutation = False + allow_population_by_field_name = True + alias_generator = lambda s: s.replace("_", "-") # noqa: E731 + + +# A workaround for mypy false positives +# see https://github.com/samuelcolvin/pydantic/issues/975#issuecomment-551147305 +# fmt: off +if TYPE_CHECKING: + CommandChainStr = str + UniqueStrList = List[str] + UniqueAliasList = List[str] +else: + CommandChainStr = constr(regex=r"^[A-Za-z0-9/._#:$-]*$") + UniqueStrList = conlist(str, unique_items=True) + UniqueAliasList = conlist(constr(regex=r"^[a-zA-Z0-9][-_.a-zA-Z0-9]*$"), unique_items=True) +# fmt: on + + +class App(ProjectModel): + """Snapcraft project app definition.""" + + command: str + autostart: Optional[str] + common_id: Optional[str] + bus_name: Optional[str] + desktop: Optional[str] + completer: Optional[str] + stop_command: Optional[str] + post_stop_command: Optional[str] + start_timeout: Optional[str] + stop_timeout: Optional[str] + watchdog_timeout: Optional[str] + reload_command: Optional[str] + restart_delay: Optional[str] + timer: Optional[str] + daemon: Optional[Literal["simple", "forking", "oneshot", "notify", "dbus"]] + after: UniqueStrList = [] + before: UniqueStrList = [] + refresh_mode: Optional[Literal["endure", "restart"]] + stop_mode: Optional[ + Literal[ + "sigterm", + "sigterm-all", + "sighup", + "sighup-all", + "sigusr1", + "sigusr1-all", + "sigusr2", + "sigusr2-all", + ] + ] + restart_condition: Optional[ + Literal[ + "on-success", + "on-failure", + "on-abnormal", + "on-abort", + "on-watchdog", + "always", + "never", + ] + ] + install_mode: Optional[Literal["enable", "disable"]] + slots: UniqueStrList = [] + plugs: UniqueStrList = [] + aliases: UniqueAliasList = [] + environment: List[Dict[str, str]] = [] + command_chain: List[CommandChainStr] = [] + # TODO: sockets + + @pydantic.validator("autostart") + @classmethod + def _validate_desktop_name(cls, name): + if not re.match(r"^[A-Za-z0-9. _#:$-]+\\.desktop$", name): + raise ValueError( + f"{name!r} is not a valid desktop file name (e.g. myapp.desktop)" + ) + + return name + + @pydantic.validator("bus_name") + @classmethod + def _validate_bus_name(cls, name): + if not re.match(r"^[A-Za-z0-9/. _#:$-]*$", name): + raise ValueError(f"{name!r} is not a valid bus name") + + return name + + @pydantic.validator( + "start_timeout", "stop_timeout", "watchdog_timeout", "restart_delay" + ) + @classmethod + def _validate_time(cls, timeval): + if not re.match(r"^[0-9]+(ns|us|ms|s|m)*$", timeval): + raise ValueError(f"{timeval!r} is not a valid time value") + + return timeval + + +class Hook(ProjectModel): + """Snapcraft project hook definition.""" + + command_chain: List[CommandChainStr] = [] + environment: List[Dict[str, str]] = [] + plugs: UniqueStrList = [] + passthrough: Optional[Dict[str, Any]] + + +class Architecture(ProjectModel): + """Snapcraft project architecture definition.""" + + build_on: Union[str, UniqueStrList] + build_to: Optional[Union[str, UniqueStrList]] + + +class Project(ProjectModel): + """Snapcraft project definition. + + See https://snapcraft.io/docs/snapcraft-yaml-reference + + XXX: Not implemented in this version + - environment (top-level) + - frameworks + - system-usernames + - package-repositories + - version-script (deprecated) + """ + + name: constr(max_length=40) # type: ignore + title: Optional[constr(max_length=40)] # type: ignore + base: Optional[str] + build_base: Optional[str] + compression: Literal["lzo", "xz"] = "xz" + version: Optional[constr(max_length=32, strict=True)] # type: ignore + contact: Optional[Union[str, UniqueStrList]] + donation: Optional[Union[str, UniqueStrList]] + issues: Optional[Union[str, UniqueStrList]] + source_code: Optional[str] # XXX: should we validate as URL? + website: Optional[str] # XXX: should we validate as URL? + summary: constr(max_length=78) # type: ignore + description: str + type: Literal["app", "base", "gadget", "kernel", "snapd"] = "app" + icon: Optional[str] + confinement: Literal["classic", "devmode", "strict"] + layout: Optional[Dict[str, Dict[str, Any]]] + license: Optional[str] + grade: Literal["stable", "devel"] + adopt_info: Optional[str] + architectures: List[Architecture] = [] + assumes: UniqueStrList = [] + hooks: Optional[Dict[str, Hook]] + passthrough: Optional[Dict[str, Any]] + apps: Optional[Dict[str, App]] + plugs: Optional[Dict[str, Dict[str, str]]] # TODO: add plug name validation + slots: Optional[Dict[str, Dict[str, str]]] # TODO: add slot name validation + parts: Dict[str, Any] # parts are handled by craft-parts + epoch: Optional[int] + + @pydantic.root_validator(pre=True) + @classmethod + def _validate_mandatory_version(cls, values): + if "version" not in values and "adopt-info" not in values: + raise ValueError("Snap version is required if not using adopt-info") + return values + + @pydantic.root_validator(pre=True) + @classmethod + def _validate_mandatory_base(cls, values): + snap_type = values.get("type") + if ("base" in values) ^ (snap_type not in ["base", "kernel", "snapd"]): + raise ValueError( + "Snap base must be declared when type is not base, kernel or snapd" + ) + return values + + @pydantic.validator("name") + @classmethod + def _validate_name(cls, name): + if not re.match(r"^[a-z0-9-]*[a-z][a-z0-9-]*$", name): + raise ValueError( + "Snap names can only use ASCII lowercase letters, numbers, and hyphens, " + "and must have at least one letter" + ) + + if name.startswith("-"): + raise ValueError("Snap names cannot start with a hyphen") + + if name.endswith("-"): + raise ValueError("Snap names cannot end with a hyphen") + + if "--" in name: + raise ValueError("Snap names cannot have two hyphens in a row") + + return name + + @pydantic.validator("version") + @classmethod + def _validate_version(cls, version): + if not re.match(r"^[a-zA-Z0-9](?:[a-zA-Z0-9:.+~-]*[a-zA-Z0-9+~])?$", version): + raise ValueError( + "Snap versions consist of upper- and lower-case alphanumeric characters, " + "as well as periods, colons, plus signs, tildes, and hyphens. They cannot " + "begin with a period, colon, plus sign, tilde, or hyphen. They cannot end " + "with a period, colon, or hyphen" + ) + + return version + + @pydantic.validator("build_base", always=True) + @classmethod + def _validate_build_base(cls, build_base, values): + """Build-base defaults to the base value if not specified.""" + if not build_base: + build_base = values.get("base") + return build_base + + @pydantic.validator("parts", each_item=True) + @classmethod + def _validate_parts(cls, item): + """Verify each part (craft-parts will re-validate this).""" + parts_validation.validate_part(item) + return item + + @classmethod + def unmarshal(cls, data: Dict[str, Any]) -> "Project": + """Create and populate a new ``Project`` object from dictionary data. + + The unmarshal method validates entries in the input dictionary, populating + the corresponding fields in the data object. + + :param data: The dictionary data to unmarshal. + + :return: The newly created object. + + :raise TypeError: If data is not a dictionary. + """ + if not isinstance(data, dict): + raise TypeError("part data is not a dictionary") + + try: + project = Project(**data) + except pydantic.ValidationError as err: + raise ProjectValidationError(_format_pydantic_errors(err.errors())) from err + + return project + + +def _format_pydantic_errors(errors, *, file_name: str = "snapcraft.yaml"): + """Format errors. + + Example 1: Single error. + + Bad snapcraft.yaml content: + - field: + reason: + + Example 2: Multiple errors. + + Bad snapcraft.yaml content: + - field: + reason: + - field: + reason: + """ + combined = [f"Bad {file_name} content:"] + for error in errors: + formatted_loc = _format_pydantic_error_location(error["loc"]) + formatted_msg = _format_pydantic_error_message(error["msg"]) + + if formatted_msg == "field required": + field_name, location = _printable_field_location_split(formatted_loc) + combined.append( + f"- field {field_name} required in {location} configuration" + ) + elif formatted_msg == "extra fields not permitted": + field_name, location = _printable_field_location_split(formatted_loc) + combined.append( + f"- extra field {field_name} not permitted in {location} configuration" + ) + elif formatted_loc == "__root__": + combined.append(f"- {formatted_msg}") + else: + combined.append(f"- {formatted_msg} (in field {formatted_loc!r})") + + return "\n".join(combined) + + +def _format_pydantic_error_location(loc): + """Format location.""" + loc_parts = [] + for loc_part in loc: + if isinstance(loc_part, str): + loc_parts.append(loc_part) + elif isinstance(loc_part, int): + # Integer indicates an index. Go + # back and fix up previous part. + previous_part = loc_parts.pop() + previous_part += f"[{loc_part}]" + loc_parts.append(previous_part) + else: + raise RuntimeError(f"unhandled loc: {loc_part}") + + loc = ".".join(loc_parts) + + # Filter out internal __root__ detail. + loc = loc.replace(".__root__", "") + return loc + + +def _format_pydantic_error_message(msg): + """Format pydantic's error message field.""" + # Replace shorthand "str" with "string". + msg = msg.replace("str type expected", "string type expected") + return msg + + +def _printable_field_location_split(location: str) -> Tuple[str, str]: + """Return split field location. + + If top-level, location is returned as unquoted "top-level". + If not top-level, location is returned as quoted location, e.g. + + (1) field1[idx].foo => 'foo', 'field1[idx]' + (2) field2 => 'field2', top-level + + :returns: Tuple of , as printable representations. + """ + loc_split = location.split(".") + field_name = repr(loc_split.pop()) + + if loc_split: + return field_name, repr(".".join(loc_split)) + + return field_name, "top-level" diff --git a/tests/unit/commands/test_lifecycle.py b/tests/unit/commands/test_lifecycle.py index fa30f108d3..301f5b7507 100644 --- a/tests/unit/commands/test_lifecycle.py +++ b/tests/unit/commands/test_lifecycle.py @@ -37,7 +37,7 @@ ], ) def test_lifecycle_command(step_name, cmd_class, mocker): - lifecycle_run_mock = mocker.patch("snapcraft.parts.run_lifecycle") + lifecycle_run_mock = mocker.patch("snapcraft.parts.lifecycle.run") cmd = cmd_class(None) cmd.run(argparse.Namespace(parts=["part1", "part2"])) assert lifecycle_run_mock.mock_calls == [ diff --git a/tests/unit/parts/test_lifecycle.py b/tests/unit/parts/test_lifecycle.py index 4e1b2c0827..b78d463ba7 100644 --- a/tests/unit/parts/test_lifecycle.py +++ b/tests/unit/parts/test_lifecycle.py @@ -22,7 +22,9 @@ import pytest -from snapcraft import errors, parts +from snapcraft import errors +from snapcraft.parts import lifecycle as parts_lifecycle +from snapcraft.projects import Project _SNAPCRAFT_YAML_FILENAMES = [ "snap/snapcraft.yaml", @@ -40,7 +42,7 @@ def write_file( content = textwrap.dedent( f""" name: mytest - version: 0.1.1 + version: '0.1' base: {base} summary: Just some test data description: This is just some test data. @@ -58,13 +60,33 @@ def write_file( return { "name": "mytest", - "version": "0.1.1", + "title": None, "base": base, + "compression": "xz", + "version": "0.1", + "contact": None, + "donation": None, + "issues": None, + "source-code": None, + "website": None, "summary": "Just some test data", "description": "This is just some test data.", - "grade": "stable", + "type": "app", "confinement": "strict", + "icon": None, + "layout": None, + "license": None, + "grade": "stable", + "adopt-info": None, + "architectures": [], + "assumes": [], + "hooks": None, + "passthrough": None, + "apps": None, + "plugs": None, + "slots": None, "parts": {"part1": {"plugin": "nil"}}, + "epoch": None, } yield write_file @@ -73,7 +95,7 @@ def write_file( def test_config_not_found(new_dir): """If snapcraft.yaml is not found, raise an error.""" with pytest.raises(errors.SnapcraftError) as raised: - parts.run_lifecycle("pull", argparse.Namespace()) + parts_lifecycle.run("pull", argparse.Namespace()) assert str(raised.value) == ( "Could not find snap/snapcraft.yaml. Are you sure you are in the right " @@ -88,10 +110,12 @@ def test_snapcraft_yaml_load(new_dir, snapcraft_yaml, filename, mocker): yaml_data = snapcraft_yaml(base="core22", filename=filename) run_step_mock = mocker.patch("snapcraft.parts.lifecycle._run_step") - parts.run_lifecycle("pull", argparse.Namespace(parts=["part1"])) + parts_lifecycle.run("pull", argparse.Namespace(parts=["part1"])) + + project = Project.unmarshal(yaml_data) assert run_step_mock.mock_calls == [ - call("pull", argparse.Namespace(parts=["part1"]), yaml_data) + call("pull", project, argparse.Namespace(parts=["part1"])) ] @@ -101,7 +125,7 @@ def test_snapcraft_yaml_parse_error(new_dir, snapcraft_yaml, mocker): run_step_mock = mocker.patch("snapcraft.parts.lifecycle._run_step") with pytest.raises(errors.SnapcraftError) as raised: - parts.run_lifecycle("pull", argparse.Namespace(parts=["part1"])) + parts_lifecycle.run("pull", argparse.Namespace(parts=["part1"])) assert str(raised.value) == ( "YAML parsing error: mapping values are not allowed here\n" @@ -114,6 +138,6 @@ def test_legacy_base_not_core22(new_dir, snapcraft_yaml): """Only core22 is processed by the new code, use legacy otherwise.""" snapcraft_yaml(base="core20") with pytest.raises(errors.LegacyFallback) as raised: - parts.run_lifecycle("pull", argparse.Namespace()) + parts_lifecycle.run("pull", argparse.Namespace()) assert str(raised.value) == "base is not core22" diff --git a/tests/unit/test_projects.py b/tests/unit/test_projects.py new file mode 100644 index 0000000000..dbfd1254c8 --- /dev/null +++ b/tests/unit/test_projects.py @@ -0,0 +1,401 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright 2022 Canonical Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from typing import Any, Dict + +import pytest + +from snapcraft import errors +from snapcraft.projects import Project + + +@pytest.fixture +def yaml_data(): + def _yaml_data( + *, name: str = "name", version: str = "0.1", summary: str = "summary", **kwargs + ) -> Dict[str, Any]: + return { + "name": name, + "version": version, + "base": "core22", + "summary": summary, + "description": "description", + "grade": "stable", + "confinement": "strict", + "parts": [], + **kwargs, + } + + yield _yaml_data + + +class TestProjectDefaults: + """Ensure unspecified items have the correct default value.""" + + def test_project_defaults(self, yaml_data): + project = Project.unmarshal(yaml_data()) + + assert project.build_base == project.base + assert project.compression == "xz" + assert project.contact is None + assert project.donation is None + assert project.issues is None + assert project.source_code is None + assert project.website is None + assert project.type == "app" + assert project.icon is None + assert project.layout is None + assert project.license is None + assert project.adopt_info is None + assert project.architectures == [] + assert project.assumes == [] + assert project.hooks is None + assert project.passthrough is None + assert project.apps is None + assert project.plugs is None + assert project.slots is None + assert project.epoch is None + + def test_app_defaults(self, yaml_data): + data = yaml_data(apps={"app1": {"command": "/bin/true"}}) + project = Project.unmarshal(data) + assert project.apps is not None + + app = project.apps["app1"] + assert app is not None + + assert app.command == "/bin/true" + assert app.autostart is None + assert app.common_id is None + assert app.bus_name is None + assert app.desktop is None + assert app.completer is None + assert app.stop_command is None + assert app.post_stop_command is None + assert app.start_timeout is None + assert app.stop_timeout is None + assert app.watchdog_timeout is None + assert app.reload_command is None + assert app.restart_delay is None + assert app.timer is None + assert app.daemon is None + assert app.after == [] + assert app.before == [] + assert app.refresh_mode is None + assert app.stop_mode is None + assert app.restart_condition is None + assert app.install_mode is None + assert app.slots == [] + assert app.plugs == [] + assert app.aliases == [] + assert app.environment == [] + assert app.command_chain == [] + + +class TestProjectValidation: + """Validate top-level project items.""" + + @pytest.mark.parametrize( + "field", + [ + "name", + "summary", + "description", + "grade", + "confinement", + "parts", + ], + ) + def test_mandatory_fields(self, field, yaml_data): + data = yaml_data() + data.pop(field) + error = f"field {field!r} required in top-level configuration" + with pytest.raises(errors.ProjectValidationError, match=error): + Project.unmarshal(data) + + @pytest.mark.parametrize( + "snap_type,requires_base", + [ + ("app", True), + ("gadget", True), + ("base", False), + ("kernel", False), + ("snapd", False), + ], + ) + def test_mandatory_base(self, snap_type, requires_base, yaml_data): + data = yaml_data(type=snap_type) + data.pop("base") + + if requires_base: + error = "Snap base must be declared when type is not" + with pytest.raises(errors.ProjectValidationError, match=error): + Project.unmarshal(data) + else: + project = Project.unmarshal(data) + assert project.base is None + + def test_mandatory_version(self, yaml_data): + data = yaml_data() + data.pop("version") + error = "Snap version is required if not using adopt-info" + with pytest.raises(errors.ProjectValidationError, match=error): + Project.unmarshal(data) + + def test_version_not_required(self, yaml_data): + data = yaml_data() + data.pop("version") + data["adopt-info"] = "part1" + project = Project.unmarshal(data) + assert project.version is None + + @pytest.mark.parametrize( + "name", + [ + "name", + "name-with-dashes", + "name0123", + "0123name", + "a234567890123456789012345678901234567890", + ], + ) + def test_project_name_valid(self, name, yaml_data): + project = Project.unmarshal(yaml_data(name=name)) + assert project.name == name + + @pytest.mark.parametrize( + "name,error", + [ + ("name_with_underscores", "Snap names can only use"), + ("name-with-UPPERCASE", "Snap names can only use"), + ("name with spaces", "Snap names can only use"), + ("-name-starts-with-hyphen", "Snap names cannot start with a hyphen"), + ("name-ends-with-hyphen-", "Snap names cannot end with a hyphen"), + ("name-has--two-hyphens", "Snap names cannot have two hyphens in a row"), + ("123456", "Snap names can only use"), + ( + "a2345678901234567890123456789012345678901", + "ensure this value has at most 40 characters", + ), + ], + ) + def test_project_name_invalid(self, name, error, yaml_data): + with pytest.raises(errors.ProjectValidationError, match=error): + Project.unmarshal(yaml_data(name=name)) + + @pytest.mark.parametrize( + "version", + [ + "1", + "1.0", + "1.0.1-5.2~build0.20.04:1+1A", + "git", + "1~", + "1+", + "12345678901234567890123456789012", + ], + ) + def test_project_version_valid(self, version, yaml_data): + project = Project.unmarshal(yaml_data(version=version)) + assert project.version == version + + @pytest.mark.parametrize( + "version,error", + [ + ("1_0", "Snap versions consist of"), # _ is an invalid character + ("1=1", "Snap versions consist of"), # = is an invalid character + (".1", "Snap versions consist of"), # cannot start with period + (":1", "Snap versions consist of"), # cannot start with colon + ("+1", "Snap versions consist of"), # cannot start with plus sign + ("~1", "Snap versions consist of"), # cannot start with tilde + ("-1", "Snap versions consist of"), # cannot start with hyphen + ("1.", "Snap versions consist of"), # cannot end with period + ("1:", "Snap versions consist of"), # cannot end with colon + ("1-", "Snap versions consist of"), # cannot end with hyphen + ( + "123456789012345678901234567890123", + "ensure this value has at most 32 characters", + ), # too large + ], + ) + def test_project_version_invalid(self, version, error, yaml_data): + with pytest.raises(errors.ProjectValidationError, match=error): + Project.unmarshal(yaml_data(version=version)) + + @pytest.mark.parametrize( + "snap_type", + ["app", "gadget", "kernel", "snapd", "base", "_invalid"], + ) + def test_project_type(self, snap_type, yaml_data): + data = yaml_data(type=snap_type) + if snap_type in ["base", "kernel", "snapd"]: + data.pop("base") + + if snap_type != "_invalid": + project = Project.unmarshal(data) + assert project.type == snap_type + else: + error = ".*unexpected value; permitted: 'app', 'base', 'gadget', 'kernel', 'snapd'" + with pytest.raises(errors.ProjectValidationError, match=error): + Project.unmarshal(data) + + @pytest.mark.parametrize( + "confinement", + ["strict", "devmode", "classic", "_invalid"], + ) + def test_project_confinement(self, confinement, yaml_data): + data = yaml_data(confinement=confinement) + + if confinement != "_invalid": + project = Project.unmarshal(data) + assert project.confinement == confinement + else: + error = ".*unexpected value; permitted: 'classic', 'devmode', 'strict'" + with pytest.raises(errors.ProjectValidationError, match=error): + Project.unmarshal(data) + + @pytest.mark.parametrize( + "grade", + ["devel", "stable", "_invalid"], + ) + def test_project_grade(self, grade, yaml_data): + data = yaml_data(grade=grade) + + if grade != "_invalid": + project = Project.unmarshal(data) + assert project.grade == grade + else: + error = ".*unexpected value; permitted: 'stable', 'devel'" + with pytest.raises(errors.ProjectValidationError, match=error): + Project.unmarshal(data) + + def test_project_summary_valid(self, yaml_data): + summary = "x" * 78 + project = Project.unmarshal(yaml_data(summary=summary)) + assert project.summary == summary + + def test_project_summary_invalid(self, yaml_data): + summary = "x" * 79 + error = "ensure this value has at most 78 characters" + with pytest.raises(errors.ProjectValidationError, match=error): + Project.unmarshal(yaml_data(summary=summary)) + + +class TestAppValidation: + """Validate apps.""" + + @pytest.mark.parametrize( + "daemon", + ["simple", "forking", "oneshot", "notify", "dbus", "_invalid"], + ) + def test_app_daemon(self, daemon, yaml_data): + data = yaml_data(apps={"app1": {"command": "/bin/true", "daemon": daemon}}) + + if daemon != "_invalid": + project = Project.unmarshal(data) + assert project.apps is not None + assert project.apps["app1"].daemon == daemon + else: + error = ".*unexpected value; permitted: 'simple', 'forking', 'oneshot'" + with pytest.raises(errors.ProjectValidationError, match=error): + Project.unmarshal(data) + + @pytest.mark.parametrize("refresh_mode", ["endure", "restart", "_invalid"]) + def test_app_refresh_mode(self, refresh_mode, yaml_data): + data = yaml_data( + apps={"app1": {"command": "/bin/true", "refresh-mode": refresh_mode}} + ) + + if refresh_mode != "_invalid": + project = Project.unmarshal(data) + assert project.apps is not None + assert project.apps["app1"].refresh_mode == refresh_mode + else: + error = ".*unexpected value; permitted: 'endure', 'restart'" + with pytest.raises(errors.ProjectValidationError, match=error): + Project.unmarshal(data) + + @pytest.mark.parametrize( + "stop_mode", + [ + "sigterm", + "sigterm-all", + "sighup", + "sighup-all", + "sigusr1", + "sigusr1-all", + "sigusr2", + "sigusr2-all", + "_invalid", + ], + ) + def test_app_stop_mode(self, stop_mode, yaml_data): + data = yaml_data( + apps={"app1": {"command": "/bin/true", "stop-mode": stop_mode}} + ) + + if stop_mode != "_invalid": + project = Project.unmarshal(data) + assert project.apps is not None + assert project.apps["app1"].stop_mode == stop_mode + else: + error = ".*unexpected value; permitted: 'sigterm', 'sigterm-all', 'sighup'" + with pytest.raises(errors.ProjectValidationError, match=error): + Project.unmarshal(data) + + @pytest.mark.parametrize( + "restart_condition", + [ + "on-success", + "on-failure", + "on-abnormal", + "on-abort", + "on-watchdog", + "always", + "never", + "_invalid", + ], + ) + def test_app_restart_condition(self, restart_condition, yaml_data): + data = yaml_data( + apps={ + "app1": {"command": "/bin/true", "restart-condition": restart_condition} + } + ) + + if restart_condition != "_invalid": + project = Project.unmarshal(data) + assert project.apps is not None + assert project.apps["app1"].restart_condition == restart_condition + else: + error = ".*unexpected value; permitted: 'on-success', 'on-failure', 'on-abnormal'" + with pytest.raises(errors.ProjectValidationError, match=error): + Project.unmarshal(data) + + @pytest.mark.parametrize("install_mode", ["enable", "disable", "_invalid"]) + def test_app_install_mode(self, install_mode, yaml_data): + data = yaml_data( + apps={"app1": {"command": "/bin/true", "install-mode": install_mode}} + ) + + if install_mode != "_invalid": + project = Project.unmarshal(data) + assert project.apps is not None + assert project.apps["app1"].install_mode == install_mode + else: + error = ".*unexpected value; permitted: 'enable', 'disable'" + with pytest.raises(errors.ProjectValidationError, match=error): + Project.unmarshal(data) From 19613e01b565871d3c95a9c36ed813a04e78dd4d Mon Sep 17 00:00:00 2001 From: Claudio Matsuoka Date: Tue, 8 Feb 2022 16:33:55 -0300 Subject: [PATCH 026/167] projects: fix epoch format and minor adjustments Fix project epoch format to match legacy validation, see https://snapcraft.io/docs/snap-epochs for details. Signed-off-by: Claudio Matsuoka --- snapcraft/projects.py | 22 +++++++++++++++------- tests/unit/test_projects.py | 33 +++++++++++++++++++++++++++++++-- 2 files changed, 46 insertions(+), 9 deletions(-) diff --git a/snapcraft/projects.py b/snapcraft/projects.py index c77bfd895b..92d6f46dd8 100644 --- a/snapcraft/projects.py +++ b/snapcraft/projects.py @@ -63,7 +63,6 @@ class App(ProjectModel): autostart: Optional[str] common_id: Optional[str] bus_name: Optional[str] - desktop: Optional[str] completer: Optional[str] stop_command: Optional[str] post_stop_command: Optional[str] @@ -160,10 +159,9 @@ class Project(ProjectModel): XXX: Not implemented in this version - environment (top-level) - - frameworks - system-usernames - package-repositories - - version-script (deprecated) + - adopt-info (after adding craftctl support to craft-parts) """ name: constr(max_length=40) # type: ignore @@ -175,8 +173,8 @@ class Project(ProjectModel): contact: Optional[Union[str, UniqueStrList]] donation: Optional[Union[str, UniqueStrList]] issues: Optional[Union[str, UniqueStrList]] - source_code: Optional[str] # XXX: should we validate as URL? - website: Optional[str] # XXX: should we validate as URL? + source_code: Optional[str] + website: Optional[str] summary: constr(max_length=78) # type: ignore description: str type: Literal["app", "base", "gadget", "kernel", "snapd"] = "app" @@ -185,7 +183,6 @@ class Project(ProjectModel): layout: Optional[Dict[str, Dict[str, Any]]] license: Optional[str] grade: Literal["stable", "devel"] - adopt_info: Optional[str] architectures: List[Architecture] = [] assumes: UniqueStrList = [] hooks: Optional[Dict[str, Hook]] @@ -194,7 +191,7 @@ class Project(ProjectModel): plugs: Optional[Dict[str, Dict[str, str]]] # TODO: add plug name validation slots: Optional[Dict[str, Dict[str, str]]] # TODO: add slot name validation parts: Dict[str, Any] # parts are handled by craft-parts - epoch: Optional[int] + epoch: Optional[str] @pydantic.root_validator(pre=True) @classmethod @@ -261,6 +258,17 @@ def _validate_parts(cls, item): parts_validation.validate_part(item) return item + @pydantic.validator("epoch") + @classmethod + def _validate_epoch(cls, epoch): + """Verify epoch format.""" + if epoch is not None and not re.match(r"^(?:0|[1-9][0-9]*[*]?)$", epoch): + raise ValueError( + "Epoch is a positive integer followed by an optional asterisk" + ) + + return epoch + @classmethod def unmarshal(cls, data: Dict[str, Any]) -> "Project": """Create and populate a new ``Project`` object from dictionary data. diff --git a/tests/unit/test_projects.py b/tests/unit/test_projects.py index dbfd1254c8..716d6bff38 100644 --- a/tests/unit/test_projects.py +++ b/tests/unit/test_projects.py @@ -59,7 +59,6 @@ def test_project_defaults(self, yaml_data): assert project.icon is None assert project.layout is None assert project.license is None - assert project.adopt_info is None assert project.architectures == [] assert project.assumes == [] assert project.hooks is None @@ -81,7 +80,6 @@ def test_app_defaults(self, yaml_data): assert app.autostart is None assert app.common_id is None assert app.bus_name is None - assert app.desktop is None assert app.completer is None assert app.stop_command is None assert app.post_stop_command is None @@ -293,6 +291,37 @@ def test_project_summary_invalid(self, yaml_data): with pytest.raises(errors.ProjectValidationError, match=error): Project.unmarshal(yaml_data(summary=summary)) + @pytest.mark.parametrize( + "epoch", + [ + "0", + "1", + "1*", + "12345", + "12345*", + ], + ) + def test_project_epoch_valid(self, epoch, yaml_data): + project = Project.unmarshal(yaml_data(epoch=epoch)) + assert project.epoch == epoch + + @pytest.mark.parametrize( + "epoch", + [ + "", + "invalid", + "0*", + "012345", + "-1", + "*1", + "1**", + ], + ) + def test_project_epoch_invalid(self, epoch, yaml_data): + error = "Epoch is a positive integer followed by an optional asterisk" + with pytest.raises(errors.ProjectValidationError, match=error): + Project.unmarshal(yaml_data(epoch=epoch)) + class TestAppValidation: """Validate apps.""" From 832bd9b49135407919059450c7a23d515f15e22a Mon Sep 17 00:00:00 2001 From: Claudio Matsuoka Date: Tue, 8 Feb 2022 16:14:30 -0300 Subject: [PATCH 027/167] parts: integrate craft-parts Use craft-parts to validate project parts and to process the parts lifecycle. Signed-off-by: Claudio Matsuoka --- setup.py | 3 +- snapcraft/cli.py | 7 ++ snapcraft/errors.py | 4 + snapcraft/parts/__init__.py | 2 + snapcraft/parts/lifecycle.py | 10 +- snapcraft/parts/parts.py | 143 ++++++++++++++++++++++++++++ snapcraft/parts/validation.py | 29 +++++- tests/conftest.py | 19 ++++ tests/unit/parts/test_parts.py | 65 +++++++++++++ tests/unit/parts/test_validation.py | 66 +++++++++++++ 10 files changed, 343 insertions(+), 5 deletions(-) create mode 100644 snapcraft/parts/parts.py create mode 100644 tests/unit/parts/test_parts.py create mode 100644 tests/unit/parts/test_validation.py diff --git a/setup.py b/setup.py index 0651e5d373..7e832915bb 100755 --- a/setup.py +++ b/setup.py @@ -46,7 +46,7 @@ def recursive_data_files(directory, install_directory): "Natural Language :: English", "Programming Language :: Python", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.8", "Topic :: Software Development :: Build Tools", "Topic :: System :: Software Distribution", ] @@ -93,6 +93,7 @@ def recursive_data_files(directory, install_directory): "attrs", "click", "craft-cli", + "craft-parts", "cryptography==3.4", "gnupg", "jsonschema==2.5.1", diff --git a/snapcraft/cli.py b/snapcraft/cli.py index d6db3e5a97..40e00f6d72 100644 --- a/snapcraft/cli.py +++ b/snapcraft/cli.py @@ -16,6 +16,7 @@ """Command-line application entry point.""" +import logging import os import sys @@ -58,6 +59,12 @@ def run(): if "-h" in sys.argv or "--help" in sys.argv: legacy.legacy_run() + # set lib loggers to debug level so that all messages are sent to Emitter + # TODO: add craft_providers + for lib_name in ("craft_parts",): + logger = logging.getLogger(lib_name) + logger.setLevel(logging.DEBUG) + emit.init(EmitterMode.NORMAL, "snapcraft", f"Starting Snapcraft {__version__}") dispatcher = craft_cli.Dispatcher( "snapcraft", diff --git a/snapcraft/errors.py b/snapcraft/errors.py index 66e8d5ecf7..112fa3903c 100644 --- a/snapcraft/errors.py +++ b/snapcraft/errors.py @@ -30,6 +30,10 @@ def __init__(self, msg: str) -> None: super().__init__(f"Command or feature not implemented: {msg}") +class PartsLifecycleError(SnapcraftError): + """Error during parts processing.""" + + class ProjectValidationError(SnapcraftError): """Error validatiing snapcraft.yaml.""" diff --git a/snapcraft/parts/__init__.py b/snapcraft/parts/__init__.py index 6148bd3efc..13e34e8d36 100644 --- a/snapcraft/parts/__init__.py +++ b/snapcraft/parts/__init__.py @@ -15,3 +15,5 @@ # along with this program. If not, see . """Parts lifecycle processing.""" + +from .parts import PartsLifecycle # noqa: F401 diff --git a/snapcraft/parts/lifecycle.py b/snapcraft/parts/lifecycle.py index 6f02de927f..582b738e0c 100644 --- a/snapcraft/parts/lifecycle.py +++ b/snapcraft/parts/lifecycle.py @@ -24,6 +24,7 @@ from craft_cli import emit from snapcraft import errors +from snapcraft.parts import PartsLifecycle from snapcraft.projects import Project if TYPE_CHECKING: @@ -76,7 +77,14 @@ def run(step_name: str, parsed_args: "argparse.Namespace") -> None: def _run_step( step_name: str, project: Project, parsed_args: "argparse.Namespace", ) -> None: - raise errors.FeatureNotImplemented(f"core22 {step_name} handler") + + # TODO: check destructive and managed modes and run in provider + _ = parsed_args + + work_dir = Path("work").absolute() + + lifecycle = PartsLifecycle(project.parts, work_dir=work_dir) + lifecycle.run(step_name) def _load_yaml(filename: Path) -> Dict[str, Any]: diff --git a/snapcraft/parts/parts.py b/snapcraft/parts/parts.py new file mode 100644 index 0000000000..32091a41fb --- /dev/null +++ b/snapcraft/parts/parts.py @@ -0,0 +1,143 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright 2022 Canonical Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +"""Craft-parts lifecycle wrapper.""" + +import pathlib +from typing import Any, Dict + +import craft_parts +from craft_cli import emit +from craft_parts import ActionType, Step +from xdg import BaseDirectory # type: ignore + +from snapcraft.errors import PartsLifecycleError + +_LIFECYCLE_STEPS = { + "pull": Step.PULL, + "overlay": Step.OVERLAY, + "build": Step.BUILD, + "stage": Step.STAGE, + "prime": Step.PRIME, +} + + +class PartsLifecycle: + """Create and manage the parts lifecycle. + + :param all_parts: A dictionary containing the parts defined in the project. + :param work_dir: The working directory for parts processing. + + :raises PartsLifecycleError: On error initializing the parts lifecycle. + """ + + def __init__( + self, all_parts: Dict[str, Any], *, work_dir: pathlib.Path, + ): + emit.progress("Initializing parts lifecycle") + + # set the cache dir for parts package management + cache_dir = BaseDirectory.save_cache_path("snapcraft") + + try: + self._lcm = craft_parts.LifecycleManager( + {"parts": all_parts}, + application_name="snapcraft", + work_dir=work_dir, + cache_dir=cache_dir, + ignore_local_sources=["*.snap"], + ) + except craft_parts.PartsError as err: + raise PartsLifecycleError(str(err)) from err + + @property + def prime_dir(self) -> pathlib.Path: + """Return the parts prime directory path.""" + return self._lcm.project_info.prime_dir + + def run(self, step_name: str) -> None: + """Run the parts lifecycle. + + :param target_step: The final step to execute. + + :raises PartsLifecycleError: On error during lifecycle. + :raises RuntimeError: On unexpected error. + """ + target_step = _LIFECYCLE_STEPS.get(step_name) + if not target_step: + raise RuntimeError(f"Invalid target step {step_name!r}") + + try: + emit.progress("Executing parts lifecycle...") + + actions = self._lcm.plan(target_step) + with self._lcm.action_executor() as aex: + for action in actions: + message = _action_message(action) + emit.progress(f"Executing parts lifecycle: {message}") + aex.execute(action) + + emit.message("Executed parts lifecycle", intermediate=True) + except RuntimeError as err: + raise RuntimeError(f"Parts processing internal error: {err}") from err + except OSError as err: + msg = err.strerror + if err.filename: + msg = f"{err.filename}: {msg}" + raise PartsLifecycleError(msg) from err + except Exception as err: + raise PartsLifecycleError(str(err)) from err + + +def _action_message(action: craft_parts.Action) -> str: + msg = { + Step.PULL: { + ActionType.RUN: "pull", + ActionType.RERUN: "repull", + ActionType.SKIP: "skip pull", + ActionType.UPDATE: "update sources for", + }, + Step.OVERLAY: { + ActionType.RUN: "overlay", + ActionType.RERUN: "re-overlay", + ActionType.SKIP: "skip overlay", + ActionType.UPDATE: "update overlay for", + ActionType.REAPPLY: "reapply", + }, + Step.BUILD: { + ActionType.RUN: "build", + ActionType.RERUN: "rebuild", + ActionType.SKIP: "skip build", + ActionType.UPDATE: "update build for", + }, + Step.STAGE: { + ActionType.RUN: "stage", + ActionType.RERUN: "restage", + ActionType.SKIP: "skip stage", + }, + Step.PRIME: { + ActionType.RUN: "prime", + ActionType.RERUN: "re-prime", + ActionType.SKIP: "skip prime", + }, + } + + message = f"{msg[action.step][action.action_type]} {action.part_name}" + + if action.reason: + message += f" ({action.reason})" + + return message diff --git a/snapcraft/parts/validation.py b/snapcraft/parts/validation.py index 77840809a2..f3a6921ae6 100644 --- a/snapcraft/parts/validation.py +++ b/snapcraft/parts/validation.py @@ -18,7 +18,30 @@ from typing import Any, Dict +from craft_parts import plugins +from craft_parts.parts import PartSpec -def validate_part(data: Dict[str, Any]) -> None: # pylint: disable=unused-argument - """Validate the given part data against common and plugin models.""" - # TODO: implement after craft-parts integration + +def validate_part(data: Dict[str, Any]) -> None: + """Validate the given part data against common and plugin models. + + :param data: The part data to validate. + """ + if not isinstance(data, dict): + raise TypeError("value must be a dictionary") + + # copy the original data, we'll modify it + spec = data.copy() + + plugin_name = spec.get("plugin") + if not plugin_name: + raise ValueError("'plugin' not defined") + + plugin_class = plugins.get_plugin_class(plugin_name) + + # validate plugin properties + plugin_class.properties_class.unmarshal(spec) + + # validate common part properties + part_spec = plugins.extract_part_properties(spec, plugin_name=plugin_name) + PartSpec(**part_spec) diff --git a/tests/conftest.py b/tests/conftest.py index 516dcd2572..fd989d1a52 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -17,6 +17,25 @@ import os import pytest +import xdg + + +@pytest.fixture(autouse=True) +def temp_xdg(tmpdir, mocker): + """Use a temporary locaction for XDG directories.""" + + mocker.patch( + "xdg.BaseDirectory.xdg_config_home", new=os.path.join(tmpdir, ".config") + ) + mocker.patch("xdg.BaseDirectory.xdg_data_home", new=os.path.join(tmpdir, ".local")) + mocker.patch("xdg.BaseDirectory.xdg_cache_home", new=os.path.join(tmpdir, ".cache")) + mocker.patch( + "xdg.BaseDirectory.xdg_config_dirs", new=[xdg.BaseDirectory.xdg_config_home] + ) + mocker.patch( + "xdg.BaseDirectory.xdg_data_dirs", new=[xdg.BaseDirectory.xdg_data_home] + ) + mocker.patch.dict(os.environ, {"XDG_CONFIG_HOME": os.path.join(tmpdir, ".config")}) @pytest.fixture diff --git a/tests/unit/parts/test_parts.py b/tests/unit/parts/test_parts.py new file mode 100644 index 0000000000..f921d1d359 --- /dev/null +++ b/tests/unit/parts/test_parts.py @@ -0,0 +1,65 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright 2022 Canonical Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from pathlib import Path + +import pytest + +from snapcraft import errors +from snapcraft.parts import PartsLifecycle + + +@pytest.fixture +def parts_data(): + yield { + "p1": {"plugin": "nil"}, + } + + +@pytest.mark.parametrize("step_name", ["pull", "overlay", "build", "stage", "prime"]) +def test_parts_lifecycle_run(parts_data, step_name, new_dir, emitter): + lifecycle = PartsLifecycle(parts_data, work_dir=new_dir) + lifecycle.run(step_name) + assert lifecycle.prime_dir == Path(new_dir, "prime") + assert lifecycle.prime_dir.is_dir() + emitter.assert_recorded([f"Executing parts lifecycle: {step_name} p1"]) + + +def test_parts_lifecycle_run_bad_step(parts_data, new_dir): + lifecycle = PartsLifecycle(parts_data, work_dir=new_dir) + with pytest.raises(RuntimeError) as raised: + lifecycle.run("invalid") + assert str(raised.value) == "Invalid target step 'invalid'" + + +def test_parts_lifecycle_run_internal_error(parts_data, new_dir, mocker): + lifecycle = PartsLifecycle(parts_data, work_dir=new_dir) + mocker.patch("craft_parts.LifecycleManager.plan", side_effect=RuntimeError("crash")) + with pytest.raises(RuntimeError) as raised: + lifecycle.run("prime") + assert str(raised.value) == "Parts processing internal error: crash" + + +def test_parts_lifecycle_run_parts_error(new_dir): + lifecycle = PartsLifecycle( + {"p1": {"plugin": "dump", "source": "foo"}}, work_dir=new_dir + ) + with pytest.raises(errors.PartsLifecycleError) as raised: + lifecycle.run("prime") + assert ( + str(raised.value) + == "Failed to pull source: unable to determine source type of 'foo'." + ) diff --git a/tests/unit/parts/test_validation.py b/tests/unit/parts/test_validation.py new file mode 100644 index 0000000000..28064f39bb --- /dev/null +++ b/tests/unit/parts/test_validation.py @@ -0,0 +1,66 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright 2022 Canonical Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import copy + +import pydantic +import pytest + +from snapcraft.parts.validation import validate_part + + +def test_part_validation_data_type(): + with pytest.raises(TypeError) as raised: + validate_part("invalid data") # type: ignore + + assert str(raised.value) == "value must be a dictionary" + + +def test_part_validation_immutable(): + data = { + "plugin": "make", + "source": "foo", + "make-parameters": ["-C bar"], + } + data_copy = copy.deepcopy(data) + + validate_part(data) + + assert data == data_copy + + +def test_part_validation_extra(): + data = { + "plugin": "make", + "source": "foo", + "make-parameters": ["-C bar"], + "unexpected-extra": True, + } + + error = r"unexpected-extra\s+extra fields not permitted" + with pytest.raises(pydantic.ValidationError, match=error): + validate_part(data) + + +def test_part_validation_missing(): + data = { + "plugin": "make", + "make-parameters": ["-C bar"], + } + + error = r"source\s+field required" + with pytest.raises(pydantic.ValidationError, match=error): + validate_part(data) From 855d76e5dede79dcadc49407e6f067486a5a5d7f Mon Sep 17 00:00:00 2001 From: Claudio Matsuoka Date: Wed, 9 Feb 2022 18:22:01 -0300 Subject: [PATCH 028/167] requirements: update dependencies Signed-off-by: Claudio Matsuoka --- requirements-devel.txt | 25 +++++++++++++++---------- requirements.txt | 16 +++++++++++----- 2 files changed, 26 insertions(+), 15 deletions(-) diff --git a/requirements-devel.txt b/requirements-devel.txt index d5a5496bb0..260c19a050 100644 --- a/requirements-devel.txt +++ b/requirements-devel.txt @@ -9,8 +9,10 @@ charset-normalizer==2.0.11 click==8.0.3 codespell==2.1.0 coverage==6.3.1 -craft-cli==0.1.0 +craft-cli==0.2.0 +craft-parts==1.1.2 cryptography==3.4 +Deprecated==1.2.13 distro==1.6.0 docutils==0.18.1 entrypoints==0.3 @@ -18,7 +20,7 @@ extras==1.0.0 fixtures==3.0.0 flake8==3.7.9 gnupg==2.3.1 -httplib2==0.20.2 +httplib2==0.20.4 hupper==1.10.3 idna==3.3 importlib-metadata==4.10.1 @@ -41,11 +43,11 @@ overrides==6.1.0 packaging==21.3 PasteDeploy==2.1.1 pathspec==0.9.0 -pbr==5.8.0 +pbr==5.8.1 pexpect==4.8.0 plaster==1.0 plaster-pastedeploy==0.7 -platformdirs==2.4.1 +platformdirs==2.5.0 pluggy==1.0.0 progressbar==2.5 protobuf==3.19.4 @@ -55,8 +57,9 @@ py==1.11.0 pycodestyle==2.5.0 pycparser==2.21 pydantic==1.9.0 +pydantic-yaml==0.6.1 pydocstyle==6.1.1 -pyelftools==0.27 +pyelftools==0.28 pyflakes==2.1.1 pyftpdlib==1.5.6 pylint==2.11.1 @@ -67,10 +70,10 @@ pymacaroons==0.13.0 pyparsing==3.0.7 pyramid==2.0 pyRFC3339==1.1 -pytest==6.2.5 +pytest==7.0.0 pytest-cov==3.0.0 pytest-mock==3.7.0 -pytest-subprocess==1.4.0 +pytest-subprocess==1.4.1 python-dateutil==2.8.2 python-debian==0.1.43 pytz==2021.3 @@ -81,7 +84,8 @@ requests==2.27.1 requests-toolbelt==0.9.1 requests-unixsocket==0.3.0 SecretStorage==3.3.1 -semantic-version==2.8.5 +semantic-version==2.9.0 +semver==3.0.0.dev3 simplejson==3.17.6 six==1.16.0 snowballstemmer==2.2.0 @@ -90,10 +94,11 @@ testscenarios==0.5.0 testtools==2.5.0 tinydb==4.6.1 toml==0.10.2 -tomli==2.0.0 +tomli==2.0.1 translationstring==1.4 +types-Deprecated==1.2.5 types-PyYAML==6.0.4 -types-setuptools==57.4.8 +types-setuptools==57.4.9 typing-utils==0.1.0 typing_extensions==4.0.1 urllib3==1.26.8 diff --git a/requirements.txt b/requirements.txt index bf47f26df1..7a7ae449f3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,12 +5,14 @@ cffi==1.15.0 chardet==4.0.0 charset-normalizer==2.0.11 click==8.0.3 -craft-cli==0.1.0 +craft-cli==0.2.0 +craft-parts==1.1.2 cryptography==3.4 +Deprecated==1.2.13 distro==1.6.0 docutils==0.18.1 gnupg==2.3.1 -httplib2==0.20.2 +httplib2==0.20.4 idna==3.3 importlib-metadata==4.10.1 jeepney==0.7.1 @@ -24,13 +26,14 @@ macaroonbakery==1.3.1 mypy-extensions==0.4.3 oauthlib==3.2.0 overrides==6.1.0 -platformdirs==2.4.1 +platformdirs==2.5.0 progressbar==2.5 protobuf==3.19.4 psutil==5.9.0 pycparser==2.21 pydantic==1.9.0 -pyelftools==0.27 +pydantic-yaml==0.6.1 +pyelftools==0.28 pylxd==2.3.1 pymacaroons==0.13.0 pyparsing==3.0.7 @@ -45,16 +48,19 @@ requests==2.27.1 requests-toolbelt==0.9.1 requests-unixsocket==0.3.0 SecretStorage==3.3.1 -semantic-version==2.8.5 +semantic-version==2.9.0 +semver==3.0.0.dev3 simplejson==3.17.6 six==1.16.0 tabulate==0.8.9 tinydb==4.6.1 toml==0.10.2 +types-Deprecated==1.2.5 typing-utils==0.1.0 typing_extensions==4.0.1 urllib3==1.26.8 wadllib==1.3.6 +wrapt==1.13.3 ws4py==0.5.1 zipp==3.7.0 python-apt @ https://launchpad.net/ubuntu/+archive/primary/+sourcefiles/python-apt/2.0.0ubuntu0.20.04.6/python-apt_2.0.0ubuntu0.20.04.6.tar.xz; sys.platform == "linux" From 0afa3dbfd99d16029662b401247cc3fd15f2c93d Mon Sep 17 00:00:00 2001 From: Claudio Matsuoka Date: Thu, 10 Feb 2022 12:53:59 -0300 Subject: [PATCH 029/167] meta: generate basic snap.yaml Create a minimal snap.yaml metadata file from data we already have available to enable snap packing. Signed-off-by: Claudio Matsuoka --- snapcraft/meta/__init__.py | 17 ++++ snapcraft/meta/snap_yaml.py | 136 ++++++++++++++++++++++++++++++ snapcraft/parts/lifecycle.py | 3 + snapcraft/parts/parts.py | 5 ++ snapcraft/projects.py | 8 +- tests/conftest.py | 6 +- tests/unit/meta/__init__.py | 0 tests/unit/meta/test_snap_yaml.py | 87 +++++++++++++++++++ tests/unit/test_projects.py | 8 +- 9 files changed, 259 insertions(+), 11 deletions(-) create mode 100644 snapcraft/meta/__init__.py create mode 100644 snapcraft/meta/snap_yaml.py create mode 100644 tests/unit/meta/__init__.py create mode 100644 tests/unit/meta/test_snap_yaml.py diff --git a/snapcraft/meta/__init__.py b/snapcraft/meta/__init__.py new file mode 100644 index 0000000000..ad61d920b5 --- /dev/null +++ b/snapcraft/meta/__init__.py @@ -0,0 +1,17 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright 2022 Canonical Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +"""Snap metadata definitions and helpers.""" diff --git a/snapcraft/meta/snap_yaml.py b/snapcraft/meta/snap_yaml.py new file mode 100644 index 0000000000..c5ebf839c8 --- /dev/null +++ b/snapcraft/meta/snap_yaml.py @@ -0,0 +1,136 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright 2022 Canonical Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +"""Create snap.yaml metadata file.""" + +import textwrap +from pathlib import Path +from typing import Dict, List, Optional, cast + +import yaml +from pydantic_yaml import YamlModel + +from snapcraft.projects import Project + + +class SnapApp(YamlModel): + """Snap.yaml app entry. + + This is currently a partial implementation, see + https://snapcraft.io/docs/snap-format for details. + + TODO: add missing properties, improve validation (CRAFT-802) + """ + + command: str + command_chain: List[str] + environment: Optional[List[Dict[str, str]]] + plugs: Optional[List[str]] + + class Config: # pylint: disable=too-few-public-methods + """Pydantic model configuration.""" + + allow_population_by_field_name = True + alias_generator = lambda s: s.replace("_", "-") # noqa: E731 + + +class SnapMetadata(YamlModel): + """The snap.yaml model. + + This is currently a partial implementation, see + https://snapcraft.io/docs/snap-format for details. + + TODO: add missing properties, improve validation (CRAFT-802) + """ + + name: str + title: Optional[str] + version: str + summary: str + description: str + license: Optional[str] + type: str + architectures: List[str] + base: str + assumes: Optional[List[str]] + epoch: Optional[str] + apps: Optional[Dict[str, SnapApp]] + confinement: str + grade: str + + +def write(project: Project, prime_dir: Path, *, arch: str): + """Create a snap.yaml file.""" + meta_dir = prime_dir / "meta" + meta_dir.mkdir(parents=True, exist_ok=True) + + _write_snapcraft_runner(prime_dir) + + snap_apps: Dict[str, SnapApp] = {} + if project.apps: + for name, app in project.apps.items(): + snap_apps[name] = SnapApp( + command=app.command, + command_chain=["snap/command-chain/snapcraft-runner"], + environment=app.environment, + plugs=app.plugs, + ) + + # FIXME: handle adopted parameters + snap_metadata = SnapMetadata( + name=project.name, + title=project.title, + version=project.version, # type: ignore + summary=project.summary, + description=project.description, + license=project.license, + type=project.type, + architectures=[arch], + base=cast(str, project.base), + assumes=["command-chain"], + epoch=project.epoch, + apps=snap_apps, + confinement=project.confinement, + grade=project.grade, + ) + + yaml.add_representer(str, _repr_str, Dumper=yaml.SafeDumper) + yaml_data = snap_metadata.yaml(exclude_none=True, sort_keys=False, width=1000) + + snap_yaml = meta_dir / "snap.yaml" + snap_yaml.write_text(yaml_data) + + +def _write_snapcraft_runner(prime_dir: Path): + content = textwrap.dedent( + """#!/bin/sh + export PATH="$SNAP/usr/sbin:$SNAP/usr/bin:$SNAP/sbin:$SNAP/bin:$PATH" + export LD_LIBRARY_PATH=$SNAP_LIBRARY_PATH:$LD_LIBRARY_PATH + exec "$@" + """ + ) + + runner_path = prime_dir / "snap/command-chain/snapcraft-runner" + runner_path.parent.mkdir(parents=True, exist_ok=True) + runner_path.write_text(content) + runner_path.chmod(0o755) + + +def _repr_str(dumper, data): + """Multi-line string representer for the YAML dumper.""" + if "\n" in data: + return dumper.represent_scalar("tag:yaml.org,2002:str", data, style="|") + return dumper.represent_scalar("tag:yaml.org,2002:str", data) diff --git a/snapcraft/parts/lifecycle.py b/snapcraft/parts/lifecycle.py index 582b738e0c..3590864dbf 100644 --- a/snapcraft/parts/lifecycle.py +++ b/snapcraft/parts/lifecycle.py @@ -24,6 +24,7 @@ from craft_cli import emit from snapcraft import errors +from snapcraft.meta import snap_yaml from snapcraft.parts import PartsLifecycle from snapcraft.projects import Project @@ -86,6 +87,8 @@ def _run_step( lifecycle = PartsLifecycle(project.parts, work_dir=work_dir) lifecycle.run(step_name) + snap_yaml.write(project, lifecycle.prime_dir, arch=lifecycle.target_arch) + def _load_yaml(filename: Path) -> Dict[str, Any]: """Load and parse a YAML-formatted file. diff --git a/snapcraft/parts/parts.py b/snapcraft/parts/parts.py index 32091a41fb..a09a07263a 100644 --- a/snapcraft/parts/parts.py +++ b/snapcraft/parts/parts.py @@ -68,6 +68,11 @@ def prime_dir(self) -> pathlib.Path: """Return the parts prime directory path.""" return self._lcm.project_info.prime_dir + @property + def target_arch(self) -> str: + """Return the parts project target architecture.""" + return self._lcm.project_info.target_arch + def run(self, step_name: str) -> None: """Run the parts lifecycle. diff --git a/snapcraft/projects.py b/snapcraft/projects.py index 92d6f46dd8..6dcdc15122 100644 --- a/snapcraft/projects.py +++ b/snapcraft/projects.py @@ -100,10 +100,10 @@ class App(ProjectModel): ] ] install_mode: Optional[Literal["enable", "disable"]] - slots: UniqueStrList = [] - plugs: UniqueStrList = [] - aliases: UniqueAliasList = [] - environment: List[Dict[str, str]] = [] + slots: Optional[UniqueStrList] + plugs: Optional[UniqueStrList] + aliases: Optional[UniqueAliasList] + environment: Optional[List[Dict[str, str]]] command_chain: List[CommandChainStr] = [] # TODO: sockets diff --git a/tests/conftest.py b/tests/conftest.py index fd989d1a52..9cc950b657 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -39,12 +39,12 @@ def temp_xdg(tmpdir, mocker): @pytest.fixture -def new_dir(tmpdir): +def new_dir(tmp_path): """Change to a new temporary directory.""" cwd = os.getcwd() - os.chdir(tmpdir) + os.chdir(tmp_path) - yield tmpdir + yield tmp_path os.chdir(cwd) diff --git a/tests/unit/meta/__init__.py b/tests/unit/meta/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/unit/meta/test_snap_yaml.py b/tests/unit/meta/test_snap_yaml.py new file mode 100644 index 0000000000..a67b2a1505 --- /dev/null +++ b/tests/unit/meta/test_snap_yaml.py @@ -0,0 +1,87 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright 2022 Canonical Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import textwrap +from pathlib import Path + +import pytest +import yaml + +from snapcraft.meta import snap_yaml +from snapcraft.projects import Project + + +@pytest.fixture +def simple_project(): + snapcraft_yaml = textwrap.dedent( + """\ + name: mytest + version: 1.29.3 + base: core22 + summary: Single-line elevator pitch for your amazing snap + description: | + This is my-snap's description. You have a paragraph or two to tell the + most important story about your snap. Keep it under 100 words though, + we live in tweetspace and your description wants to look good in the snap + store. + + grade: stable + confinement: strict + + parts: + part1: + plugin: nil + + apps: + app1: + command: bin/mytest + """ + ) + data = yaml.safe_load(snapcraft_yaml) + yield Project.unmarshal(data) + + +def test_snap_yaml(simple_project, new_dir): + snap_yaml.write(simple_project, prime_dir=Path(new_dir), arch="arch") + yaml_file = Path("meta/snap.yaml") + assert yaml_file.is_file() + + content = yaml_file.read_text() + assert content == textwrap.dedent( + """\ + name: mytest + version: 1.29.3 + summary: Single-line elevator pitch for your amazing snap + description: | + This is my-snap's description. You have a paragraph or two to tell the + most important story about your snap. Keep it under 100 words though, + we live in tweetspace and your description wants to look good in the snap + store. + type: app + architectures: + - arch + base: core22 + assumes: + - command-chain + apps: + app1: + command: bin/mytest + command_chain: + - snap/command-chain/snapcraft-runner + confinement: strict + grade: stable + """ + ) diff --git a/tests/unit/test_projects.py b/tests/unit/test_projects.py index 716d6bff38..3b0bcef3df 100644 --- a/tests/unit/test_projects.py +++ b/tests/unit/test_projects.py @@ -96,10 +96,10 @@ def test_app_defaults(self, yaml_data): assert app.stop_mode is None assert app.restart_condition is None assert app.install_mode is None - assert app.slots == [] - assert app.plugs == [] - assert app.aliases == [] - assert app.environment == [] + assert app.slots is None + assert app.plugs is None + assert app.aliases is None + assert app.environment is None assert app.command_chain == [] From 9568677233b2bdc9f35bf4a7dc4f5faa6829976c Mon Sep 17 00:00:00 2001 From: Claudio Matsuoka Date: Fri, 11 Feb 2022 16:38:54 -0300 Subject: [PATCH 030/167] commands: add pack command and set it as default Add the `pack` command to create a snap package from the current project. When snapcraft is invoked without an explicit command, `pack` will be executed. Signed-off-by: Claudio Matsuoka --- snapcraft/cli.py | 2 + snapcraft/commands/__init__.py | 1 + snapcraft/commands/lifecycle.py | 69 ++++++++++++++++++--- snapcraft/pack.py | 68 +++++++++++++++++++++ snapcraft/parts/lifecycle.py | 23 +++++-- tests/unit/cli/test_default_command.py | 42 +++++++++++++ tests/unit/cli/test_lifecycle.py | 28 +++++++++ tests/unit/cli/test_version.py | 10 +-- tests/unit/commands/test_lifecycle.py | 25 +++++++- tests/unit/parts/test_lifecycle.py | 45 ++++++++++++-- tests/unit/test_pack.py | 84 ++++++++++++++++++++++++++ 11 files changed, 369 insertions(+), 28 deletions(-) create mode 100644 snapcraft/pack.py create mode 100644 tests/unit/cli/test_default_command.py create mode 100644 tests/unit/test_pack.py diff --git a/snapcraft/cli.py b/snapcraft/cli.py index 40e00f6d72..0b18e90769 100644 --- a/snapcraft/cli.py +++ b/snapcraft/cli.py @@ -36,6 +36,7 @@ commands.BuildCommand, commands.StageCommand, commands.PrimeCommand, + commands.PackCommand, ], ), craft_cli.CommandGroup("Other", [commands.VersionCommand]), @@ -71,6 +72,7 @@ def run(): COMMAND_GROUPS, summary="What's the app about", extra_global_args=GLOBAL_ARGS, + default_command=commands.PackCommand, ) try: diff --git a/snapcraft/commands/__init__.py b/snapcraft/commands/__init__.py index b4d2ca060c..9a2da35c83 100644 --- a/snapcraft/commands/__init__.py +++ b/snapcraft/commands/__init__.py @@ -18,6 +18,7 @@ from .lifecycle import ( # noqa: F401 BuildCommand, + PackCommand, PrimeCommand, PullCommand, StageCommand, diff --git a/snapcraft/commands/lifecycle.py b/snapcraft/commands/lifecycle.py index 1888165300..127a9a7aeb 100644 --- a/snapcraft/commands/lifecycle.py +++ b/snapcraft/commands/lifecycle.py @@ -23,6 +23,7 @@ from craft_cli import BaseCommand, emit from overrides import overrides +from snapcraft import pack from snapcraft.parts import lifecycle as parts_lifecycle if TYPE_CHECKING: @@ -30,13 +31,12 @@ class _LifecycleCommand(BaseCommand, abc.ABC): - """Run lifecycle commands.""" + """Run lifecycle-related commands.""" @overrides def fill_parser(self, parser: "argparse.ArgumentParser") -> None: - parser.add_argument( - "parts", metavar="parts", type=str, nargs="*", help="Parts to process" - ) + # TODO: add arguments for all step commands and pack + pass @overrides def run(self, parsed_args): @@ -48,7 +48,18 @@ def run(self, parsed_args): parts_lifecycle.run(self.name, parsed_args) -class PullCommand(_LifecycleCommand): +class _LifecycleStepCommand(_LifecycleCommand): + """Run lifecycle step commands.""" + + @overrides + def fill_parser(self, parser: "argparse.ArgumentParser") -> None: + super().fill_parser(parser) + parser.add_argument( + "parts", metavar="parts", type=str, nargs="*", help="Parts to process" + ) + + +class PullCommand(_LifecycleStepCommand): """Run the lifecycle up to the pull step.""" name = "pull" @@ -62,7 +73,7 @@ class PullCommand(_LifecycleCommand): ) -class BuildCommand(_LifecycleCommand): +class BuildCommand(_LifecycleStepCommand): """Run the lifecycle up to the build step.""" name = "build" @@ -75,7 +86,7 @@ class BuildCommand(_LifecycleCommand): ) -class StageCommand(_LifecycleCommand): +class StageCommand(_LifecycleStepCommand): """Run the lifecycle up to the stage step.""" name = "stage" @@ -89,7 +100,7 @@ class StageCommand(_LifecycleCommand): ) -class PrimeCommand(_LifecycleCommand): +class PrimeCommand(_LifecycleStepCommand): """Prepare the final payload for packing.""" name = "prime" @@ -101,3 +112,45 @@ class PrimeCommand(_LifecycleCommand): those parts will be primed. The default is to prime all parts. """ ) + + +class PackCommand(_LifecycleCommand): + """Prepare the final payload for packing.""" + + name = "pack" + help_msg = "Build artifacts defined for a part" + overview = textwrap.dedent( + """ + Prepare the final payload to be packed as a snap. If part names are + specified only those parts will be primed. The default is to prime + all parts. + """ + ) + + @overrides + def fill_parser(self, parser: "argparse.ArgumentParser") -> None: + """Add arguments specific to the pack command.""" + super().fill_parser(parser) + parser.add_argument( + "directory", + metavar="directory", + type=str, + nargs="?", + default=None, + help="Directory to pack", + ) + parser.add_argument( + "-o", + "--output", + metavar="filename", + type=str, + help="Path to the resulting snap", + ) + + @overrides + def run(self, parsed_args): + """Run the command.""" + if parsed_args.directory: + pack.pack_snap(parsed_args.directory, output=parsed_args.output) + else: + super().run(parsed_args) diff --git a/snapcraft/pack.py b/snapcraft/pack.py new file mode 100644 index 0000000000..ff4ba07c05 --- /dev/null +++ b/snapcraft/pack.py @@ -0,0 +1,68 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright 2022 Canonical Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +"""Snap file packing.""" + +import subprocess +from pathlib import Path +from typing import List, Optional, Union + +from craft_cli import emit + +from snapcraft import errors + + +def pack_snap( + directory: Path, *, output: Optional[str], compression: Optional[str] = None +) -> None: + """Pack snap contents. + + :param output: Snap file name or directory. + :param compression: Compression type to use, None for defaults. + """ + output_file = None + output_dir = None + + if output: + output_path = Path(output) + output_parent = output_path.parent + if output_path.is_dir(): + output_dir = str(output_path) + elif output_parent and output_parent != Path("."): + output_dir = str(output_parent) + output_file = output_path.name + else: + output_file = output + + command: List[Union[str, Path]] = ["snap", "pack"] + if output_file is not None: + command.extend(["--filename", output_file]) + + # When None, just use snap pack's default settings. + if compression is not None: + command.extend(["--compression", compression]) + + command.append(directory) + + if output_dir is not None: + command.append(output_dir) + + emit.progress("Creating snap package...") + try: + subprocess.run(command, capture_output=True, check=True) # type: ignore + except subprocess.CalledProcessError as err: + raise errors.SnapcraftError(f"Cannot pack snap file: {err!s}") + emit.message("Created snap package", intermediate=True) diff --git a/snapcraft/parts/lifecycle.py b/snapcraft/parts/lifecycle.py index 3590864dbf..d0f3b7b07a 100644 --- a/snapcraft/parts/lifecycle.py +++ b/snapcraft/parts/lifecycle.py @@ -23,7 +23,7 @@ import yaml.error from craft_cli import emit -from snapcraft import errors +from snapcraft import errors, pack from snapcraft.meta import snap_yaml from snapcraft.parts import PartsLifecycle from snapcraft.projects import Project @@ -40,14 +40,14 @@ ] -def run(step_name: str, parsed_args: "argparse.Namespace") -> None: +def run(command_name: str, parsed_args: "argparse.Namespace") -> None: """Run the parts lifecycle. :raises SnapcraftError: if the step name is invalid, or the project yaml file cannot be loaded. :raises LegacyFallback: if the project's base is not core22. """ - emit.trace(f"{step_name} parts, arguments: {parsed_args}") + emit.trace(f"command: {command_name}, arguments: {parsed_args}") yaml_data = {} for project_file in _PROJECT_FILES: if project_file.is_file(): @@ -72,16 +72,20 @@ def run(step_name: str, parsed_args: "argparse.Namespace") -> None: project = Project.unmarshal(yaml_data) - _run_step(step_name, project, parsed_args) + _run_command(command_name, project, parsed_args) -def _run_step( - step_name: str, project: Project, parsed_args: "argparse.Namespace", +def _run_command( + command_name: str, + project: Project, + parsed_args: "argparse.Namespace", ) -> None: # TODO: check destructive and managed modes and run in provider _ = parsed_args + step_name = "prime" if command_name == "pack" else command_name + work_dir = Path("work").absolute() lifecycle = PartsLifecycle(project.parts, work_dir=work_dir) @@ -89,6 +93,13 @@ def _run_step( snap_yaml.write(project, lifecycle.prime_dir, arch=lifecycle.target_arch) + if command_name == "pack": + pack.pack_snap( + lifecycle.prime_dir, + output=parsed_args.output, + compression=project.compression, + ) + def _load_yaml(filename: Path) -> Dict[str, Any]: """Load and parse a YAML-formatted file. diff --git a/tests/unit/cli/test_default_command.py b/tests/unit/cli/test_default_command.py new file mode 100644 index 0000000000..af7f24f314 --- /dev/null +++ b/tests/unit/cli/test_default_command.py @@ -0,0 +1,42 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright 2022 Canonical Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import argparse +import sys +from unittest.mock import call + +import pytest + +from snapcraft import cli + + +def test_default_command(mocker): + mocker.patch.object(sys, "argv", ["cmd"]) + mock_pack_cmd = mocker.patch("snapcraft.commands.lifecycle.PackCommand.run") + cli.run() + assert mock_pack_cmd.mock_calls == [ + call(argparse.Namespace(directory=None, output=None)) + ] + + +@pytest.mark.parametrize("option", ["-o", "--output"]) +def test_default_command_output(mocker, option): + mocker.patch.object(sys, "argv", ["cmd", option, "name"]) + mock_pack_cmd = mocker.patch("snapcraft.commands.lifecycle.PackCommand.run") + cli.run() + assert mock_pack_cmd.mock_calls == [ + call(argparse.Namespace(directory=None, output="name")) + ] diff --git a/tests/unit/cli/test_lifecycle.py b/tests/unit/cli/test_lifecycle.py index 73191a75ca..a5b6d1afcd 100644 --- a/tests/unit/cli/test_lifecycle.py +++ b/tests/unit/cli/test_lifecycle.py @@ -55,3 +55,31 @@ def test_lifecycle_command_arguments(cmd, run_method, mocker): assert mock_lifecycle_cmd.mock_calls == [ call(argparse.Namespace(parts=["part1", "part2"])) ] + + +def test_lifecycle_command_pack(mocker): + mocker.patch.object(sys, "argv", ["cmd", "pack"]) + mock_pack_cmd = mocker.patch("snapcraft.commands.lifecycle.PackCommand.run") + cli.run() + assert mock_pack_cmd.mock_calls == [ + call(argparse.Namespace(directory=None, output=None)) + ] + + +@pytest.mark.parametrize("option", ["-o", "--output"]) +def test_lifecycle_command_pack_output(mocker, option): + mocker.patch.object(sys, "argv", ["cmd", "pack", option, "name"]) + mock_pack_cmd = mocker.patch("snapcraft.commands.lifecycle.PackCommand.run") + cli.run() + assert mock_pack_cmd.mock_calls == [ + call(argparse.Namespace(directory=None, output="name")) + ] + + +def test_lifecycle_command_pack_directory(mocker): + mocker.patch.object(sys, "argv", ["cmd", "pack", "name"]) + mock_pack_cmd = mocker.patch("snapcraft.commands.lifecycle.PackCommand.run") + cli.run() + assert mock_pack_cmd.mock_calls == [ + call(argparse.Namespace(directory="name", output=None)) + ] diff --git a/tests/unit/cli/test_version.py b/tests/unit/cli/test_version.py index 96556e64e8..087f27887b 100644 --- a/tests/unit/cli/test_version.py +++ b/tests/unit/cli/test_version.py @@ -18,8 +18,6 @@ import sys from unittest.mock import call -import pytest - from snapcraft import __version__, cli @@ -30,12 +28,10 @@ def test_version_command(mocker): assert mock_version_cmd.mock_calls == [call(argparse.Namespace())] -def test_version_argument(mocker, capsys): +def test_version_argument(mocker, emitter): mocker.patch.object(sys, "argv", ["cmd", "--version"]) - # FIXME: handled by legacy, change after craft-cli handles default command - with pytest.raises(SystemExit): - cli.run() - assert capsys.readouterr().out == f"snapcraft {__version__}\n" + cli.run() + emitter.assert_recorded([f"snapcraft {__version__}"]) def test_version_argument_with_command(mocker, emitter): diff --git a/tests/unit/commands/test_lifecycle.py b/tests/unit/commands/test_lifecycle.py index 301f5b7507..d7d5864de9 100644 --- a/tests/unit/commands/test_lifecycle.py +++ b/tests/unit/commands/test_lifecycle.py @@ -21,6 +21,7 @@ from snapcraft.commands.lifecycle import ( BuildCommand, + PackCommand, PrimeCommand, PullCommand, StageCommand, @@ -28,7 +29,7 @@ @pytest.mark.parametrize( - "step_name,cmd_class", + "cmd_name,cmd_class", [ ("pull", PullCommand), ("build", BuildCommand), @@ -36,10 +37,28 @@ ("prime", PrimeCommand), ], ) -def test_lifecycle_command(step_name, cmd_class, mocker): +def test_lifecycle_command(cmd_name, cmd_class, mocker): lifecycle_run_mock = mocker.patch("snapcraft.parts.lifecycle.run") cmd = cmd_class(None) cmd.run(argparse.Namespace(parts=["part1", "part2"])) assert lifecycle_run_mock.mock_calls == [ - call(step_name, argparse.Namespace(parts=["part1", "part2"])) + call(cmd_name, argparse.Namespace(parts=["part1", "part2"])) ] + + +def test_pack_command(mocker): + lifecycle_run_mock = mocker.patch("snapcraft.parts.lifecycle.run") + cmd = PackCommand(None) + cmd.run(argparse.Namespace(directory=None, output=None, compression=None)) + assert lifecycle_run_mock.mock_calls == [ + call("pack", argparse.Namespace(directory=None, output=None, compression=None)) + ] + + +def test_pack_command_with_directory(mocker): + lifecycle_run_mock = mocker.patch("snapcraft.parts.lifecycle.run") + pack_mock = mocker.patch("snapcraft.pack.pack_snap") + cmd = PackCommand(None) + cmd.run(argparse.Namespace(directory=".", output=None, compression=None)) + assert lifecycle_run_mock.mock_calls == [] + assert pack_mock.mock_calls == [call(".", output=None)] diff --git a/tests/unit/parts/test_lifecycle.py b/tests/unit/parts/test_lifecycle.py index b78d463ba7..23885f9977 100644 --- a/tests/unit/parts/test_lifecycle.py +++ b/tests/unit/parts/test_lifecycle.py @@ -108,13 +108,13 @@ def test_config_not_found(new_dir): def test_snapcraft_yaml_load(new_dir, snapcraft_yaml, filename, mocker): """Snapcraft.yaml should be parsed as a valid yaml file.""" yaml_data = snapcraft_yaml(base="core22", filename=filename) - run_step_mock = mocker.patch("snapcraft.parts.lifecycle._run_step") + run_command_mock = mocker.patch("snapcraft.parts.lifecycle._run_command") parts_lifecycle.run("pull", argparse.Namespace(parts=["part1"])) project = Project.unmarshal(yaml_data) - assert run_step_mock.mock_calls == [ + assert run_command_mock.mock_calls == [ call("pull", project, argparse.Namespace(parts=["part1"])) ] @@ -122,7 +122,7 @@ def test_snapcraft_yaml_load(new_dir, snapcraft_yaml, filename, mocker): def test_snapcraft_yaml_parse_error(new_dir, snapcraft_yaml, mocker): """If snapcraft.yaml is not a valid yaml, raise an error.""" snapcraft_yaml(base="invalid: true") - run_step_mock = mocker.patch("snapcraft.parts.lifecycle._run_step") + run_command_mock = mocker.patch("snapcraft.parts.lifecycle._run_command") with pytest.raises(errors.SnapcraftError) as raised: parts_lifecycle.run("pull", argparse.Namespace(parts=["part1"])) @@ -131,7 +131,7 @@ def test_snapcraft_yaml_parse_error(new_dir, snapcraft_yaml, mocker): "YAML parsing error: mapping values are not allowed here\n" ' in "snap/snapcraft.yaml", line 4, column 14' ) - assert run_step_mock.mock_calls == [] + assert run_command_mock.mock_calls == [] def test_legacy_base_not_core22(new_dir, snapcraft_yaml): @@ -141,3 +141,40 @@ def test_legacy_base_not_core22(new_dir, snapcraft_yaml): parts_lifecycle.run("pull", argparse.Namespace()) assert str(raised.value) == "base is not core22" + + +@pytest.mark.parametrize( + "cmd,step", + [ + ("pull", "pull"), + ("build", "build"), + ("stage", "stage"), + ("prime", "prime"), + ], +) +def test_lifecycle_run_command_step(cmd, step, snapcraft_yaml, new_dir, mocker): + project = Project.unmarshal(snapcraft_yaml(base="core22")) + run_mock = mocker.patch("snapcraft.parts.PartsLifecycle.run") + mocker.patch("snapcraft.meta.snap_yaml.write") + pack_mock = mocker.patch("snapcraft.pack.pack_snap") + + parts_lifecycle._run_command(cmd, project, argparse.Namespace()) + + assert run_mock.mock_calls == [call(step)] + assert pack_mock.mock_calls == [] + + +def test_lifecycle_run_command_pack(snapcraft_yaml, new_dir, mocker): + project = Project.unmarshal(snapcraft_yaml(base="core22")) + run_mock = mocker.patch("snapcraft.parts.PartsLifecycle.run") + mocker.patch("snapcraft.meta.snap_yaml.write") + pack_mock = mocker.patch("snapcraft.pack.pack_snap") + + parts_lifecycle._run_command( + "pack", project, argparse.Namespace(directory=None, output=None) + ) + + assert run_mock.mock_calls == [call("prime")] + assert pack_mock.mock_calls == [ + call(new_dir / "work/prime", output=None, compression="xz") + ] diff --git a/tests/unit/test_pack.py b/tests/unit/test_pack.py new file mode 100644 index 0000000000..9653e3055d --- /dev/null +++ b/tests/unit/test_pack.py @@ -0,0 +1,84 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright 2022 Canonical Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import subprocess +from unittest.mock import call + +import pytest + +from snapcraft import errors, pack + + +def test_pack_snap(mocker, new_dir): + mock_run = mocker.patch("subprocess.run") + pack.pack_snap(new_dir, output=None) + assert mock_run.mock_calls == [ + call(["snap", "pack", new_dir], capture_output=True, check=True) + ] + + +def test_pack_snap_compression_none(mocker, new_dir): + mock_run = mocker.patch("subprocess.run") + pack.pack_snap(new_dir, output=None, compression=None) + assert mock_run.mock_calls == [ + call(["snap", "pack", new_dir], capture_output=True, check=True) + ] + + +def test_pack_snap_compression(mocker, new_dir): + mock_run = mocker.patch("subprocess.run") + pack.pack_snap(new_dir, output=None, compression="zz") + assert mock_run.mock_calls == [ + call( + ["snap", "pack", "--compression", "zz", new_dir], + capture_output=True, + check=True, + ) + ] + + +def test_pack_snap_output_file(mocker, new_dir): + mock_run = mocker.patch("subprocess.run") + pack.pack_snap(new_dir, output="/tmp/foo") + assert mock_run.mock_calls == [ + call( + ["snap", "pack", "--filename", "foo", new_dir, "/tmp"], + capture_output=True, + check=True, + ) + ] + + +def test_pack_snap_output_dir(mocker, new_dir): + mock_run = mocker.patch("subprocess.run") + pack.pack_snap(new_dir, output=str(new_dir)) + assert mock_run.mock_calls == [ + call( + ["snap", "pack", new_dir, str(new_dir)], + capture_output=True, + check=True, + ) + ] + + +def test_pack_snap_error(mocker, new_dir): + mocker.patch("subprocess.run", side_effect=subprocess.CalledProcessError(42, "cmd")) + with pytest.raises(errors.SnapcraftError) as raised: + pack.pack_snap(new_dir, output=str(new_dir)) + + assert str(raised.value) == ( + "Cannot pack snap file: Command 'cmd' returned non-zero exit status 42." + ) From 462a3e45fadf56ee42216f407f861582bc6494b9 Mon Sep 17 00:00:00 2001 From: Claudio Matsuoka Date: Fri, 11 Feb 2022 18:00:39 -0300 Subject: [PATCH 031/167] ci: update pull request template Signed-off-by: Claudio Matsuoka --- .github/PULL_REQUEST_TEMPLATE.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index b290371971..45892f49ae 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,6 +1,6 @@ - [ ] Have you followed the [guidelines for contributing](https://github.com/snapcore/snapcraft/blob/master/CONTRIBUTING.md)? - [ ] Have you signed the [CLA](http://www.ubuntu.com/legal/contributors/)? -- [ ] Have you successfully run `./runtests.sh static`? -- [ ] Have you successfully run `./runtests.sh tests/unit`? +- [ ] Have you successfully run `make lint`? +- [ ] Have you successfully run `pytest tests/unit`? ----- From e448600fa1f2faea278efbe707886b75af43e82b Mon Sep 17 00:00:00 2001 From: Sergio Schvezov Date: Fri, 18 Feb 2022 13:50:08 -0300 Subject: [PATCH 032/167] grammar: migrate to external package Signed-off-by: Sergio Schvezov --- requirements.txt | 1 + setup.py | 1 + .../internal/project_loader/_config.py | 7 +- .../internal/project_loader/_parts_config.py | 3 +- .../project_loader/grammar/__init__.py | 20 - .../project_loader/grammar/_compound.py | 74 --- .../internal/project_loader/grammar/_on.py | 130 ------ .../project_loader/grammar/_processor.py | 283 ------------ .../project_loader/grammar/_statement.py | 166 ------- .../internal/project_loader/grammar/_to.py | 121 ----- .../internal/project_loader/grammar/_try.py | 74 --- .../internal/project_loader/grammar/errors.py | 55 --- .../internal/project_loader/grammar/typing.py | 6 - .../_global_grammar_processor.py | 22 +- .../_package_transformer.py | 12 +- .../_part_grammar_processor.py | 81 ++-- tests/legacy/unit/part_loader.py | 6 +- .../unit/project_loader/grammar/__init__.py | 0 .../grammar/test_compound_statement.py | 271 ----------- .../grammar/test_on_statement.py | 264 ----------- .../project_loader/grammar/test_processor.py | 436 ------------------ .../grammar/test_to_statement.py | 303 ------------ .../grammar/test_try_statement.py | 135 ------ .../test_part_grammar_processor.py | 83 ++-- 24 files changed, 115 insertions(+), 2439 deletions(-) delete mode 100644 snapcraft_legacy/internal/project_loader/grammar/__init__.py delete mode 100644 snapcraft_legacy/internal/project_loader/grammar/_compound.py delete mode 100644 snapcraft_legacy/internal/project_loader/grammar/_on.py delete mode 100644 snapcraft_legacy/internal/project_loader/grammar/_processor.py delete mode 100644 snapcraft_legacy/internal/project_loader/grammar/_statement.py delete mode 100644 snapcraft_legacy/internal/project_loader/grammar/_to.py delete mode 100644 snapcraft_legacy/internal/project_loader/grammar/_try.py delete mode 100644 snapcraft_legacy/internal/project_loader/grammar/errors.py delete mode 100644 snapcraft_legacy/internal/project_loader/grammar/typing.py delete mode 100644 tests/legacy/unit/project_loader/grammar/__init__.py delete mode 100644 tests/legacy/unit/project_loader/grammar/test_compound_statement.py delete mode 100644 tests/legacy/unit/project_loader/grammar/test_on_statement.py delete mode 100644 tests/legacy/unit/project_loader/grammar/test_processor.py delete mode 100644 tests/legacy/unit/project_loader/grammar/test_to_statement.py delete mode 100644 tests/legacy/unit/project_loader/grammar/test_try_statement.py diff --git a/requirements.txt b/requirements.txt index 7a7ae449f3..c6a4a42162 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,6 +6,7 @@ chardet==4.0.0 charset-normalizer==2.0.11 click==8.0.3 craft-cli==0.2.0 +craft-grammar==1.0.0 craft-parts==1.1.2 cryptography==3.4 Deprecated==1.2.13 diff --git a/setup.py b/setup.py index 7e832915bb..ab9851e4d7 100755 --- a/setup.py +++ b/setup.py @@ -93,6 +93,7 @@ def recursive_data_files(directory, install_directory): "attrs", "click", "craft-cli", + "craft-grammar", "craft-parts", "cryptography==3.4", "gnupg", diff --git a/snapcraft_legacy/internal/project_loader/_config.py b/snapcraft_legacy/internal/project_loader/_config.py index a8628edaba..2f2253eb31 100644 --- a/snapcraft_legacy/internal/project_loader/_config.py +++ b/snapcraft_legacy/internal/project_loader/_config.py @@ -213,7 +213,7 @@ def __init__(self, project: project.Project) -> None: self._ensure_no_duplicate_app_aliases() self._global_grammar_processor = grammar_processing.GlobalGrammarProcessor( - properties=self.data, project=project + properties=self.data, arch=project.deb_arch, target_arch=project.target_arch ) # XXX: Resetting snap_meta due to above mangling of data. @@ -257,7 +257,10 @@ def _get_required_package_repositories(self) -> List[PackageRepository]: return package_repos def _verify_all_key_assets_installed( - self, *, key_assets: pathlib.Path, key_manager: apt_key_manager.AptKeyManager, + self, + *, + key_assets: pathlib.Path, + key_manager: apt_key_manager.AptKeyManager, ) -> None: """Verify all configured key assets are utilized, error if not.""" for key_asset in key_assets.glob("*"): diff --git a/snapcraft_legacy/internal/project_loader/_parts_config.py b/snapcraft_legacy/internal/project_loader/_parts_config.py index ae4ffb88e0..d27ec843fa 100644 --- a/snapcraft_legacy/internal/project_loader/_parts_config.py +++ b/snapcraft_legacy/internal/project_loader/_parts_config.py @@ -194,7 +194,8 @@ def load_part(self, part_name, plugin_name, part_properties): grammar_processor = grammar_processing.PartGrammarProcessor( plugin=plugin, properties=part_properties, - project=self._project, + arch=self._project.deb_arch, + target_arch=self._project.target_arch, repo=stage_packages_repo, ) diff --git a/snapcraft_legacy/internal/project_loader/grammar/__init__.py b/snapcraft_legacy/internal/project_loader/grammar/__init__.py deleted file mode 100644 index c1b7095226..0000000000 --- a/snapcraft_legacy/internal/project_loader/grammar/__init__.py +++ /dev/null @@ -1,20 +0,0 @@ -# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- -# -# Copyright (C) 2017, 2018 Canonical Ltd -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License version 3 as -# published by the Free Software Foundation. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -from ._compound import CompoundStatement # noqa -from ._processor import GrammarProcessor # noqa -from ._statement import Statement # noqa -from ._to import ToStatement # noqa diff --git a/snapcraft_legacy/internal/project_loader/grammar/_compound.py b/snapcraft_legacy/internal/project_loader/grammar/_compound.py deleted file mode 100644 index aef0fac68a..0000000000 --- a/snapcraft_legacy/internal/project_loader/grammar/_compound.py +++ /dev/null @@ -1,74 +0,0 @@ -# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- -# -# Copyright (C) 2018 Canonical Ltd -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License version 3 as -# published by the Free Software Foundation. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -from typing import TYPE_CHECKING, List - -from . import typing -from ._statement import Statement - -# Don't use circular imports unless type checking -if TYPE_CHECKING: - from ._processor import GrammarProcessor # noqa: F401 - - -class CompoundStatement(Statement): - """Multiple statements that need to be treated as a group.""" - - def __init__( - self, - *, - statements: List[Statement], - body: typing.Grammar, - processor: "GrammarProcessor", - call_stack: typing.CallStack = None - ) -> None: - """Create an CompoundStatement instance. - - :param list statements: List of compound statements - :param list body: The body of the clause. - :param GrammarProcessor process: GrammarProcessor to use for processing - this statement. - :param list call_stack: Call stack leading to this statement. - """ - super().__init__(body=body, processor=processor, call_stack=call_stack) - - self.statements = statements - - def _check(self) -> bool: - """Check if each statement checks True, in order - - :return: True if each statement agrees that they should be processed, - False if elses should be processed. - :rtype: bool - """ - for statement in self.statements: - if not statement._check(): - return False - - return True - - def __eq__(self, other) -> bool: - if type(other) is type(self): - return self.statements == other.statements - - return False - - def __str__(self) -> str: - representation = "" - for statement in self.statements: - representation += "{!s} ".format(statement) - - return representation.strip() diff --git a/snapcraft_legacy/internal/project_loader/grammar/_on.py b/snapcraft_legacy/internal/project_loader/grammar/_on.py deleted file mode 100644 index 75fccbdfc4..0000000000 --- a/snapcraft_legacy/internal/project_loader/grammar/_on.py +++ /dev/null @@ -1,130 +0,0 @@ -# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- -# -# Copyright (C) 2017, 2018 Canonical Ltd -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License version 3 as -# published by the Free Software Foundation. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -import re -from typing import TYPE_CHECKING, Optional, Set - -import snapcraft_legacy - -from . import typing -from ._statement import Statement -from .errors import OnStatementSyntaxError - -# Don't use circular imports unless type checking -if TYPE_CHECKING: - from ._processor import GrammarProcessor # noqa: F401 - -_SELECTOR_PATTERN = re.compile(r"\Aon\s+([^,\s](?:,?[^,]+)*)\Z") -_WHITESPACE_PATTERN = re.compile(r"\A.*\s.*\Z") - - -class OnStatement(Statement): - """Process an 'on' statement in the grammar. - - For example: - >>> from snapcraft_legacy import ProjectOptions - >>> from snapcraft_legacy.internal.project_loader import grammar - >>> from unittest import mock - >>> - >>> def checker(primitive): - ... return True - >>> options = ProjectOptions() - >>> processor = grammar.GrammarProcessor(None, options, checker) - >>> - >>> clause = OnStatement(on='on amd64', body=['foo'], processor=processor) - >>> clause.add_else(['bar']) - >>> with mock.patch('platform.machine') as mock_machine: - ... # Pretend this machine is an i686, not amd64 - ... mock_machine.return_value = 'i686' - ... clause.process() - {'bar'} - """ - - def __init__( - self, - *, - on: str, - body: Optional[typing.Grammar], - processor: "GrammarProcessor", - call_stack: typing.CallStack = None - ) -> None: - """Create an OnStatement instance. - - :param str on: The 'on ' part of the clause. - :param list body: The body of the clause. - :param GrammarProcessor process: GrammarProcessor to use for processing - this statement. - :param list call_stack: Call stack leading to this statement. - """ - super().__init__(body=body, processor=processor, call_stack=call_stack) - - self.selectors = _extract_on_clause_selectors(on) - - def _check(self) -> bool: - """Check if a statement main body should be processed. - - :return: True if main body should be processed, False if elses should - be processed. - :rtype: bool - """ - # A new ProjectOptions instance defaults to the host architecture - # whereas self._project_options would yield the target architecture - host_arch = snapcraft_legacy.ProjectOptions().deb_arch - - # The only selector currently supported is the host arch. Since - # selectors are matched with an AND, not OR, there should only be one - # selector. - return (len(self.selectors) == 1) and (host_arch in self.selectors) - - def __eq__(self, other) -> bool: - if type(other) is type(self): - return self.selectors == other.selectors - - return False - - def __str__(self) -> str: - return "on {}".format(",".join(sorted(self.selectors))) - - -def _extract_on_clause_selectors(on: str) -> Set[str]: - """Extract the list of selectors within an on clause. - - :param str on: The 'on ' part of the 'on' clause. - - :return: Selectors found within the 'on' clause. - - For example: - >>> _extract_on_clause_selectors('on amd64,i386') == {'amd64', 'i386'} - True - """ - - match = _SELECTOR_PATTERN.match(on) - if match is None: - raise OnStatementSyntaxError(on, message="selectors are missing") - - try: - selector_group = match.group(1) - except IndexError: - raise OnStatementSyntaxError(on) - - # This could be part of the _SELECTOR_PATTERN, but that would require us - # to provide a very generic error when we can try to be more helpful. - if _WHITESPACE_PATTERN.match(selector_group): - raise OnStatementSyntaxError( - on, message="spaces are not allowed in the selectors" - ) - - return {selector.strip() for selector in selector_group.split(",")} diff --git a/snapcraft_legacy/internal/project_loader/grammar/_processor.py b/snapcraft_legacy/internal/project_loader/grammar/_processor.py deleted file mode 100644 index 1bf5c3ec17..0000000000 --- a/snapcraft_legacy/internal/project_loader/grammar/_processor.py +++ /dev/null @@ -1,283 +0,0 @@ -# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- -# -# Copyright (C) 2017, 2018 Canonical Ltd -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License version 3 as -# published by the Free Software Foundation. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -import re -from typing import Any, Callable, Dict, List, Optional, Tuple - -from snapcraft_legacy import project - -from . import typing -from ._compound import CompoundStatement -from ._on import OnStatement -from ._statement import Statement -from ._to import ToStatement -from ._try import TryStatement -from .errors import GrammarSyntaxError - -_ON_TO_CLAUSE_PATTERN = re.compile(r"(\Aon\s+\S+)\s+(to\s+\S+\Z)") -_ON_CLAUSE_PATTERN = re.compile(r"\Aon\s+") -_TO_CLAUSE_PATTERN = re.compile(r"\Ato\s+") -_TRY_CLAUSE_PATTERN = re.compile(r"\Atry\Z") -_ELSE_CLAUSE_PATTERN = re.compile(r"\Aelse\Z") -_ELSE_FAIL_PATTERN = re.compile(r"\Aelse\s+fail\Z") - - -class GrammarProcessor: - """The GrammarProcessor extracts desired primitives from grammar.""" - - def __init__( - self, - grammar: typing.Grammar, - project: project.Project, - checker: Callable[[Any], bool], - *, - transformer: Callable[[List[Statement], str, project.Project], str] = None, - ) -> None: - """Create a new GrammarProcessor. - - :param list grammar: Unprocessed grammar. - :param project: Instance of Project to use to determine appropriate - primitives. - :type project: snapcraft_legacy.project.Project - :param callable checker: callable accepting a single primitive, - returning true if it is valid. - :param callable transformer: callable accepting a call stack, single - primitive, and project, and returning a - transformed primitive. - """ - self._grammar = grammar - self.project = project - self.checker = checker - - if transformer: - self._transformer = transformer - else: - # By default, no transformation - self._transformer = lambda s, p, o: p - - def process( - self, *, grammar: typing.Grammar = None, call_stack: typing.CallStack = None - ) -> List[Any]: - """Process grammar and extract desired primitives. - - :param list grammar: Unprocessed grammar (defaults to that set in - init). - :param list call_stack: Call stack of statements leading to now. - - :return: Primitives selected - """ - - if grammar is None: - grammar = self._grammar - - if call_stack is None: - call_stack = [] - - primitives: List[Any] = list() - statements = _StatementCollection() - statement: Optional[Statement] = None - - for section in grammar: - if isinstance(section, str): - # If the section is just a string, it's either "else fail" or a - # primitive name. - if _ELSE_FAIL_PATTERN.match(section): - _handle_else(statement, None) - else: - # Processing a string primitive indicates the previous section - # is finalized (if any), process it first before this primitive. - self._process_statement( - statement=statement, - statements=statements, - primitives=primitives, - ) - statement = None - - primitive = self._transformer(call_stack, section, self.project) - primitives.append(primitive) - elif isinstance(section, dict): - statement, finalized_statement = self._parse_section_dictionary( - call_stack=call_stack, section=section, statement=statement, - ) - - # Process any finalized statement (if any). - if finalized_statement is not None: - self._process_statement( - statement=finalized_statement, - statements=statements, - primitives=primitives, - ) - - # If this section does not belong to a statement, it is - # a primitive to be recorded. - if statement is None: - primitives.append(section) - - else: - # jsonschema should never let us get here. - raise GrammarSyntaxError( - "expected grammar section to be either of type 'str' or " - "type 'dict', but got {!r}".format(type(section)) - ) - - # Process the final statement (if any). - self._process_statement( - statement=statement, statements=statements, primitives=primitives, - ) - - return primitives - - def _process_statement( - self, - *, - statement: Optional[Statement], - statements: "_StatementCollection", - primitives: List[Any], - ): - if statement is None: - return - - statements.add(statement) - processed_primitives = statement.process() - primitives.extend(processed_primitives) - - def _parse_section_dictionary( - self, - *, - section: Dict[str, Any], - statement: Optional[Statement], - call_stack: typing.CallStack, - ) -> Tuple[Optional[Statement], Optional[Statement]]: - finalized_statement: Optional[Statement] = None - for key, value in section.items(): - # Grammar is always written as a list of selectors but the value - # can be a list or a string. In the latter case we wrap it so no - # special care needs to be taken when fetching the result from the - # primitive. - if not isinstance(value, list): - value = [value] - - on_to_clause_match = _ON_TO_CLAUSE_PATTERN.match(key) - on_clause_match = _ON_CLAUSE_PATTERN.match(key) - if on_to_clause_match: - # We've come across the beginning of a compound statement - # with both 'on' and 'to'. - finalized_statement = statement - - # First, extract each statement's part of the string - on, to = on_to_clause_match.groups() - - # Now create a list of statements, in order - compound_statements = [ - OnStatement( - on=on, body=None, processor=self, call_stack=call_stack - ), - ToStatement( - to=to, body=None, processor=self, call_stack=call_stack - ), - ] - - # Now our statement is a compound statement - statement = CompoundStatement( - statements=compound_statements, - body=value, - processor=self, - call_stack=call_stack, - ) - - elif on_clause_match: - # We've come across the beginning of an 'on' statement. - # That means any previous statement we found is complete. - finalized_statement = statement - - statement = OnStatement( - on=key, body=value, processor=self, call_stack=call_stack - ) - - elif _TO_CLAUSE_PATTERN.match(key): - # We've come across the beginning of a 'to' statement. - # That means any previous statement we found is complete. - finalized_statement = statement - - statement = ToStatement( - to=key, body=value, processor=self, call_stack=call_stack - ) - - elif _TRY_CLAUSE_PATTERN.match(key): - # We've come across the beginning of a 'try' statement. - # That means any previous statement we found is complete. - finalized_statement = statement - - statement = TryStatement( - body=value, processor=self, call_stack=call_stack - ) - - elif _ELSE_CLAUSE_PATTERN.match(key): - _handle_else(statement, value) - else: - # Since this section is a dictionary, if there are no - # markers to indicate the start or change of statement, - # the current statement is complete and this section - # is a primitive to be collected. - finalized_statement = statement - statement = None - - return statement, finalized_statement - - -def _handle_else(statement: Optional[Statement], else_body: Optional[typing.Grammar]): - """Add else body to current statement. - - :param statement: The currently-active statement. If None it will be - ignored. - :param else_body: The body of the else clause to add. - - :raises GrammarSyntaxError: If there isn't a currently-active - statement. - """ - - if statement is None: - raise GrammarSyntaxError( - "'else' doesn't seem to correspond to an 'on' or 'try'" - ) - - statement.add_else(else_body) - - -class _StatementCollection: - """Unique collection of statements to run at a later time.""" - - def __init__(self) -> None: - self._statements = [] # type: List[Statement] - - def add(self, statement: Optional[Statement]) -> None: - """Add new statement to collection. - - :param statement: New statement. - - :raises GrammarSyntaxError: If statement is already in collection. - """ - - if not statement: - return - - if statement in self._statements: - raise GrammarSyntaxError( - "found duplicate {!r} statements. These should be " - "merged.".format(statement) - ) - - self._statements.append(statement) diff --git a/snapcraft_legacy/internal/project_loader/grammar/_statement.py b/snapcraft_legacy/internal/project_loader/grammar/_statement.py deleted file mode 100644 index 783c28efec..0000000000 --- a/snapcraft_legacy/internal/project_loader/grammar/_statement.py +++ /dev/null @@ -1,166 +0,0 @@ -# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- -# -# Copyright (C) 2018 Canonical Ltd -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License version 3 as -# published by the Free Software Foundation. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -import re -from typing import TYPE_CHECKING, Iterable, List, Optional - -from . import typing -from .errors import UnsatisfiedStatementError - -# Don't use circular imports unless type checking -if TYPE_CHECKING: - from ._processor import GrammarProcessor # noqa: F401 - -_SELECTOR_PATTERN = re.compile(r"\Aon\s+([^,\s](?:,?[^,]+)*)\Z") -_WHITESPACE_PATTERN = re.compile(r"\A.*\s.*\Z") - - -class Statement: - """Base class for all grammar statements""" - - def __init__( - self, - *, - body: Optional[typing.Grammar], - processor: "GrammarProcessor", - call_stack: Optional[typing.CallStack], - check_primitives: bool = False - ) -> None: - """Create an Statement instance. - - :param list body: The body of the clause. - :param GrammarProcessor process: GrammarProcessor to use for processing - this statement. - :param list call_stack: Call stack leading to this statement. - :param bool check_primitives: Whether or not the primitives should be - checked for validity as part of - evaluating the elses. - """ - if call_stack: - self.__call_stack = call_stack - else: - self.__call_stack = [] - - self._body = body - self._processor = processor - self._check_primitives = check_primitives - self._else_bodies: List[Optional[typing.Grammar]] = [] - - self.__processed_body: Optional[List[str]] = None - self.__processed_else: Optional[List[str]] = None - - def add_else(self, else_body: Optional[typing.Grammar]) -> None: - """Add an 'else' clause to the statement. - - :param list else_body: The body of an 'else' clause. - - The 'else' clauses will be processed in the order they are added. - """ - self._else_bodies.append(else_body) - - def process(self) -> List[str]: - """Process this statement. - - :return: Primitives as determined by evaluating the statement or its - else clauses. - """ - if self._check(): - return self._process_body() - else: - return self._process_else() - - def _process_body(self) -> List[str]: - """Process the main body of this statement. - - :return: Primitives as determined by processing the main body. - """ - if self.__processed_body is None: - self.__processed_body = self._processor.process( - grammar=self._body, call_stack=self._call_stack(include_self=True) - ) - - return self.__processed_body - - def _process_else(self) -> List[str]: - """Process the else clauses of this statement in order. - - :return: Primitives as determined by processing the else clauses. - """ - if self.__processed_else is not None: - return self.__processed_else - - self.__processed_else = list() - for else_body in self._else_bodies: - if not else_body: - # Handle the 'else fail' case. - raise UnsatisfiedStatementError(self) - - processed_else = self._processor.process( - grammar=else_body, call_stack=self._call_stack() - ) - if processed_else: - self.__processed_else = processed_else - if not self._check_primitives or self._validate_primitives( - processed_else - ): - break - - return self.__processed_else - - def _validate_primitives(self, primitives: Iterable[str]) -> bool: - """Ensure that all primitives are valid. - - :param primitives: Iterable container of primitives. - - :return: Whether or not all primitives are valid. - :rtype: bool - """ - for primitive in primitives: - if not self._processor.checker(primitive): - return False - return True - - def _call_stack(self, *, include_self=False) -> List["Statement"]: - """The call stack when processing this statement. - - :param bool include_self: Whether or not this statement should be - included in the stack. - - :return: The call stack - :rtype: list - """ - if include_self: - return self.__call_stack + [self] - else: - return self.__call_stack - - def __repr__(self): - return "{!r}".format(self.__str__()) - - def _check(self) -> bool: - """Check if a statement main body should be processed. - - :return: True if main body should be processed, False if elses should - be processed. - :rtype: bool - """ - raise NotImplementedError("this must be implemented by child classes") - - def __eq__(self, other) -> bool: - raise NotImplementedError("this must be implemented by child classes") - - def __str__(self) -> str: - raise NotImplementedError("this must be implemented by child classes") diff --git a/snapcraft_legacy/internal/project_loader/grammar/_to.py b/snapcraft_legacy/internal/project_loader/grammar/_to.py deleted file mode 100644 index f9a8b31c1b..0000000000 --- a/snapcraft_legacy/internal/project_loader/grammar/_to.py +++ /dev/null @@ -1,121 +0,0 @@ -# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- -# -# Copyright (C) 2017, 2018 Canonical Ltd -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License version 3 as -# published by the Free Software Foundation. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -import re -from typing import TYPE_CHECKING, Optional, Set - -from . import typing -from ._statement import Statement -from .errors import ToStatementSyntaxError - -# Don't use circular imports unless type checking -if TYPE_CHECKING: - from ._processor import GrammarProcessor # noqa: F401 - -_SELECTOR_PATTERN = re.compile(r"\Ato\s+([^,\s](?:,?[^,]+)*)\Z") -_WHITESPACE_PATTERN = re.compile(r"\A.*\s.*\Z") - - -class ToStatement(Statement): - """Process a 'to' statement in the grammar. - - For example: - >>> import tempfile - >>> from snapcraft_legacy import ProjectOptions - >>> from snapcraft_legacy.internal.project_loader import grammar - >>> def checker(primitive): - ... return True - >>> options = ProjectOptions(target_deb_arch='i386') - >>> processor = grammar.GrammarProcessor(None, options, checker) - >>> clause = ToStatement(to='to armhf', body=['foo'], processor=processor) - >>> clause.add_else(['bar']) - >>> clause.process() - {'bar'} - """ - - def __init__( - self, - *, - to: str, - body: Optional[typing.Grammar], - processor: "GrammarProcessor", - call_stack: typing.CallStack = None - ) -> None: - """Create a ToStatement instance. - - :param str to: The 'to ' part of the clause. - :param list body: The body of the clause. - :param GrammarProcessor process: GrammarProcessor to use for processing - this statement. - :param list call_stack: Call stack leading to this statement. - """ - super().__init__(body=body, processor=processor, call_stack=call_stack) - - self.selectors = _extract_to_clause_selectors(to) - - def _check(self) -> bool: - """Check if a statement main body should be processed. - - :return: True if main body should be processed, False if elses should - be processed. - :rtype: bool - """ - target_arch = self._processor.project.deb_arch - - # The only selector currently supported is the target arch. Since - # selectors are matched with an AND, not OR, there should only be one - # selector. - return (len(self.selectors) == 1) and (target_arch in self.selectors) - - def __eq__(self, other) -> bool: - if type(other) is type(self): - return self.selectors == other.selectors - - return False - - def __str__(self) -> str: - return "to {}".format(",".join(sorted(self.selectors))) - - -def _extract_to_clause_selectors(to: str) -> Set[str]: - """Extract the list of selectors within a to clause. - - :param str to: The 'to ' part of the 'to' clause. - - :return: Selectors found within the 'to' clause. - - For example: - >>> _extract_to_clause_selectors('to amd64,i386') == {'amd64', 'i386'} - True - """ - - match = _SELECTOR_PATTERN.match(to) - if match is None: - raise ToStatementSyntaxError(to, message="selectors are missing") - - try: - selector_group = match.group(1) - except IndexError: - raise ToStatementSyntaxError(to) - - # This could be part of the _SELECTOR_PATTERN, but that would require us - # to provide a very generic error when we can try to be more helpful. - if _WHITESPACE_PATTERN.match(selector_group): - raise ToStatementSyntaxError( - to, message="spaces are not allowed in the selectors" - ) - - return {selector.strip() for selector in selector_group.split(",")} diff --git a/snapcraft_legacy/internal/project_loader/grammar/_try.py b/snapcraft_legacy/internal/project_loader/grammar/_try.py deleted file mode 100644 index 663f9ba5bc..0000000000 --- a/snapcraft_legacy/internal/project_loader/grammar/_try.py +++ /dev/null @@ -1,74 +0,0 @@ -# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- -# -# Copyright (C) 2017, 2018 Canonical Ltd -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License version 3 as -# published by the Free Software Foundation. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -from typing import TYPE_CHECKING - -from . import typing -from ._statement import Statement - -# Don't use circular imports unless type checking -if TYPE_CHECKING: - from ._processor import GrammarProcessor # noqa: F401 - - -class TryStatement(Statement): - """Process a 'try' statement in the grammar. - - For example: - >>> from snapcraft_legacy import ProjectOptions - >>> from ._processor import GrammarProcessor - >>> def checker(primitive): - ... return 'invalid' not in primitive - >>> options = ProjectOptions() - >>> processor = GrammarProcessor(None, options, checker) - >>> clause = TryStatement(body=['invalid'], processor=processor) - >>> clause.add_else(['valid']) - >>> clause.process() - {'valid'} - """ - - def __init__( - self, - *, - body: typing.Grammar, - processor: "GrammarProcessor", - call_stack: typing.CallStack = None - ) -> None: - """Create a TryStatement instance. - - :param list body: The body of the clause. - :param GrammarProcessor process: GrammarProcessor to use for processing - this statement. - :param list call_stack: Call stack leading to this statement. - """ - super().__init__( - body=body, processor=processor, call_stack=call_stack, check_primitives=True - ) - - def _check(self) -> bool: - """Check if a statement main body should be processed. - - :return: True if main body should be processed, False if elses should - be processed. - :rtype: bool - """ - return self._validate_primitives(self._process_body()) - - def __eq__(self, other) -> bool: - return False - - def __str__(self) -> str: - return "try" diff --git a/snapcraft_legacy/internal/project_loader/grammar/errors.py b/snapcraft_legacy/internal/project_loader/grammar/errors.py deleted file mode 100644 index e5de341e5d..0000000000 --- a/snapcraft_legacy/internal/project_loader/grammar/errors.py +++ /dev/null @@ -1,55 +0,0 @@ -# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- -# -# Copyright (C) 2017 Canonical Ltd -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License version 3 as -# published by the Free Software Foundation. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -from snapcraft_legacy.internal import errors - - -class GrammarError(errors.SnapcraftError): - """Base class for grammar-related errors.""" - - pass - - -class GrammarSyntaxError(GrammarError): - - fmt = "Invalid grammar syntax: {message}" - - def __init__(self, message): - super().__init__(message=message) - - -class OnStatementSyntaxError(GrammarSyntaxError): - def __init__(self, on_statement, *, message=None): - components = ["{!r} is not a valid 'on' clause".format(on_statement)] - if message: - components.append(message) - super().__init__(message=": ".join(components)) - - -class ToStatementSyntaxError(GrammarSyntaxError): - def __init__(self, to_statement, *, message=None): - components = ["{!r} is not a valid 'to' clause".format(to_statement)] - if message: - components.append(message) - super().__init__(message=": ".join(components)) - - -class UnsatisfiedStatementError(GrammarError): - - fmt = "Unable to satisfy {statement!r}, failure forced" - - def __init__(self, statement): - super().__init__(statement=statement) diff --git a/snapcraft_legacy/internal/project_loader/grammar/typing.py b/snapcraft_legacy/internal/project_loader/grammar/typing.py deleted file mode 100644 index 95b98c537a..0000000000 --- a/snapcraft_legacy/internal/project_loader/grammar/typing.py +++ /dev/null @@ -1,6 +0,0 @@ -from typing import Any, Dict, List, Sequence, Union - -Grammar = Sequence[Union[str, Dict[str, Any]]] -CallStack = List["Statement"] - -from ._statement import Statement # noqa: F401 diff --git a/snapcraft_legacy/internal/project_loader/grammar_processing/_global_grammar_processor.py b/snapcraft_legacy/internal/project_loader/grammar_processing/_global_grammar_processor.py index 2e44eef96c..38b1cb5307 100644 --- a/snapcraft_legacy/internal/project_loader/grammar_processing/_global_grammar_processor.py +++ b/snapcraft_legacy/internal/project_loader/grammar_processing/_global_grammar_processor.py @@ -16,9 +16,10 @@ from typing import Any, Dict, Set +from craft_grammar import GrammarProcessor + from snapcraft_legacy import project from snapcraft_legacy.internal import repo -from snapcraft_legacy.internal.project_loader import grammar class GlobalGrammarProcessor: @@ -34,19 +35,24 @@ class GlobalGrammarProcessor: {'hello'} """ - def __init__(self, *, properties: Dict[str, Any], project: project.Project) -> None: - self._project = project + def __init__( + self, *, properties: Dict[str, Any], arch: str, target_arch: str + ) -> None: + self._arch = arch + self._target_arch = target_arch self._build_package_grammar = properties.get("build-packages", []) self.__build_packages = set() # type: Set[str] def get_build_packages(self) -> Set[str]: if not self.__build_packages: - processor = grammar.GrammarProcessor( - self._build_package_grammar, - self._project, - repo.Repo.build_package_is_valid, + processor = GrammarProcessor( + arch=self._arch, + target_arch=self._target_arch, + checker=repo.Repo.build_package_is_valid, + ) + self.__build_packages = set( + processor.process(grammar=self._build_package_grammar) ) - self.__build_packages = set(processor.process()) return self.__build_packages diff --git a/snapcraft_legacy/internal/project_loader/grammar_processing/_package_transformer.py b/snapcraft_legacy/internal/project_loader/grammar_processing/_package_transformer.py index ffba98f35a..8b32d90ba4 100644 --- a/snapcraft_legacy/internal/project_loader/grammar_processing/_package_transformer.py +++ b/snapcraft_legacy/internal/project_loader/grammar_processing/_package_transformer.py @@ -14,13 +14,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -from snapcraft_legacy import project -from snapcraft_legacy.internal.project_loader.grammar import ( - CompoundStatement, - Statement, - ToStatement, - typing, -) +from craft_grammar import CallStack, CompoundStatement, Statement, ToStatement def _is_or_contains_to_statement(statement: Statement) -> bool: @@ -37,11 +31,11 @@ def _is_or_contains_to_statement(statement: Statement) -> bool: def package_transformer( - call_stack: typing.CallStack, package_name: str, project: project.Project + call_stack: CallStack, package_name: str, target_arch: str ) -> str: if any(_is_or_contains_to_statement(s) for s in call_stack): if ":" not in package_name: # deb_arch is target arch or host arch if both are the same - package_name += ":{}".format(project.deb_arch) + package_name = f"{package_name}:{target_arch}" return package_name diff --git a/snapcraft_legacy/internal/project_loader/grammar_processing/_part_grammar_processor.py b/snapcraft_legacy/internal/project_loader/grammar_processing/_part_grammar_processor.py index a2dc1aec80..2ca6367d44 100644 --- a/snapcraft_legacy/internal/project_loader/grammar_processing/_part_grammar_processor.py +++ b/snapcraft_legacy/internal/project_loader/grammar_processing/_part_grammar_processor.py @@ -16,9 +16,10 @@ from typing import Any, Dict, List, Set +from craft_grammar import Grammar, GrammarProcessor + from snapcraft_legacy import BasePlugin, project from snapcraft_legacy.internal import repo -from snapcraft_legacy.internal.project_loader import grammar from ._package_transformer import package_transformer @@ -77,12 +78,14 @@ def __init__( *, plugin: BasePlugin, properties: Dict[str, Any], - project: project.Project, + arch: str, + target_arch: str, repo: "repo.Ubuntu" ) -> None: self._plugin = plugin self._properties = properties - self._project = project + self._arch = arch + self._target_arch = target_arch self._repo = repo self.__build_environment: List[Dict[str, str]] = list() @@ -103,68 +106,86 @@ def get_source(self) -> str: if not self.__source: # The grammar is array-based, even though we only support a single # source. - processor = grammar.GrammarProcessor( - self._source_grammar, self._project, lambda s: True + processor = GrammarProcessor( + arch=self._arch, + target_arch=self._target_arch, + checker=lambda s: True, ) - source_array = processor.process() + source_array = processor.process(grammar=self._source_grammar) if len(source_array) > 0: self.__source = source_array.pop() return self.__source - def _get_property(self, attr: str) -> grammar.typing.Grammar: + def _get_property(self, attr: str) -> Grammar: prop = self._properties.get(attr, set()) return getattr(self._plugin, attr.replace("-", "_"), prop) def get_build_environment(self) -> List[Dict[str, str]]: if not self.__build_environment: - processor = grammar.GrammarProcessor( - self._get_property("build-environment"), self._project, lambda x: True, + processor = GrammarProcessor( + arch=self._arch, + target_arch=self._target_arch, + checker=lambda s: True, + ) + self.__build_environment = processor.process( + grammar=self._get_property("build-environment"), ) - self.__build_environment = processor.process() return self.__build_environment def get_build_snaps(self) -> Set[str]: if not self.__build_snaps: - processor = grammar.GrammarProcessor( - self._get_property("build-snaps"), - self._project, - repo.snaps.SnapPackage.is_valid_snap, + processor = GrammarProcessor( + arch=self._arch, + target_arch=self._target_arch, + checker=repo.snaps.SnapPackage.is_valid_snap, + ) + self.__build_snaps = set( + processor.process(grammar=self._get_property("build-snaps")) ) - self.__build_snaps = set(processor.process()) return self.__build_snaps def get_stage_snaps(self) -> Set[str]: if not self.__stage_snaps: - processor = grammar.GrammarProcessor( - self._get_property("stage-snaps"), - self._project, - repo.snaps.SnapPackage.is_valid_snap, + processor = GrammarProcessor( + arch=self._arch, + target_arch=self._target_arch, + checker=repo.snaps.SnapPackage.is_valid_snap, + ) + self.__stage_snaps = set( + processor.process(grammar=self._get_property("stage-snaps")) ) - self.__stage_snaps = set(processor.process()) return self.__stage_snaps def get_build_packages(self) -> Set[str]: if not self.__build_packages: - processor = grammar.GrammarProcessor( - self._get_property("build-packages"), - self._project, - self._repo.build_package_is_valid, + processor = GrammarProcessor( + arch=self._arch, + target_arch=self._target_arch, + checker=self._repo.build_package_is_valid, + ) + self.__build_packages = set( + processor.process( + grammar=self._get_property("build-packages"), + ) ) - self.__build_packages = set(processor.process()) return self.__build_packages def get_stage_packages(self) -> Set[str]: if not self.__stage_packages: - processor = grammar.GrammarProcessor( - self._get_property("stage-packages"), - self._project, - self._repo.build_package_is_valid, + processor = GrammarProcessor( + arch=self._arch, + target_arch=self._target_arch, + checker=self._repo.build_package_is_valid, transformer=package_transformer, ) - self.__stage_packages = set(processor.process()) + self.__stage_packages = set( + processor.process( + grammar=self._get_property("stage-packages"), + ) + ) return self.__stage_packages diff --git a/tests/legacy/unit/part_loader.py b/tests/legacy/unit/part_loader.py index bac11adc81..91d2103562 100644 --- a/tests/legacy/unit/part_loader.py +++ b/tests/legacy/unit/part_loader.py @@ -67,7 +67,11 @@ def load_part( if not stage_packages_repo: stage_packages_repo = mock.Mock() grammar_processor = grammar_processing.PartGrammarProcessor( - plugin=plugin, properties=properties, project=project, repo=stage_packages_repo + plugin=plugin, + properties=properties, + arch=project.deb_arch, + target_arch=project.target_arch, + repo=stage_packages_repo, ) return pluginhandler.PluginHandler( diff --git a/tests/legacy/unit/project_loader/grammar/__init__.py b/tests/legacy/unit/project_loader/grammar/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/legacy/unit/project_loader/grammar/test_compound_statement.py b/tests/legacy/unit/project_loader/grammar/test_compound_statement.py deleted file mode 100644 index 2350889a2a..0000000000 --- a/tests/legacy/unit/project_loader/grammar/test_compound_statement.py +++ /dev/null @@ -1,271 +0,0 @@ -# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- -# -# Copyright (C) 2017 Canonical Ltd -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License version 3 as -# published by the Free Software Foundation. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -import platform -import re - -import pytest - -import snapcraft_legacy -import snapcraft_legacy.internal.project_loader.grammar._compound as compound -import snapcraft_legacy.internal.project_loader.grammar._on as on -import snapcraft_legacy.internal.project_loader.grammar._to as to -from snapcraft_legacy.internal.project_loader import grammar - - -class TestCompoundStatementGrammar: - - scenarios = [ - ( - "on amd64", - { - "on_arch": "on amd64", - "to_arch": "to armhf", - "body": ["foo"], - "else_bodies": [], - "host_arch": "x86_64", - "expected_packages": ["foo"], - }, - ), - ( - "on i386", - { - "on_arch": "on amd64", - "to_arch": "to armhf", - "body": ["foo"], - "else_bodies": [], - "host_arch": "i686", - "expected_packages": list(), - }, - ), - ( - "ignored else", - { - "on_arch": "on amd64", - "to_arch": "to armhf", - "body": ["foo"], - "else_bodies": [["bar"]], - "host_arch": "x86_64", - "expected_packages": ["foo"], - }, - ), - ( - "used else", - { - "on_arch": "on amd64", - "to_arch": "to i386", - "body": ["foo"], - "else_bodies": [["bar"]], - "host_arch": "i686", - "expected_packages": ["bar"], - }, - ), - ( - "third else ignored", - { - "on_arch": "on amd64", - "to_arch": "to i386", - "body": ["foo"], - "else_bodies": [["bar"], ["baz"]], - "host_arch": "i686", - "expected_packages": ["bar"], - }, - ), - ( - "third else followed", - { - "on_arch": "on amd64", - "to_arch": "to i386", - "body": ["foo"], - "else_bodies": [[{"on armhf": ["bar"]}], ["baz"]], - "host_arch": "i686", - "expected_packages": ["baz"], - }, - ), - ( - "nested amd64", - { - "on_arch": "on amd64", - "to_arch": "to armhf", - "body": [{"on amd64": ["foo"]}, {"on i386": ["bar"]}], - "else_bodies": [], - "host_arch": "x86_64", - "expected_packages": ["foo"], - }, - ), - ( - "nested i386", - { - "on_arch": "on i386", - "to_arch": "to armhf", - "body": [{"on amd64": ["foo"]}, {"on i386": ["bar"]}], - "else_bodies": [], - "host_arch": "i686", - "expected_packages": ["bar"], - }, - ), - ( - "nested body ignored else", - { - "on_arch": "on amd64", - "to_arch": "to armhf", - "body": [{"on amd64": ["foo"]}, {"else": ["bar"]}], - "else_bodies": [], - "host_arch": "x86_64", - "expected_packages": ["foo"], - }, - ), - ( - "nested body used else", - { - "on_arch": "on i386", - "to_arch": "to armhf", - "body": [{"on amd64": ["foo"]}, {"else": ["bar"]}], - "else_bodies": [], - "host_arch": "i686", - "expected_packages": ["bar"], - }, - ), - ( - "nested else ignored else", - { - "on_arch": "on armhf", - "to_arch": "to i386", - "body": ["foo"], - "else_bodies": [[{"on amd64": ["bar"]}, {"else": ["baz"]}]], - "host_arch": "x86_64", - "expected_packages": ["bar"], - }, - ), - ( - "nested else used else", - { - "on_arch": "on armhf", - "to_arch": "to i386", - "body": ["foo"], - "else_bodies": [[{"on amd64": ["bar"]}, {"else": ["baz"]}]], - "host_arch": "i686", - "expected_packages": ["baz"], - }, - ), - ( - "with hyphen", - { - "on_arch": "on other-arch", - "to_arch": "to yet-another-arch", - "body": ["foo"], - "else_bodies": [], - "host_arch": "x86_64", - "expected_packages": list(), - }, - ), - ( - "multiple selectors", - { - "on_arch": "on amd64,i386", - "to_arch": "to armhf,arm64", - "body": ["foo"], - "else_bodies": [], - "host_arch": "x86_64", - "expected_packages": list(), - }, - ), - ] - - def test( - self, - monkeypatch, - on_arch, - to_arch, - body, - else_bodies, - host_arch, - expected_packages, - ): - monkeypatch.setattr(platform, "machine", lambda: host_arch) - monkeypatch.setattr(platform, "architecture", lambda: ("64bit", "ELF")) - - processor = grammar.GrammarProcessor( - None, - snapcraft_legacy.ProjectOptions(target_deb_arch="armhf"), - lambda x: True, - ) - statements = [ - on.OnStatement(on=on_arch, body=None, processor=processor), - to.ToStatement(to=to_arch, body=None, processor=processor), - ] - statement = compound.CompoundStatement( - statements=statements, body=body, processor=processor - ) - - for else_body in else_bodies: - statement.add_else(else_body) - - assert statement.process() == expected_packages - - -class TestCompoundStatementInvalidGrammar: - - scenarios = [ - ( - "spaces in on selectors", - { - "on_arch": "on amd64, ubuntu", - "to_arch": "to i386", - "body": ["foo"], - "else_bodies": [], - "expected_exception": grammar.errors.OnStatementSyntaxError, - "expected_message": ".*not a valid 'on' clause.*spaces are not allowed in the " - "selectors.*", - }, - ), - ( - "spaces in to selectors", - { - "on_arch": "on amd64,ubuntu", - "to_arch": "to i386, armhf", - "body": ["foo"], - "else_bodies": [], - "expected_exception": grammar.errors.ToStatementSyntaxError, - "expected_message": ".*not a valid 'to' clause.*spaces are not allowed in the " - "selectors.*", - }, - ), - ] - - def test( - self, on_arch, to_arch, body, else_bodies, expected_exception, expected_message - ): - with pytest.raises(expected_exception) as error: - processor = grammar.GrammarProcessor( - None, - snapcraft_legacy.ProjectOptions(target_deb_arch="armhf"), - lambda x: "invalid" not in x, - ) - statements = [ - on.OnStatement(on=on_arch, body=None, processor=processor), - to.ToStatement(to=to_arch, body=None, processor=processor), - ] - statement = compound.CompoundStatement( - statements=statements, body=body, processor=processor - ) - - for else_body in else_bodies: - statement.add_else(else_body) - - statement.process() - - assert re.match(expected_message, str(error.value)) diff --git a/tests/legacy/unit/project_loader/grammar/test_on_statement.py b/tests/legacy/unit/project_loader/grammar/test_on_statement.py deleted file mode 100644 index 1769719704..0000000000 --- a/tests/legacy/unit/project_loader/grammar/test_on_statement.py +++ /dev/null @@ -1,264 +0,0 @@ -# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- -# -# Copyright (C) 2017, 2018 Canonical Ltd -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License version 3 as -# published by the Free Software Foundation. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -import doctest -import platform -import re - -import pytest - -import snapcraft_legacy -import snapcraft_legacy.internal.project_loader.grammar._on as on -from snapcraft_legacy.internal.project_loader import grammar - - -def load_tests(loader, tests, ignore): - tests.addTests(doctest.DocTestSuite(on)) - return tests - - -class TestOnStatementGrammar: - - scenarios = [ - ( - "on amd64", - { - "on_arch": "on amd64", - "body": ["foo"], - "else_bodies": [], - "host_arch": "x86_64", - "expected_packages": ["foo"], - }, - ), - ( - "on i386", - { - "on_arch": "on amd64", - "body": ["foo"], - "else_bodies": [], - "host_arch": "i686", - "expected_packages": list(), - }, - ), - ( - "ignored else", - { - "on_arch": "on amd64", - "body": ["foo"], - "else_bodies": [["bar"]], - "host_arch": "x86_64", - "expected_packages": ["foo"], - }, - ), - ( - "used else", - { - "on_arch": "on amd64", - "body": ["foo"], - "else_bodies": [["bar"]], - "host_arch": "i686", - "expected_packages": ["bar"], - }, - ), - ( - "third else ignored", - { - "on_arch": "on amd64", - "body": ["foo"], - "else_bodies": [["bar"], ["baz"]], - "host_arch": "i686", - "expected_packages": ["bar"], - }, - ), - ( - "third else followed", - { - "on_arch": "on amd64", - "body": ["foo"], - "else_bodies": [[{"on armhf": ["bar"]}], ["baz"]], - "host_arch": "i686", - "expected_packages": ["baz"], - }, - ), - ( - "nested amd64", - { - "on_arch": "on amd64", - "body": [{"on amd64": ["foo"]}, {"on i386": ["bar"]}], - "else_bodies": [], - "host_arch": "x86_64", - "expected_packages": ["foo"], - }, - ), - ( - "nested i386", - { - "on_arch": "on i386", - "body": [{"on amd64": ["foo"]}, {"on i386": ["bar"]}], - "else_bodies": [], - "host_arch": "i686", - "expected_packages": ["bar"], - }, - ), - ( - "nested body ignored else", - { - "on_arch": "on amd64", - "body": [{"on amd64": ["foo"]}, {"else": ["bar"]}], - "else_bodies": [], - "host_arch": "x86_64", - "expected_packages": ["foo"], - }, - ), - ( - "nested body used else", - { - "on_arch": "on i386", - "body": [{"on amd64": ["foo"]}, {"else": ["bar"]}], - "else_bodies": [], - "host_arch": "i686", - "expected_packages": ["bar"], - }, - ), - ( - "nested else ignored else", - { - "on_arch": "on armhf", - "body": ["foo"], - "else_bodies": [[{"on amd64": ["bar"]}, {"else": ["baz"]}]], - "host_arch": "x86_64", - "expected_packages": ["bar"], - }, - ), - ( - "nested else used else", - { - "on_arch": "on armhf", - "body": ["foo"], - "else_bodies": [[{"on amd64": ["bar"]}, {"else": ["baz"]}]], - "host_arch": "i686", - "expected_packages": ["baz"], - }, - ), - ] - - def test( - self, monkeypatch, on_arch, body, else_bodies, host_arch, expected_packages - ): - monkeypatch.setattr(platform, "machine", lambda: host_arch) - monkeypatch.setattr(platform, "architecture", lambda: ("64bit", "ELF")) - - processor = grammar.GrammarProcessor( - None, snapcraft_legacy.ProjectOptions(), lambda x: True - ) - statement = on.OnStatement(on=on_arch, body=body, processor=processor) - - for else_body in else_bodies: - statement.add_else(else_body) - - assert statement.process() == expected_packages - - -class TestOnStatementInvalidGrammar: - - scenarios = [ - ( - "spaces in selectors", - { - "on_arch": "on amd64, ubuntu", - "body": ["foo"], - "else_bodies": [], - "expected_exception": ".*not a valid 'on' clause.*spaces are not allowed in the " - "selectors.*", - }, - ), - ( - "beginning with comma", - { - "on_arch": "on ,amd64", - "body": ["foo"], - "else_bodies": [], - "expected_exception": ".*not a valid 'on' clause", - }, - ), - ( - "ending with comma", - { - "on_arch": "on amd64,", - "body": ["foo"], - "else_bodies": [], - "expected_exception": ".*not a valid 'on' clause", - }, - ), - ( - "multiple commas", - { - "on_arch": "on amd64,,ubuntu", - "body": ["foo"], - "else_bodies": [], - "expected_exception": ".*not a valid 'on' clause", - }, - ), - ( - "invalid selector format", - { - "on_arch": "on", - "body": ["foo"], - "else_bodies": [], - "expected_exception": ".*not a valid 'on' clause.*selectors are missing", - }, - ), - ( - "not even close", - { - "on_arch": "im-invalid", - "body": ["foo"], - "else_bodies": [], - "expected_exception": ".*not a valid 'on' clause", - }, - ), - ] - - def test(self, on_arch, body, else_bodies, expected_exception): - with pytest.raises(grammar.errors.OnStatementSyntaxError) as error: - processor = grammar.GrammarProcessor( - None, snapcraft_legacy.ProjectOptions(), lambda x: "invalid" not in x - ) - statement = on.OnStatement(on=on_arch, body=body, processor=processor) - - for else_body in else_bodies: - statement.add_else(else_body) - - statement.process() - - assert re.match(expected_exception, str(error.value)) - - -def test_else_fail(monkeypatch): - monkeypatch.setattr(platform, "machine", lambda: "x86_64") - monkeypatch.setattr(platform, "architecture", lambda: ("64bit", "ELF")) - - processor = grammar.GrammarProcessor( - None, snapcraft_legacy.ProjectOptions(), lambda x: True - ) - statement = on.OnStatement(on="on i386", body=["foo"], processor=processor) - - statement.add_else(None) - - with pytest.raises(grammar.errors.UnsatisfiedStatementError) as error: - statement.process() - - assert str(error.value) == "Unable to satisfy 'on i386', failure forced" diff --git a/tests/legacy/unit/project_loader/grammar/test_processor.py b/tests/legacy/unit/project_loader/grammar/test_processor.py deleted file mode 100644 index 0d132b6fd8..0000000000 --- a/tests/legacy/unit/project_loader/grammar/test_processor.py +++ /dev/null @@ -1,436 +0,0 @@ -# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- -# -# Copyright (C) 2017, 2018 Canonical Ltd -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License version 3 as -# published by the Free Software Foundation. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -import platform -import re - -import pytest - -import snapcraft_legacy -import snapcraft_legacy.internal.project_loader.grammar._to as _to -from snapcraft_legacy.internal.project_loader import grammar - - -@pytest.mark.parametrize( - "entry", - [ - [{"on amd64,i386": ["foo"]}, {"on amd64,i386": ["bar"]}], - [{"on amd64,i386": ["foo"]}, {"on i386,amd64": ["bar"]}], - ], -) -def test_duplicates(entry): - """Test that multiple identical selector sets is an error.""" - - processor = grammar.GrammarProcessor( - entry, snapcraft_legacy.ProjectOptions(), lambda x: True - ) - with pytest.raises(grammar.errors.GrammarSyntaxError) as error: - processor.process() - - expected = ( - "Invalid grammar syntax: found duplicate 'on amd64,i386' " - "statements. These should be merged." - ) - assert expected in str(error.value) - - -class TestBasicGrammar: - - scenarios = [ - ( - "unconditional", - { - "grammar_entry": ["foo", "bar"], - "host_arch": "x86_64", - "target_arch": "amd64", - "expected_results": ["foo", "bar"], - }, - ), - ( - "unconditional dict", - { - "grammar_entry": [{"foo": "bar"}], - "host_arch": "x86_64", - "target_arch": "amd64", - "expected_results": [{"foo": "bar"}], - }, - ), - ( - "unconditional multi-dict", - { - "grammar_entry": [{"foo": "bar"}, {"foo2": "bar2"}], - "host_arch": "x86_64", - "target_arch": "amd64", - "expected_results": [{"foo": "bar"}, {"foo2": "bar2"}], - }, - ), - ( - "mixed including", - { - "grammar_entry": ["foo", {"on i386": ["bar"]}], - "host_arch": "i686", - "target_arch": "i386", - "expected_results": ["foo", "bar"], - }, - ), - ( - "mixed excluding", - { - "grammar_entry": ["foo", {"on i386": ["bar"]}], - "host_arch": "x86_64", - "target_arch": "amd64", - "expected_results": ["foo"], - }, - ), - ( - "on amd64", - { - "grammar_entry": [{"on amd64": ["foo"]}, {"on i386": ["bar"]}], - "host_arch": "x86_64", - "target_arch": "amd64", - "expected_results": ["foo"], - }, - ), - ( - "on i386", - { - "grammar_entry": [{"on amd64": ["foo"]}, {"on i386": ["bar"]}], - "host_arch": "i686", - "target_arch": "i386", - "expected_results": ["bar"], - }, - ), - ( - "ignored else", - { - "grammar_entry": [{"on amd64": ["foo"]}, {"else": ["bar"]}], - "host_arch": "x86_64", - "target_arch": "amd64", - "expected_results": ["foo"], - }, - ), - ( - "used else", - { - "grammar_entry": [{"on amd64": ["foo"]}, {"else": ["bar"]}], - "host_arch": "i686", - "target_arch": "i386", - "expected_results": ["bar"], - }, - ), - ( - "nested amd64", - { - "grammar_entry": [ - {"on amd64": [{"on amd64": ["foo"]}, {"on i386": ["bar"]}]} - ], - "host_arch": "x86_64", - "target_arch": "amd64", - "expected_results": ["foo"], - }, - ), - ( - "nested amd64 dict", - { - "grammar_entry": [ - {"on amd64": [{"on amd64": [{"foo": "bar"}]}, {"on i386": ["bar"]}]} - ], - "host_arch": "x86_64", - "target_arch": "amd64", - "expected_results": [{"foo": "bar"}], - }, - ), - ( - "nested i386", - { - "grammar_entry": [ - {"on i386": [{"on amd64": ["foo"]}, {"on i386": ["bar"]}]} - ], - "host_arch": "i686", - "target_arch": "i386", - "expected_results": ["bar"], - }, - ), - ( - "nested ignored else", - { - "grammar_entry": [ - {"on amd64": [{"on amd64": ["foo"]}, {"else": ["bar"]}]} - ], - "host_arch": "x86_64", - "target_arch": "amd64", - "expected_results": ["foo"], - }, - ), - ( - "nested used else", - { - "grammar_entry": [ - {"on i386": [{"on amd64": ["foo"]}, {"else": ["bar"]}]} - ], - "host_arch": "i686", - "target_arch": "amd64", - "expected_results": ["bar"], - }, - ), - ( - "try", - { - "grammar_entry": [{"try": ["valid"]}], - "host_arch": "x86_64", - "target_arch": "amd64", - "expected_results": ["valid"], - }, - ), - ( - "try else", - { - "grammar_entry": [{"try": ["invalid"]}, {"else": ["valid"]}], - "host_arch": "x86_64", - "target_arch": "amd64", - "expected_results": ["valid"], - }, - ), - ( - "nested try", - { - "grammar_entry": [{"on amd64": [{"try": ["foo"]}, {"else": ["bar"]}]}], - "host_arch": "x86_64", - "target_arch": "amd64", - "expected_results": ["foo"], - }, - ), - ( - "nested try else", - { - "grammar_entry": [ - {"on i386": [{"try": ["invalid"]}, {"else": ["bar"]}]} - ], - "host_arch": "i686", - "target_arch": "i686", - "expected_results": ["bar"], - }, - ), - ( - "optional", - { - "grammar_entry": ["foo", {"try": ["invalid"]}], - "host_arch": "i686", - "target_arch": "i386", - "expected_results": ["foo"], - }, - ), - ( - "multi", - { - "grammar_entry": [ - "foo", - {"on amd64": ["foo2"]}, - {"on amd64 to arm64": ["foo3"]}, - ], - "host_arch": "x86_64", - "target_arch": "i386", - "expected_results": ["foo", "foo2"], - }, - ), - ( - "multi-ordering", - { - "grammar_entry": [ - "foo", - {"on amd64": ["on-foo"]}, - "after-on", - {"on amd64 to i386": ["on-to-foo"]}, - {"on amd64 to arm64": ["no-show"]}, - "n-1", - "n", - ], - "host_arch": "x86_64", - "target_arch": "i386", - "expected_results": [ - "foo", - "on-foo", - "after-on", - "on-to-foo", - "n-1", - "n", - ], - }, - ), - ( - "complex nested dicts", - { - "grammar_entry": [ - {"yes1": "yes1"}, - { - "on amd64": [ - {"yes2": "yes2"}, - {"on amd64": [{"yes3": "yes3"}]}, - {"yes4": "yes4"}, - {"on i386": [{"no1": "no1"}]}, - {"else": [{"yes5": "yes5"}]}, - {"yes6": "yes6"}, - ], - }, - {"else": [{"no2": "no2"}]}, - {"yes7": "yes7"}, - {"on i386": [{"no3": "no3"}]}, - {"else": [{"yes8": "yes8"}]}, - {"yes9": "yes9"}, - ], - "host_arch": "x86_64", - "target_arch": "amd64", - "expected_results": [ - {"yes1": "yes1"}, - {"yes2": "yes2"}, - {"yes3": "yes3"}, - {"yes4": "yes4"}, - {"yes5": "yes5"}, - {"yes6": "yes6"}, - {"yes7": "yes7"}, - {"yes8": "yes8"}, - {"yes9": "yes9"}, - ], - }, - ), - ] - - def test_basic_grammar( - self, monkeypatch, grammar_entry, host_arch, target_arch, expected_results - ): - monkeypatch.setattr(platform, "machine", lambda: host_arch) - monkeypatch.setattr(platform, "architecture", lambda: ("64bit", "ELF")) - - project = snapcraft_legacy.ProjectOptions(target_deb_arch=target_arch) - - processor = grammar.GrammarProcessor( - grammar_entry, project, lambda x: "invalid" not in x - ) - assert processor.process() == expected_results - - -class TestTransformerGrammar: - - scenarios = [ - ( - "unconditional", - { - "grammar_entry": ["foo", "bar"], - "host_arch": "x86_64", - "expected_results": ["foo", "bar"], - }, - ), - ( - "mixed including", - { - "grammar_entry": ["foo", {"on i386": ["bar"]}], - "host_arch": "i686", - "expected_results": ["foo", "bar"], - }, - ), - ( - "mixed excluding", - { - "grammar_entry": ["foo", {"on i386": ["bar"]}], - "host_arch": "x86_64", - "expected_results": ["foo"], - }, - ), - ( - "to", - { - "grammar_entry": [{"to i386": ["foo"]}], - "host_arch": "x86_64", - "expected_results": ["foo:i386"], - }, - ), - ( - "transform applies to nested", - { - "grammar_entry": [{"to i386": [{"on amd64": ["foo"]}]}], - "host_arch": "x86_64", - "expected_results": ["foo:i386"], - }, - ), - ( - "not to", - { - "grammar_entry": [{"to amd64": ["foo"]}, {"else": ["bar"]}], - "host_arch": "x86_64", - "expected_results": ["bar"], - }, - ), - ] - - def test_grammar_with_transformer( - self, monkeypatch, grammar_entry, host_arch, expected_results - ): - monkeypatch.setattr(platform, "machine", lambda: host_arch) - monkeypatch.setattr(platform, "architecture", lambda: ("64bit", "ELF")) - - # Transform all 'to' statements to include arch - def _transformer(call_stack, package_name, project_options): - if any(isinstance(s, _to.ToStatement) for s in call_stack): - if ":" not in package_name: - package_name += ":{}".format(project_options.deb_arch) - - return package_name - - processor = grammar.GrammarProcessor( - grammar_entry, - snapcraft_legacy.ProjectOptions(target_deb_arch="i386"), - lambda x: True, - transformer=_transformer, - ) - - assert processor.process() == expected_results - - -class TestInvalidGrammar: - - scenarios = [ - ( - "unmatched else", - { - "grammar_entry": [{"else": ["foo"]}], - "expected_exception": ".*'else' doesn't seem to correspond.*", - }, - ), - ( - "unmatched else fail", - { - "grammar_entry": ["else fail"], - "expected_exception": ".*'else' doesn't seem to correspond.*", - }, - ), - ( - "unexpected type", - { - "grammar_entry": [5], - "expected_exception": ".*expected grammar section.*but got.*", - }, - ), - ] - - def test_invalid_grammar(self, grammar_entry, expected_exception): - processor = grammar.GrammarProcessor( - grammar_entry, snapcraft_legacy.ProjectOptions(), lambda x: True - ) - - with pytest.raises(grammar.errors.GrammarSyntaxError) as error: - processor.process() - - assert re.match(expected_exception, str(error.value)) diff --git a/tests/legacy/unit/project_loader/grammar/test_to_statement.py b/tests/legacy/unit/project_loader/grammar/test_to_statement.py deleted file mode 100644 index f3082c586e..0000000000 --- a/tests/legacy/unit/project_loader/grammar/test_to_statement.py +++ /dev/null @@ -1,303 +0,0 @@ -# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- -# -# Copyright (C) 2017, 2018 Canonical Ltd -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License version 3 as -# published by the Free Software Foundation. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -import doctest -import platform -import re - -import pytest - -import snapcraft_legacy -import snapcraft_legacy.internal.project_loader.grammar._to as to -from snapcraft_legacy.internal.project_loader import grammar - - -def load_tests(loader, tests, ignore): - tests.addTests(doctest.DocTestSuite(to)) - return tests - - -class TestToStatementGrammar: - - scenarios = [ - ( - "no target arch", - { - "to_arch": "to amd64", - "body": ["foo"], - "else_bodies": [], - "target_arch": None, - "expected_packages": ["foo"], - }, - ), - ( - "amd64 to armhf", - { - "to_arch": "to armhf", - "body": ["foo"], - "else_bodies": [], - "target_arch": "armhf", - "expected_packages": ["foo"], - }, - ), - ( - "amd64 to armhf, arch specified", - { - "to_arch": "to armhf", - "body": ["foo:amd64"], - "else_bodies": [], - "target_arch": "armhf", - "expected_packages": ["foo:amd64"], - }, - ), - ( - "amd64 to i386", - { - "to_arch": "to armhf", - "body": ["foo"], - "else_bodies": [], - "target_arch": "i386", - "expected_packages": list(), - }, - ), - ( - "ignored else", - { - "to_arch": "to armhf", - "body": ["foo"], - "else_bodies": [["bar"]], - "target_arch": "armhf", - "expected_packages": ["foo"], - }, - ), - ( - "used else", - { - "to_arch": "to armhf", - "body": ["foo"], - "else_bodies": [["bar"]], - "target_arch": "i386", - "expected_packages": ["bar"], - }, - ), - ( - "used else, arch specified", - { - "to_arch": "to armhf", - "body": ["foo"], - "else_bodies": [["bar:amd64"]], - "target_arch": "i386", - "expected_packages": ["bar:amd64"], - }, - ), - ( - "third else ignored", - { - "to_arch": "to armhf", - "body": ["foo"], - "else_bodies": [["bar"], ["baz"]], - "target_arch": "i386", - "expected_packages": ["bar"], - }, - ), - ( - "third else followed", - { - "to_arch": "to armhf", - "body": ["foo"], - "else_bodies": [[{"to armhf": ["bar"]}], ["baz"]], - "target_arch": "i386", - "expected_packages": ["baz"], - }, - ), - ( - "nested armhf", - { - "to_arch": "to armhf", - "body": [{"to armhf": ["foo"]}, {"to i386": ["bar"]}], - "else_bodies": [], - "target_arch": "armhf", - "expected_packages": ["foo"], - }, - ), - ( - "nested i386", - { - "to_arch": "to i386", - "body": [{"to armhf": ["foo"]}, {"to i386": ["bar"]}], - "else_bodies": [], - "target_arch": "i386", - "expected_packages": ["bar"], - }, - ), - ( - "nested body ignored else", - { - "to_arch": "to armhf", - "body": [{"to armhf": ["foo"]}, {"else": ["bar"]}], - "else_bodies": [], - "target_arch": "armhf", - "expected_packages": ["foo"], - }, - ), - ( - "nested body used else", - { - "to_arch": "to i386", - "body": [{"to armhf": ["foo"]}, {"else": ["bar"]}], - "else_bodies": [], - "target_arch": "i386", - "expected_packages": ["bar"], - }, - ), - ( - "nested else ignored else", - { - "to_arch": "to i386", - "body": ["foo"], - "else_bodies": [[{"to armhf": ["bar"]}, {"else": ["baz"]}]], - "target_arch": "armhf", - "expected_packages": ["bar"], - }, - ), - ( - "nested else used else", - { - "to_arch": "to armhf", - "body": ["foo"], - "else_bodies": [[{"to armhf": ["bar"]}, {"else": ["baz"]}]], - "target_arch": "i386", - "expected_packages": ["baz"], - }, - ), - ] - - def test( - self, monkeypatch, to_arch, body, else_bodies, target_arch, expected_packages - ): - monkeypatch.setattr(platform, "machine", lambda: "x86_64") - monkeypatch.setattr(platform, "architecture", lambda: ("64bit", "ELF")) - processor = grammar.GrammarProcessor( - None, - snapcraft_legacy.ProjectOptions(target_deb_arch=target_arch), - lambda x: True, - ) - statement = to.ToStatement(to=to_arch, body=body, processor=processor) - - for else_body in else_bodies: - statement.add_else(else_body) - - assert statement.process() == expected_packages - - -class TestToStatementInvalidGrammar: - - scenarios = [ - ( - "spaces in selectors", - { - "to_arch": "to armhf, ubuntu", - "body": ["foo"], - "else_bodies": [], - "target_arch": "armhf", - "expected_exception": ".*not a valid 'to' clause.*spaces are not allowed in the " - "selectors.*", - }, - ), - ( - "beginning with comma", - { - "to_arch": "to ,armhf", - "body": ["foo"], - "else_bodies": [], - "target_arch": "armhf", - "expected_exception": ".*not a valid 'to' clause", - }, - ), - ( - "ending with comma", - { - "to_arch": "to armhf,", - "body": ["foo"], - "else_bodies": [], - "target_arch": "armhf", - "expected_exception": ".*not a valid 'to' clause", - }, - ), - ( - "multiple commas", - { - "to_arch": "to armhf,,ubuntu", - "body": ["foo"], - "else_bodies": [], - "target_arch": "armhf", - "expected_exception": ".*not a valid 'to' clause", - }, - ), - ( - "invalid selector format", - { - "to_arch": "to_arch", - "body": ["foo"], - "else_bodies": [], - "target_arch": "armhf", - "expected_exception": ".*not a valid 'to' clause.*selectors are missing", - }, - ), - ( - "not even close", - { - "to_arch": "im-invalid", - "body": ["foo"], - "else_bodies": [], - "target_arch": "armhf", - "expected_exception": ".*not a valid 'to' clause", - }, - ), - ] - - def test(self, to_arch, body, else_bodies, target_arch, expected_exception): - with pytest.raises(grammar.errors.ToStatementSyntaxError) as error: - processor = grammar.GrammarProcessor( - None, - snapcraft_legacy.ProjectOptions(target_deb_arch=target_arch), - lambda x: True, - ) - statement = to.ToStatement(to=to_arch, body=body, processor=processor) - - for else_body in else_bodies: - statement.add_else(else_body) - - statement.process() - - assert re.match(expected_exception, str(error.value)) - - -def test_else_fail(monkeypatch): - monkeypatch.setattr(platform, "machine", lambda: "x86_64") - monkeypatch.setattr(platform, "architecture", lambda: ("64bit", "ELF")) - - processor = grammar.GrammarProcessor( - None, snapcraft_legacy.ProjectOptions(target_deb_arch="i386"), lambda x: True - ) - statement = to.ToStatement(to="to armhf", body=["foo"], processor=processor) - - statement.add_else(None) - - with pytest.raises(grammar.errors.UnsatisfiedStatementError) as error: - statement.process() - - assert str(error.value) == "Unable to satisfy 'to armhf', failure forced" diff --git a/tests/legacy/unit/project_loader/grammar/test_try_statement.py b/tests/legacy/unit/project_loader/grammar/test_try_statement.py deleted file mode 100644 index cf2b76ee9d..0000000000 --- a/tests/legacy/unit/project_loader/grammar/test_try_statement.py +++ /dev/null @@ -1,135 +0,0 @@ -# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- -# -# Copyright (C) 2017, 2018 Canonical Ltd -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License version 3 as -# published by the Free Software Foundation. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -import doctest - -import pytest - -import snapcraft_legacy -import snapcraft_legacy.internal.project_loader.grammar._try as _try -from snapcraft_legacy.internal.project_loader import grammar - - -def load_tests(loader, tests, ignore): - tests.addTests(doctest.DocTestSuite(_try)) - return tests - - -class TestTryStatementGrammar: - - scenarios = [ - ( - "followed body", - { - "body": ["foo", "bar"], - "else_bodies": [], - "expected_packages": ["foo", "bar"], - }, - ), - ( - "followed else", - { - "body": ["invalid"], - "else_bodies": [["valid"]], - "expected_packages": ["valid"], - }, - ), - ( - "optional without else", - {"body": ["invalid"], "else_bodies": [], "expected_packages": list()}, - ), - ( - "followed chained else", - { - "body": ["invalid1"], - "else_bodies": [["invalid2"], ["finally-valid"]], - "expected_packages": ["finally-valid"], - }, - ), - ( - "nested body followed body", - { - "body": [{"try": ["foo"]}, {"else": ["bar"]}], - "else_bodies": [], - "expected_packages": ["foo"], - }, - ), - ( - "nested body followed else", - { - "body": [{"try": ["invalid"]}, {"else": ["bar"]}], - "else_bodies": [], - "expected_packages": ["bar"], - }, - ), - ( - "nested else followed body", - { - "body": ["invalid"], - "else_bodies": [[{"try": ["foo"]}, {"else": ["bar"]}]], - "expected_packages": ["foo"], - }, - ), - ( - "nested else followed else", - { - "body": ["invalid"], - "else_bodies": [[{"try": ["invalid"]}, {"else": ["bar"]}]], - "expected_packages": ["bar"], - }, - ), - ( - "multiple elses", - { - "body": ["invalid1"], - "else_bodies": [["invalid2"], ["valid"]], - "expected_packages": ["valid"], - }, - ), - ( - "multiple elses all invalid", - { - "body": ["invalid1"], - "else_bodies": [["invalid2"], ["invalid3"]], - "expected_packages": ["invalid3"], - }, - ), - ] - - def test_try_statement_grammar(self, body, else_bodies, expected_packages): - processor = grammar.GrammarProcessor( - None, snapcraft_legacy.ProjectOptions(), lambda x: "invalid" not in x - ) - statement = _try.TryStatement(body=body, processor=processor) - - for else_body in else_bodies: - statement.add_else(else_body) - - assert statement.process() == expected_packages - - -def test_else_fail(): - processor = grammar.GrammarProcessor( - None, snapcraft_legacy.ProjectOptions(), lambda x: "invalid" not in x - ) - statement = _try.TryStatement(body=["invalid"], processor=processor) - - statement.add_else(None) - - with pytest.raises(grammar.errors.UnsatisfiedStatementError) as error: - statement.process() - - assert "Unable to satisfy 'try', failure forced" in str(error.value) diff --git a/tests/legacy/unit/project_loader/grammar_processing/test_part_grammar_processor.py b/tests/legacy/unit/project_loader/grammar_processing/test_part_grammar_processor.py index 8cb994fab5..61e1a15914 100644 --- a/tests/legacy/unit/project_loader/grammar_processing/test_part_grammar_processor.py +++ b/tests/legacy/unit/project_loader/grammar_processing/test_part_grammar_processor.py @@ -198,26 +198,22 @@ class TestPartGrammarSource: ] arch_scenarios = [ - ("amd64", {"host_arch": "x86_64", "target_arch": "amd64"}), - ("i386", {"host_arch": "i686", "target_arch": "i386"}), - ("amd64 to armhf", {"host_arch": "x86_64", "target_arch": "armhf"}), + ("amd64", {"arch": "amd64", "target_arch": "amd64"}), + ("i386", {"arch": "i386", "target_arch": "i386"}), + ("amd64 to armhf", {"arch": "amd64", "target_arch": "armhf"}), ] scenarios = multiply_scenarios(source_scenarios, arch_scenarios) def test( self, - monkeypatch, - host_arch, + arch, target_arch, properties, expected_amd64, expected_i386, expected_armhf, ): - monkeypatch.setattr(platform, "machine", lambda: host_arch) - monkeypatch.setattr(platform, "architecture", lambda: ("64bit", "ELF")) - repo = mock.Mock() plugin = mock.Mock() plugin.properties = properties.copy() @@ -231,7 +227,8 @@ def test( PartGrammarProcessor( plugin=plugin, properties=plugin.properties, - project=project.Project(target_deb_arch=target_arch), + arch=arch, + target_arch=target_arch, repo=repo, ).get_source() == expected_arch[f"expected_{target_arch}"] @@ -310,9 +307,9 @@ class TestPartGrammarBuildAndStageSnaps: ] arch_scenarios = [ - ("amd64", {"host_arch": "x86_64", "target_arch": "amd64"}), - ("i386", {"host_arch": "i686", "target_arch": "i386"}), - ("amd64 to armhf", {"host_arch": "x86_64", "target_arch": "armhf"}), + ("amd64", {"arch": "amd64", "target_arch": "amd64"}), + ("i386", {"arch": "i386", "target_arch": "i386"}), + ("amd64 to armhf", {"arch": "amd64", "target_arch": "armhf"}), ] scenarios = multiply_scenarios(source_scenarios, arch_scenarios) @@ -320,15 +317,13 @@ class TestPartGrammarBuildAndStageSnaps: def test_snaps( self, monkeypatch, - host_arch, + arch, target_arch, snaps, expected_amd64, expected_i386, expected_armhf, ): - monkeypatch.setattr(platform, "machine", lambda: host_arch) - monkeypatch.setattr(platform, "architecture", lambda: ("64bit", "ELF")) monkeypatch.setattr( snapcraft_repo.snaps.SnapPackage, "is_valid_snap", @@ -348,7 +343,8 @@ class Plugin: "build-snaps": {"plugin-preferred"}, "stage-snaps": "plugin-preferred", }, - project=project.Project(target_deb_arch=target_arch), + arch=arch, + target_arch=target_arch, repo=repo, ) @@ -363,15 +359,13 @@ class Plugin: def test_snaps_no_plugin_attribute( self, monkeypatch, - host_arch, + arch, target_arch, snaps, expected_amd64, expected_i386, expected_armhf, ): - monkeypatch.setattr(platform, "machine", lambda: host_arch) - monkeypatch.setattr(platform, "architecture", lambda: ("64bit", "ELF")) monkeypatch.setattr( snapcraft_repo.snaps.SnapPackage, "is_valid_snap", @@ -387,7 +381,8 @@ class Plugin: processor = PartGrammarProcessor( plugin=plugin, properties={"build-snaps": snaps, "stage-snaps": snaps}, - project=project.Project(target_deb_arch=target_arch), + arch=arch, + target_arch=target_arch, repo=repo, ) @@ -460,26 +455,22 @@ class TestPartGrammarStagePackages: ] arch_scenarios = [ - ("amd64", {"host_arch": "x86_64", "target_arch": "amd64"}), - ("i386", {"host_arch": "i686", "target_arch": "i386"}), - ("amd64 to armhf", {"host_arch": "x86_64", "target_arch": "armhf"}), + ("amd64", {"arch": "amd64", "target_arch": "amd64"}), + ("i386", {"arch": "i386", "target_arch": "i386"}), + ("amd64 to armhf", {"arch": "amd64", "target_arch": "armhf"}), ] scenarios = multiply_scenarios(source_scenarios, arch_scenarios) def test_packages( self, - monkeypatch, - host_arch, + arch, target_arch, packages, expected_amd64, expected_i386, expected_armhf, ): - monkeypatch.setattr(platform, "machine", lambda: host_arch) - monkeypatch.setattr(platform, "architecture", lambda: ("64bit", "ELF")) - repo = mock.Mock() class Plugin: @@ -490,7 +481,8 @@ class Plugin: processor = PartGrammarProcessor( plugin=plugin, properties={"stage-packages": "plugin-preferred"}, - project=project.Project(target_deb_arch=target_arch), + arch=arch, + target_arch=target_arch, repo=repo, ) @@ -505,17 +497,13 @@ class Plugin: def test_packages_plugin_no_attr( self, - monkeypatch, - host_arch, + arch, target_arch, packages, expected_amd64, expected_i386, expected_armhf, ): - monkeypatch.setattr(platform, "machine", lambda: host_arch) - monkeypatch.setattr(platform, "architecture", lambda: ("64bit", "ELF")) - repo = mock.Mock() class Plugin: @@ -525,7 +513,8 @@ class Plugin: processor = PartGrammarProcessor( plugin=plugin, properties={"stage-packages": packages}, - project=project.Project(target_deb_arch=target_arch), + arch=arch, + target_arch=target_arch, repo=repo, ) @@ -599,26 +588,22 @@ class TestPartGrammarBuildPackages: ] arch_scenarios = [ - ("amd64", {"host_arch": "x86_64", "target_arch": "amd64"}), - ("i386", {"host_arch": "i686", "target_arch": "i386"}), - ("amd64 to armhf", {"host_arch": "x86_64", "target_arch": "armhf"}), + ("amd64", {"arch": "amd64", "target_arch": "amd64"}), + ("i386", {"arch": "i386", "target_arch": "i386"}), + ("amd64 to armhf", {"arch": "amd64", "target_arch": "armhf"}), ] scenarios = multiply_scenarios(source_scenarios, arch_scenarios) def test_packages( self, - monkeypatch, - host_arch, + arch, target_arch, packages, expected_amd64, expected_i386, expected_armhf, ): - monkeypatch.setattr(platform, "machine", lambda: host_arch) - monkeypatch.setattr(platform, "architecture", lambda: ("64bit", "ELF")) - repo = mock.Mock() class Plugin: @@ -629,7 +614,8 @@ class Plugin: processor = PartGrammarProcessor( plugin=plugin, properties={"build-packages": {"plugin-preferred"}}, - project=project.Project(target_deb_arch=target_arch), + arch=arch, + target_arch=target_arch, repo=repo, ) @@ -644,17 +630,13 @@ class Plugin: def test_packages_plugin_no_attr( self, - monkeypatch, - host_arch, + arch, target_arch, packages, expected_amd64, expected_i386, expected_armhf, ): - monkeypatch.setattr(platform, "machine", lambda: host_arch) - monkeypatch.setattr(platform, "architecture", lambda: ("64bit", "ELF")) - repo = mock.Mock() class Plugin: @@ -664,7 +646,8 @@ class Plugin: processor = PartGrammarProcessor( plugin=plugin, properties={"build-packages": packages}, - project=project.Project(target_deb_arch=target_arch), + arch=arch, + target_arch=target_arch, repo=repo, ) From 8ea1eeb103e13dcef2f6ea2f4e5f668861a12ee7 Mon Sep 17 00:00:00 2001 From: Claudio Matsuoka Date: Sat, 19 Feb 2022 10:43:39 -0300 Subject: [PATCH 033/167] utils: gracefully determine host os Create a class around parsing `/etc/os-release` so we can gracefully fallback as appropriate when e.g. `VERSION_CODENAME` isn't set. This is a forward port of a25cfcc (PR #1636) by Kyle Fazzari. Co-authored-by: Kyle Fazzari Signed-off-by: Claudio Matsuoka --- pyproject.toml | 3 + snapcraft/os_release.py | 93 +++++++++++++++++++++ tests/unit/test_os_release.py | 148 ++++++++++++++++++++++++++++++++++ 3 files changed, 244 insertions(+) create mode 100644 snapcraft/os_release.py create mode 100644 tests/unit/test_os_release.py diff --git a/pyproject.toml b/pyproject.toml index 64f647c93a..ca6cd5a163 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,6 +33,9 @@ line_length = 88 [tool.pylint.messages_control] disable = "too-few-public-methods,fixme" +[tool.pylint.format] +good-names = "id" + [tool.pylint.MASTER] extension-pkg-allow-list = [ "pydantic" diff --git a/snapcraft/os_release.py b/snapcraft/os_release.py new file mode 100644 index 0000000000..cbf3bee9f9 --- /dev/null +++ b/snapcraft/os_release.py @@ -0,0 +1,93 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright 2017-2022 Canonical Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +"""OS release information helpers.""" + +import contextlib +from pathlib import Path +from typing import Dict + +from snapcraft import errors + +_ID_TO_UBUNTU_CODENAME = { + "17.10": "artful", + "17.04": "zesty", + "16.04": "xenial", + "14.04": "trusty", +} + + +class OsRelease: + """A class to intelligently determine the OS on which we're running.""" + + def __init__(self, *, os_release_file: Path = Path("/etc/os-release")) -> None: + """Create a new OsRelease instance. + + :param str os_release_file: Path to os-release file to be parsed. + """ + with contextlib.suppress(FileNotFoundError): + self._os_release = {} # type: Dict[str, str] + with os_release_file.open(encoding="utf-8") as release_file: + for line in release_file: + entry = line.rstrip().split("=") + if len(entry) == 2: + self._os_release[entry[0]] = entry[1].strip('"') + + def id(self) -> str: + """Return the OS ID. + + :raises SnapcraftError: If no ID can be determined. + """ + with contextlib.suppress(KeyError): + return self._os_release["ID"] + + raise errors.SnapcraftError("Unable to determine host OS ID") + + def name(self) -> str: + """Return the OS name. + + :raises SnapcraftError: If no name can be determined. + """ + with contextlib.suppress(KeyError): + return self._os_release["NAME"] + + raise errors.SnapcraftError("Unable to determine host OS name") + + def version_id(self) -> str: + """Return the OS version ID. + + :raises SnapcraftError: If no version ID can be determined. + """ + with contextlib.suppress(KeyError): + return self._os_release["VERSION_ID"] + + raise errors.SnapcraftError("Unable to determine host OS version ID") + + def version_codename(self) -> str: + """Return the OS version codename. + + This first tries to use the VERSION_CODENAME. If that's missing, it + tries to use the VERSION_ID to figure out the codename on its own. + + :raises SnapcraftError: If no version codename can be determined. + """ + with contextlib.suppress(KeyError): + return self._os_release["VERSION_CODENAME"] + + with contextlib.suppress(KeyError): + return _ID_TO_UBUNTU_CODENAME[self._os_release["VERSION_ID"]] + + raise errors.SnapcraftError("Unable to determine host OS version codename") diff --git a/tests/unit/test_os_release.py b/tests/unit/test_os_release.py new file mode 100644 index 0000000000..80116c9cf0 --- /dev/null +++ b/tests/unit/test_os_release.py @@ -0,0 +1,148 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright 2017-2022 Canonical Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from pathlib import Path +from textwrap import dedent + +import pytest + +from snapcraft import errors, os_release + + +@pytest.fixture +def _os_release(new_dir): + def _release_data(contents): + path = Path("os-release") + path.write_text(contents) + release = os_release.OsRelease(os_release_file=path) + return release + + return _release_data + + +def test_blank_lines(_os_release): + release = _os_release( + dedent( + """\ + NAME="Arch Linux" + + PRETTY_NAME="Arch Linux" + ID=arch + ID_LIKE=archlinux + VERSION_ID="foo" + VERSION_CODENAME="bar" + + """ + ) + ) + + assert release.id() == "arch" + assert release.name() == "Arch Linux" + assert release.version_id() == "foo" + assert release.version_codename() == "bar" + + +def test_no_id(_os_release): + release = _os_release( + dedent( + """\ + NAME="Arch Linux" + PRETTY_NAME="Arch Linux" + ID_LIKE=archlinux + VERSION_ID="foo" + VERSION_CODENAME="bar" + """ + ) + ) + + with pytest.raises(errors.SnapcraftError) as raised: + release.id() + + assert str(raised.value) == "Unable to determine host OS ID" + + +def test_no_name(_os_release): + release = _os_release( + dedent( + """\ + ID=arch + PRETTY_NAME="Arch Linux" + ID_LIKE=archlinux + VERSION_ID="foo" + VERSION_CODENAME="bar" + """ + ) + ) + + with pytest.raises(errors.SnapcraftError) as raised: + release.name() + + assert str(raised.value) == "Unable to determine host OS name" + + +def test_no_version_id(_os_release): + release = _os_release( + dedent( + """\ + NAME="Arch Linux" + ID=arch + PRETTY_NAME="Arch Linux" + ID_LIKE=archlinux + VERSION_CODENAME="bar" + """ + ) + ) + + with pytest.raises(errors.SnapcraftError) as raised: + release.version_id() + + assert str(raised.value) == "Unable to determine host OS version ID" + + +def test_no_version_codename(_os_release): + """Test that version codename can also come from VERSION_ID""" + release = _os_release( + dedent( + """\ + NAME="Ubuntu" + VERSION="14.04.5 LTS, Trusty Tahr" + ID=ubuntu + ID_LIKE=debian + PRETTY_NAME="Ubuntu 14.04.5 LTS" + VERSION_ID="14.04" + """ + ) + ) + + assert release.version_codename() == "trusty" + + +def test_no_version_codename_or_version_id(_os_release): + release = _os_release( + dedent( + """\ + NAME="Ubuntu" + ID=ubuntu + ID_LIKE=debian + PRETTY_NAME="Ubuntu 16.04.3 LTS" + """ + ) + ) + + with pytest.raises(errors.SnapcraftError) as raised: + release.version_codename() + + assert str(raised.value) == "Unable to determine host OS version codename" From 883cfd088b59940251f8516b4ef3263221cc0a0f Mon Sep 17 00:00:00 2001 From: Claudio Matsuoka Date: Sat, 19 Feb 2022 20:55:17 -0300 Subject: [PATCH 034/167] repo: add package repository handling Port definitions and helpers to add apt repositories and PPAs. Original code from PRs #2911, #3341, #3359, #3363. Co-authored-by: Chris Patterson Signed-off-by: Claudio Matsuoka --- pyproject.toml | 1 + snapcraft/meta/snap_yaml.py | 2 +- snapcraft/projects.py | 30 +- snapcraft/repo/__init__.py | 23 + snapcraft/repo/apt_key_manager.py | 228 +++++++++ snapcraft/repo/apt_ppa.py | 50 ++ snapcraft/repo/apt_sources_manager.py | 216 +++++++++ snapcraft/repo/errors.py | 101 ++++ snapcraft/repo/package_repository.py | 513 ++++++++++++++++++++ tests/unit/repo/__init__.py | 0 tests/unit/repo/test_apt_key_manager.py | 318 ++++++++++++ tests/unit/repo/test_apt_ppa.py | 62 +++ tests/unit/repo/test_apt_sources_manager.py | 192 ++++++++ tests/unit/repo/test_package_repository.py | 426 ++++++++++++++++ tests/unit/test_projects.py | 136 +++++- 15 files changed, 2294 insertions(+), 4 deletions(-) create mode 100644 snapcraft/repo/__init__.py create mode 100644 snapcraft/repo/apt_key_manager.py create mode 100644 snapcraft/repo/apt_ppa.py create mode 100644 snapcraft/repo/apt_sources_manager.py create mode 100644 snapcraft/repo/errors.py create mode 100644 snapcraft/repo/package_repository.py create mode 100644 tests/unit/repo/__init__.py create mode 100644 tests/unit/repo/test_apt_key_manager.py create mode 100644 tests/unit/repo/test_apt_ppa.py create mode 100644 tests/unit/repo/test_apt_sources_manager.py create mode 100644 tests/unit/repo/test_package_repository.py diff --git a/pyproject.toml b/pyproject.toml index ca6cd5a163..f75f0cd37b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,6 +34,7 @@ line_length = 88 disable = "too-few-public-methods,fixme" [tool.pylint.format] +max-attributes = 15 good-names = "id" [tool.pylint.MASTER] diff --git a/snapcraft/meta/snap_yaml.py b/snapcraft/meta/snap_yaml.py index c5ebf839c8..22b628ca4d 100644 --- a/snapcraft/meta/snap_yaml.py +++ b/snapcraft/meta/snap_yaml.py @@ -37,7 +37,7 @@ class SnapApp(YamlModel): command: str command_chain: List[str] - environment: Optional[List[Dict[str, str]]] + environment: Optional[Dict[str, str]] plugs: Optional[List[str]] class Config: # pylint: disable=too-few-public-methods diff --git a/snapcraft/projects.py b/snapcraft/projects.py index 6dcdc15122..4fa438cd18 100644 --- a/snapcraft/projects.py +++ b/snapcraft/projects.py @@ -47,10 +47,12 @@ class Config: # pylint: disable=too-few-public-methods # fmt: off if TYPE_CHECKING: CommandChainStr = str + KeyIdStr = str UniqueStrList = List[str] UniqueAliasList = List[str] else: CommandChainStr = constr(regex=r"^[A-Za-z0-9/._#:$-]*$") + KeyIdStr = constr(regex=r"^[A-Z0-9]{40}$") UniqueStrList = conlist(str, unique_items=True) UniqueAliasList = conlist(constr(regex=r"^[a-zA-Z0-9][-_.a-zA-Z0-9]*$"), unique_items=True) # fmt: on @@ -103,7 +105,7 @@ class App(ProjectModel): slots: Optional[UniqueStrList] plugs: Optional[UniqueStrList] aliases: Optional[UniqueAliasList] - environment: Optional[List[Dict[str, str]]] + environment: Optional[Dict[str, str]] command_chain: List[CommandChainStr] = [] # TODO: sockets @@ -152,6 +154,27 @@ class Architecture(ProjectModel): build_to: Optional[Union[str, UniqueStrList]] +class AptDeb(ProjectModel): + """Apt package repository definition.""" + + type: Literal["apt"] + url: str + key_id: KeyIdStr + architectures: Optional[List[str]] + formats: Optional[List[Literal["deb", "deb-src"]]] + components: Optional[List[str]] + key_server: Optional[str] + path: Optional[str] + suites: Optional[List[str]] + + +class AptPPA(ProjectModel): + """PPA package repository definition.""" + + type: Literal["apt"] + ppa: str + + class Project(ProjectModel): """Snapcraft project definition. @@ -160,8 +183,10 @@ class Project(ProjectModel): XXX: Not implemented in this version - environment (top-level) - system-usernames - - package-repositories - adopt-info (after adding craftctl support to craft-parts) + + FIXME: package-repositories needs better validation and less + confusing error messages """ name: constr(max_length=40) # type: ignore @@ -185,6 +210,7 @@ class Project(ProjectModel): grade: Literal["stable", "devel"] architectures: List[Architecture] = [] assumes: UniqueStrList = [] + package_repositories: List[Union[AptDeb, AptPPA]] = [] hooks: Optional[Dict[str, Hook]] passthrough: Optional[Dict[str, Any]] apps: Optional[Dict[str, App]] diff --git a/snapcraft/repo/__init__.py b/snapcraft/repo/__init__.py new file mode 100644 index 0000000000..1f69f90a86 --- /dev/null +++ b/snapcraft/repo/__init__.py @@ -0,0 +1,23 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright 2022 Canonical Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +"""Package repository helpers.""" + +from .apt_key_manager import AptKeyManager # noqa: F401 +from .apt_sources_manager import AptSourcesManager # noqa: F401 +from .package_repository import PackageRepository # noqa: F401 +from .package_repository import PackageRepositoryApt # noqa: F401 +from .package_repository import PackageRepositoryAptPPA # noqa: F401 diff --git a/snapcraft/repo/apt_key_manager.py b/snapcraft/repo/apt_key_manager.py new file mode 100644 index 0000000000..89aa3be7a5 --- /dev/null +++ b/snapcraft/repo/apt_key_manager.py @@ -0,0 +1,228 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright 2015-2022 Canonical Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +"""APT key management helpers.""" + +import pathlib +import subprocess +import tempfile +from typing import List, Optional + +import gnupg +from craft_cli import emit + +from . import apt_ppa, errors, package_repository + + +class AptKeyManager: + """Manage APT repository keys.""" + + def __init__( + self, + *, + gpg_keyring: pathlib.Path = pathlib.Path( + "/etc/apt/trusted.gpg.d/snapcraft.gpg" + ), + key_assets: pathlib.Path, + ) -> None: + self._gpg_keyring = gpg_keyring + self._key_assets = key_assets + + def find_asset_with_key_id(self, *, key_id: str) -> Optional[pathlib.Path]: + """Find snap key asset matching key_id. + + The key asset much be named with the last 8 characters of the key + identifier, in upper case. + + :param key_id: Key ID to search for. + + :returns: Path of key asset if match found, otherwise None. + """ + key_file = key_id[-8:].upper() + ".asc" + key_path = self._key_assets / key_file + + if key_path.exists(): + return key_path + + return None + + @classmethod + def get_key_fingerprints(cls, *, key: str) -> List[str]: + """List fingerprints found in specified key. + + Do this by importing the key into a temporary keyring, + then querying the keyring for fingerprints. + + :param key: Key data (string) to parse. + + :returns: List of key fingerprints/IDs. + """ + with tempfile.NamedTemporaryFile(suffix="keyring") as temp_file: + return ( + gnupg.GPG(keyring=temp_file.name).import_keys(key_data=key).fingerprints + ) + + @classmethod + def is_key_installed(cls, *, key_id: str) -> bool: + """Check if specified key_id is installed. + + Check if key is installed by attempting to export the key. + Unfortunately, apt-key does not exit with error and + we have to do our best to parse the output. + + :param key_id: Key ID to check for. + + :returns: True if key is installed. + """ + try: + proc = subprocess.run( + ["apt-key", "export", key_id], + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + check=True, + ) + except subprocess.CalledProcessError as error: + # Export shouldn't exit with failure based on testing, + # but assume the key is not installed and log a warning. + emit.message( + f"Unexpected apt-key failure: {error.output}", intermediate=True + ) + return False + + apt_key_output = proc.stdout.decode() + + if "BEGIN PGP PUBLIC KEY BLOCK" in apt_key_output: + return True + + if "nothing exported" in apt_key_output: + return False + + # The two strings above have worked in testing, but if neither is + # present for whatever reason, assume the key is not installed + # and log a warning. + emit.message(f"Unexpected apt-key output: {apt_key_output}", intermediate=True) + return False + + def install_key(self, *, key: str) -> None: + """Install given key. + + :param key: Key to install. + + :raises: AptGPGKeyInstallError if unable to install key. + """ + cmd = [ + "apt-key", + "--keyring", + str(self._gpg_keyring), + "add", + "-", + ] + + try: + emit.trace(f"Executing: {cmd!r}") + env = {} + env["LANG"] = "C.UTF-8" + subprocess.run( + cmd, + input=key.encode(), + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + check=True, + env=env, + ) + except subprocess.CalledProcessError as error: + raise errors.AptGPGKeyInstallError(error.output.decode(), key=key) + + emit.trace(f"Installed apt repository key:\n{key}") + + def install_key_from_keyserver( + self, *, key_id: str, key_server: str = "keyserver.ubuntu.com" + ) -> None: + """Install key from specified key server. + + :param key_id: Key ID to install. + :param key_server: Key server to query. + + :raises: AptGPGKeyInstallError if unable to install key. + """ + env = {} + env["LANG"] = "C.UTF-8" + + cmd = [ + "apt-key", + "--keyring", + str(self._gpg_keyring), + "adv", + "--keyserver", + key_server, + "--recv-keys", + key_id, + ] + + try: + emit.trace(f"Executing: {cmd!r}") + subprocess.run( + cmd, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + check=True, + env=env, + ) + except subprocess.CalledProcessError as error: + raise errors.AptGPGKeyInstallError( + error.output.decode(), key_id=key_id, key_server=key_server + ) + + def install_package_repository_key( + self, *, package_repo: package_repository.PackageRepository + ) -> bool: + """Install required key for specified package repository. + + For both PPA and other Apt package repositories: + 1) If key is already installed, return False. + 2) Install key from local asset, if available. + 3) Install key from key server, if available. An unspecified + keyserver will default to using keyserver.ubuntu.com. + + :param package_repo: Apt PackageRepository configuration. + + :returns: True if key configuration was changed. False if + key already installed. + + :raises: AptGPGKeyInstallError if unable to install key. + """ + key_server: Optional[str] = None + if isinstance(package_repo, package_repository.PackageRepositoryAptPPA): + key_id = apt_ppa.get_launchpad_ppa_key_id(ppa=package_repo.ppa) + elif isinstance(package_repo, package_repository.PackageRepositoryApt): + key_id = package_repo.key_id + key_server = package_repo.key_server + else: + raise RuntimeError(f"unhandled package repo type: {package_repo!r}") + + # Already installed, nothing to do. + if self.is_key_installed(key_id=key_id): + return False + + key_path = self.find_asset_with_key_id(key_id=key_id) + if key_path is not None: + self.install_key(key=key_path.read_text()) + else: + if key_server is None: + key_server = "keyserver.ubuntu.com" + self.install_key_from_keyserver(key_id=key_id, key_server=key_server) + + return True diff --git a/snapcraft/repo/apt_ppa.py b/snapcraft/repo/apt_ppa.py new file mode 100644 index 0000000000..d022a04bad --- /dev/null +++ b/snapcraft/repo/apt_ppa.py @@ -0,0 +1,50 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright 2020-2022 Canonical Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +"""Personal Package Archive helpers.""" + +from typing import Tuple + +import lazr.restfulclient.errors +from craft_cli import emit +from launchpadlib.launchpad import Launchpad + +from . import errors + + +def split_ppa_parts(*, ppa: str) -> Tuple[str, str]: + """Obtain user and repository components from a PPA line.""" + ppa_split = ppa.split("/") + if len(ppa_split) != 2: + raise errors.AptPPAInstallError(ppa, "invalid PPA format") + return ppa_split[0], ppa_split[1] + + +def get_launchpad_ppa_key_id(*, ppa: str) -> str: + """Query Launchpad for PPA's key ID.""" + owner, name = split_ppa_parts(ppa=ppa) + launchpad = Launchpad.login_anonymously("snapcraft", "production") + launchpad_url = f"~{owner}/+archive/{name}" + + emit.trace(f"Loading launchpad url: {launchpad_url}") + try: + key_id = launchpad.load(launchpad_url).signing_key_fingerprint + except lazr.restfulclient.errors.NotFound as error: + raise errors.AptPPAInstallError(ppa, "not found on launchpad") from error + + emit.trace(f"Retrieved launchpad PPA key ID: {key_id}") + + return key_id diff --git a/snapcraft/repo/apt_sources_manager.py b/snapcraft/repo/apt_sources_manager.py new file mode 100644 index 0000000000..c0685aefb4 --- /dev/null +++ b/snapcraft/repo/apt_sources_manager.py @@ -0,0 +1,216 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright 2015-2022 Canonical Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +"""Manage the host's apt source repository configuration.""" + +import io +import pathlib +import re +from typing import List, Optional + +from craft_cli import emit + +from snapcraft import os_release +from snapcraft_legacy.project._project_options import ProjectOptions + +from . import apt_ppa, package_repository + + +def _construct_deb822_source( + *, + architectures: Optional[List[str]] = None, + components: Optional[List[str]] = None, + formats: Optional[List[str]] = None, + suites: List[str], + url: str, +) -> str: + """Construct deb-822 formatted sources.list config string.""" + with io.StringIO() as deb822: + if formats: + type_text = " ".join(formats) + else: + type_text = "deb" + + print(f"Types: {type_text}", file=deb822) + + print(f"URIs: {url}", file=deb822) + + suites_text = " ".join(suites) + print(f"Suites: {suites_text}", file=deb822) + + if components: + components_text = " ".join(components) + print(f"Components: {components_text}", file=deb822) + + if architectures: + arch_text = " ".join(architectures) + else: + arch_text = _get_host_arch() + + print(f"Architectures: {arch_text}", file=deb822) + + return deb822.getvalue() + + +def _get_host_arch() -> str: + return ProjectOptions().deb_arch + + +class AptSourcesManager: + """Manage apt source configuration in /etc/apt/sources.list.d. + + :param sources_list_d: Path to sources.list.d directory. + """ + + # pylint: disable=too-few-public-methods + def __init__( + self, + *, + sources_list_d: pathlib.Path = pathlib.Path("/etc/apt/sources.list.d"), + ) -> None: + self._sources_list_d = sources_list_d + + def _install_sources( + self, + *, + architectures: Optional[List[str]] = None, + components: Optional[List[str]] = None, + formats: Optional[List[str]] = None, + name: str, + suites: List[str], + url: str, + ) -> bool: + """Install sources list configuration. + + Write config to: + /etc/apt/sources.list.d/snapcraft-.sources + + :returns: True if configuration was changed. + """ + config = _construct_deb822_source( + architectures=architectures, + components=components, + formats=formats, + suites=suites, + url=url, + ) + + if name not in ["default", "default-security"]: + name = "snapcraft-" + name + + config_path = self._sources_list_d / f"{name}.sources" + if config_path.exists() and config_path.read_text() == config: + # Already installed and matches, nothing to do. + emit.trace(f"Ignoring unchanged sources: {config_path!s}") + return False + + config_path.write_text(config) + emit.trace(f"Installed sources: {config_path!s}") + return True + + def _install_sources_apt( + self, *, package_repo: package_repository.PackageRepositoryApt + ) -> bool: + """Install repository configuration. + + 1) First check to see if package repo is implied path, + or "bare repository" config. This is indicated when no + path, components, or suites are indicated. + 2) If path is specified, convert path to a suite entry, + ending with "/". + + Relatedly, this assumes all of the error-checking has been + done already on the package_repository object in a proper + fashion, but do some sanity checks here anyways. + + :returns: True if source configuration was changed. + """ + if ( + not package_repo.path + and not package_repo.components + and not package_repo.suites + ): + suites = ["/"] + elif package_repo.path: + # Suites denoting exact path must end with '/'. + path = package_repo.path + if not path.endswith("/"): + path += "/" + suites = [path] + elif package_repo.suites: + suites = package_repo.suites + if not package_repo.components: + raise RuntimeError("no components with suite") + else: + raise RuntimeError("no suites or path") + + if package_repo.name: + name = package_repo.name + else: + name = re.sub(r"\W+", "_", package_repo.url) + + return self._install_sources( + architectures=package_repo.architectures, + components=package_repo.components, + formats=package_repo.formats, + name=name, + suites=suites, + url=package_repo.url, + ) + + def _install_sources_ppa( + self, *, package_repo: package_repository.PackageRepositoryAptPPA + ) -> bool: + """Install PPA formatted repository. + + Create a sources list config by: + - Looking up the codename of the host OS and using it as the "suites" + entry. + - Formulate deb URL to point to PPA. + - Enable only "deb" formats. + + :returns: True if source configuration was changed. + """ + owner, name = apt_ppa.split_ppa_parts(ppa=package_repo.ppa) + codename = os_release.OsRelease().version_codename() + + return self._install_sources( + components=["main"], + formats=["deb"], + name=f"ppa-{owner}_{name}", + suites=[codename], + url=f"http://ppa.launchpad.net/{owner}/{name}/ubuntu", + ) + + def install_package_repository_sources( + self, + *, + package_repo: package_repository.PackageRepository, + ) -> bool: + """Install configured package repositories. + + :param package_repo: Repository to install the source configuration for. + + :returns: True if source configuration was changed. + """ + emit.trace(f"Processing repo: {package_repo!r}") + if isinstance(package_repo, package_repository.PackageRepositoryAptPPA): + return self._install_sources_ppa(package_repo=package_repo) + + if isinstance(package_repo, package_repository.PackageRepositoryApt): + return self._install_sources_apt(package_repo=package_repo) + + raise RuntimeError(f"unhandled package repository: {package_repository!r}") diff --git a/snapcraft/repo/errors.py b/snapcraft/repo/errors.py new file mode 100644 index 0000000000..06a0a49b10 --- /dev/null +++ b/snapcraft/repo/errors.py @@ -0,0 +1,101 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright 2022 Canonical Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +"""Package repository error definitions.""" + +from typing import Optional + +from snapcraft.errors import SnapcraftError + + +class PackageRepositoryValidationError(SnapcraftError): + """Package repository is invalid.""" + + def __init__( + self, + url: str, + brief: str, + details: Optional[str] = None, + resolution: Optional[str] = None, + ): + super().__init__( + f"Invalid package repository for {url!r}: {brief}", + details=details, + resolution=resolution, + ) + + +class AptPPAInstallError(SnapcraftError): + """Installation of a PPA repository failed.""" + + def __init__(self, ppa: str, reason: str): + super().__init__( + f"Failed to install PPA {ppa!r}: {reason}", + resolution="Verify PPA is correct and try again", + ) + + +class AptGPGKeyInstallError(SnapcraftError): + """Installation of GPG key failed.""" + + def __init__( + self, + output: str, + *, + key: Optional[str] = None, + key_id: Optional[str] = None, + key_server: Optional[str] = None, + ): + """Convert apt-key's output into a more user-friendly message.""" + message = output.replace( + "Warning: apt-key output should not be parsed (stdout is not a terminal)", + "", + ).strip() + + # Improve error messages that we can. + if ( + "gpg: keyserver receive failed: No data" in message + and key_id + and key_server + ): + message = f"GPG key {key_id!r} not found on key server {key_server!r}" + elif ( + "gpg: keyserver receive failed: Server indicated a failure" in message + and key_server + ): + message = f"unable to establish connection to key server {key_server!r}" + elif ( + "gpg: keyserver receive failed: Connection timed out" in message + and key_server + ): + message = ( + f"unable to establish connection to key server {key_server!r} " + f"(connection timed out)" + ) + + details = "" + if key: + details += f"GPG key:\n{key}\n" + if key_id: + details += f"GPG key ID: {key_id}\n" + if key_server: + details += f"GPG key server: {key_server}" + + super().__init__( + f"Failed to install GPG key: {message}", + details=details, + resolution="Verify any configured GPG keys", + ) diff --git a/snapcraft/repo/package_repository.py b/snapcraft/repo/package_repository.py new file mode 100644 index 0000000000..0cfb0ba850 --- /dev/null +++ b/snapcraft/repo/package_repository.py @@ -0,0 +1,513 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright 2019-2022 Canonical Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +"""Package repository definitions.""" + +import abc +import re +from copy import deepcopy +from typing import Any, Dict, List, Optional + +from overrides import overrides + +from . import errors + + +class PackageRepository(abc.ABC): + """The base class for package repositories.""" + + @abc.abstractmethod + def marshal(self) -> Dict[str, Any]: + """Return the package repository data as a dictionary.""" + + @classmethod + def unmarshal(cls, data: Dict[str, str]) -> "PackageRepository": + """Create a package repository object from the given data.""" + if not isinstance(data, dict): + raise errors.PackageRepositoryValidationError( + url=str(data), + brief="invalid object.", + details="Package repository must be a valid dictionary object.", + resolution=( + "Verify repository configuration and ensure that the " + "correct syntax is used." + ), + ) + + if "ppa" in data: + return PackageRepositoryAptPPA.unmarshal(data) + + return PackageRepositoryApt.unmarshal(data) + + @classmethod + def unmarshal_package_repositories(cls, data: Any) -> List["PackageRepository"]: + """Create multiple package repositories from the given data.""" + repositories = [] + + if data is not None: + if not isinstance(data, list): + raise errors.PackageRepositoryValidationError( + url=str(data), + brief="invalid list object.", + details="Package repositories must be a list of objects.", + resolution=( + "Verify 'package-repositories' configuration and ensure " + "that the correct syntax is used." + ), + ) + + for repository in data: + package_repo = cls.unmarshal(repository) + repositories.append(package_repo) + + return repositories + + +class PackageRepositoryAptPPA(PackageRepository): + """A PPA package repository.""" + + def __init__(self, *, ppa: str) -> None: + self.type = "apt" + self.ppa = ppa + + self.validate() + + @overrides + def marshal(self) -> Dict[str, Any]: + """Return the package repository data as a dictionary.""" + data: Dict[str, Any] = {"type": "apt"} + data["ppa"] = self.ppa + return data + + def validate(self) -> None: + """Ensure the current repository data is valid.""" + if not self.ppa: + raise errors.PackageRepositoryValidationError( + url=self.ppa, + brief="invalid PPA.", + details="PPAs must be non-empty strings.", + resolution=( + "Verify repository configuration and ensure that " + "'ppa' is correctly specified." + ), + ) + + @classmethod + @overrides + def unmarshal(cls, data: Dict[str, str]) -> "PackageRepositoryAptPPA": + """Create a package repository object from the given data.""" + if not isinstance(data, dict): + raise errors.PackageRepositoryValidationError( + url=str(data), + brief="invalid object.", + details="Package repository must be a valid dictionary object.", + resolution=( + "Verify repository configuration and ensure that the correct " + "syntax is used." + ), + ) + + data_copy = deepcopy(data) + + ppa = data_copy.pop("ppa", "") + repo_type = data_copy.pop("type", None) + + if repo_type != "apt": + raise errors.PackageRepositoryValidationError( + url=ppa, + brief=f"unsupported type {repo_type!r}.", + details="The only currently supported type is 'apt'.", + resolution=( + "Verify repository configuration and ensure that 'type' " + "is correctly specified." + ), + ) + + if not isinstance(ppa, str): + raise errors.PackageRepositoryValidationError( + url=ppa, + brief=f"Invalid PPA {ppa!r}.", + details="PPA must be a valid string.", + resolution=( + "Verify repository configuration and ensure that 'ppa' " + "is correctly specified." + ), + ) + + if data_copy: + keys = ", ".join([repr(k) for k in data_copy.keys()]) + raise errors.PackageRepositoryValidationError( + url=ppa, + brief=f"unsupported properties {keys}.", + resolution=( + "Verify repository configuration and ensure that it is correct." + ), + ) + + return cls(ppa=ppa) + + +class PackageRepositoryApt(PackageRepository): + """An APT package repository.""" + + def __init__( + self, + *, + architectures: Optional[List[str]] = None, + components: Optional[List[str]] = None, + formats: Optional[List[str]] = None, + key_id: str, + key_server: Optional[str] = None, + name: Optional[str] = None, + path: Optional[str] = None, + suites: Optional[List[str]] = None, + url: str, + ) -> None: + self.type = "apt" + self.architectures = architectures + self.components = components + self.formats = formats + self.key_id = key_id + self.key_server = key_server + + if name is None: + # Default name is URL, stripping non-alphanumeric characters. + self.name: str = re.sub(r"\W+", "_", url) + else: + self.name = name + + self.path = path + self.suites = suites + self.url = url + + self.validate() + + @overrides + def marshal(self) -> Dict[str, Any]: + """Return the package repository data as a dictionary.""" + data: Dict[str, Any] = {"type": "apt"} + + if self.architectures: + data["architectures"] = self.architectures + + if self.components: + data["components"] = self.components + + if self.formats: + data["formats"] = self.formats + + data["key-id"] = self.key_id + + if self.key_server: + data["key-server"] = self.key_server + + data["name"] = self.name + + if self.path: + data["path"] = self.path + + if self.suites: + data["suites"] = self.suites + + data["url"] = self.url + + return data + + # pylint: disable=too-many-branches + + def validate(self) -> None: # noqa: C901 + """Ensure the current repository data is valid.""" + if self.formats is not None: + for repo_format in self.formats: + if repo_format not in ["deb", "deb-src"]: + raise errors.PackageRepositoryValidationError( + url=self.url, + brief=f"invalid format {repo_format!r}.", + details="Valid formats include: deb and deb-src.", + resolution=( + "Verify the repository configuration and ensure that " + "'formats' is correctly specified." + ), + ) + + if not self.key_id or not re.match(r"^[0-9A-F]{40}$", self.key_id): + raise errors.PackageRepositoryValidationError( + url=self.url, + brief=f"invalid key identifier {self.key_id!r}.", + details="Key IDs must be 40 upper-case hex characters.", + resolution=( + "Verify the repository configuration and ensure that 'key-id' " + "is correctly specified." + ), + ) + + if not self.url: + raise errors.PackageRepositoryValidationError( + url=self.url, + brief="invalid URL.", + details="URLs must be non-empty strings.", + resolution=( + "Verify the repository configuration and ensure that 'url' " + "is correctly specified." + ), + ) + + if self.suites: + for suite in self.suites: + if suite.endswith("/"): + raise errors.PackageRepositoryValidationError( + url=self.url, + brief=f"invalid suite {suite!r}.", + details="Suites must not end with a '/'.", + resolution=( + "Verify the repository configuration and remove the " + "trailing '/' from suites or use the 'path' property " + "to define a path." + ), + ) + + if self.path is not None and self.path == "": + raise errors.PackageRepositoryValidationError( + url=self.url, + brief=f"invalid path {self.path!r}.", + details="Paths must be non-empty strings.", + resolution=( + "Verify the repository configuration and ensure that 'path' " + "is a non-empty string such as '/'." + ), + ) + + if self.path and self.components: + raise errors.PackageRepositoryValidationError( + url=self.url, + brief=( + f"components {self.components!r} cannot be combined with " + f"path {self.path!r}." + ), + details="Path and components are incomptiable options.", + resolution=( + "Verify the repository configuration and remove 'path' " + "or 'components'." + ), + ) + + if self.path and self.suites: + raise errors.PackageRepositoryValidationError( + url=self.url, + brief=( + f"suites {self.suites!r} cannot be combined with " + f"path {self.path!r}." + ), + details="Path and suites are incomptiable options.", + resolution=( + "Verify the repository configuration and remove 'path' or 'suites'." + ), + ) + + if self.suites and not self.components: + raise errors.PackageRepositoryValidationError( + url=self.url, + brief="no components specified.", + details="Components are required when using suites.", + resolution=( + "Verify the repository configuration and ensure that 'components' " + "is correctly specified." + ), + ) + + if self.components and not self.suites: + raise errors.PackageRepositoryValidationError( + url=self.url, + brief="no suites specified.", + details="Suites are required when using components.", + resolution=( + "Verify the repository configuration and ensure that 'suites' " + "is correctly specified." + ), + ) + + # pylint: enable=too-many-branches + + @classmethod # noqa: C901 + @overrides + def unmarshal(cls, data: Dict[str, Any]) -> "PackageRepositoryApt": # noqa: C901 + """Create a package repository object from the given data.""" + if not isinstance(data, dict): + raise errors.PackageRepositoryValidationError( + url=str(data), + brief="invalid object.", + details="Package repository must be a valid dictionary object.", + resolution=( + "Verify repository configuration and ensure that the " + "correct syntax is used." + ), + ) + + data_copy = deepcopy(data) + + architectures = data_copy.pop("architectures", None) + components = data_copy.pop("components", None) + formats = data_copy.pop("formats", None) + key_id = data_copy.pop("key-id", None) + key_server = data_copy.pop("key-server", None) + name = data_copy.pop("name", None) + path = data_copy.pop("path", None) + suites = data_copy.pop("suites", None) + url = data_copy.pop("url", "") + repo_type = data_copy.pop("type", None) + + if repo_type != "apt": + raise errors.PackageRepositoryValidationError( + url=url, + brief=f"unsupported type {repo_type!r}.", + details="The only currently supported type is 'apt'.", + resolution=( + "Verify repository configuration and ensure that 'type' " + "is correctly specified." + ), + ) + + if architectures is not None and ( + not isinstance(architectures, list) + or not all(isinstance(x, str) for x in architectures) + ): + raise errors.PackageRepositoryValidationError( + url=url, + brief=f"invalid architectures {architectures!r}.", + details="Architectures must be a list of valid architecture strings.", + resolution=( + "Verify repository configuration and ensure that 'architectures' " + "is correctly specified." + ), + ) + + if components is not None and ( + not isinstance(components, list) + or not all(isinstance(x, str) for x in components) + or not components + ): + raise errors.PackageRepositoryValidationError( + url=url, + brief=f"invalid components {components!r}.", + details="Components must be a list of strings.", + resolution=( + "Verify repository configuration and ensure that 'components' " + "is correctly specified." + ), + ) + + if formats is not None and ( + not isinstance(formats, list) + or not all(isinstance(x, str) for x in formats) + ): + raise errors.PackageRepositoryValidationError( + url=url, + brief=f"invalid formats {formats!r}.", + details="Formats must be a list of strings.", + resolution=( + "Verify repository configuration and ensure that 'formats' " + "is correctly specified." + ), + ) + + if not isinstance(key_id, str): + raise errors.PackageRepositoryValidationError( + url=url, + brief=f"invalid key identifier {key_id!r}.", + details="Key identifiers must be a valid string.", + resolution=( + "Verify repository configuration and ensure that 'key-id' " + "is correctly specified." + ), + ) + + if key_server is not None and not isinstance(key_server, str): + raise errors.PackageRepositoryValidationError( + url=url, + brief=f"invalid key server {key_server!r}.", + details="Key servers must be a valid string.", + resolution=( + "Verify repository configuration and ensure that 'key-server' " + "is correctly specified." + ), + ) + + if name is not None and not isinstance(name, str): + raise errors.PackageRepositoryValidationError( + url=url, + brief=f"invalid name {name!r}.", + details="Names must be a valid string.", + resolution=( + "Verify repository configuration and ensure that 'name' " + "is correctly specified." + ), + ) + + if path is not None and not isinstance(path, str): + raise errors.PackageRepositoryValidationError( + url=url, + brief=f"invalid path {path!r}.", + details="Paths must be a valid string.", + resolution=( + "Verify repository configuration and ensure that 'path' " + "is correctly specified." + ), + ) + + if suites is not None and ( + not isinstance(suites, list) + or not all(isinstance(x, str) for x in suites) + or not suites + ): + raise errors.PackageRepositoryValidationError( + url=url, + brief=f"invalid suites {suites!r}.", + details="Suites must be a list of strings.", + resolution=( + "Verify repository configuration and ensure that 'suites' " + "is correctly specified." + ), + ) + + if not isinstance(url, str): + raise errors.PackageRepositoryValidationError( + url=url, + brief="invalid URL.", + details="URLs must be a valid string.", + resolution=( + "Verify repository configuration and ensure that 'url' " + "is correctly specified." + ), + ) + + if data_copy: + keys = ", ".join([repr(k) for k in data_copy.keys()]) + raise errors.PackageRepositoryValidationError( + url=url, + brief=f"unsupported properties {keys}.", + resolution="Verify repository configuration and ensure it is correct.", + ) + + return cls( + architectures=architectures, + components=components, + formats=formats, + key_id=key_id, + key_server=key_server, + name=name, + suites=suites, + url=url, + ) diff --git a/tests/unit/repo/__init__.py b/tests/unit/repo/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/unit/repo/test_apt_key_manager.py b/tests/unit/repo/test_apt_key_manager.py new file mode 100644 index 0000000000..4f3929b3e2 --- /dev/null +++ b/tests/unit/repo/test_apt_key_manager.py @@ -0,0 +1,318 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright 2020-2022 Canonical Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + + +import subprocess +from unittest import mock +from unittest.mock import call + +import gnupg +import pytest + +from snapcraft.repo import apt_ppa, errors +from snapcraft.repo.apt_key_manager import AptKeyManager +from snapcraft.repo.package_repository import ( + PackageRepositoryApt, + PackageRepositoryAptPPA, +) + + +@pytest.fixture(autouse=True) +def mock_environ_copy(mocker): + yield mocker.patch("os.environ.copy") + + +@pytest.fixture(autouse=True) +def mock_gnupg(tmp_path, mocker): + m = mocker.patch("gnupg.GPG", spec=gnupg.GPG) + m.return_value.import_keys.return_value.fingerprints = ["FAKE-KEY-ID-FROM-GNUPG"] + yield m + + +@pytest.fixture(autouse=True) +def mock_run(mocker): + yield mocker.patch("subprocess.run", spec=subprocess.run) + + +@pytest.fixture(autouse=True) +def mock_apt_ppa_get_signing_key(mocker): + yield mocker.patch( + "snapcraft.repo.apt_ppa.get_launchpad_ppa_key_id", + spec=apt_ppa.get_launchpad_ppa_key_id, + return_value="FAKE-PPA-SIGNING-KEY", + ) + + +@pytest.fixture +def key_assets(tmp_path): + assets = tmp_path / "key-assets" + assets.mkdir(parents=True) + yield assets + + +@pytest.fixture +def gpg_keyring(tmp_path): + yield tmp_path / "keyring.gpg" + + +@pytest.fixture +def apt_gpg(key_assets, gpg_keyring): + yield AptKeyManager( + gpg_keyring=gpg_keyring, + key_assets=key_assets, + ) + + +def test_find_asset( + apt_gpg, + key_assets, +): + key_id = "8" * 40 + expected_key_path = key_assets / ("8" * 8 + ".asc") + expected_key_path.write_text("key") + + key_path = apt_gpg.find_asset_with_key_id(key_id=key_id) + + assert key_path == expected_key_path + + +def test_find_asset_none( + apt_gpg, +): + key_path = apt_gpg.find_asset_with_key_id(key_id="foo") + + assert key_path is None + + +def test_get_key_fingerprints( + apt_gpg, + mock_gnupg, +): + with mock.patch("tempfile.NamedTemporaryFile") as m: + m.return_value.__enter__.return_value.name = "/tmp/foo" + ids = apt_gpg.get_key_fingerprints(key="8" * 40) + + assert ids == ["FAKE-KEY-ID-FROM-GNUPG"] + assert mock_gnupg.mock_calls == [ + call(keyring="/tmp/foo"), + call().import_keys(key_data="8888888888888888888888888888888888888888"), + ] + + +@pytest.mark.parametrize( + "stdout,expected", + [ + (b"nothing exported", False), + (b"BEGIN PGP PUBLIC KEY BLOCK", True), + (b"invalid", False), + ], +) +def test_is_key_installed( + stdout, + expected, + apt_gpg, + mock_run, +): + mock_run.return_value.stdout = stdout + + is_installed = apt_gpg.is_key_installed(key_id="foo") + + assert is_installed is expected + assert mock_run.mock_calls == [ + call( + ["apt-key", "export", "foo"], + check=True, + stderr=subprocess.STDOUT, + stdout=subprocess.PIPE, + ) + ] + + +def test_is_key_installed_with_apt_key_failure( + apt_gpg, + mock_run, +): + mock_run.side_effect = subprocess.CalledProcessError( + cmd=["apt-key"], returncode=1, output=b"some error" + ) + + is_installed = apt_gpg.is_key_installed(key_id="foo") + + assert is_installed is False + + +def test_install_key( + apt_gpg, + gpg_keyring, + mock_run, +): + key = "some-fake-key" + apt_gpg.install_key(key=key) + + assert mock_run.mock_calls == [ + call( + ["apt-key", "--keyring", str(gpg_keyring), "add", "-"], + check=True, + env={"LANG": "C.UTF-8"}, + input=b"some-fake-key", + stderr=subprocess.STDOUT, + stdout=subprocess.PIPE, + ) + ] + + +def test_install_key_with_apt_key_failure(apt_gpg, mock_run): + mock_run.side_effect = subprocess.CalledProcessError( + cmd=["foo"], returncode=1, output=b"some error" + ) + + with pytest.raises(errors.AptGPGKeyInstallError) as raised: + apt_gpg.install_key(key="FAKEKEY") + + assert str(raised.value) == "Failed to install GPG key: some error" + + +def test_install_key_from_keyserver(apt_gpg, gpg_keyring, mock_run): + apt_gpg.install_key_from_keyserver(key_id="FAKE_KEYID", key_server="key.server") + + assert mock_run.mock_calls == [ + call( + [ + "apt-key", + "--keyring", + str(gpg_keyring), + "adv", + "--keyserver", + "key.server", + "--recv-keys", + "FAKE_KEYID", + ], + check=True, + env={"LANG": "C.UTF-8"}, + stderr=subprocess.STDOUT, + stdout=subprocess.PIPE, + ) + ] + + +def test_install_key_from_keyserver_with_apt_key_failure( + apt_gpg, gpg_keyring, mock_run +): + mock_run.side_effect = subprocess.CalledProcessError( + cmd=["apt-key"], returncode=1, output=b"some error" + ) + + with pytest.raises(errors.AptGPGKeyInstallError) as raised: + apt_gpg.install_key_from_keyserver( + key_id="fake-key-id", key_server="fake-server" + ) + + assert str(raised.value) == "Failed to install GPG key: some error" + + +@pytest.mark.parametrize( + "is_installed", + [True, False], +) +def test_install_package_repository_key_already_installed( + is_installed, apt_gpg, mocker +): + mocker.patch( + "snapcraft.repo.apt_key_manager.AptKeyManager.is_key_installed", + return_value=is_installed, + ) + package_repo = PackageRepositoryApt( + components=["main", "multiverse"], + key_id="8" * 40, + key_server="xkeyserver.com", + suites=["xenial"], + url="http://archive.ubuntu.com/ubuntu", + ) + + updated = apt_gpg.install_package_repository_key(package_repo=package_repo) + + assert updated is not is_installed + + +def test_install_package_repository_key_from_asset(apt_gpg, key_assets, mocker): + mocker.patch( + "snapcraft.repo.apt_key_manager.AptKeyManager.is_key_installed", + return_value=False, + ) + mock_install_key = mocker.patch( + "snapcraft.repo.apt_key_manager.AptKeyManager.install_key" + ) + + key_id = "123456789012345678901234567890123456AABB" + expected_key_path = key_assets / "3456AABB.asc" + expected_key_path.write_text("key-data") + + package_repo = PackageRepositoryApt( + components=["main", "multiverse"], + key_id=key_id, + suites=["xenial"], + url="http://archive.ubuntu.com/ubuntu", + ) + + updated = apt_gpg.install_package_repository_key(package_repo=package_repo) + + assert updated is True + assert mock_install_key.mock_calls == [call(key="key-data")] + + +def test_install_package_repository_key_apt_from_keyserver(apt_gpg, mocker): + mock_install_key_from_keyserver = mocker.patch( + "snapcraft.repo.apt_key_manager.AptKeyManager.install_key_from_keyserver" + ) + mocker.patch( + "snapcraft.repo.apt_key_manager.AptKeyManager.is_key_installed", + return_value=False, + ) + + key_id = "8" * 40 + + package_repo = PackageRepositoryApt( + components=["main", "multiverse"], + key_id=key_id, + key_server="key.server", + suites=["xenial"], + url="http://archive.ubuntu.com/ubuntu", + ) + + updated = apt_gpg.install_package_repository_key(package_repo=package_repo) + + assert updated is True + assert mock_install_key_from_keyserver.mock_calls == [ + call(key_id=key_id, key_server="key.server") + ] + + +def test_install_package_repository_key_ppa_from_keyserver(apt_gpg, mocker): + mock_install_key_from_keyserver = mocker.patch( + "snapcraft.repo.apt_key_manager.AptKeyManager.install_key_from_keyserver" + ) + mocker.patch( + "snapcraft.repo.apt_key_manager.AptKeyManager.is_key_installed", + return_value=False, + ) + + package_repo = PackageRepositoryAptPPA(ppa="test/ppa") + updated = apt_gpg.install_package_repository_key(package_repo=package_repo) + + assert updated is True + assert mock_install_key_from_keyserver.mock_calls == [ + call(key_id="FAKE-PPA-SIGNING-KEY", key_server="keyserver.ubuntu.com") + ] diff --git a/tests/unit/repo/test_apt_ppa.py b/tests/unit/repo/test_apt_ppa.py new file mode 100644 index 0000000000..e65d23f147 --- /dev/null +++ b/tests/unit/repo/test_apt_ppa.py @@ -0,0 +1,62 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright 2020-2022 Canonical Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + + +from unittest.mock import call + +import launchpadlib +import pytest + +from snapcraft.repo import apt_ppa, errors + + +@pytest.fixture(autouse=True) +def mock_launchpad(mocker): + m = mocker.patch( + "snapcraft.repo.apt_ppa.Launchpad", spec=launchpadlib.launchpad.Launchpad + ) + m.login_anonymously.return_value.load.return_value.signing_key_fingerprint = ( + "FAKE-PPA-SIGNING-KEY" + ) + yield m + + +def test_split_ppa_parts(): + owner, name = apt_ppa.split_ppa_parts(ppa="test-owner/test-name") + + assert owner == "test-owner" + assert name == "test-name" + + +def test_split_ppa_parts_invalid(): + with pytest.raises(errors.AptPPAInstallError) as raised: + apt_ppa.split_ppa_parts(ppa="ppa-missing-slash") + + assert str(raised.value) == ( + "Failed to install PPA 'ppa-missing-slash': invalid PPA format" + ) + + +def test_get_launchpad_ppa_key_id( + mock_launchpad, +): + key_id = apt_ppa.get_launchpad_ppa_key_id(ppa="ppa-owner/ppa-name") + + assert key_id == "FAKE-PPA-SIGNING-KEY" + assert mock_launchpad.mock_calls == [ + call.login_anonymously("snapcraft", "production"), + call.login_anonymously().load("~ppa-owner/+archive/ppa-name"), + ] diff --git a/tests/unit/repo/test_apt_sources_manager.py b/tests/unit/repo/test_apt_sources_manager.py new file mode 100644 index 0000000000..4d0981cd2f --- /dev/null +++ b/tests/unit/repo/test_apt_sources_manager.py @@ -0,0 +1,192 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright 2021-2022 Canonical Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + + +from textwrap import dedent + +import pytest + +from snapcraft.repo import apt_ppa, apt_sources_manager, errors +from snapcraft.repo.package_repository import ( + PackageRepositoryApt, + PackageRepositoryAptPPA, +) + + +@pytest.fixture(autouse=True) +def mock_apt_ppa_get_signing_key(mocker): + yield mocker.patch( + "snapcraft.repo.apt_ppa.get_launchpad_ppa_key_id", + spec=apt_ppa.get_launchpad_ppa_key_id, + return_value="FAKE-PPA-SIGNING-KEY", + ) + + +@pytest.fixture(autouse=True) +def mock_environ_copy(mocker): + yield mocker.patch("os.environ.copy") + + +@pytest.fixture(autouse=True) +def mock_host_arch(mocker): + m = mocker.patch("snapcraft.repo.apt_sources_manager.ProjectOptions") + m.return_value.deb_arch = "FAKE-HOST-ARCH" + + yield m + + +@pytest.fixture(autouse=True) +def mock_run(mocker): + yield mocker.patch("subprocess.run") + + +@pytest.fixture(autouse=True) +def mock_version_codename(mocker): + yield mocker.patch( + "snapcraft.os_release.OsRelease.version_codename", + return_value="FAKE-CODENAME", + ) + + +@pytest.fixture +def apt_sources_mgr(tmp_path): + sources_list_d = tmp_path / "sources.list.d" + sources_list_d.mkdir(parents=True) + + yield apt_sources_manager.AptSourcesManager( + sources_list_d=sources_list_d, + ) + + +@pytest.mark.parametrize( + "package_repo,name,content", + [ + ( + PackageRepositoryApt( + architectures=["amd64", "arm64"], + components=["test-component"], + formats=["deb", "deb-src"], + key_id="A" * 40, + suites=["test-suite1", "test-suite2"], + url="http://test.url/ubuntu", + ), + "snapcraft-http_test_url_ubuntu.sources", + dedent( + """\ + Types: deb deb-src + URIs: http://test.url/ubuntu + Suites: test-suite1 test-suite2 + Components: test-component + Architectures: amd64 arm64 + """ + ).encode(), + ), + ( + PackageRepositoryApt( + architectures=["amd64", "arm64"], + components=["test-component"], + key_id="A" * 40, + name="NO-FORMAT", + suites=["test-suite1", "test-suite2"], + url="http://test.url/ubuntu", + ), + "snapcraft-NO-FORMAT.sources", + dedent( + """\ + Types: deb + URIs: http://test.url/ubuntu + Suites: test-suite1 test-suite2 + Components: test-component + Architectures: amd64 arm64 + """ + ).encode(), + ), + ( + PackageRepositoryApt( + key_id="A" * 40, + name="WITH-PATH", + path="some-path", + url="http://test.url/ubuntu", + ), + "snapcraft-WITH-PATH.sources", + dedent( + """\ + Types: deb + URIs: http://test.url/ubuntu + Suites: some-path/ + Architectures: FAKE-HOST-ARCH + """ + ).encode(), + ), + ( + PackageRepositoryApt( + key_id="A" * 40, + name="IMPLIED-PATH", + url="http://test.url/ubuntu", + ), + "snapcraft-IMPLIED-PATH.sources", + dedent( + """\ + Types: deb + URIs: http://test.url/ubuntu + Suites: / + Architectures: FAKE-HOST-ARCH + """ + ).encode(), + ), + ( + PackageRepositoryAptPPA(ppa="test/ppa"), + "snapcraft-ppa-test_ppa.sources", + dedent( + """\ + Types: deb + URIs: http://ppa.launchpad.net/test/ppa/ubuntu + Suites: FAKE-CODENAME + Components: main + Architectures: FAKE-HOST-ARCH + """ + ).encode(), + ), + ], +) +def test_install(package_repo, name, content, apt_sources_mgr): + sources_path = apt_sources_mgr._sources_list_d / name + + changed = apt_sources_mgr.install_package_repository_sources( + package_repo=package_repo + ) + + assert changed is True + assert sources_path.read_bytes() == content + + # Verify a second-run does not incur any changes. + changed = apt_sources_mgr.install_package_repository_sources( + package_repo=package_repo + ) + + assert changed is False + assert sources_path.read_bytes() == content + + +def test_install_ppa_invalid(apt_sources_mgr): + repo = PackageRepositoryAptPPA(ppa="ppa-missing-slash") + + with pytest.raises(errors.AptPPAInstallError) as raised: + apt_sources_mgr.install_package_repository_sources(package_repo=repo) + + assert str(raised.value) == ( + "Failed to install PPA 'ppa-missing-slash': invalid PPA format" + ) diff --git a/tests/unit/repo/test_package_repository.py b/tests/unit/repo/test_package_repository.py new file mode 100644 index 0000000000..45337bc232 --- /dev/null +++ b/tests/unit/repo/test_package_repository.py @@ -0,0 +1,426 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright 2019-2022 Canonical Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + + +import pytest + +from snapcraft.repo import errors +from snapcraft.repo.package_repository import ( + PackageRepository, + PackageRepositoryApt, + PackageRepositoryAptPPA, +) + + +def test_apt_name(): + repo = PackageRepositoryApt( + architectures=["amd64", "i386"], + components=["main", "multiverse"], + formats=["deb", "deb-src"], + key_id="A" * 40, + key_server="keyserver.ubuntu.com", + suites=["xenial", "xenial-updates"], + url="http://archive.ubuntu.com/ubuntu", + ) + + assert repo.name == "http_archive_ubuntu_com_ubuntu" + + +@pytest.mark.parametrize( + "arch", ["amd64", "armhf", "arm64", "i386", "ppc64el", "riscv", "s390x"] +) +def test_apt_valid_architectures(arch): + package_repo = PackageRepositoryApt( + key_id="A" * 40, url="http://test", architectures=[arch] + ) + + assert package_repo.architectures == [arch] + + +def test_apt_invalid_url(): + with pytest.raises(errors.PackageRepositoryValidationError) as raised: + PackageRepositoryApt( + key_id="A" * 40, + url="", + ) + + err = raised.value + assert str(err) == "Invalid package repository for '': invalid URL." + assert err.details == "URLs must be non-empty strings." + assert err.resolution == ( + "Verify the repository configuration and ensure that 'url' " + "is correctly specified." + ) + + +def test_apt_invalid_path(): + with pytest.raises(errors.PackageRepositoryValidationError) as raised: + PackageRepositoryApt( + key_id="A" * 40, + path="", + url="http://archive.ubuntu.com/ubuntu", + ) + + err = raised.value + assert str(err) == ( + "Invalid package repository for 'http://archive.ubuntu.com/ubuntu': " + "invalid path ''." + ) + assert err.details == "Paths must be non-empty strings." + assert err.resolution == ( + "Verify the repository configuration and ensure that 'path' " + "is a non-empty string such as '/'." + ) + + +def test_apt_invalid_path_with_suites(): + with pytest.raises(errors.PackageRepositoryValidationError) as raised: + PackageRepositoryApt( + key_id="A" * 40, + path="/", + suites=["xenial", "xenial-updates"], + url="http://archive.ubuntu.com/ubuntu", + ) + + err = raised.value + assert str(err) == ( + "Invalid package repository for 'http://archive.ubuntu.com/ubuntu': " + "suites ['xenial', 'xenial-updates'] cannot be combined with path '/'." + ) + assert err.details == "Path and suites are incomptiable options." + assert err.resolution == ( + "Verify the repository configuration and remove 'path' or 'suites'." + ) + + +def test_apt_invalid_path_with_components(): + with pytest.raises(errors.PackageRepositoryValidationError) as raised: + PackageRepositoryApt( + key_id="A" * 40, + path="/", + components=["main"], + url="http://archive.ubuntu.com/ubuntu", + ) + + err = raised.value + assert str(err) == ( + "Invalid package repository for 'http://archive.ubuntu.com/ubuntu': " + "components ['main'] cannot be combined with path '/'." + ) + assert err.details == "Path and components are incomptiable options." + assert err.resolution == ( + "Verify the repository configuration and remove 'path' or 'components'." + ) + + +def test_apt_invalid_missing_components(): + with pytest.raises(errors.PackageRepositoryValidationError) as raised: + PackageRepositoryApt( + key_id="A" * 40, + suites=["xenial", "xenial-updates"], + url="http://archive.ubuntu.com/ubuntu", + ) + + err = raised.value + assert str(err) == ( + "Invalid package repository for 'http://archive.ubuntu.com/ubuntu': " + "no components specified." + ) + assert err.details == "Components are required when using suites." + assert err.resolution == ( + "Verify the repository configuration and ensure that 'components' " + "is correctly specified." + ) + + +def test_apt_invalid_missing_suites(): + with pytest.raises(errors.PackageRepositoryValidationError) as raised: + PackageRepositoryApt( + key_id="A" * 40, + components=["main"], + url="http://archive.ubuntu.com/ubuntu", + ) + + err = raised.value + assert str(err) == ( + "Invalid package repository for 'http://archive.ubuntu.com/ubuntu': " + "no suites specified." + ) + assert err.details == "Suites are required when using components." + assert err.resolution == ( + "Verify the repository configuration and ensure that 'suites' " + "is correctly specified." + ) + + +def test_apt_invalid_suites_as_path(): + with pytest.raises(errors.PackageRepositoryValidationError) as raised: + PackageRepositoryApt( + key_id="A" * 40, + suites=["my-suite/"], + url="http://archive.ubuntu.com/ubuntu", + ) + + err = raised.value + assert str(err) == ( + "Invalid package repository for 'http://archive.ubuntu.com/ubuntu': " + "invalid suite 'my-suite/'." + ) + assert err.details == "Suites must not end with a '/'." + assert err.resolution == ( + "Verify the repository configuration and remove the trailing '/' " + "from suites or use the 'path' property to define a path." + ) + + +def test_apt_marshal(): + repo = PackageRepositoryApt( + architectures=["amd64", "i386"], + components=["main", "multiverse"], + formats=["deb", "deb-src"], + key_id="A" * 40, + key_server="xkeyserver.ubuntu.com", + name="test-name", + suites=["xenial", "xenial-updates"], + url="http://archive.ubuntu.com/ubuntu", + ) + + assert repo.marshal() == { + "architectures": ["amd64", "i386"], + "components": ["main", "multiverse"], + "formats": ["deb", "deb-src"], + "key-id": "A" * 40, + "key-server": "xkeyserver.ubuntu.com", + "name": "test-name", + "suites": ["xenial", "xenial-updates"], + "type": "apt", + "url": "http://archive.ubuntu.com/ubuntu", + } + + +def test_apt_unmarshal_invalid_extra_keys(): + test_dict = { + "architectures": ["amd64", "i386"], + "components": ["main", "multiverse"], + "formats": ["deb", "deb-src"], + "key-id": "A" * 40, + "key-server": "keyserver.ubuntu.com", + "name": "test-name", + "suites": ["xenial", "xenial-updates"], + "type": "apt", + "url": "http://archive.ubuntu.com/ubuntu", + "foo": "bar", + "foo2": "bar", + } + + with pytest.raises(errors.PackageRepositoryValidationError) as raised: + PackageRepositoryApt.unmarshal(test_dict) + + err = raised.value + assert str(err) == ( + "Invalid package repository for 'http://archive.ubuntu.com/ubuntu': " + "unsupported properties 'foo', 'foo2'." + ) + assert err.details is None + assert err.resolution == "Verify repository configuration and ensure it is correct." + + +def test_apt_unmarshal_invalid_data(): + test_dict = "not-a-dict" + + with pytest.raises(errors.PackageRepositoryValidationError) as raised: + PackageRepositoryApt.unmarshal(test_dict) # type: ignore + + err = raised.value + assert str(err) == "Invalid package repository for 'not-a-dict': invalid object." + assert err.details == "Package repository must be a valid dictionary object." + assert err.resolution == ( + "Verify repository configuration and ensure that the correct syntax is used." + ) + + +def test_apt_unmarshal_invalid_type(): + test_dict = { + "architectures": ["amd64", "i386"], + "components": ["main", "multiverse"], + "formats": ["deb", "deb-src"], + "key-id": "A" * 40, + "key-server": "keyserver.ubuntu.com", + "name": "test-name", + "suites": ["xenial", "xenial-updates"], + "type": "aptx", + "url": "http://archive.ubuntu.com/ubuntu", + } + + with pytest.raises(errors.PackageRepositoryValidationError) as raised: + PackageRepositoryApt.unmarshal(test_dict) + + err = raised.value + assert str(err) == ( + "Invalid package repository for 'http://archive.ubuntu.com/ubuntu': " + "unsupported type 'aptx'." + ) + assert err.details == "The only currently supported type is 'apt'." + assert err.resolution == ( + "Verify repository configuration and ensure that 'type' is correctly specified." + ) + + +def test_ppa_marshal(): + repo = PackageRepositoryAptPPA(ppa="test/ppa") + + assert repo.marshal() == {"type": "apt", "ppa": "test/ppa"} + + +def test_ppa_invalid_ppa(): + with pytest.raises(errors.PackageRepositoryValidationError) as raised: + PackageRepositoryAptPPA(ppa="") + + err = raised.value + assert str(err) == "Invalid package repository for '': invalid PPA." + assert err.details == "PPAs must be non-empty strings." + assert err.resolution == ( + "Verify repository configuration and ensure that 'ppa' is correctly specified." + ) + + +def test_ppa_unmarshal_invalid_data(): + test_dict = "not-a-dict" + + with pytest.raises(errors.PackageRepositoryValidationError) as raised: + PackageRepositoryAptPPA.unmarshal(test_dict) # type: ignore + + err = raised.value + assert str(err) == "Invalid package repository for 'not-a-dict': invalid object." + assert err.details == "Package repository must be a valid dictionary object." + assert err.resolution == ( + "Verify repository configuration and ensure that the correct syntax is used." + ) + + +def test_ppa_unmarshal_invalid_apt_ppa_type(): + test_dict = {"type": "aptx", "ppa": "test/ppa"} + + with pytest.raises(errors.PackageRepositoryValidationError) as raised: + PackageRepositoryAptPPA.unmarshal(test_dict) + + err = raised.value + assert str(err) == ( + "Invalid package repository for 'test/ppa': unsupported type 'aptx'." + ) + assert err.details == "The only currently supported type is 'apt'." + assert err.resolution == ( + "Verify repository configuration and ensure that 'type' is correctly specified." + ) + + +def test_ppa_unmarshal_invalid_apt_ppa_extra_keys(): + test_dict = {"type": "apt", "ppa": "test/ppa", "test": "foo"} + + with pytest.raises(errors.PackageRepositoryValidationError) as raised: + PackageRepositoryAptPPA.unmarshal(test_dict) + + err = raised.value + assert str(err) == ( + "Invalid package repository for 'test/ppa': unsupported properties 'test'." + ) + assert err.details is None + assert err.resolution == ( + "Verify repository configuration and ensure that it is correct." + ) + + +def test_unmarshal_package_repositories_list_none(): + assert PackageRepository.unmarshal_package_repositories(None) == [] + + +def test_unmarshal_package_repositories_list_empty(): + assert PackageRepository.unmarshal_package_repositories([]) == [] + + +def test_unmarshal_package_repositories_list_ppa(): + test_dict = {"type": "apt", "ppa": "test/foo"} + test_list = [test_dict] + + unmarshalled_list = [ + repo.marshal() + for repo in PackageRepository.unmarshal_package_repositories(test_list) + ] + + assert unmarshalled_list == test_list + + +def test_unmarshal_package_repositories_list_apt(): + test_dict = { + "architectures": ["amd64", "i386"], + "components": ["main", "multiverse"], + "formats": ["deb", "deb-src"], + "key-id": "A" * 40, + "key-server": "keyserver.ubuntu.com", + "name": "test-name", + "suites": ["xenial", "xenial-updates"], + "type": "apt", + "url": "http://archive.ubuntu.com/ubuntu", + } + + test_list = [test_dict] + + unmarshalled_list = [ + repo.marshal() + for repo in PackageRepository.unmarshal_package_repositories(test_list) + ] + + assert unmarshalled_list == test_list + + +def test_unmarshal_package_repositories_list_all(): + test_ppa = {"type": "apt", "ppa": "test/foo"} + + test_deb = { + "architectures": ["amd64", "i386"], + "components": ["main", "multiverse"], + "formats": ["deb", "deb-src"], + "key-id": "A" * 40, + "key-server": "keyserver.ubuntu.com", + "name": "test-name", + "suites": ["xenial", "xenial-updates"], + "type": "apt", + "url": "http://archive.ubuntu.com/ubuntu", + } + + test_list = [test_ppa, test_deb] + + unmarshalled_list = [ + repo.marshal() + for repo in PackageRepository.unmarshal_package_repositories(test_list) + ] + + assert unmarshalled_list == test_list + + +def test_unmarshal_package_repositories_invalid_data(): + with pytest.raises(errors.PackageRepositoryValidationError) as raised: + PackageRepository.unmarshal_package_repositories("not-a-list") + + err = raised.value + assert str(err) == ( + "Invalid package repository for 'not-a-list': invalid list object." + ) + assert err.details == "Package repositories must be a list of objects." + assert err.resolution == ( + "Verify 'package-repositories' configuration and ensure that " + "the correct syntax is used." + ) diff --git a/tests/unit/test_projects.py b/tests/unit/test_projects.py index 3b0bcef3df..0432715f29 100644 --- a/tests/unit/test_projects.py +++ b/tests/unit/test_projects.py @@ -18,7 +18,7 @@ import pytest -from snapcraft import errors +from snapcraft import errors, projects from snapcraft.projects import Project @@ -60,6 +60,7 @@ def test_project_defaults(self, yaml_data): assert project.layout is None assert project.license is None assert project.architectures == [] + assert project.package_repositories == [] assert project.assumes == [] assert project.hooks is None assert project.passthrough is None @@ -322,6 +323,24 @@ def test_project_epoch_invalid(self, epoch, yaml_data): with pytest.raises(errors.ProjectValidationError, match=error): Project.unmarshal(yaml_data(epoch=epoch)) + def test_project_package_repository(self, yaml_data): + repos = [ + { + "type": "apt", + "ppa": "test/somerepo", + }, + { + "type": "apt", + "url": "https://some/url", + "key_id": "KEYID12345" * 4, + }, + ] + project = Project.unmarshal(yaml_data(package_repositories=repos)) + assert project.package_repositories == [ + projects.AptPPA(**repos[0]), # type: ignore + projects.AptDeb(**repos[1]), # type: ignore + ] + class TestAppValidation: """Validate apps.""" @@ -428,3 +447,118 @@ def test_app_install_mode(self, install_mode, yaml_data): error = ".*unexpected value; permitted: 'enable', 'disable'" with pytest.raises(errors.ProjectValidationError, match=error): Project.unmarshal(data) + + +class TestAptPPAValidation: + """AptPPA field validation.""" + + def test_apt_ppa_valid(self, yaml_data): + repos = [ + { + "type": "apt", + "ppa": "test/somerepo", + }, + ] + project = Project.unmarshal(yaml_data(package_repositories=repos)) + assert project.package_repositories == [ + projects.AptPPA(**x) for x in repos # type: ignore + ] + + def test_apt_ppa_repository_invalid(self, yaml_data): + repos = [ + { + "ppa": "test/somerepo", + }, + ] + error = "field 'type' required" + with pytest.raises(errors.ProjectValidationError, match=error): + Project.unmarshal(yaml_data(package_repositories=repos)) + + def test_project_package_ppa_repository_bad_type(self, yaml_data): + repos = [ + { + "type": "invalid", + "ppa": "test/somerepo", + }, + ] + error = "unexpected value; permitted: 'apt'" + with pytest.raises(errors.ProjectValidationError, match=error): + Project.unmarshal(yaml_data(package_repositories=repos)) + + +class TestAptDebValidation: + """AptDeb field validation.""" + + @pytest.mark.parametrize( + "repo", + [ + { + "type": "apt", + "url": "https://some/url", + "key_id": "KEYID12345" * 4, + }, + { + "type": "apt", + "url": "https://some/url", + "key_id": "KEYID12345" * 4, + "formats": ["deb"], + "components": ["some", "components"], + "keyserver": "my-key-server", + "path": "my/path", + "suites": ["some", "suites"], + }, + ], + ) + def test_apt_deb_valid(self, yaml_data, repo): + project = Project.unmarshal(yaml_data(package_repositories=[repo])) + assert project.package_repositories == [projects.AptDeb(**repo)] + + @pytest.mark.parametrize( + "key_id,error", + [ + ("KEYID12345" * 4, None), + ("KEYID12345", "string does not match regex"), + ("keyid12345" * 4, "string does not match regex"), + ], + ) + def test_apt_deb_key_id(self, yaml_data, key_id, error): + repo = { + "type": "apt", + "url": "https://some/url", + "key_id": key_id, + } + + data = yaml_data(package_repositories=[repo]) + + if not error: + project = Project.unmarshal(data) + assert project.package_repositories == [projects.AptDeb(**repo)] + else: + with pytest.raises(errors.ProjectValidationError, match=error): + Project.unmarshal(data) + + @pytest.mark.parametrize( + "formats", + [ + ["deb"], + ["deb-src"], + ["deb", "deb-src"], + ["_invalid"], + ], + ) + def test_apt_deb_formats(self, formats, yaml_data): + repo = { + "type": "apt", + "url": "https://some/url", + "key_id": "KEYID12345" * 4, + "formats": formats, + } + data = yaml_data(package_repositories=[repo]) + + if formats != ["_invalid"]: + project = Project.unmarshal(data) + assert project.package_repositories == [projects.AptDeb(**repo)] + else: + error = ".*unexpected value; permitted: 'deb', 'deb-src'" + with pytest.raises(errors.ProjectValidationError, match=error): + Project.unmarshal(data) From e9a0706d87560178d89338dcde51cb18950aa4f6 Mon Sep 17 00:00:00 2001 From: Claudio Matsuoka Date: Mon, 21 Feb 2022 18:58:58 -0300 Subject: [PATCH 035/167] repo: refactor to reduce repo package coupling Move AptDeb and AptPPA definitions to the repo package, to reduce coupling and make it easier to create a stand-alone craft-repositories package if needed. The repo API is now similar to the craft-parts API, where data is handled as a raw dictionary and validated externally. Signed-off-by: Claudio Matsuoka --- pyproject.toml | 5 +- snapcraft/projects.py | 38 ++------ snapcraft/repo/__init__.py | 10 +- snapcraft/repo/errors.py | 10 +- snapcraft/repo/projects.py | 91 +++++++++++++++++++ tests/unit/parts/test_lifecycle.py | 1 - tests/unit/repo/test_projects.py | 134 +++++++++++++++++++++++++++ tests/unit/test_projects.py | 141 +++++------------------------ 8 files changed, 272 insertions(+), 158 deletions(-) create mode 100644 snapcraft/repo/projects.py create mode 100644 tests/unit/repo/test_projects.py diff --git a/pyproject.toml b/pyproject.toml index f75f0cd37b..2d4bc330c8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,7 +31,7 @@ ensure_newline_before_comments = true line_length = 88 [tool.pylint.messages_control] -disable = "too-few-public-methods,fixme" +disable = "too-few-public-methods,fixme,use-implicit-booleaness-not-comparison" [tool.pylint.format] max-attributes = 15 @@ -39,6 +39,7 @@ good-names = "id" [tool.pylint.MASTER] extension-pkg-allow-list = [ - "pydantic" + "pydantic", + "pytest", ] load-plugins = "pylint_fixme_info,pylint_pytest" diff --git a/snapcraft/projects.py b/snapcraft/projects.py index 4fa438cd18..58b7d8e8e5 100644 --- a/snapcraft/projects.py +++ b/snapcraft/projects.py @@ -25,6 +25,7 @@ import pydantic from pydantic import conlist, constr +from snapcraft import repo from snapcraft.errors import ProjectValidationError from snapcraft.parts import validation as parts_validation @@ -36,7 +37,7 @@ class Config: # pylint: disable=too-few-public-methods """Pydantic model configuration.""" validate_assignment = True - # extra = "forbid" + extra = "allow" # FIXME: change to 'forbid' after model complete allow_mutation = False allow_population_by_field_name = True alias_generator = lambda s: s.replace("_", "-") # noqa: E731 @@ -47,12 +48,10 @@ class Config: # pylint: disable=too-few-public-methods # fmt: off if TYPE_CHECKING: CommandChainStr = str - KeyIdStr = str UniqueStrList = List[str] UniqueAliasList = List[str] else: CommandChainStr = constr(regex=r"^[A-Za-z0-9/._#:$-]*$") - KeyIdStr = constr(regex=r"^[A-Z0-9]{40}$") UniqueStrList = conlist(str, unique_items=True) UniqueAliasList = conlist(constr(regex=r"^[a-zA-Z0-9][-_.a-zA-Z0-9]*$"), unique_items=True) # fmt: on @@ -154,27 +153,6 @@ class Architecture(ProjectModel): build_to: Optional[Union[str, UniqueStrList]] -class AptDeb(ProjectModel): - """Apt package repository definition.""" - - type: Literal["apt"] - url: str - key_id: KeyIdStr - architectures: Optional[List[str]] - formats: Optional[List[Literal["deb", "deb-src"]]] - components: Optional[List[str]] - key_server: Optional[str] - path: Optional[str] - suites: Optional[List[str]] - - -class AptPPA(ProjectModel): - """PPA package repository definition.""" - - type: Literal["apt"] - ppa: str - - class Project(ProjectModel): """Snapcraft project definition. @@ -184,9 +162,6 @@ class Project(ProjectModel): - environment (top-level) - system-usernames - adopt-info (after adding craftctl support to craft-parts) - - FIXME: package-repositories needs better validation and less - confusing error messages """ name: constr(max_length=40) # type: ignore @@ -210,7 +185,7 @@ class Project(ProjectModel): grade: Literal["stable", "devel"] architectures: List[Architecture] = [] assumes: UniqueStrList = [] - package_repositories: List[Union[AptDeb, AptPPA]] = [] + package_repositories: Optional[List[Any]] = [] # handled by repo hooks: Optional[Dict[str, Hook]] passthrough: Optional[Dict[str, Any]] apps: Optional[Dict[str, App]] @@ -277,6 +252,13 @@ def _validate_build_base(cls, build_base, values): build_base = values.get("base") return build_base + @pydantic.validator("package_repositories", each_item=True) + @classmethod + def _validate_package_repositories(cls, item): + """Ensure package-repositories format is correct.""" + repo.validate_repository(item) + return item + @pydantic.validator("parts", each_item=True) @classmethod def _validate_parts(cls, item): diff --git a/snapcraft/repo/__init__.py b/snapcraft/repo/__init__.py index 1f69f90a86..cbd8b5fd81 100644 --- a/snapcraft/repo/__init__.py +++ b/snapcraft/repo/__init__.py @@ -16,8 +16,8 @@ """Package repository helpers.""" -from .apt_key_manager import AptKeyManager # noqa: F401 -from .apt_sources_manager import AptSourcesManager # noqa: F401 -from .package_repository import PackageRepository # noqa: F401 -from .package_repository import PackageRepositoryApt # noqa: F401 -from .package_repository import PackageRepositoryAptPPA # noqa: F401 +from .projects import validate_repository + +__all__ = [ + "validate_repository", +] diff --git a/snapcraft/repo/errors.py b/snapcraft/repo/errors.py index 06a0a49b10..d452b5c971 100644 --- a/snapcraft/repo/errors.py +++ b/snapcraft/repo/errors.py @@ -21,7 +21,11 @@ from snapcraft.errors import SnapcraftError -class PackageRepositoryValidationError(SnapcraftError): +class PackageRepositoryError(SnapcraftError): + """Package repository error base.""" + + +class PackageRepositoryValidationError(PackageRepositoryError): """Package repository is invalid.""" def __init__( @@ -38,7 +42,7 @@ def __init__( ) -class AptPPAInstallError(SnapcraftError): +class AptPPAInstallError(PackageRepositoryError): """Installation of a PPA repository failed.""" def __init__(self, ppa: str, reason: str): @@ -48,7 +52,7 @@ def __init__(self, ppa: str, reason: str): ) -class AptGPGKeyInstallError(SnapcraftError): +class AptGPGKeyInstallError(PackageRepositoryError): """Installation of GPG key failed.""" def __init__( diff --git a/snapcraft/repo/projects.py b/snapcraft/repo/projects.py new file mode 100644 index 0000000000..c839664e15 --- /dev/null +++ b/snapcraft/repo/projects.py @@ -0,0 +1,91 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright 2019-2022 Canonical Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +"""Project model definitions and helpers.""" + +from typing import Literal # type: ignore +from typing import TYPE_CHECKING, Any, Dict, List, Optional + +import pydantic +from pydantic import constr + +# Workaround for mypy +# see https://github.com/samuelcolvin/pydantic/issues/975#issuecomment-551147305 +if TYPE_CHECKING: + KeyIdStr = str +else: + KeyIdStr = constr(regex=r"^[A-Z0-9]{40}$") + + +class ProjectModel(pydantic.BaseModel): + """Base model for project repository classes.""" + + class Config: # pylint: disable=too-few-public-methods + """Pydantic model configuration.""" + + validate_assignment = True + allow_mutation = False + allow_population_by_field_name = True + alias_generator = lambda s: s.replace("_", "-") # noqa: E731 + extra = "forbid" + + +class AptDeb(ProjectModel): + """Apt package repository definition.""" + + type: Literal["apt"] + url: str + key_id: KeyIdStr + architectures: Optional[List[str]] + formats: Optional[List[Literal["deb", "deb-src"]]] + components: Optional[List[str]] + key_server: Optional[str] + path: Optional[str] + suites: Optional[List[str]] + + @classmethod + def unmarshal(cls, data: Dict[str, Any]) -> "AptDeb": + """Create an AptDeb object from dictionary data.""" + return cls(**data) + + +class AptPPA(ProjectModel): + """PPA package repository definition.""" + + type: Literal["apt"] + ppa: str + + @classmethod + def unmarshal(cls, data: Dict[str, Any]) -> "AptPPA": + """Create an AptPPA object from dictionary data.""" + return cls(**data) + + +def validate_repository(data: Dict[str, Any]): + """Validate a package repository. + + :param data: The repository data to validate. + """ + if not isinstance(data, dict): + raise TypeError("value must be a dictionary") + + try: + AptPPA(**data) + return + except pydantic.ValidationError: + pass + + AptDeb(**data) diff --git a/tests/unit/parts/test_lifecycle.py b/tests/unit/parts/test_lifecycle.py index 23885f9977..79d45f7199 100644 --- a/tests/unit/parts/test_lifecycle.py +++ b/tests/unit/parts/test_lifecycle.py @@ -77,7 +77,6 @@ def write_file( "layout": None, "license": None, "grade": "stable", - "adopt-info": None, "architectures": [], "assumes": [], "hooks": None, diff --git a/tests/unit/repo/test_projects.py b/tests/unit/repo/test_projects.py new file mode 100644 index 0000000000..3d91e02686 --- /dev/null +++ b/tests/unit/repo/test_projects.py @@ -0,0 +1,134 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright 2022 Canonical Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import pydantic +import pytest + +from snapcraft.repo.projects import AptDeb, AptPPA + + +class TestAptPPAValidation: + """AptPPA field validation.""" + + def test_apt_ppa_valid(self): + repo = { + "type": "apt", + "ppa": "test/somerepo", + } + apt_ppa = AptPPA.unmarshal(repo) + assert apt_ppa.type == "apt" + assert apt_ppa.ppa == "test/somerepo" + + def test_apt_ppa_repository_invalid(self): + repo = { + "ppa": "test/somerepo", + } + error = r"type\s+field required" + with pytest.raises(pydantic.ValidationError, match=error): + AptPPA.unmarshal(repo) + + def test_project_package_ppa_repository_bad_type(self): + repo = { + "type": "invalid", + "ppa": "test/somerepo", + } + error = "unexpected value; permitted: 'apt'" + with pytest.raises(pydantic.ValidationError, match=error): + AptPPA.unmarshal(repo) + + +class TestAptDebValidation: + """AptDeb field validation.""" + + @pytest.mark.parametrize( + "repo", + [ + { + "type": "apt", + "url": "https://some/url", + "key_id": "KEYID12345" * 4, + }, + { + "type": "apt", + "url": "https://some/url", + "key_id": "KEYID12345" * 4, + "formats": ["deb"], + "components": ["some", "components"], + "key-server": "my-key-server", + "path": "my/path", + "suites": ["some", "suites"], + }, + ], + ) + def test_apt_deb_valid(self, repo): + apt_deb = AptDeb.unmarshal(repo) + assert apt_deb.type == "apt" + assert apt_deb.url == "https://some/url" + assert apt_deb.key_id == "KEYID12345" * 4 + assert apt_deb.formats == (["deb"] if "formats" in repo else None) + assert apt_deb.components == ( + ["some", "components"] if "components" in repo else None + ) + assert apt_deb.key_server == ("my-key-server" if "key-server" in repo else None) + assert apt_deb.path == ("my/path" if "path" in repo else None) + assert apt_deb.suites == (["some", "suites"] if "suites" in repo else None) + + @pytest.mark.parametrize( + "key_id,error", + [ + ("KEYID12345" * 4, None), + ("KEYID12345", "string does not match regex"), + ("keyid12345" * 4, "string does not match regex"), + ], + ) + def test_apt_deb_key_id(self, key_id, error): + repo = { + "type": "apt", + "url": "https://some/url", + "key-id": key_id, + } + + if not error: + apt_deb = AptDeb.unmarshal(repo) + assert apt_deb.key_id == key_id + else: + with pytest.raises(pydantic.ValidationError, match=error): + AptDeb.unmarshal(repo) + + @pytest.mark.parametrize( + "formats", + [ + ["deb"], + ["deb-src"], + ["deb", "deb-src"], + ["_invalid"], + ], + ) + def test_apt_deb_formats(self, formats): + repo = { + "type": "apt", + "url": "https://some/url", + "key-id": "KEYID12345" * 4, + "formats": formats, + } + + if formats != ["_invalid"]: + apt_deb = AptDeb.unmarshal(repo) + assert apt_deb.formats == formats + else: + error = ".*unexpected value; permitted: 'deb', 'deb-src'" + with pytest.raises(pydantic.ValidationError, match=error): + AptDeb.unmarshal(repo) diff --git a/tests/unit/test_projects.py b/tests/unit/test_projects.py index 0432715f29..ffe22776c4 100644 --- a/tests/unit/test_projects.py +++ b/tests/unit/test_projects.py @@ -18,7 +18,7 @@ import pytest -from snapcraft import errors, projects +from snapcraft import errors from snapcraft.projects import Project @@ -336,10 +336,28 @@ def test_project_package_repository(self, yaml_data): }, ] project = Project.unmarshal(yaml_data(package_repositories=repos)) - assert project.package_repositories == [ - projects.AptPPA(**repos[0]), # type: ignore - projects.AptDeb(**repos[1]), # type: ignore + assert project.package_repositories == repos + + def test_project_package_repository_missing_fields(self, yaml_data): + repos = [ + { + "type": "apt", + }, ] + error = r".*\n- field 'url' required .*\n- field 'key-id' required" + with pytest.raises(errors.ProjectValidationError, match=error): + Project.unmarshal(yaml_data(package_repositories=repos)) + + def test_project_package_repository_extra_fields(self, yaml_data): + repos = [ + { + "type": "apt", + "extra": "something", + }, + ] + error = r".*\n- extra field 'extra' not permitted" + with pytest.raises(errors.ProjectValidationError, match=error): + Project.unmarshal(yaml_data(package_repositories=repos)) class TestAppValidation: @@ -447,118 +465,3 @@ def test_app_install_mode(self, install_mode, yaml_data): error = ".*unexpected value; permitted: 'enable', 'disable'" with pytest.raises(errors.ProjectValidationError, match=error): Project.unmarshal(data) - - -class TestAptPPAValidation: - """AptPPA field validation.""" - - def test_apt_ppa_valid(self, yaml_data): - repos = [ - { - "type": "apt", - "ppa": "test/somerepo", - }, - ] - project = Project.unmarshal(yaml_data(package_repositories=repos)) - assert project.package_repositories == [ - projects.AptPPA(**x) for x in repos # type: ignore - ] - - def test_apt_ppa_repository_invalid(self, yaml_data): - repos = [ - { - "ppa": "test/somerepo", - }, - ] - error = "field 'type' required" - with pytest.raises(errors.ProjectValidationError, match=error): - Project.unmarshal(yaml_data(package_repositories=repos)) - - def test_project_package_ppa_repository_bad_type(self, yaml_data): - repos = [ - { - "type": "invalid", - "ppa": "test/somerepo", - }, - ] - error = "unexpected value; permitted: 'apt'" - with pytest.raises(errors.ProjectValidationError, match=error): - Project.unmarshal(yaml_data(package_repositories=repos)) - - -class TestAptDebValidation: - """AptDeb field validation.""" - - @pytest.mark.parametrize( - "repo", - [ - { - "type": "apt", - "url": "https://some/url", - "key_id": "KEYID12345" * 4, - }, - { - "type": "apt", - "url": "https://some/url", - "key_id": "KEYID12345" * 4, - "formats": ["deb"], - "components": ["some", "components"], - "keyserver": "my-key-server", - "path": "my/path", - "suites": ["some", "suites"], - }, - ], - ) - def test_apt_deb_valid(self, yaml_data, repo): - project = Project.unmarshal(yaml_data(package_repositories=[repo])) - assert project.package_repositories == [projects.AptDeb(**repo)] - - @pytest.mark.parametrize( - "key_id,error", - [ - ("KEYID12345" * 4, None), - ("KEYID12345", "string does not match regex"), - ("keyid12345" * 4, "string does not match regex"), - ], - ) - def test_apt_deb_key_id(self, yaml_data, key_id, error): - repo = { - "type": "apt", - "url": "https://some/url", - "key_id": key_id, - } - - data = yaml_data(package_repositories=[repo]) - - if not error: - project = Project.unmarshal(data) - assert project.package_repositories == [projects.AptDeb(**repo)] - else: - with pytest.raises(errors.ProjectValidationError, match=error): - Project.unmarshal(data) - - @pytest.mark.parametrize( - "formats", - [ - ["deb"], - ["deb-src"], - ["deb", "deb-src"], - ["_invalid"], - ], - ) - def test_apt_deb_formats(self, formats, yaml_data): - repo = { - "type": "apt", - "url": "https://some/url", - "key_id": "KEYID12345" * 4, - "formats": formats, - } - data = yaml_data(package_repositories=[repo]) - - if formats != ["_invalid"]: - project = Project.unmarshal(data) - assert project.package_repositories == [projects.AptDeb(**repo)] - else: - error = ".*unexpected value; permitted: 'deb', 'deb-src'" - with pytest.raises(errors.ProjectValidationError, match=error): - Project.unmarshal(data) From bd78df613e0ffedbc846221df21914bf6ffb63f4 Mon Sep 17 00:00:00 2001 From: Callahan Kovacs Date: Fri, 18 Feb 2022 14:56:52 -0600 Subject: [PATCH 036/167] meta: parse environment variables Parse `environment` dictionary in snapcraft.yaml, both for apps and at the root level. Signed-off-by: Callahan Kovacs --- snapcraft/meta/snap_yaml.py | 6 ++- snapcraft/projects.py | 4 +- tests/unit/meta/test_snap_yaml.py | 9 +++++ tests/unit/test_projects.py | 63 +++++++++++++++++++++++++++++++ 4 files changed, 78 insertions(+), 4 deletions(-) diff --git a/snapcraft/meta/snap_yaml.py b/snapcraft/meta/snap_yaml.py index 22b628ca4d..5d8c2f3283 100644 --- a/snapcraft/meta/snap_yaml.py +++ b/snapcraft/meta/snap_yaml.py @@ -18,7 +18,7 @@ import textwrap from pathlib import Path -from typing import Dict, List, Optional, cast +from typing import Any, Dict, List, Optional, cast import yaml from pydantic_yaml import YamlModel @@ -37,7 +37,7 @@ class SnapApp(YamlModel): command: str command_chain: List[str] - environment: Optional[Dict[str, str]] + environment: Optional[Dict[str, Any]] plugs: Optional[List[str]] class Config: # pylint: disable=too-few-public-methods @@ -70,6 +70,7 @@ class SnapMetadata(YamlModel): apps: Optional[Dict[str, SnapApp]] confinement: str grade: str + environment: Optional[Dict[str, Any]] def write(project: Project, prime_dir: Path, *, arch: str): @@ -105,6 +106,7 @@ def write(project: Project, prime_dir: Path, *, arch: str): apps=snap_apps, confinement=project.confinement, grade=project.grade, + environment=project.environment, ) yaml.add_representer(str, _repr_str, Dumper=yaml.SafeDumper) diff --git a/snapcraft/projects.py b/snapcraft/projects.py index 58b7d8e8e5..e44a176bcc 100644 --- a/snapcraft/projects.py +++ b/snapcraft/projects.py @@ -104,7 +104,7 @@ class App(ProjectModel): slots: Optional[UniqueStrList] plugs: Optional[UniqueStrList] aliases: Optional[UniqueAliasList] - environment: Optional[Dict[str, str]] + environment: Optional[Dict[str, Any]] command_chain: List[CommandChainStr] = [] # TODO: sockets @@ -159,7 +159,6 @@ class Project(ProjectModel): See https://snapcraft.io/docs/snapcraft-yaml-reference XXX: Not implemented in this version - - environment (top-level) - system-usernames - adopt-info (after adding craftctl support to craft-parts) """ @@ -193,6 +192,7 @@ class Project(ProjectModel): slots: Optional[Dict[str, Dict[str, str]]] # TODO: add slot name validation parts: Dict[str, Any] # parts are handled by craft-parts epoch: Optional[str] + environment: Optional[Dict[str, Any]] @pydantic.root_validator(pre=True) @classmethod diff --git a/tests/unit/meta/test_snap_yaml.py b/tests/unit/meta/test_snap_yaml.py index a67b2a1505..ebfca00595 100644 --- a/tests/unit/meta/test_snap_yaml.py +++ b/tests/unit/meta/test_snap_yaml.py @@ -41,6 +41,9 @@ def simple_project(): grade: stable confinement: strict + environment: + GLOBAL_VARIABLE: "my-global-variable" + parts: part1: plugin: nil @@ -48,6 +51,8 @@ def simple_project(): apps: app1: command: bin/mytest + environment: + APP_VARIABLE: "my-app-variable" """ ) data = yaml.safe_load(snapcraft_yaml) @@ -81,7 +86,11 @@ def test_snap_yaml(simple_project, new_dir): command: bin/mytest command_chain: - snap/command-chain/snapcraft-runner + environment: + APP_VARIABLE: my-app-variable confinement: strict grade: stable + environment: + GLOBAL_VARIABLE: my-global-variable """ ) diff --git a/tests/unit/test_projects.py b/tests/unit/test_projects.py index ffe22776c4..982a3bc625 100644 --- a/tests/unit/test_projects.py +++ b/tests/unit/test_projects.py @@ -14,6 +14,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +import re from typing import Any, Dict import pytest @@ -68,6 +69,7 @@ def test_project_defaults(self, yaml_data): assert project.plugs is None assert project.slots is None assert project.epoch is None + assert project.environment is None def test_app_defaults(self, yaml_data): data = yaml_data(apps={"app1": {"command": "/bin/true"}}) @@ -359,6 +361,32 @@ def test_project_package_repository_extra_fields(self, yaml_data): with pytest.raises(errors.ProjectValidationError, match=error): Project.unmarshal(yaml_data(package_repositories=repos)) + @pytest.mark.parametrize( + "environment", + [ + {"SINGLE_VARIABLE": "foo"}, + {"FIRST_VARIABLE": "foo", "SECOND_VARIABLE": "bar"}, + ], + ) + def test_project_environment_valid(self, environment, yaml_data): + project = Project.unmarshal(yaml_data(environment=environment)) + assert project.environment == environment + + @pytest.mark.parametrize( + "environment", + [ + "i am a string", + ["i", "am", "a", "list"], + [{"i": "am"}, {"a": "list"}, {"of": "dictionaries"}], + ], + ) + def test_project_environment_invalid(self, environment, yaml_data): + error = re.escape( + "Bad snapcraft.yaml content:\n- value is not a valid dict (in field 'environment')" + ) + with pytest.raises(errors.ProjectValidationError, match=error): + Project.unmarshal(yaml_data(environment=environment)) + class TestAppValidation: """Validate apps.""" @@ -465,3 +493,38 @@ def test_app_install_mode(self, install_mode, yaml_data): error = ".*unexpected value; permitted: 'enable', 'disable'" with pytest.raises(errors.ProjectValidationError, match=error): Project.unmarshal(data) + + @pytest.mark.parametrize( + "environment", + [ + {"SINGLE_VARIABLE": "foo"}, + {"FIRST_VARIABLE": "foo", "SECOND_VARIABLE": "bar"}, + ], + ) + def test_app_environment_valid(self, environment, yaml_data): + data = yaml_data( + apps={"app1": {"command": "/bin/true", "environment": environment}} + ) + project = Project.unmarshal(data) + assert project.apps is not None + assert project.apps["app1"].environment == environment + + @pytest.mark.parametrize( + "environment", + [ + "i am a string", + ["i", "am", "a", "list"], + [{"i": "am"}, {"a": "list"}, {"of": "dictionaries"}], + ], + ) + def test_app_environment_invalid(self, environment, yaml_data): + data = yaml_data( + apps={"app1": {"command": "/bin/true", "environment": environment}} + ) + + error = re.escape( + "Bad snapcraft.yaml content:\n" + "- value is not a valid dict (in field 'apps.app1.environment')" + ) + with pytest.raises(errors.ProjectValidationError, match=error): + Project.unmarshal(data) From ac02c36ec4c0ec04ff4f3b40000013191711254e Mon Sep 17 00:00:00 2001 From: Callahan Kovacs Date: Tue, 8 Feb 2022 15:19:05 -0600 Subject: [PATCH 037/167] sources: make submodule fetching configurable Configure which submodules to fetch from the source tree in snapcraft.yaml with `source-submodules: `. If source-submodules in defined and empty, no submodules are fetched. If source-submodules is not defined, all submodules are fetched (default behavior). LP: #1957767 Signed-off-by: Callahan Kovacs --- schema/snapcraft.json | 9 ++ .../internal/pluginhandler/__init__.py | 1 + .../internal/remote_build/_worktree.py | 1 + snapcraft_legacy/internal/sources/_7z.py | 2 + snapcraft_legacy/internal/sources/__init__.py | 9 ++ snapcraft_legacy/internal/sources/_base.py | 2 + snapcraft_legacy/internal/sources/_bazaar.py | 2 + snapcraft_legacy/internal/sources/_deb.py | 2 + snapcraft_legacy/internal/sources/_git.py | 46 ++++--- .../internal/sources/_mercurial.py | 2 + snapcraft_legacy/internal/sources/_rpm.py | 2 + snapcraft_legacy/internal/sources/_script.py | 2 + snapcraft_legacy/internal/sources/_snap.py | 2 + .../internal/sources/_subversion.py | 2 + snapcraft_legacy/internal/sources/_tar.py | 2 + snapcraft_legacy/internal/sources/_zip.py | 2 + .../internal/states/_pull_state.py | 1 + tests/legacy/unit/pluginhandler/test_state.py | 9 +- .../legacy/unit/remote_build/test_worktree.py | 1 + tests/legacy/unit/sources/test_git.py | 129 ++++++++++++++++++ tests/legacy/unit/states/test_pull.py | 6 +- .../snaps/git-submodules/snapcraft.yaml | 15 ++ tests/spread/general/sources/task.yaml | 1 + .../x-local/local-plugin-x-compat/state-pull | 1 + .../v1/x-local/local-plugin/state-pull | 1 + 25 files changed, 230 insertions(+), 22 deletions(-) create mode 100644 tests/spread/general/sources/snaps/git-submodules/snapcraft.yaml diff --git a/schema/snapcraft.json b/schema/snapcraft.json index e896c1b621..76149f9128 100644 --- a/schema/snapcraft.json +++ b/schema/snapcraft.json @@ -983,6 +983,15 @@ "type": "integer", "default": 0 }, + "source-submodules": { + "type": "array", + "minItems": 0, + "uniqueItems": true, + "items": { + "type": "string", + "description": "submodules to fetch, by pathname in source tree" + } + }, "source-subdir": { "type": "string", "default": "" diff --git a/snapcraft_legacy/internal/pluginhandler/__init__.py b/snapcraft_legacy/internal/pluginhandler/__init__.py index 051b8746d8..ba9d6fe24f 100644 --- a/snapcraft_legacy/internal/pluginhandler/__init__.py +++ b/snapcraft_legacy/internal/pluginhandler/__init__.py @@ -215,6 +215,7 @@ def _get_source_handler(self, properties): source_tag=properties["source-tag"], source_depth=properties["source-depth"], source_commit=properties["source-commit"], + source_submodules=properties["source-submodules"], ) return source_handler diff --git a/snapcraft_legacy/internal/remote_build/_worktree.py b/snapcraft_legacy/internal/remote_build/_worktree.py index e41c052ee2..465e381a28 100644 --- a/snapcraft_legacy/internal/remote_build/_worktree.py +++ b/snapcraft_legacy/internal/remote_build/_worktree.py @@ -99,6 +99,7 @@ def _get_part_source_handler(self, part_name: str, source: str, source_dir: str) source_tag=part_config.get("source-tag"), source_depth=part_config.get("source-depth"), source_commit=part_config.get("source-commit"), + source_submodules=part_config.get("source-submodules"), ) def _get_part_cache_dir(self, part_name: str, selector=None) -> str: diff --git a/snapcraft_legacy/internal/sources/_7z.py b/snapcraft_legacy/internal/sources/_7z.py index 6add745981..74e7619a33 100644 --- a/snapcraft_legacy/internal/sources/_7z.py +++ b/snapcraft_legacy/internal/sources/_7z.py @@ -33,6 +33,7 @@ def __init__( source_branch=None, source_depth=None, source_checksum=None, + source_submodules=None, ): super().__init__( source, @@ -42,6 +43,7 @@ def __init__( source_branch, source_depth, source_checksum, + source_submodules, "7zip", ) if source_tag: diff --git a/snapcraft_legacy/internal/sources/__init__.py b/snapcraft_legacy/internal/sources/__init__.py index 3b8fadf9bd..6c7c64a88a 100644 --- a/snapcraft_legacy/internal/sources/__init__.py +++ b/snapcraft_legacy/internal/sources/__init__.py @@ -65,6 +65,13 @@ When building, Snapcraft will set the working directory to be this subdirectory within the source. + - source-submodules: + + Configure which submodules to fetch from the source tree. + If source-submodules in defined and empty, no submodules are fetched. + If source-submodules is not defined, all submodules are fetched (default + behavior). + Note that plugins might well define their own semantics for the 'source' keywords, because they handle specific build systems, and many languages have their own built-in packaging systems (think CPAN, PyPI, NPM). In those @@ -147,6 +154,7 @@ "source-type": None, "source-branch": None, "source-subdir": None, + "source-submodules": None, } @@ -168,6 +176,7 @@ def get(sourcedir, builddir, options): source_tag=getattr(options, "source_tag", None), source_commit=getattr(options, "source_commit", None), source_branch=getattr(options, "source_branch", None), + source_submodules=getattr(options, "source_submodules", None), ) handler_class = get_source_handler(options.source, source_type=source_type) diff --git a/snapcraft_legacy/internal/sources/_base.py b/snapcraft_legacy/internal/sources/_base.py index da2f67cacc..6096d5a2b4 100644 --- a/snapcraft_legacy/internal/sources/_base.py +++ b/snapcraft_legacy/internal/sources/_base.py @@ -41,6 +41,7 @@ def __init__( source_branch=None, source_depth=None, source_checksum=None, + source_submodules=None, command=None, ): self.source = source @@ -50,6 +51,7 @@ def __init__( self.source_branch = source_branch self.source_depth = source_depth self.source_checksum = source_checksum + self.source_submodules = source_submodules self.source_details = None self.command = command diff --git a/snapcraft_legacy/internal/sources/_bazaar.py b/snapcraft_legacy/internal/sources/_bazaar.py index 8ed256fd41..60cf824b01 100644 --- a/snapcraft_legacy/internal/sources/_bazaar.py +++ b/snapcraft_legacy/internal/sources/_bazaar.py @@ -31,6 +31,7 @@ def __init__( source_branch=None, source_depth=None, source_checksum=None, + source_submodules=None, silent=False, ): super().__init__( @@ -41,6 +42,7 @@ def __init__( source_branch, source_depth, source_checksum, + source_submodules, "bzr", ) if source_branch: diff --git a/snapcraft_legacy/internal/sources/_deb.py b/snapcraft_legacy/internal/sources/_deb.py index b6d3b30070..10779a55d0 100644 --- a/snapcraft_legacy/internal/sources/_deb.py +++ b/snapcraft_legacy/internal/sources/_deb.py @@ -34,6 +34,7 @@ def __init__( source_branch=None, source_depth=None, source_checksum=None, + source_submodules=None, ): super().__init__( source, @@ -43,6 +44,7 @@ def __init__( source_branch, source_depth, source_checksum, + source_submodules, ) if source_tag: raise errors.SnapcraftSourceInvalidOptionError("deb", "source-tag") diff --git a/snapcraft_legacy/internal/sources/_git.py b/snapcraft_legacy/internal/sources/_git.py index b6d2b5d3cc..13ef300713 100644 --- a/snapcraft_legacy/internal/sources/_git.py +++ b/snapcraft_legacy/internal/sources/_git.py @@ -110,6 +110,7 @@ def __init__( source_depth=None, silent=False, source_checksum=None, + source_submodules=None, ): super().__init__( source, @@ -119,6 +120,7 @@ def __init__( source_branch, source_depth, source_checksum, + source_submodules, "git", ) if source_tag and source_branch: @@ -175,26 +177,25 @@ def _pull_existing(self): reset_spec = refspec if refspec != "HEAD" else "origin/master" - self._run( - [ - self.command, - "-C", - self.source_dir, - "fetch", - "--prune", - "--recurse-submodules=yes", - ], - **self._call_kwargs - ) + command = [ + self.command, + "-C", + self.source_dir, + "fetch", + "--prune", + ] + + if self.source_submodules is None or len(self.source_submodules) > 0: + command.extend(["--recurse-submodules=yes"]) + self._run(command, **self._call_kwargs) self._run( [self.command, "-C", self.source_dir, "reset", "--hard", reset_spec], **self._call_kwargs ) - # Merge any updates for the submodules (if any). - self._run( - [ + if self.source_submodules is None or len(self.source_submodules) > 0: + command = [ self.command, "-C", self.source_dir, @@ -202,12 +203,21 @@ def _pull_existing(self): "update", "--recursive", "--force", - ], - **self._call_kwargs - ) + ] + + if self.source_submodules: + for submodule in self.source_submodules: + command.extend([submodule]) + + self._run(command, **self._call_kwargs) def _clone_new(self): - command = [self.command, "clone", "--recursive"] + command = [self.command, "clone"] + if self.source_submodules is None: + command.extend(["--recursive"]) + else: + for submodule in self.source_submodules: + command.extend(["--recursive=" + submodule]) if self.source_tag or self.source_branch: command.extend(["--branch", self.source_tag or self.source_branch]) if self.source_depth: diff --git a/snapcraft_legacy/internal/sources/_mercurial.py b/snapcraft_legacy/internal/sources/_mercurial.py index 1c34e7b9df..f39b8ef000 100644 --- a/snapcraft_legacy/internal/sources/_mercurial.py +++ b/snapcraft_legacy/internal/sources/_mercurial.py @@ -32,6 +32,7 @@ def __init__( source_depth=None, source_checksum=None, silent=False, + source_submodules=None, ): super().__init__( source, @@ -41,6 +42,7 @@ def __init__( source_branch, source_depth, source_checksum, + source_submodules, "hg", ) if source_tag and source_branch: diff --git a/snapcraft_legacy/internal/sources/_rpm.py b/snapcraft_legacy/internal/sources/_rpm.py index c6101fe550..33c67005a3 100644 --- a/snapcraft_legacy/internal/sources/_rpm.py +++ b/snapcraft_legacy/internal/sources/_rpm.py @@ -33,6 +33,7 @@ def __init__( source_branch=None, source_depth=None, source_checksum=None, + source_submodules=None, ): super().__init__( source, @@ -42,6 +43,7 @@ def __init__( source_branch, source_depth, source_checksum, + source_submodules, "rpm2cpio", ) if source_tag: diff --git a/snapcraft_legacy/internal/sources/_script.py b/snapcraft_legacy/internal/sources/_script.py index 40b02630d0..220179af2c 100644 --- a/snapcraft_legacy/internal/sources/_script.py +++ b/snapcraft_legacy/internal/sources/_script.py @@ -30,6 +30,7 @@ def __init__( source_branch=None, source_depth=None, source_checksum=None, + source_submodules=None, ): super().__init__( source, @@ -39,6 +40,7 @@ def __init__( source_branch, source_depth, source_checksum, + source_submodules, ) def download(self, filepath: str = None) -> str: diff --git a/snapcraft_legacy/internal/sources/_snap.py b/snapcraft_legacy/internal/sources/_snap.py index f171725442..5144376876 100644 --- a/snapcraft_legacy/internal/sources/_snap.py +++ b/snapcraft_legacy/internal/sources/_snap.py @@ -41,6 +41,7 @@ def __init__( source_branch: str = None, source_depth: str = None, source_checksum: str = None, + source_submodules=None, ) -> None: super().__init__( source, @@ -50,6 +51,7 @@ def __init__( source_branch, source_depth, source_checksum, + source_submodules, "unsquashfs", ) if source_tag: diff --git a/snapcraft_legacy/internal/sources/_subversion.py b/snapcraft_legacy/internal/sources/_subversion.py index 79aac0cfe8..0835a4a60e 100644 --- a/snapcraft_legacy/internal/sources/_subversion.py +++ b/snapcraft_legacy/internal/sources/_subversion.py @@ -32,6 +32,7 @@ def __init__( source_depth=None, source_checksum=None, silent=False, + source_submodules=None, ): super().__init__( source, @@ -41,6 +42,7 @@ def __init__( source_branch, source_depth, source_checksum, + source_submodules, "svn", ) if source_tag: diff --git a/snapcraft_legacy/internal/sources/_tar.py b/snapcraft_legacy/internal/sources/_tar.py index f36700fc21..bfa468c201 100644 --- a/snapcraft_legacy/internal/sources/_tar.py +++ b/snapcraft_legacy/internal/sources/_tar.py @@ -34,6 +34,7 @@ def __init__( source_branch=None, source_depth=None, source_checksum=None, + source_submodules=None, ): super().__init__( source, @@ -43,6 +44,7 @@ def __init__( source_branch, source_depth, source_checksum, + source_submodules, ) if source_tag: raise errors.SnapcraftSourceInvalidOptionError("tar", "source-tag") diff --git a/snapcraft_legacy/internal/sources/_zip.py b/snapcraft_legacy/internal/sources/_zip.py index fdcabb6646..784b6c5041 100644 --- a/snapcraft_legacy/internal/sources/_zip.py +++ b/snapcraft_legacy/internal/sources/_zip.py @@ -32,6 +32,7 @@ def __init__( source_branch=None, source_depth=None, source_checksum=None, + source_submodules=None, ): super().__init__( source, @@ -41,6 +42,7 @@ def __init__( source_branch, source_depth, source_checksum, + source_submodules, ) if source_tag: raise errors.SnapcraftSourceInvalidOptionError("zip", "source-tag") diff --git a/snapcraft_legacy/internal/states/_pull_state.py b/snapcraft_legacy/internal/states/_pull_state.py index e77b021013..273a80371e 100644 --- a/snapcraft_legacy/internal/states/_pull_state.py +++ b/snapcraft_legacy/internal/states/_pull_state.py @@ -30,6 +30,7 @@ def _schema_properties(): "source-type", "source-branch", "source-subdir", + "source-submodules", "stage-packages", } diff --git a/tests/legacy/unit/pluginhandler/test_state.py b/tests/legacy/unit/pluginhandler/test_state.py index e1a4584c6d..3dbc406ab1 100644 --- a/tests/legacy/unit/pluginhandler/test_state.py +++ b/tests/legacy/unit/pluginhandler/test_state.py @@ -116,7 +116,7 @@ def test_pull_state(self, repo_mock): self.assertTrue(state, "Expected pull to save state YAML") self.assertTrue(type(state) is states.PullState) self.assertTrue(type(state.properties) is OrderedDict) - self.assertThat(len(state.properties), Equals(11)) + self.assertThat(len(state.properties), Equals(12)) for expected in [ "source", "source-branch", @@ -125,6 +125,7 @@ def test_pull_state(self, repo_mock): "source-subdir", "source-tag", "source-type", + "source-submodules", "plugin", "stage-packages", "parse-info", @@ -170,7 +171,7 @@ def _fake_extractor(file_path, workdir): self.assertTrue(state, "Expected pull to save state YAML") self.assertTrue(type(state) is states.PullState) self.assertTrue(type(state.properties) is OrderedDict) - self.assertThat(len(state.properties), Equals(11)) + self.assertThat(len(state.properties), Equals(12)) for expected in [ "source", "source-branch", @@ -179,6 +180,7 @@ def _fake_extractor(file_path, workdir): "source-subdir", "source-tag", "source-type", + "source-submodules", "plugin", "stage-packages", "parse-info", @@ -227,7 +229,7 @@ def test_pull_state_with_scriptlet_metadata(self, repo_mock): self.assertTrue(state, "Expected pull to save state YAML") self.assertTrue(type(state) is states.PullState) self.assertTrue(type(state.properties) is OrderedDict) - self.assertThat(len(state.properties), Equals(11)) + self.assertThat(len(state.properties), Equals(12)) for expected in [ "source", "source-branch", @@ -236,6 +238,7 @@ def test_pull_state_with_scriptlet_metadata(self, repo_mock): "source-subdir", "source-tag", "source-type", + "source-submodules", "plugin", "stage-packages", "parse-info", diff --git a/tests/legacy/unit/remote_build/test_worktree.py b/tests/legacy/unit/remote_build/test_worktree.py index 634aa62e10..e5de547195 100644 --- a/tests/legacy/unit/remote_build/test_worktree.py +++ b/tests/legacy/unit/remote_build/test_worktree.py @@ -204,6 +204,7 @@ def test_stripped_source_keys(self): "source-subdir": "test-sub-dir", "source-tag": "strip-me", "source-type": "local", + "source-submodules": "strip-me", }, ) diff --git a/tests/legacy/unit/sources/test_git.py b/tests/legacy/unit/sources/test_git.py index b7789df168..cb630b87b5 100644 --- a/tests/legacy/unit/sources/test_git.py +++ b/tests/legacy/unit/sources/test_git.py @@ -270,6 +270,44 @@ def test_pull_commit(self): ] ) + def test_pull_with_submodules_default(self): + git = sources.Git("git://my-source", "source_dir") + + git.pull() + + self.mock_run.assert_called_once_with( + ["git", "clone", "--recursive", "git://my-source", "source_dir"] + ) + + def test_pull_with_submodules_empty(self): + git = sources.Git("git://my-source", "source_dir", source_submodules=[]) + + git.pull() + + self.mock_run.assert_called_once_with( + ["git", "clone", "git://my-source", "source_dir"] + ) + + def test_pull_with_submodules(self): + git = sources.Git( + "git://my-source", + "source_dir", + source_submodules=["submodule_1", "dir/submodule_2"], + ) + + git.pull() + + self.mock_run.assert_called_once_with( + [ + "git", + "clone", + "--recursive=submodule_1", + "--recursive=dir/submodule_2", + "git://my-source", + "source_dir", + ] + ) + def test_pull_existing(self): self.mock_path_exists.return_value = True @@ -428,6 +466,97 @@ def test_pull_existing_with_branch(self): ] ) + def test_pull_existing_with_submodules_default(self): + self.mock_path_exists.return_value = True + + git = sources.Git("git://my-source", "source_dir") + git.pull() + + self.mock_run.assert_has_calls( + [ + mock.call( + [ + "git", + "-C", + "source_dir", + "fetch", + "--prune", + "--recurse-submodules=yes", + ] + ), + mock.call( + ["git", "-C", "source_dir", "reset", "--hard", "origin/master"] + ), + mock.call( + [ + "git", + "-C", + "source_dir", + "submodule", + "update", + "--recursive", + "--force", + ] + ), + ] + ) + + def test_pull_existing_with_submodules_empty(self): + self.mock_path_exists.return_value = True + + git = sources.Git("git://my-source", "source_dir", source_submodules=[]) + git.pull() + + self.mock_run.assert_has_calls( + [ + mock.call(["git", "-C", "source_dir", "fetch", "--prune"]), + mock.call( + ["git", "-C", "source_dir", "reset", "--hard", "origin/master"] + ), + ] + ) + + def test_pull_existing_with_submodules(self): + self.mock_path_exists.return_value = True + + git = sources.Git( + "git://my-source", + "source_dir", + source_submodules=["submodule_1", "dir/submodule_2"], + ) + git.pull() + + self.mock_run.assert_has_calls( + [ + mock.call( + [ + "git", + "-C", + "source_dir", + "fetch", + "--prune", + "--recurse-submodules=yes", + ] + ), + mock.call( + ["git", "-C", "source_dir", "reset", "--hard", "origin/master"] + ), + mock.call( + [ + "git", + "-C", + "source_dir", + "submodule", + "update", + "--recursive", + "--force", + "submodule_1", + "dir/submodule_2", + ] + ), + ] + ) + def test_init_with_source_branch_and_tag_raises_exception(self): raised = self.assertRaises( sources.errors.SnapcraftSourceIncompatibleOptionsError, diff --git a/tests/legacy/unit/states/test_pull.py b/tests/legacy/unit/states/test_pull.py index 59dabc523d..568d92ade2 100644 --- a/tests/legacy/unit/states/test_pull.py +++ b/tests/legacy/unit/states/test_pull.py @@ -78,11 +78,12 @@ def test_properties_of_interest(self): "source-type": "test-source-type", "source-branch": "test-source-branch", "source-subdir": "test-source-subdir", + "source-submodules": "test-source-submodules", } ) properties = self.state.properties_of_interest(self.part_properties) - self.assertThat(len(properties), Equals(12)) + self.assertThat(len(properties), Equals(13)) self.assertThat(properties["foo"], Equals("bar")) self.assertThat(properties["override-pull"], Equals("touch override-pull")) self.assertThat(properties["plugin"], Equals("test-plugin")) @@ -95,6 +96,9 @@ def test_properties_of_interest(self): self.assertThat(properties["source-type"], Equals("test-source-type")) self.assertThat(properties["source-branch"], Equals("test-source-branch")) self.assertThat(properties["source-subdir"], Equals("test-source-subdir")) + self.assertThat( + properties["source-submodules"], Equals("test-source-submodules"), + ) def test_project_options_of_interest(self): options = self.state.project_options_of_interest(self.project) diff --git a/tests/spread/general/sources/snaps/git-submodules/snapcraft.yaml b/tests/spread/general/sources/snaps/git-submodules/snapcraft.yaml new file mode 100644 index 0000000000..c5c42bdc6d --- /dev/null +++ b/tests/spread/general/sources/snaps/git-submodules/snapcraft.yaml @@ -0,0 +1,15 @@ +name: git-recurse-submodules +base: core20 +version: "0.1" +summary: Test the use of source-submodules +description: Make sure source-submodules works +confinement: strict + +parts: + git: + plugin: dump + source: https://github.com/snapcore/core18 + source-type: git + source-submodules: + - submodule_1 + - dir/submodule_2 diff --git a/tests/spread/general/sources/task.yaml b/tests/spread/general/sources/task.yaml index 08c00f20ba..5e0dc3423c 100644 --- a/tests/spread/general/sources/task.yaml +++ b/tests/spread/general/sources/task.yaml @@ -11,6 +11,7 @@ environment: SNAP_DIR/git_commit: snaps/git-commit SNAP_DIR/git_depth: snaps/git-depth SNAP_DIR/git_head: snaps/git-head + SNAP_DIR/git_submodules: snaps/git-submodules SNAP_DIR/local_source: snaps/local-source SNAP_DIR/local_source_subfolders: snaps/local-source-subfolders SNAP_DIR/local_source_type: snaps/local-source-type diff --git a/tests/spread/plugins/v1/x-local/local-plugin-x-compat/state-pull b/tests/spread/plugins/v1/x-local/local-plugin-x-compat/state-pull index 1a356bbfba..9bfc05a491 100644 --- a/tests/spread/plugins/v1/x-local/local-plugin-x-compat/state-pull +++ b/tests/spread/plugins/v1/x-local/local-plugin-x-compat/state-pull @@ -21,6 +21,7 @@ properties: source-commit: '' source-depth: 0 source-subdir: '' + source-submodules: null source-tag: '' source-type: '' stage-packages: [] diff --git a/tests/spread/plugins/v1/x-local/local-plugin/state-pull b/tests/spread/plugins/v1/x-local/local-plugin/state-pull index ad49c951e8..85b6a84d1b 100644 --- a/tests/spread/plugins/v1/x-local/local-plugin/state-pull +++ b/tests/spread/plugins/v1/x-local/local-plugin/state-pull @@ -21,6 +21,7 @@ properties: source-commit: '' source-depth: 0 source-subdir: '' + source-submodules: null source-tag: '' source-type: '' stage-packages: [] From 4104923f9bf75767db7e2f3b56e29e0f26e054d6 Mon Sep 17 00:00:00 2001 From: Claudio Matsuoka Date: Mon, 21 Feb 2022 10:31:13 -0300 Subject: [PATCH 038/167] parts: integrate package-repositories Add apt package repositories to the host system according to the `package-repositories` entry listed in snapcraft.yaml. Also add a separate set of spread tests for core22. These can be removed and integrated to the existing tests when better 22.04 support is added to the testing infrastructure and more options are implemented in the 7.0 branch. Note: The pydantic model for package repositories are very similar to the legacy internal classes used to manipulate deb and ppa repositories. We should investigate if these can be consolidated, moving manual validation of repository parameters to the pydantic model. Signed-off-by: Claudio Matsuoka --- snapcraft/parts/lifecycle.py | 20 +++- snapcraft/parts/parts.py | 41 ++++++-- snapcraft/projects.py | 2 +- snapcraft/repo/__init__.py | 2 + snapcraft/repo/installer.py | 93 +++++++++++++++++++ snapcraft/repo/projects.py | 7 +- tests/spread/general/core22/task.yaml | 36 +++++++ .../snap/keys/FC42E99D.asc | 29 ++++++ .../snap/snapcraft.yaml | 25 +++++ .../test-apt-key-name/snap/keys/FC42E99D.asc | 29 ++++++ .../test-apt-key-name/snap/snapcraft.yaml | 25 +++++ .../test-apt-keyserver/snap/snapcraft.yaml | 25 +++++ .../core22/test-apt-path/snap/snapcraft.yaml | 19 ++++ .../core22/test-apt-ppa/snap/snapcraft.yaml | 21 +++++ .../general/package-repositories/task.yaml | 2 +- tests/spread/tools/restore.sh | 2 +- tests/unit/parts/test_lifecycle.py | 22 ++++- tests/unit/parts/test_parts.py | 17 +++- tests/unit/repo/test_installer.py | 43 +++++++++ tests/unit/repo/test_projects.py | 14 +-- tests/unit/test_projects.py | 2 +- 21 files changed, 447 insertions(+), 29 deletions(-) create mode 100644 snapcraft/repo/installer.py create mode 100644 tests/spread/general/core22/task.yaml create mode 100644 tests/spread/general/core22/test-apt-key-fingerprint/snap/keys/FC42E99D.asc create mode 100644 tests/spread/general/core22/test-apt-key-fingerprint/snap/snapcraft.yaml create mode 100644 tests/spread/general/core22/test-apt-key-name/snap/keys/FC42E99D.asc create mode 100644 tests/spread/general/core22/test-apt-key-name/snap/snapcraft.yaml create mode 100644 tests/spread/general/core22/test-apt-keyserver/snap/snapcraft.yaml create mode 100644 tests/spread/general/core22/test-apt-path/snap/snapcraft.yaml create mode 100644 tests/spread/general/core22/test-apt-ppa/snap/snapcraft.yaml create mode 100644 tests/unit/repo/test_installer.py diff --git a/snapcraft/parts/lifecycle.py b/snapcraft/parts/lifecycle.py index d0f3b7b07a..6d93fbb93b 100644 --- a/snapcraft/parts/lifecycle.py +++ b/snapcraft/parts/lifecycle.py @@ -49,8 +49,14 @@ def run(command_name: str, parsed_args: "argparse.Namespace") -> None: """ emit.trace(f"command: {command_name}, arguments: {parsed_args}") yaml_data = {} + assets_dir = Path("snap") + for project_file in _PROJECT_FILES: if project_file.is_file(): + + if project_file.parent.name == "snap": + assets_dir = project_file.parent + yaml_data = _load_yaml(project_file) break else: @@ -72,12 +78,16 @@ def run(command_name: str, parsed_args: "argparse.Namespace") -> None: project = Project.unmarshal(yaml_data) - _run_command(command_name, project, parsed_args) + _run_command( + command_name, project=project, assets_dir=assets_dir, parsed_args=parsed_args + ) def _run_command( command_name: str, + *, project: Project, + assets_dir: Path, parsed_args: "argparse.Namespace", ) -> None: @@ -85,10 +95,14 @@ def _run_command( _ = parsed_args step_name = "prime" if command_name == "pack" else command_name - work_dir = Path("work").absolute() - lifecycle = PartsLifecycle(project.parts, work_dir=work_dir) + lifecycle = PartsLifecycle( + project.parts, + work_dir=work_dir, + assets_dir=assets_dir, + package_repositories=project.package_repositories, + ) lifecycle.run(step_name) snap_yaml.write(project, lifecycle.prime_dir, arch=lifecycle.target_arch) diff --git a/snapcraft/parts/parts.py b/snapcraft/parts/parts.py index a09a07263a..1970fddcf8 100644 --- a/snapcraft/parts/parts.py +++ b/snapcraft/parts/parts.py @@ -17,14 +17,14 @@ """Craft-parts lifecycle wrapper.""" import pathlib -from typing import Any, Dict +from typing import Any, Dict, List import craft_parts from craft_cli import emit from craft_parts import ActionType, Step from xdg import BaseDirectory # type: ignore -from snapcraft.errors import PartsLifecycleError +from snapcraft import errors, repo _LIFECYCLE_STEPS = { "pull": Step.PULL, @@ -40,18 +40,33 @@ class PartsLifecycle: :param all_parts: A dictionary containing the parts defined in the project. :param work_dir: The working directory for parts processing. + :param assets_dir: The directory containing project assets. :raises PartsLifecycleError: On error initializing the parts lifecycle. """ def __init__( - self, all_parts: Dict[str, Any], *, work_dir: pathlib.Path, + self, + all_parts: Dict[str, Any], + *, + work_dir: pathlib.Path, + assets_dir: pathlib.Path, + package_repositories: List[Dict[str, Any]], ): + self._assets_dir = assets_dir + self._package_repositories = package_repositories + emit.progress("Initializing parts lifecycle") # set the cache dir for parts package management cache_dir = BaseDirectory.save_cache_path("snapcraft") + extra_build_packages = [] + if self._package_repositories: + # Install pre-requisite packages for apt-key, if not installed. + # FIXME: package names should be plataform-specific + extra_build_packages.extend(["gnupg", "dirmngr"]) + try: self._lcm = craft_parts.LifecycleManager( {"parts": all_parts}, @@ -61,7 +76,7 @@ def __init__( ignore_local_sources=["*.snap"], ) except craft_parts.PartsError as err: - raise PartsLifecycleError(str(err)) from err + raise errors.PartsLifecycleError(str(err)) from err @property def prime_dir(self) -> pathlib.Path: @@ -86,9 +101,21 @@ def run(self, step_name: str) -> None: raise RuntimeError(f"Invalid target step {step_name!r}") try: + actions = self._lcm.plan(target_step) + + emit.progress("Installing package repositories...") + + if self._package_repositories: + refresh_required = repo.install( + self._package_repositories, key_assets=self._assets_dir / "keys" + ) + if refresh_required: + self._lcm.refresh_packages_list() + + emit.message("Installed package repositories", intermediate=True) + emit.progress("Executing parts lifecycle...") - actions = self._lcm.plan(target_step) with self._lcm.action_executor() as aex: for action in actions: message = _action_message(action) @@ -102,9 +129,9 @@ def run(self, step_name: str) -> None: msg = err.strerror if err.filename: msg = f"{err.filename}: {msg}" - raise PartsLifecycleError(msg) from err + raise errors.PartsLifecycleError(msg) from err except Exception as err: - raise PartsLifecycleError(str(err)) from err + raise errors.PartsLifecycleError(str(err)) from err def _action_message(action: craft_parts.Action) -> str: diff --git a/snapcraft/projects.py b/snapcraft/projects.py index e44a176bcc..d4ffc75e66 100644 --- a/snapcraft/projects.py +++ b/snapcraft/projects.py @@ -184,7 +184,7 @@ class Project(ProjectModel): grade: Literal["stable", "devel"] architectures: List[Architecture] = [] assumes: UniqueStrList = [] - package_repositories: Optional[List[Any]] = [] # handled by repo + package_repositories: List[Dict[str, Any]] = [] # handled by repo hooks: Optional[Dict[str, Hook]] passthrough: Optional[Dict[str, Any]] apps: Optional[Dict[str, App]] diff --git a/snapcraft/repo/__init__.py b/snapcraft/repo/__init__.py index cbd8b5fd81..326519fa5b 100644 --- a/snapcraft/repo/__init__.py +++ b/snapcraft/repo/__init__.py @@ -16,8 +16,10 @@ """Package repository helpers.""" +from .installer import install from .projects import validate_repository __all__ = [ + "install", "validate_repository", ] diff --git a/snapcraft/repo/installer.py b/snapcraft/repo/installer.py new file mode 100644 index 0000000000..0bc173318f --- /dev/null +++ b/snapcraft/repo/installer.py @@ -0,0 +1,93 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright 2019-2022 Canonical Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +"""Package repository installer.""" + +import pathlib +from typing import Any, Dict, List + +from . import errors +from .apt_key_manager import AptKeyManager +from .apt_sources_manager import AptSourcesManager +from .package_repository import ( + PackageRepository, + PackageRepositoryApt, + PackageRepositoryAptPPA, +) + + +def install( + project_repositories: List[Dict[str, Any]], *, key_assets: pathlib.Path +) -> bool: + """Add package repositories to the host system. + + :param package_repositories: A list of package repositories to install. + :param key_assets: The directory containing repository keys. + + :return: Whether a package list refresh is required. + """ + key_manager = AptKeyManager(key_assets=key_assets) + sources_manager = AptSourcesManager() + + package_repositories = _unmarshal_repositories(project_repositories) + + refresh_required = False + for package_repo in package_repositories: + refresh_required |= key_manager.install_package_repository_key( + package_repo=package_repo + ) + refresh_required |= sources_manager.install_package_repository_sources( + package_repo=package_repo + ) + + _verify_all_key_assets_installed(key_assets=key_assets, key_manager=key_manager) + + return refresh_required + + +def _verify_all_key_assets_installed( + *, + key_assets: pathlib.Path, + key_manager: AptKeyManager, +) -> None: + """Verify all configured key assets are utilized, error if not.""" + for key_asset in key_assets.glob("*"): + key = key_asset.read_text() + for key_id in key_manager.get_key_fingerprints(key=key): + if not key_manager.is_key_installed(key_id=key_id): + raise errors.PackageRepositoryError( + "Found unused key asset {key_asset!r}.", + details="All configured key assets must be utilized.", + resolution="Verify key usage and remove all unused keys.", + ) + + +def _unmarshal_repositories( + project_repositories: List[Dict[str, Any]] +) -> List[PackageRepository]: + """Create package repositories objects from project data.""" + repositories = [] + for data in project_repositories: + pkg_repo: PackageRepository + + if "ppa" in data: + pkg_repo = PackageRepositoryAptPPA.unmarshal(data) + else: + pkg_repo = PackageRepositoryApt.unmarshal(data) + + repositories.append(pkg_repo) + + return repositories diff --git a/snapcraft/repo/projects.py b/snapcraft/repo/projects.py index c839664e15..e1fd4b216b 100644 --- a/snapcraft/repo/projects.py +++ b/snapcraft/repo/projects.py @@ -27,7 +27,7 @@ if TYPE_CHECKING: KeyIdStr = str else: - KeyIdStr = constr(regex=r"^[A-Z0-9]{40}$") + KeyIdStr = constr(regex=r"^[0-9A-F]{40}$") class ProjectModel(pydantic.BaseModel): @@ -43,6 +43,11 @@ class Config: # pylint: disable=too-few-public-methods extra = "forbid" +# TODO: Project repo definitions are almost the same as PackageRepository +# ported from legacy. Check if we can consolidate them and remove +# field validation (moving all validation rules to pydantic). + + class AptDeb(ProjectModel): """Apt package repository definition.""" diff --git a/tests/spread/general/core22/task.yaml b/tests/spread/general/core22/task.yaml new file mode 100644 index 0000000000..873cf3fcc8 --- /dev/null +++ b/tests/spread/general/core22/task.yaml @@ -0,0 +1,36 @@ +summary: Test various package-repository configurations on core22 + +environment: + SNAP/test_apt_key_fingerprint: test-apt-key-fingerprint + SNAP/test_apt_key_name: test-apt-key-name + SNAP/test_apt_keyserver: test-apt-keyserver + SNAP/test_apt_ppa: test-apt-ppa + SNAPCRAFT_BUILD_ENVIRONMENT: "" + +prepare: | + #shellcheck source=tests/spread/tools/snapcraft-yaml.sh + . "$TOOLS_DIR/snapcraft-yaml.sh" + # set_base "$SNAP/snap/snapcraft.yaml" + snap install core22 --edge + +restore: | + cd "$SNAP" + rm -f ./*.snap + rm -Rf work + + #shellcheck source=tests/spread/tools/snapcraft-yaml.sh + . "$TOOLS_DIR/snapcraft-yaml.sh" + restore_yaml "snap/snapcraft.yaml" + +execute: | + cd "$SNAP" + + # Build what we have. + if [ "$SPREAD_SYSTEM" = "ubuntu-20.04-64" ]; then + snapcraft --verbose + + # And verify the snap runs as expected. + snap install "${SNAP}"_1.0_*.snap --dangerous + snap_executable="${SNAP}.test-ppa" + [ "$("${snap_executable}")" = "hello!" ] + fi diff --git a/tests/spread/general/core22/test-apt-key-fingerprint/snap/keys/FC42E99D.asc b/tests/spread/general/core22/test-apt-key-fingerprint/snap/keys/FC42E99D.asc new file mode 100644 index 0000000000..f1976277b3 --- /dev/null +++ b/tests/spread/general/core22/test-apt-key-fingerprint/snap/keys/FC42E99D.asc @@ -0,0 +1,29 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +xsFNBFRt70cBEADH/8JgKzFnwQQqtllZ3nqxYQ1cZguLCbyu9s1AwRDNu0P2oWOR +UN9YoUS15kuWtTuneVlLbdbda3N/S/HApvOWu7Q1oIrRRkpO4Jv4xN+1KaSpaTy1 +vG+HepH1D0tCSV0dmbX0S07yd0Ml7o4gMx2svBXeX41RHzjwCNkMUQJGuMF/w0hC +/Wqz6Sbki6QcqQx+YAjwVyUU1KdDRlm9efelQOskDwdr1j9Vk6ky8q+p29dEX5q2 +FApKnwJb7YPwgRDMT/kCMJzHpLxW9Zj0OLkY4epADRi+eNiMblJsWRULs5l7T5oj +yEaXFrGHzOi2HaxidUTUUro2Mb0qZUXRYoEnZV0ntmFxUPIS75sFapJdRbLF0mqy +aMFe9PtmKyFOJXC/MfMaqhMxChWRZm0f8d12zDcVe5LTnVgZaeYr+vPnhqRaDI7w +WZBtCdeMGd4BLa1b3fwY0id2Ti6egFbJzVu2v4GGojBTRkZmlw+Srdzm3w9FA/oj +mAQV/R7snK6bc2o9gtIvPGlZceUTSOtySwlOBCd50YpL2K4GdT1GlEm/DAPSPAWP +Zn9gtZOe8XLxyWd2Qca/NTU0sYeG5xdQGes7pdHz9Mqb0vN14ojE8VdqS8qZx74v +qhnN3+xJ7BDNOjAjjhOAcn1mulX4N9u/WlUw7O67Ht5V/8ODwVTh2L3lLQARAQAB +zSNMYXVuY2hwYWQgUFBBIGZvciBTbmFwcHkgRGV2ZWxvcGVyc8LBeAQTAQIAIgUC +VG3vRwIbAwYLCQgHAwIGFQgCCQoLBBYCAwECHgECF4AACgkQ8YMd2vxC6Z2y1RAA +w7jFWZomYHUkUmm0FNEeRko6kv5iDNGqQXpp0JaZz06kC3dW7vjE3kNgwmgMdcA+ +/a+Jgf3ii8AHyplUQXuopHAXvZyz6YS6r17B2TuKt47MtMkWSk56UZ6av0VnE1Ms +yf6FeBEtQwojLW7ZHNZPq0BlwcvK3/H+qNHitDaIdCmCDDu9mwuerd0ZoNwbW0A1 +RPPl+Jw3uJ+tZWBAkJV+5dGzT/FJlCL28NjywktGjduhGE2nM5Q/Kd0S+kovwf9q +wmPMF8BLwUwshZoHKjLmalu08DzoyO6Bfcl6SThlO1iHoSayFnP6hJZeWkTaF/L+ +Uzbbfnjz+fWAutUoZSxHsK50VfykqgUiG9t7Kv4q5B/3s7X42O4270yEc4OSZM+Y +Ij3EOKWCgHkR3YH9/wk3w1jPiVKjO+jfZnX7FV77vVxbsR/+ibzEPEo51nWcp64q +bBf+bSSGotGv5ef6ETWw4k0cOF9Dws/zmLs9g9CYpuv5DG5d/pvSUKVmqcb2iEc2 +bymJDuKD3kE9MNCqdtnCbwVUpyRauzKhjzY8vmYlFzhlJB5WU0tR6VMMQZNcmXst +1T/RVTcIlXZUYfgbUwvPX6SOLERX1do9vtbD+XvWAYQ/J7G4knHRtf5RpiW1xQkp +FSbrQ9ACQFlqN49Ogbl47J6TZ7BrjDpROote55ixmrU= +=PEEJ +-----END PGP PUBLIC KEY BLOCK----- + diff --git a/tests/spread/general/core22/test-apt-key-fingerprint/snap/snapcraft.yaml b/tests/spread/general/core22/test-apt-key-fingerprint/snap/snapcraft.yaml new file mode 100644 index 0000000000..cc90a644a0 --- /dev/null +++ b/tests/spread/general/core22/test-apt-key-fingerprint/snap/snapcraft.yaml @@ -0,0 +1,25 @@ +name: test-apt-key-fingerprint +version: '1.0' +summary: test +description: test +grade: stable +confinement: strict +base: core22 + +parts: + test-ppa: + plugin: nil + stage-packages: + - test-ppa + +apps: + test-ppa: + command: usr/bin/test-ppa + +package-repositories: + - type: apt + formats: [deb, deb-src] + components: [main] + suites: [focal] + key-id: 78E1918602959B9C59103100F1831DDAFC42E99D + url: http://ppa.launchpad.net/snappy-dev/snapcraft-daily/ubuntu diff --git a/tests/spread/general/core22/test-apt-key-name/snap/keys/FC42E99D.asc b/tests/spread/general/core22/test-apt-key-name/snap/keys/FC42E99D.asc new file mode 100644 index 0000000000..f1976277b3 --- /dev/null +++ b/tests/spread/general/core22/test-apt-key-name/snap/keys/FC42E99D.asc @@ -0,0 +1,29 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +xsFNBFRt70cBEADH/8JgKzFnwQQqtllZ3nqxYQ1cZguLCbyu9s1AwRDNu0P2oWOR +UN9YoUS15kuWtTuneVlLbdbda3N/S/HApvOWu7Q1oIrRRkpO4Jv4xN+1KaSpaTy1 +vG+HepH1D0tCSV0dmbX0S07yd0Ml7o4gMx2svBXeX41RHzjwCNkMUQJGuMF/w0hC +/Wqz6Sbki6QcqQx+YAjwVyUU1KdDRlm9efelQOskDwdr1j9Vk6ky8q+p29dEX5q2 +FApKnwJb7YPwgRDMT/kCMJzHpLxW9Zj0OLkY4epADRi+eNiMblJsWRULs5l7T5oj +yEaXFrGHzOi2HaxidUTUUro2Mb0qZUXRYoEnZV0ntmFxUPIS75sFapJdRbLF0mqy +aMFe9PtmKyFOJXC/MfMaqhMxChWRZm0f8d12zDcVe5LTnVgZaeYr+vPnhqRaDI7w +WZBtCdeMGd4BLa1b3fwY0id2Ti6egFbJzVu2v4GGojBTRkZmlw+Srdzm3w9FA/oj +mAQV/R7snK6bc2o9gtIvPGlZceUTSOtySwlOBCd50YpL2K4GdT1GlEm/DAPSPAWP +Zn9gtZOe8XLxyWd2Qca/NTU0sYeG5xdQGes7pdHz9Mqb0vN14ojE8VdqS8qZx74v +qhnN3+xJ7BDNOjAjjhOAcn1mulX4N9u/WlUw7O67Ht5V/8ODwVTh2L3lLQARAQAB +zSNMYXVuY2hwYWQgUFBBIGZvciBTbmFwcHkgRGV2ZWxvcGVyc8LBeAQTAQIAIgUC +VG3vRwIbAwYLCQgHAwIGFQgCCQoLBBYCAwECHgECF4AACgkQ8YMd2vxC6Z2y1RAA +w7jFWZomYHUkUmm0FNEeRko6kv5iDNGqQXpp0JaZz06kC3dW7vjE3kNgwmgMdcA+ +/a+Jgf3ii8AHyplUQXuopHAXvZyz6YS6r17B2TuKt47MtMkWSk56UZ6av0VnE1Ms +yf6FeBEtQwojLW7ZHNZPq0BlwcvK3/H+qNHitDaIdCmCDDu9mwuerd0ZoNwbW0A1 +RPPl+Jw3uJ+tZWBAkJV+5dGzT/FJlCL28NjywktGjduhGE2nM5Q/Kd0S+kovwf9q +wmPMF8BLwUwshZoHKjLmalu08DzoyO6Bfcl6SThlO1iHoSayFnP6hJZeWkTaF/L+ +Uzbbfnjz+fWAutUoZSxHsK50VfykqgUiG9t7Kv4q5B/3s7X42O4270yEc4OSZM+Y +Ij3EOKWCgHkR3YH9/wk3w1jPiVKjO+jfZnX7FV77vVxbsR/+ibzEPEo51nWcp64q +bBf+bSSGotGv5ef6ETWw4k0cOF9Dws/zmLs9g9CYpuv5DG5d/pvSUKVmqcb2iEc2 +bymJDuKD3kE9MNCqdtnCbwVUpyRauzKhjzY8vmYlFzhlJB5WU0tR6VMMQZNcmXst +1T/RVTcIlXZUYfgbUwvPX6SOLERX1do9vtbD+XvWAYQ/J7G4knHRtf5RpiW1xQkp +FSbrQ9ACQFlqN49Ogbl47J6TZ7BrjDpROote55ixmrU= +=PEEJ +-----END PGP PUBLIC KEY BLOCK----- + diff --git a/tests/spread/general/core22/test-apt-key-name/snap/snapcraft.yaml b/tests/spread/general/core22/test-apt-key-name/snap/snapcraft.yaml new file mode 100644 index 0000000000..9d65650e1f --- /dev/null +++ b/tests/spread/general/core22/test-apt-key-name/snap/snapcraft.yaml @@ -0,0 +1,25 @@ +name: test-apt-key-name +version: '1.0' +summary: test +description: test +grade: stable +confinement: strict +base: core22 + +parts: + test-ppa: + plugin: nil + stage-packages: + - test-ppa + +apps: + test-ppa: + command: usr/bin/test-ppa + +package-repositories: + - type: apt + formats: [deb, deb-src] + components: [main] + suites: [focal] + key-id: 78E1918602959B9C59103100F1831DDAFC42E99D + url: http://ppa.launchpad.net/snappy-dev/snapcraft-daily/ubuntu diff --git a/tests/spread/general/core22/test-apt-keyserver/snap/snapcraft.yaml b/tests/spread/general/core22/test-apt-keyserver/snap/snapcraft.yaml new file mode 100644 index 0000000000..65ace50d40 --- /dev/null +++ b/tests/spread/general/core22/test-apt-keyserver/snap/snapcraft.yaml @@ -0,0 +1,25 @@ +name: test-apt-keyserver +version: '1.0' +summary: test +description: test +grade: stable +confinement: strict +base: core22 + +parts: + test-ppa: + plugin: nil + stage-packages: + - test-ppa + +apps: + test-ppa: + command: usr/bin/test-ppa + +package-repositories: + - type: apt + formats: [deb, deb-src] + components: [main] + suites: [focal] + key-id: 78E1918602959B9C59103100F1831DDAFC42E99D + url: http://ppa.launchpad.net/snappy-dev/snapcraft-daily/ubuntu diff --git a/tests/spread/general/core22/test-apt-path/snap/snapcraft.yaml b/tests/spread/general/core22/test-apt-path/snap/snapcraft.yaml new file mode 100644 index 0000000000..f82ce23637 --- /dev/null +++ b/tests/spread/general/core22/test-apt-path/snap/snapcraft.yaml @@ -0,0 +1,19 @@ +name: test-apt-ppa +version: '1.0' +summary: test +description: test +grade: stable +confinement: strict +base: core22 + +parts: + test-ppa: + plugin: nil + stage-packages: + - datacenter-gpu-manager + +package-repositories: + - type: apt + url: http://developer.download.nvidia.com/compute/cuda/repos/ubuntu1804/x86_64 + key-id: AE09FE4BBD223A84B2CCFCE3F60F4B3D7FA2AF80 + path: / diff --git a/tests/spread/general/core22/test-apt-ppa/snap/snapcraft.yaml b/tests/spread/general/core22/test-apt-ppa/snap/snapcraft.yaml new file mode 100644 index 0000000000..1f12cfdb09 --- /dev/null +++ b/tests/spread/general/core22/test-apt-ppa/snap/snapcraft.yaml @@ -0,0 +1,21 @@ +name: test-apt-ppa +version: '1.0' +summary: test +description: test +grade: stable +confinement: strict +base: core22 + +parts: + test-ppa: + plugin: nil + stage-packages: + - test-ppa + +apps: + test-ppa: + command: usr/bin/test-ppa + +package-repositories: + - type: apt + ppa: snappy-dev/snapcraft-daily diff --git a/tests/spread/general/package-repositories/task.yaml b/tests/spread/general/package-repositories/task.yaml index 307ddb8267..3b82bafa43 100644 --- a/tests/spread/general/package-repositories/task.yaml +++ b/tests/spread/general/package-repositories/task.yaml @@ -17,7 +17,7 @@ restore: | if [ "$SPREAD_SYSTEM" = "ubuntu-16.04-64" ] || [ "$SPREAD_SYSTEM" = "ubuntu-18.04-64" ] || [ "$SPREAD_SYSTEM" = "ubuntu-20.04-64" ]; then snapcraft clean --use-lxd else - snapcraft --destructive-mode + snapcraft clean --destructive-mode fi rm -f ./*.snap diff --git a/tests/spread/tools/restore.sh b/tests/spread/tools/restore.sh index 1211a8ab65..2c5f80e551 100755 --- a/tests/spread/tools/restore.sh +++ b/tests/spread/tools/restore.sh @@ -13,7 +13,7 @@ apt-get autoremove --purge -y snaps="$(snap list | awk '{if (NR!=1) {print $1}}')" for snap in $snaps; do case "$snap" in - "bare" | "core" | "core18" | "core20" | "snapcraft" | "multipass" | "lxd" | "snapd") + "bare" | "core" | "core18" | "core20" | "core22" | "snapcraft" | "multipass" | "lxd" | "snapd") # Do not or cannot remove these ;; *) diff --git a/tests/unit/parts/test_lifecycle.py b/tests/unit/parts/test_lifecycle.py index 79d45f7199..2fb36dd1f7 100644 --- a/tests/unit/parts/test_lifecycle.py +++ b/tests/unit/parts/test_lifecycle.py @@ -78,6 +78,7 @@ def write_file( "license": None, "grade": "stable", "architectures": [], + "package-repositories": [], "assumes": [], "hooks": None, "passthrough": None, @@ -113,8 +114,18 @@ def test_snapcraft_yaml_load(new_dir, snapcraft_yaml, filename, mocker): project = Project.unmarshal(yaml_data) + if filename == "build-aux/snap/snapcraft.yaml": + assets_dir = Path("build-aux/snap") + else: + assets_dir = Path("snap") + assert run_command_mock.mock_calls == [ - call("pull", project, argparse.Namespace(parts=["part1"])) + call( + "pull", + project=project, + assets_dir=assets_dir, + parsed_args=argparse.Namespace(parts=["part1"]) + ), ] @@ -157,7 +168,9 @@ def test_lifecycle_run_command_step(cmd, step, snapcraft_yaml, new_dir, mocker): mocker.patch("snapcraft.meta.snap_yaml.write") pack_mock = mocker.patch("snapcraft.pack.pack_snap") - parts_lifecycle._run_command(cmd, project, argparse.Namespace()) + parts_lifecycle._run_command( + cmd, project=project, assets_dir=Path(), parsed_args=argparse.Namespace() + ) assert run_mock.mock_calls == [call(step)] assert pack_mock.mock_calls == [] @@ -170,7 +183,10 @@ def test_lifecycle_run_command_pack(snapcraft_yaml, new_dir, mocker): pack_mock = mocker.patch("snapcraft.pack.pack_snap") parts_lifecycle._run_command( - "pack", project, argparse.Namespace(directory=None, output=None) + "pack", + project=project, + assets_dir=Path(), + parsed_args=argparse.Namespace(directory=None, output=None), ) assert run_mock.mock_calls == [call("prime")] diff --git a/tests/unit/parts/test_parts.py b/tests/unit/parts/test_parts.py index f921d1d359..d2282935b9 100644 --- a/tests/unit/parts/test_parts.py +++ b/tests/unit/parts/test_parts.py @@ -31,7 +31,9 @@ def parts_data(): @pytest.mark.parametrize("step_name", ["pull", "overlay", "build", "stage", "prime"]) def test_parts_lifecycle_run(parts_data, step_name, new_dir, emitter): - lifecycle = PartsLifecycle(parts_data, work_dir=new_dir) + lifecycle = PartsLifecycle( + parts_data, work_dir=new_dir, assets_dir=new_dir, package_repositories=[] + ) lifecycle.run(step_name) assert lifecycle.prime_dir == Path(new_dir, "prime") assert lifecycle.prime_dir.is_dir() @@ -39,14 +41,18 @@ def test_parts_lifecycle_run(parts_data, step_name, new_dir, emitter): def test_parts_lifecycle_run_bad_step(parts_data, new_dir): - lifecycle = PartsLifecycle(parts_data, work_dir=new_dir) + lifecycle = PartsLifecycle( + parts_data, work_dir=new_dir, assets_dir=new_dir, package_repositories=[] + ) with pytest.raises(RuntimeError) as raised: lifecycle.run("invalid") assert str(raised.value) == "Invalid target step 'invalid'" def test_parts_lifecycle_run_internal_error(parts_data, new_dir, mocker): - lifecycle = PartsLifecycle(parts_data, work_dir=new_dir) + lifecycle = PartsLifecycle( + parts_data, work_dir=new_dir, assets_dir=new_dir, package_repositories=[] + ) mocker.patch("craft_parts.LifecycleManager.plan", side_effect=RuntimeError("crash")) with pytest.raises(RuntimeError) as raised: lifecycle.run("prime") @@ -55,7 +61,10 @@ def test_parts_lifecycle_run_internal_error(parts_data, new_dir, mocker): def test_parts_lifecycle_run_parts_error(new_dir): lifecycle = PartsLifecycle( - {"p1": {"plugin": "dump", "source": "foo"}}, work_dir=new_dir + {"p1": {"plugin": "dump", "source": "foo"}}, + work_dir=new_dir, + assets_dir=new_dir, + package_repositories=[], ) with pytest.raises(errors.PartsLifecycleError) as raised: lifecycle.run("prime") diff --git a/tests/unit/repo/test_installer.py b/tests/unit/repo/test_installer.py new file mode 100644 index 0000000000..0c57fb2dc0 --- /dev/null +++ b/tests/unit/repo/test_installer.py @@ -0,0 +1,43 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright 2022 Canonical Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from snapcraft.repo import installer +from snapcraft.repo.package_repository import ( + PackageRepositoryApt, + PackageRepositoryAptPPA, +) + + +def test_unmarshal_repositories(): + data = [ + { + "type": "apt", + "ppa": "test/somerepo", + }, + { + "type": "apt", + "url": "https://some/url", + "key-id": "ABCDE12345" * 4, + }, + ] + + pkg_repos = installer._unmarshal_repositories(data) + assert len(pkg_repos) == 2 + assert isinstance(pkg_repos[0], PackageRepositoryAptPPA) + assert pkg_repos[0].ppa == "test/somerepo" + assert isinstance(pkg_repos[1], PackageRepositoryApt) + assert pkg_repos[1].url == "https://some/url" + assert pkg_repos[1].key_id == "ABCDE12345" * 4 diff --git a/tests/unit/repo/test_projects.py b/tests/unit/repo/test_projects.py index 3d91e02686..f04bc73623 100644 --- a/tests/unit/repo/test_projects.py +++ b/tests/unit/repo/test_projects.py @@ -59,12 +59,12 @@ class TestAptDebValidation: { "type": "apt", "url": "https://some/url", - "key_id": "KEYID12345" * 4, + "key-id": "BCDEF12345" * 4, }, { "type": "apt", "url": "https://some/url", - "key_id": "KEYID12345" * 4, + "key-id": "BCDEF12345" * 4, "formats": ["deb"], "components": ["some", "components"], "key-server": "my-key-server", @@ -77,7 +77,7 @@ def test_apt_deb_valid(self, repo): apt_deb = AptDeb.unmarshal(repo) assert apt_deb.type == "apt" assert apt_deb.url == "https://some/url" - assert apt_deb.key_id == "KEYID12345" * 4 + assert apt_deb.key_id == "BCDEF12345" * 4 assert apt_deb.formats == (["deb"] if "formats" in repo else None) assert apt_deb.components == ( ["some", "components"] if "components" in repo else None @@ -89,9 +89,9 @@ def test_apt_deb_valid(self, repo): @pytest.mark.parametrize( "key_id,error", [ - ("KEYID12345" * 4, None), - ("KEYID12345", "string does not match regex"), - ("keyid12345" * 4, "string does not match regex"), + ("ABCDE12345" * 4, None), + ("KEYID12345" * 4, "string does not match regex"), + ("abcde12345" * 4, "string does not match regex"), ], ) def test_apt_deb_key_id(self, key_id, error): @@ -121,7 +121,7 @@ def test_apt_deb_formats(self, formats): repo = { "type": "apt", "url": "https://some/url", - "key-id": "KEYID12345" * 4, + "key-id": "ABCDE12345" * 4, "formats": formats, } diff --git a/tests/unit/test_projects.py b/tests/unit/test_projects.py index 982a3bc625..7104aa907d 100644 --- a/tests/unit/test_projects.py +++ b/tests/unit/test_projects.py @@ -334,7 +334,7 @@ def test_project_package_repository(self, yaml_data): { "type": "apt", "url": "https://some/url", - "key_id": "KEYID12345" * 4, + "key-id": "ABCDE12345" * 4, }, ] project = Project.unmarshal(yaml_data(package_repositories=repos)) From 7baf444124b4221b6a0ff4a60a3165eeffa41036 Mon Sep 17 00:00:00 2001 From: Sergio Schvezov Date: Wed, 23 Feb 2022 16:34:38 -0300 Subject: [PATCH 039/167] tests: pass proper type to run for version Signed-off-by: Sergio Schvezov --- tests/unit/commands/test_version.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/unit/commands/test_version.py b/tests/unit/commands/test_version.py index 7af1f59a5c..4c543ddc1c 100644 --- a/tests/unit/commands/test_version.py +++ b/tests/unit/commands/test_version.py @@ -14,11 +14,13 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +from argparse import Namespace + from snapcraft import __version__ from snapcraft.commands.version import VersionCommand def test_version_command(emitter): cmd = VersionCommand(None) - cmd.run([]) + cmd.run(Namespace()) emitter.assert_recorded([f"snapcraft {__version__}"]) From cfbe60ac40d4a6c79be25284671d4b2cd0f75689 Mon Sep 17 00:00:00 2001 From: Sergio Schvezov Date: Wed, 23 Feb 2022 13:03:46 -0300 Subject: [PATCH 040/167] parts: support for grammar parsing Signed-off-by: Sergio Schvezov --- snapcraft/parts/grammar.py | 79 ++++++++++++++++ snapcraft/parts/lifecycle.py | 18 +++- tests/unit/parts/test_grammar.py | 153 +++++++++++++++++++++++++++++++ 3 files changed, 248 insertions(+), 2 deletions(-) create mode 100644 snapcraft/parts/grammar.py create mode 100644 tests/unit/parts/test_grammar.py diff --git a/snapcraft/parts/grammar.py b/snapcraft/parts/grammar.py new file mode 100644 index 0000000000..af20e61abf --- /dev/null +++ b/snapcraft/parts/grammar.py @@ -0,0 +1,79 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright 2022 Canonical Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +"""Grammar processor.""" + +from typing import Any, Dict + +from craft_grammar import GrammarProcessor + +_KEYS = [ + "source", + "build-environment", + "build-packages", + "stage-packages", + "build-snaps", + "stage-snaps", +] + +_SCALAR_VALUES = ["source"] + + +def process_part( + *, part_yaml_data: Dict[str, Any], processor: GrammarProcessor +) -> Dict[str, Any]: + """Process grammar for a given part.""" + existing_keys = (key for key in _KEYS if key in part_yaml_data) + + for key in existing_keys: + unprocessed_grammar = part_yaml_data[key] + + if key in _SCALAR_VALUES and isinstance(unprocessed_grammar, str): + unprocessed_grammar = [unprocessed_grammar] + + processed_grammar = processor.process(grammar=unprocessed_grammar) + + if key in _SCALAR_VALUES and isinstance(processed_grammar, list): + if processed_grammar: + processed_grammar = processed_grammar[0] + else: + processed_grammar = None + part_yaml_data[key] = processed_grammar + + return part_yaml_data + + +def process_parts( + *, parts_yaml_data: Dict[str, Any], arch: str, target_arch: str +) -> Dict[str, Any]: + """Process grammar for parts. + + :param yaml_data: unprocessed snapcraft.yaml. + :returns: process snapcraft.yaml. + """ + # TODO: make checker optional in craft-grammar. + processor = GrammarProcessor( + arch=arch, + target_arch=target_arch, + checker=lambda x: x == x, # pylint: disable=comparison-with-itself + ) + + for part_name in parts_yaml_data: + parts_yaml_data[part_name] = process_part( + part_yaml_data=parts_yaml_data[part_name], processor=processor + ) + + return parts_yaml_data diff --git a/snapcraft/parts/lifecycle.py b/snapcraft/parts/lifecycle.py index 6d93fbb93b..f4a9be7f5c 100644 --- a/snapcraft/parts/lifecycle.py +++ b/snapcraft/parts/lifecycle.py @@ -22,12 +22,15 @@ import yaml import yaml.error from craft_cli import emit +from craft_parts import infos from snapcraft import errors, pack from snapcraft.meta import snap_yaml from snapcraft.parts import PartsLifecycle from snapcraft.projects import Project +from . import grammar + if TYPE_CHECKING: import argparse @@ -73,8 +76,12 @@ def run(command_name: str, parsed_args: "argparse.Namespace") -> None: # TODO: apply extensions # yaml_data = apply_extensions(yaml_data) - # TODO: process grammar - # yaml_data = process_grammar(yaml_data) + # TODO: support for target_arch + arch = _get_arch() + if "parts" in yaml_data: + yaml_data["parts"] = grammar.process_parts( + parts_yaml_data=yaml_data["parts"], arch=arch, target_arch=arch + ) project = Project.unmarshal(yaml_data) @@ -132,3 +139,10 @@ def _load_yaml(filename: Path) -> Dict[str, Any]: raise errors.SnapcraftError(msg) from err except yaml.error.YAMLError as err: raise errors.SnapcraftError(f"YAML parsing error: {err!s}") from err + + +# TODO Needs exposure from craft-parts. +def _get_arch() -> str: + machine = infos._get_host_architecture() # pylint: disable=protected-access + # FIXME Raise the potential KeyError. + return infos._ARCH_TRANSLATIONS[machine]["deb"] # pylint: disable=protected-access diff --git a/tests/unit/parts/test_grammar.py b/tests/unit/parts/test_grammar.py new file mode 100644 index 0000000000..856d2bfd20 --- /dev/null +++ b/tests/unit/parts/test_grammar.py @@ -0,0 +1,153 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright 2022 Canonical Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +"""Grammar processor tests.""" + +from collections import namedtuple + +import pytest +from craft_grammar import GrammarProcessor + +from snapcraft.parts.grammar import process_part, process_parts + +_PROCESSOR = GrammarProcessor( + arch="amd64", + target_arch="amd64", + checker=lambda x: x == x, # pylint: disable=comparison-with-itself +) +GrammarEntry = namedtuple("GrammarEntry", ["value", "expected"]) + +GRAMMAR_SCALAR_ENTRIES = [ + # no grammar. + GrammarEntry("entry", "entry"), + # on arch match. + GrammarEntry([{"on amd64": "entry"}], "entry"), + # on else match. + GrammarEntry([{"on arm64": "entry"}, {"else": "else-entry"}], "else-entry"), + # on other-arch no else. + GrammarEntry([{"on arm64": "entry"}], None), + # TODO: on to match +] + + +@pytest.mark.parametrize("grammar_entry", GRAMMAR_SCALAR_ENTRIES) +@pytest.mark.parametrize("key", ["source"]) +def test_scalar_values(key, grammar_entry): + part_yaml_data = {key: grammar_entry.value} + + value = process_part(part_yaml_data=part_yaml_data, processor=_PROCESSOR) + + expected = {key: grammar_entry.expected} + assert value == expected + + +GRAMMAR_LIST_ENTRIES = [ + # no grammar. + GrammarEntry(["entry"], ["entry"]), + # on arch match. + GrammarEntry([{"on amd64": ["entry"]}], ["entry"]), + # on else match. + GrammarEntry([{"on arm64": ["entry"]}, {"else": ["else-entry"]}], ["else-entry"]), + # on other-arch no else. + GrammarEntry([{"on arm64": ["entry"]}], []), + # TODO: on to match +] + + +@pytest.mark.parametrize("grammar_entry", GRAMMAR_LIST_ENTRIES) +@pytest.mark.parametrize( + "key", + [ + "build-environment", + "build-packages", + "stage-packages", + "build-snaps", + "stage-snaps", + ], +) +def test_list_values(key, grammar_entry): + part_yaml_data = {key: grammar_entry.value} + + value = process_part(part_yaml_data=part_yaml_data, processor=_PROCESSOR) + + expected = {key: grammar_entry.expected} + assert value == expected + + +def test_process_grammar(): + assert process_parts( + parts_yaml_data={ + "no-grammar": { + "source": "source-foo", + "build-environment": ["env-foo"], + "build-packages": ["build-pkg-foo"], + "stage-packages": ["stage-pkg-foo"], + "build-snaps": ["build-snap-foo"], + "stage-snaps": ["stage-snap-foo"], + }, + "grammar": { + "source": [ + { + "on amd64": "source-foo", + }, + ], + "build-environment": [ + { + "on amd64": ["env-foo"], + }, + ], + "build-packages": [ + { + "on amd64": ["build-pkg-foo"], + }, + ], + "stage-packages": [ + { + "on amd64": ["stage-pkg-foo"], + }, + ], + "build-snaps": [ + { + "on amd64": ["build-snap-foo"], + }, + ], + "stage-snaps": [ + { + "on amd64": ["stage-snap-foo"], + }, + ], + }, + }, + arch="amd64", + target_arch="amd64", + ) == { + "no-grammar": { + "source": "source-foo", + "build-environment": ["env-foo"], + "build-packages": ["build-pkg-foo"], + "stage-packages": ["stage-pkg-foo"], + "build-snaps": ["build-snap-foo"], + "stage-snaps": ["stage-snap-foo"], + }, + "grammar": { + "source": "source-foo", + "build-environment": ["env-foo"], + "build-packages": ["build-pkg-foo"], + "stage-packages": ["stage-pkg-foo"], + "build-snaps": ["build-snap-foo"], + "stage-snaps": ["stage-snap-foo"], + }, + } From abe7e76bbcd8b0f646601dbc43b156b366928695 Mon Sep 17 00:00:00 2001 From: Callahan Kovacs Date: Tue, 22 Feb 2022 07:47:20 -0600 Subject: [PATCH 041/167] lint: apply autoformat-black to source files Signed-off-by: Callahan Kovacs --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index a782a9c4b1..256e7f4775 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,7 @@ SOURCES=setup.py snapcraft tests/*.py tests/unit .PHONY: autoformat-black autoformat-black: - black . + black $(SOURCES) .PHONY: freeze-requirements freeze-requirements: From de33c39b596136ebb77cd81be7657a5ec980583b Mon Sep 17 00:00:00 2001 From: Sergio Schvezov Date: Tue, 22 Feb 2022 14:54:19 -0300 Subject: [PATCH 042/167] ci: add python test matrix for unit tests Signed-off-by: Sergio Schvezov --- .github/workflows/tests.yaml | 35 ++++++++++++++++++++++++++++++++--- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 3be16a5f76..7b4e87a2c0 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -1,8 +1,14 @@ name: Python Environment Tests -on: [pull_request, push] +on: + push: + branches: + - "main" + - "snapcraft/7.0" + - "release/*" + pull_request: jobs: - static-and-unit-tests: + linters: runs-on: ubuntu-20.04 steps: - name: Checkout code @@ -50,9 +56,32 @@ jobs: sudo snap install --classic node sudo snap install --classic pyright make test-pyright + + tests: + strategy: + fail-fast: false + matrix: + python-version: ["3.8", "3.9", "3.10"] + + runs-on: ubuntu-20.04 + steps: + - name: Checkout code + uses: actions/checkout@v2 + with: + fetch-depth: 0 + - name: Set up Python ${{ matrix.python-version }} on ${{ matrix.os }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + sudo apt update + sudo apt install -y libapt-pkg-dev libyaml-dev xdelta3 shellcheck + pip install -U wheel setuptools pip + pip install -U -r requirements.txt -r requirements-devel.txt + pip install . - name: Run unit tests run: | - source ${HOME}/.venv/snapcraft/bin/activate make test-units - name: Upload code coverage uses: codecov/codecov-action@v1 From 1a3781e6293383e6f695fcda6d355d8cf678987a Mon Sep 17 00:00:00 2001 From: Sergio Schvezov Date: Tue, 22 Feb 2022 15:40:19 -0300 Subject: [PATCH 043/167] legacy: add environment to ignore missing C YAML Not working with Python 3.10, enable for tests. Signed-off-by: Sergio Schvezov --- .github/workflows/tests.yaml | 2 ++ snapcraft_legacy/yaml_utils/__init__.py | 16 +++++++++++----- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 7b4e87a2c0..b62bdc79db 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -81,6 +81,8 @@ jobs: pip install -U -r requirements.txt -r requirements-devel.txt pip install . - name: Run unit tests + env: + SNAPCRAFT_IGNORE_YAML_BINDINGS: "1" run: | make test-units - name: Upload code coverage diff --git a/snapcraft_legacy/yaml_utils/__init__.py b/snapcraft_legacy/yaml_utils/__init__.py index 9ecb94e8db..42e49f4a35 100644 --- a/snapcraft_legacy/yaml_utils/__init__.py +++ b/snapcraft_legacy/yaml_utils/__init__.py @@ -17,6 +17,7 @@ import codecs import collections import logging +import os from typing import Any, Dict, Optional, TextIO, Union import yaml @@ -29,9 +30,14 @@ # The C-based loaders/dumpers aren't available everywhere, but they're much faster. # Use them if possible. If not, we could fallback to the normal loader/dumper, but # they actually behave differently, so raise an error instead. - from yaml import CSafeDumper, CSafeLoader # type: ignore + from yaml import CSafeDumper as SafeDumper # type: ignore + from yaml import CSafeLoader as SafeLoader # type: ignore except ImportError: - raise RuntimeError("Snapcraft requires PyYAML to be built with libyaml bindings") + if not os.getenv("SNAPCRAFT_IGNORE_YAML_BINDINGS"): + raise RuntimeError( + "Snapcraft requires PyYAML to be built with libyaml bindings" + ) + from yaml import SafeDumper, SafeLoader def load_yaml_file(yaml_file_path: str) -> collections.OrderedDict: @@ -96,7 +102,7 @@ def dump( ) -class _SafeOrderedLoader(CSafeLoader): +class _SafeOrderedLoader(SafeLoader): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -105,7 +111,7 @@ def __init__(self, *args, **kwargs): ) -class _SafeOrderedDumper(CSafeDumper): +class _SafeOrderedDumper(SafeDumper): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.add_representer(str, _str_presenter) @@ -166,7 +172,7 @@ def _str_presenter(dumper, data): class OctInt(SnapcraftYAMLObject): """An int represented in octal form.""" - yaml_tag = u"!OctInt" + yaml_tag = "!OctInt" def __init__(self, value): super().__init__() From 6eb5e214f02ffd4ca55e128e4ee078aac46545ff Mon Sep 17 00:00:00 2001 From: Sergio Schvezov Date: Tue, 22 Feb 2022 22:16:51 -0300 Subject: [PATCH 044/167] legacy elf tests: switch to a pie executable Remove the variances from using sys.executable in tests now that there's a python strategy matrix for github actions. Signed-off-by: Sergio Schvezov --- tests/legacy/unit/test_elf.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/legacy/unit/test_elf.py b/tests/legacy/unit/test_elf.py index 83e2774d89..2f2089cb6f 100644 --- a/tests/legacy/unit/test_elf.py +++ b/tests/legacy/unit/test_elf.py @@ -68,9 +68,9 @@ def test_extract_ld_library_paths(self): class TestElfFileSmoketest(unit.TestCase): def test_bin_echo(self): # Try parsing a file without the pyelftools logic mocked out - elf_file = elf.ElfFile(path=sys.executable) + elf_file = elf.ElfFile(path="/bin/ls") - self.assertThat(elf_file.path, Equals(sys.executable)) + self.assertThat(elf_file.path, Equals("/bin/ls")) # The arch attribute will be a tuple of three strings self.assertTrue(isinstance(elf_file.arch, tuple)) @@ -110,7 +110,7 @@ def test_bin_echo(self): self.assertTrue(isinstance(elf_file.has_debug_info, bool)) # Ensure type is detered as executable. - self.assertThat(elf_file.elf_type, Equals("ET_EXEC")) + self.assertThat(elf_file.elf_type, Equals("ET_DYN")) class TestInvalidElf(unit.TestCase): From aaea4e054796389ef76ae112cf1fa00bd269652d Mon Sep 17 00:00:00 2001 From: Sergio Schvezov Date: Tue, 22 Feb 2022 22:24:03 -0300 Subject: [PATCH 045/167] legacy unit tests: mock LD_LIBRARY_PATH for wstool Signed-off-by: Sergio Schvezov --- tests/legacy/unit/plugins/v1/ros/test_wstool.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/legacy/unit/plugins/v1/ros/test_wstool.py b/tests/legacy/unit/plugins/v1/ros/test_wstool.py index 9263000bdb..799dd91107 100644 --- a/tests/legacy/unit/plugins/v1/ros/test_wstool.py +++ b/tests/legacy/unit/plugins/v1/ros/test_wstool.py @@ -19,6 +19,7 @@ import subprocess from unittest import mock +import fixtures from testtools.matchers import Contains, Equals import snapcraft_legacy @@ -172,6 +173,8 @@ def test_run(self): # properly. os.makedirs(os.path.join(wstool._wstool_install_path, "lib")) + self.useFixture(fixtures.EnvironmentVariable("LD_LIBRARY_PATH", None)) + wstool._run(["init"]) class check_env: From d658b1df92e35f72f6f7ac81d19961e4940541d6 Mon Sep 17 00:00:00 2001 From: Sergio Schvezov Date: Thu, 24 Feb 2022 10:45:33 -0300 Subject: [PATCH 046/167] spread: debug output on project errors Signed-off-by: Sergio Schvezov --- spread.yaml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/spread.yaml b/spread.yaml index c4769edd19..7120b6f407 100644 --- a/spread.yaml +++ b/spread.yaml @@ -187,8 +187,12 @@ prepare: | # Hold snap refreshes for 24h. snap set system refresh.hold="$(date --date=tomorrow +%Y-%m-%dT%H:%M:%S%:z)" - snap watch --last=auto-refresh? - snap watch --last=install? + if ! snap watch --last=auto-refresh?; then + journalctl -xe + fi + if ! snap watch --last=install?; then + journalctl -xe + fi if [ "$SPREAD_SYSTEM" = "ubuntu-18.04-64" ] || [ "$SPREAD_SYSTEM" = "ubuntu-20.04-64" ]; then /snap/bin/lxd waitready --timeout=30 From 30ef5a1086a380c5b148a3d0a4f3abfd414a4096 Mon Sep 17 00:00:00 2001 From: Callahan Kovacs Date: Tue, 22 Feb 2022 12:50:10 -0600 Subject: [PATCH 047/167] meta: support application fields Signed-off-by: Callahan Kovacs --- Makefile | 2 +- pyproject.toml | 3 + schema/snapcraft.json | 3 +- snapcraft/meta/snap_yaml.py | 78 ++- snapcraft/projects.py | 73 ++- .../legacy/unit/project_loader/test_schema.py | 2 +- tests/unit/meta/test_snap_yaml.py | 141 ++++- tests/unit/test_projects.py | 485 +++++++++++++++--- 8 files changed, 675 insertions(+), 112 deletions(-) diff --git a/Makefile b/Makefile index 256e7f4775..2c8ea3c202 100644 --- a/Makefile +++ b/Makefile @@ -35,7 +35,7 @@ test-pydocstyle: .PHONY: test-pylint test-pylint: pylint snapcraft - pylint tests/*.py tests/unit --disable=invalid-name,missing-module-docstring,missing-function-docstring,no-self-use,duplicate-code,protected-access,unspecified-encoding + pylint tests/*.py tests/unit --disable=invalid-name,missing-module-docstring,missing-function-docstring,no-self-use,duplicate-code,protected-access,unspecified-encoding,too-many-public-methods .PHONY: test-pyright test-pyright: diff --git a/pyproject.toml b/pyproject.toml index 2d4bc330c8..bbe92e37f0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,3 +43,6 @@ extension-pkg-allow-list = [ "pytest", ] load-plugins = "pylint_fixme_info,pylint_pytest" + +[tool.pylint.SIMILARITIES] +min-similarity-lines=10 diff --git a/schema/snapcraft.json b/schema/snapcraft.json index 76149f9128..501f06cb55 100644 --- a/schema/snapcraft.json +++ b/schema/snapcraft.json @@ -820,7 +820,8 @@ "uniqueItems": true, "items": { "type": "string", - "pattern": "^[a-zA-Z0-9][-_.a-zA-Z0-9]*$" + "pattern": "^[a-zA-Z0-9][-_.a-zA-Z0-9]*$", + "validation-failure": "{.instance!r} is not a valid alias. Aliases must be strings, begin with an ASCII alphanumeric character, and can only use ASCII alphanumeric characters and the following special characters: . _ -" } }, "environment": { diff --git a/snapcraft/meta/snap_yaml.py b/snapcraft/meta/snap_yaml.py index 5d8c2f3283..afd82fac4b 100644 --- a/snapcraft/meta/snap_yaml.py +++ b/snapcraft/meta/snap_yaml.py @@ -18,7 +18,7 @@ import textwrap from pathlib import Path -from typing import Any, Dict, List, Optional, cast +from typing import Any, Dict, List, Optional, Union, cast import yaml from pydantic_yaml import YamlModel @@ -26,19 +26,51 @@ from snapcraft.projects import Project +class Socket(YamlModel): + """snap.yaml app socket entry.""" + + listen_stream: Union[int, str] + socket_mode: Optional[int] + + class SnapApp(YamlModel): """Snap.yaml app entry. This is currently a partial implementation, see https://snapcraft.io/docs/snap-format for details. - TODO: add missing properties, improve validation (CRAFT-802) + TODO: implement desktop (CRAFT-804) + TODO: implement extensions (CRAFT-805) + TODO: implement passthrough (CRAFT-854) + TODO: implement slots (CRAFT-816) """ command: str - command_chain: List[str] - environment: Optional[Dict[str, Any]] + autostart: Optional[str] + common_id: Optional[str] + bus_name: Optional[str] + completer: Optional[str] + stop_command: Optional[str] + post_stop_command: Optional[str] + start_timeout: Optional[str] + stop_timeout: Optional[str] + watchdog_timeout: Optional[str] + reload_command: Optional[str] + restart_delay: Optional[str] + timer: Optional[str] + daemon: Optional[str] + after: Optional[List[str]] + before: Optional[List[str]] + refresh_mode: Optional[str] + stop_mode: Optional[str] + restart_condition: Optional[str] + install_mode: Optional[str] plugs: Optional[List[str]] + aliases: Optional[List[str]] + environment: Optional[Dict[str, Any]] + adapter: Optional[str] + command_chain: List[str] + sockets: Optional[Dict[str, Socket]] class Config: # pylint: disable=too-few-public-methods """Pydantic model configuration.""" @@ -53,7 +85,8 @@ class SnapMetadata(YamlModel): This is currently a partial implementation, see https://snapcraft.io/docs/snap-format for details. - TODO: add missing properties, improve validation (CRAFT-802) + TODO: implement adopt-info (CRAFT-803) + TODO: implement hooks (CRAFT-808) """ name: str @@ -83,11 +116,42 @@ def write(project: Project, prime_dir: Path, *, arch: str): snap_apps: Dict[str, SnapApp] = {} if project.apps: for name, app in project.apps.items(): + + app_sockets: Dict[str, Socket] = {} + if app.sockets: + for socket_name, socket in app.sockets.items(): + app_sockets[socket_name] = Socket( + listen_stream=socket.listen_stream, + socket_mode=socket.socket_mode, + ) + snap_apps[name] = SnapApp( command=app.command, - command_chain=["snap/command-chain/snapcraft-runner"], - environment=app.environment, + autostart=app.autostart, + common_id=app.common_id, + bus_name=app.bus_name, + completer=app.completer, + stop_command=app.stop_command, + post_stop_command=app.post_stop_command, + start_timeout=app.start_timeout, + stop_timeout=app.stop_timeout, + watchdog_timeout=app.watchdog_timeout, + reload_command=app.reload_command, + restart_delay=app.restart_delay, + timer=app.timer, + daemon=app.daemon, + after=app.after if app.after else None, + before=app.before if app.before else None, + refresh_mode=app.refresh_mode, + stop_mode=app.stop_mode, + restart_condition=app.restart_condition, + install_mode=app.install_mode, plugs=app.plugs, + aliases=app.aliases, + environment=app.environment, + adapter=app.adapter, + command_chain=["snap/command-chain/snapcraft-runner"], + sockets=app_sockets if app_sockets else None, ) # FIXME: handle adopted parameters diff --git a/snapcraft/projects.py b/snapcraft/projects.py index d4ffc75e66..982fd7d351 100644 --- a/snapcraft/projects.py +++ b/snapcraft/projects.py @@ -47,16 +47,35 @@ class Config: # pylint: disable=too-few-public-methods # see https://github.com/samuelcolvin/pydantic/issues/975#issuecomment-551147305 # fmt: off if TYPE_CHECKING: - CommandChainStr = str UniqueStrList = List[str] - UniqueAliasList = List[str] else: - CommandChainStr = constr(regex=r"^[A-Za-z0-9/._#:$-]*$") UniqueStrList = conlist(str, unique_items=True) - UniqueAliasList = conlist(constr(regex=r"^[a-zA-Z0-9][-_.a-zA-Z0-9]*$"), unique_items=True) # fmt: on +class Socket(ProjectModel): + """Snapcraft app socket definition.""" + + listen_stream: Union[int, str] + socket_mode: Optional[int] + + @pydantic.validator("listen_stream") + @classmethod + def _validate_list_stream(cls, listen_stream): + if isinstance(listen_stream, int): + if listen_stream < 1 or listen_stream > 65535: + raise ValueError( + f"{listen_stream!r} is not an integer between 1 and 65535 (inclusive)." + ) + elif isinstance(listen_stream, str): + if not re.match(r"^[A-Za-z0-9/._#:$-]*$", listen_stream): + raise ValueError( + f"{listen_stream!r} is not a valid socket path (e.g. /tmp/mysocket.sock)." + ) + + return listen_stream + + class App(ProjectModel): """Snapcraft project app definition.""" @@ -64,6 +83,7 @@ class App(ProjectModel): autostart: Optional[str] common_id: Optional[str] bus_name: Optional[str] + desktop: Optional[str] completer: Optional[str] stop_command: Optional[str] post_stop_command: Optional[str] @@ -103,15 +123,17 @@ class App(ProjectModel): install_mode: Optional[Literal["enable", "disable"]] slots: Optional[UniqueStrList] plugs: Optional[UniqueStrList] - aliases: Optional[UniqueAliasList] + aliases: Optional[UniqueStrList] environment: Optional[Dict[str, Any]] - command_chain: List[CommandChainStr] = [] - # TODO: sockets + adapter: Optional[Literal["none", "full"]] + command_chain: List[str] = [] + sockets: Optional[Dict[str, Socket]] + # TODO: implement passthrough (CRAFT-854) @pydantic.validator("autostart") @classmethod - def _validate_desktop_name(cls, name): - if not re.match(r"^[A-Za-z0-9. _#:$-]+\\.desktop$", name): + def _validate_autostart_name(cls, name): + if not re.match(r"^[A-Za-z0-9. _#:$-]+\.desktop$", name): raise ValueError( f"{name!r} is not a valid desktop file name (e.g. myapp.desktop)" ) @@ -136,11 +158,37 @@ def _validate_time(cls, timeval): return timeval + @pydantic.validator("command_chain") + @classmethod + def _validate_command_chain(cls, command_chains): + for command_chain in command_chains: + if not re.match(r"^[A-Za-z0-9/._#:$-]*$", command_chain): + raise ValueError( + f"{command_chain!r} is not a valid command chain. Command chain entries must " + "be strings, and can only use ASCII alphanumeric characters and the following " + "special characters: / . _ # : $ -" + ) + + return command_chains + + @pydantic.validator("aliases") + @classmethod + def _validate_aliases(cls, aliases): + for alias in aliases: + if not re.match(r"^[a-zA-Z0-9][-_.a-zA-Z0-9]*$", alias): + raise ValueError( + f"{alias!r} is not a valid alias. Aliases must be strings, begin with an ASCII " + "alphanumeric character, and can only use ASCII alphanumeric characters and " + "the following special characters: . _ -" + ) + + return aliases + class Hook(ProjectModel): """Snapcraft project hook definition.""" - command_chain: List[CommandChainStr] = [] + command_chain: List[str] = [] environment: List[Dict[str, str]] = [] plugs: UniqueStrList = [] passthrough: Optional[Dict[str, Any]] @@ -333,6 +381,11 @@ def _format_pydantic_errors(errors, *, file_name: str = "snapcraft.yaml"): combined.append( f"- extra field {field_name} not permitted in {location} configuration" ) + elif formatted_msg == "the list has duplicated items": + field_name, location = _printable_field_location_split(formatted_loc) + combined.append( + f" - duplicate entries in {field_name} not permitted in {location} configuration" + ) elif formatted_loc == "__root__": combined.append(f"- {formatted_msg}") else: diff --git a/tests/legacy/unit/project_loader/test_schema.py b/tests/legacy/unit/project_loader/test_schema.py index 1030015d7a..f16a952c63 100644 --- a/tests/legacy/unit/project_loader/test_schema.py +++ b/tests/legacy/unit/project_loader/test_schema.py @@ -286,7 +286,7 @@ def test_invalid_alias(self): ) expected = ( "The {path!r} property does not match the required schema: " - "{alias!r} does not match ".format( + "{alias!r} is not a valid alias.".format( path="apps/test/aliases[0]", alias=".test" ) ) diff --git a/tests/unit/meta/test_snap_yaml.py b/tests/unit/meta/test_snap_yaml.py index ebfca00595..ce948e0001 100644 --- a/tests/unit/meta/test_snap_yaml.py +++ b/tests/unit/meta/test_snap_yaml.py @@ -26,6 +26,69 @@ @pytest.fixture def simple_project(): + snapcraft_yaml = textwrap.dedent( + """\ + name: mytest + version: 1.29.3 + base: core22 + summary: Single-line elevator pitch for your amazing snap + description: | + This is my-snap's description. You have a paragraph or two to tell the + most important story about your snap. Keep it under 100 words though, + we live in tweetspace and your description wants to look good in the snap + store. + + grade: stable + confinement: strict + + parts: + part1: + plugin: nil + + apps: + app1: + command: bin/mytest + """ + ) + data = yaml.safe_load(snapcraft_yaml) + yield Project.unmarshal(data) + + +def test_simple_snap_yaml(simple_project, new_dir): + snap_yaml.write(simple_project, prime_dir=Path(new_dir), arch="arch") + yaml_file = Path("meta/snap.yaml") + assert yaml_file.is_file() + + content = yaml_file.read_text() + assert content == textwrap.dedent( + """\ + name: mytest + version: 1.29.3 + summary: Single-line elevator pitch for your amazing snap + description: | + This is my-snap's description. You have a paragraph or two to tell the + most important story about your snap. Keep it under 100 words though, + we live in tweetspace and your description wants to look good in the snap + store. + type: app + architectures: + - arch + base: core22 + assumes: + - command-chain + apps: + app1: + command: bin/mytest + command_chain: + - snap/command-chain/snapcraft-runner + confinement: strict + grade: stable + """ + ) + + +@pytest.fixture +def complex_project(): snapcraft_yaml = textwrap.dedent( """\ name: mytest @@ -42,7 +105,7 @@ def simple_project(): confinement: strict environment: - GLOBAL_VARIABLE: "my-global-variable" + GLOBAL_VARIABLE: test-global-variable parts: part1: @@ -51,16 +114,46 @@ def simple_project(): apps: app1: command: bin/mytest + autostart: test-app.desktop + common_id: test-common-id + bus_name: test-bus-name + completer: test-completer + stop_command: test-stop-command + post_stop_command: test-post-stop-command + start_timeout: 1s + stop_timeout: 2s + watchdog_timeout: 3s + reload_command: test-reload-command + restart_delay: 4s + timer: test-timer + daemon: simple + after: [test-after-1, test-after-2] + before: [test-before-1, test-before-2] + refresh_mode: endure + stop_mode: sigterm + restart_condition: on-success + install_mode: enable + aliases: [test-alias-1, test-alias-2] environment: - APP_VARIABLE: "my-app-variable" + APP_VARIABLE: test-app-variable + adapter: none + command_chain: + - snap/command-chain/snapcraft-runner + sockets: + test-socket-1: + listen_stream: /tmp/test-socket.sock + socket_mode: 0 + test-socket-2: + listen_stream: 100 + socket_mode: 1 """ ) data = yaml.safe_load(snapcraft_yaml) yield Project.unmarshal(data) -def test_snap_yaml(simple_project, new_dir): - snap_yaml.write(simple_project, prime_dir=Path(new_dir), arch="arch") +def test_complex_snap_yaml(complex_project, new_dir): + snap_yaml.write(complex_project, prime_dir=Path(new_dir), arch="arch") yaml_file = Path("meta/snap.yaml") assert yaml_file.is_file() @@ -84,13 +177,47 @@ def test_snap_yaml(simple_project, new_dir): apps: app1: command: bin/mytest + autostart: test-app.desktop + common_id: test-common-id + bus_name: test-bus-name + completer: test-completer + stop_command: test-stop-command + post_stop_command: test-post-stop-command + start_timeout: 1s + stop_timeout: 2s + watchdog_timeout: 3s + reload_command: test-reload-command + restart_delay: 4s + timer: test-timer + daemon: simple + after: + - test-after-1 + - test-after-2 + before: + - test-before-1 + - test-before-2 + refresh_mode: endure + stop_mode: sigterm + restart_condition: on-success + install_mode: enable + aliases: + - test-alias-1 + - test-alias-2 + environment: + APP_VARIABLE: test-app-variable + adapter: none command_chain: - snap/command-chain/snapcraft-runner - environment: - APP_VARIABLE: my-app-variable + sockets: + test-socket-1: + listen_stream: /tmp/test-socket.sock + socket_mode: 0 + test-socket-2: + listen_stream: 100 + socket_mode: 1 confinement: strict grade: stable environment: - GLOBAL_VARIABLE: my-global-variable + GLOBAL_VARIABLE: test-global-variable """ ) diff --git a/tests/unit/test_projects.py b/tests/unit/test_projects.py index 7104aa907d..656656ced3 100644 --- a/tests/unit/test_projects.py +++ b/tests/unit/test_projects.py @@ -14,7 +14,6 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -import re from typing import Any, Dict import pytest @@ -24,8 +23,8 @@ @pytest.fixture -def yaml_data(): - def _yaml_data( +def project_yaml_data(): + def _project_yaml_data( *, name: str = "name", version: str = "0.1", summary: str = "summary", **kwargs ) -> Dict[str, Any]: return { @@ -40,14 +39,34 @@ def _yaml_data( **kwargs, } - yield _yaml_data + yield _project_yaml_data + + +@pytest.fixture +def app_yaml_data(project_yaml_data): + def _app_yaml_data(**kwargs) -> Dict[str, Any]: + data = project_yaml_data() + data["apps"] = {"app1": {"command": "/bin/true", **kwargs}} + return data + + yield _app_yaml_data + + +@pytest.fixture +def socket_yaml_data(app_yaml_data): + def _socket_yaml_data(**kwargs) -> Dict[str, Any]: + data = app_yaml_data() + data["apps"]["app1"]["sockets"] = {"socket1": {**kwargs}} + return data + + yield _socket_yaml_data class TestProjectDefaults: """Ensure unspecified items have the correct default value.""" - def test_project_defaults(self, yaml_data): - project = Project.unmarshal(yaml_data()) + def test_project_defaults(self, project_yaml_data): + project = Project.unmarshal(project_yaml_data()) assert project.build_base == project.base assert project.compression == "xz" @@ -71,8 +90,8 @@ def test_project_defaults(self, yaml_data): assert project.epoch is None assert project.environment is None - def test_app_defaults(self, yaml_data): - data = yaml_data(apps={"app1": {"command": "/bin/true"}}) + def test_app_defaults(self, project_yaml_data): + data = project_yaml_data(apps={"app1": {"command": "/bin/true"}}) project = Project.unmarshal(data) assert project.apps is not None @@ -120,8 +139,8 @@ class TestProjectValidation: "parts", ], ) - def test_mandatory_fields(self, field, yaml_data): - data = yaml_data() + def test_mandatory_fields(self, field, project_yaml_data): + data = project_yaml_data() data.pop(field) error = f"field {field!r} required in top-level configuration" with pytest.raises(errors.ProjectValidationError, match=error): @@ -137,8 +156,8 @@ def test_mandatory_fields(self, field, yaml_data): ("snapd", False), ], ) - def test_mandatory_base(self, snap_type, requires_base, yaml_data): - data = yaml_data(type=snap_type) + def test_mandatory_base(self, snap_type, requires_base, project_yaml_data): + data = project_yaml_data(type=snap_type) data.pop("base") if requires_base: @@ -149,15 +168,15 @@ def test_mandatory_base(self, snap_type, requires_base, yaml_data): project = Project.unmarshal(data) assert project.base is None - def test_mandatory_version(self, yaml_data): - data = yaml_data() + def test_mandatory_version(self, project_yaml_data): + data = project_yaml_data() data.pop("version") error = "Snap version is required if not using adopt-info" with pytest.raises(errors.ProjectValidationError, match=error): Project.unmarshal(data) - def test_version_not_required(self, yaml_data): - data = yaml_data() + def test_version_not_required(self, project_yaml_data): + data = project_yaml_data() data.pop("version") data["adopt-info"] = "part1" project = Project.unmarshal(data) @@ -173,8 +192,8 @@ def test_version_not_required(self, yaml_data): "a234567890123456789012345678901234567890", ], ) - def test_project_name_valid(self, name, yaml_data): - project = Project.unmarshal(yaml_data(name=name)) + def test_project_name_valid(self, name, project_yaml_data): + project = Project.unmarshal(project_yaml_data(name=name)) assert project.name == name @pytest.mark.parametrize( @@ -193,9 +212,9 @@ def test_project_name_valid(self, name, yaml_data): ), ], ) - def test_project_name_invalid(self, name, error, yaml_data): + def test_project_name_invalid(self, name, error, project_yaml_data): with pytest.raises(errors.ProjectValidationError, match=error): - Project.unmarshal(yaml_data(name=name)) + Project.unmarshal(project_yaml_data(name=name)) @pytest.mark.parametrize( "version", @@ -209,8 +228,8 @@ def test_project_name_invalid(self, name, error, yaml_data): "12345678901234567890123456789012", ], ) - def test_project_version_valid(self, version, yaml_data): - project = Project.unmarshal(yaml_data(version=version)) + def test_project_version_valid(self, version, project_yaml_data): + project = Project.unmarshal(project_yaml_data(version=version)) assert project.version == version @pytest.mark.parametrize( @@ -232,16 +251,16 @@ def test_project_version_valid(self, version, yaml_data): ), # too large ], ) - def test_project_version_invalid(self, version, error, yaml_data): + def test_project_version_invalid(self, version, error, project_yaml_data): with pytest.raises(errors.ProjectValidationError, match=error): - Project.unmarshal(yaml_data(version=version)) + Project.unmarshal(project_yaml_data(version=version)) @pytest.mark.parametrize( "snap_type", ["app", "gadget", "kernel", "snapd", "base", "_invalid"], ) - def test_project_type(self, snap_type, yaml_data): - data = yaml_data(type=snap_type) + def test_project_type(self, snap_type, project_yaml_data): + data = project_yaml_data(type=snap_type) if snap_type in ["base", "kernel", "snapd"]: data.pop("base") @@ -257,8 +276,8 @@ def test_project_type(self, snap_type, yaml_data): "confinement", ["strict", "devmode", "classic", "_invalid"], ) - def test_project_confinement(self, confinement, yaml_data): - data = yaml_data(confinement=confinement) + def test_project_confinement(self, confinement, project_yaml_data): + data = project_yaml_data(confinement=confinement) if confinement != "_invalid": project = Project.unmarshal(data) @@ -272,8 +291,8 @@ def test_project_confinement(self, confinement, yaml_data): "grade", ["devel", "stable", "_invalid"], ) - def test_project_grade(self, grade, yaml_data): - data = yaml_data(grade=grade) + def test_project_grade(self, grade, project_yaml_data): + data = project_yaml_data(grade=grade) if grade != "_invalid": project = Project.unmarshal(data) @@ -283,16 +302,16 @@ def test_project_grade(self, grade, yaml_data): with pytest.raises(errors.ProjectValidationError, match=error): Project.unmarshal(data) - def test_project_summary_valid(self, yaml_data): + def test_project_summary_valid(self, project_yaml_data): summary = "x" * 78 - project = Project.unmarshal(yaml_data(summary=summary)) + project = Project.unmarshal(project_yaml_data(summary=summary)) assert project.summary == summary - def test_project_summary_invalid(self, yaml_data): + def test_project_summary_invalid(self, project_yaml_data): summary = "x" * 79 error = "ensure this value has at most 78 characters" with pytest.raises(errors.ProjectValidationError, match=error): - Project.unmarshal(yaml_data(summary=summary)) + Project.unmarshal(project_yaml_data(summary=summary)) @pytest.mark.parametrize( "epoch", @@ -304,8 +323,8 @@ def test_project_summary_invalid(self, yaml_data): "12345*", ], ) - def test_project_epoch_valid(self, epoch, yaml_data): - project = Project.unmarshal(yaml_data(epoch=epoch)) + def test_project_epoch_valid(self, epoch, project_yaml_data): + project = Project.unmarshal(project_yaml_data(epoch=epoch)) assert project.epoch == epoch @pytest.mark.parametrize( @@ -320,12 +339,12 @@ def test_project_epoch_valid(self, epoch, yaml_data): "1**", ], ) - def test_project_epoch_invalid(self, epoch, yaml_data): + def test_project_epoch_invalid(self, epoch, project_yaml_data): error = "Epoch is a positive integer followed by an optional asterisk" with pytest.raises(errors.ProjectValidationError, match=error): - Project.unmarshal(yaml_data(epoch=epoch)) + Project.unmarshal(project_yaml_data(epoch=epoch)) - def test_project_package_repository(self, yaml_data): + def test_project_package_repository(self, project_yaml_data): repos = [ { "type": "apt", @@ -337,29 +356,29 @@ def test_project_package_repository(self, yaml_data): "key-id": "ABCDE12345" * 4, }, ] - project = Project.unmarshal(yaml_data(package_repositories=repos)) + project = Project.unmarshal(project_yaml_data(package_repositories=repos)) assert project.package_repositories == repos - def test_project_package_repository_missing_fields(self, yaml_data): + def test_project_package_repository_missing_fields(self, project_yaml_data): repos = [ { "type": "apt", }, ] - error = r".*\n- field 'url' required .*\n- field 'key-id' required" + error = r".*- field 'url' required .*\n- field 'key-id' required" with pytest.raises(errors.ProjectValidationError, match=error): - Project.unmarshal(yaml_data(package_repositories=repos)) + Project.unmarshal(project_yaml_data(package_repositories=repos)) - def test_project_package_repository_extra_fields(self, yaml_data): + def test_project_package_repository_extra_fields(self, project_yaml_data): repos = [ { "type": "apt", "extra": "something", }, ] - error = r".*\n- extra field 'extra' not permitted" + error = r".*- extra field 'extra' not permitted" with pytest.raises(errors.ProjectValidationError, match=error): - Project.unmarshal(yaml_data(package_repositories=repos)) + Project.unmarshal(project_yaml_data(package_repositories=repos)) @pytest.mark.parametrize( "environment", @@ -368,8 +387,8 @@ def test_project_package_repository_extra_fields(self, yaml_data): {"FIRST_VARIABLE": "foo", "SECOND_VARIABLE": "bar"}, ], ) - def test_project_environment_valid(self, environment, yaml_data): - project = Project.unmarshal(yaml_data(environment=environment)) + def test_project_environment_valid(self, environment, project_yaml_data): + project = Project.unmarshal(project_yaml_data(environment=environment)) assert project.environment == environment @pytest.mark.parametrize( @@ -380,23 +399,175 @@ def test_project_environment_valid(self, environment, yaml_data): [{"i": "am"}, {"a": "list"}, {"of": "dictionaries"}], ], ) - def test_project_environment_invalid(self, environment, yaml_data): - error = re.escape( - "Bad snapcraft.yaml content:\n- value is not a valid dict (in field 'environment')" - ) + def test_project_environment_invalid(self, environment, project_yaml_data): + error = ".*value is not a valid dict" with pytest.raises(errors.ProjectValidationError, match=error): - Project.unmarshal(yaml_data(environment=environment)) + Project.unmarshal(project_yaml_data(environment=environment)) class TestAppValidation: """Validate apps.""" + def test_app_command(self, app_yaml_data): + data = app_yaml_data(command="test-command") + project = Project.unmarshal(data) + assert project.apps is not None + assert project.apps["app1"].command == "test-command" + + @pytest.mark.parametrize( + "autostart", + ["myapp.desktop", "_invalid"], + ) + def test_app_autostart(self, autostart, app_yaml_data): + data = app_yaml_data(autostart=autostart) + + if autostart != "_invalid": + project = Project.unmarshal(data) + assert project.apps is not None + assert project.apps["app1"].autostart == autostart + else: + error = ".*'_invalid' is not a valid desktop file name" + with pytest.raises(errors.ProjectValidationError, match=error): + Project.unmarshal(data) + + def test_app_common_id(self, app_yaml_data): + data = app_yaml_data(common_id="test-common-id") + project = Project.unmarshal(data) + assert project.apps is not None + assert project.apps["app1"].common_id == "test-common-id" + + @pytest.mark.parametrize( + "bus_name", + ["test-bus-name", "_invalid!"], + ) + def test_app_bus_name(self, bus_name, app_yaml_data): + data = app_yaml_data(bus_name=bus_name) + + if bus_name != "_invalid!": + project = Project.unmarshal(data) + assert project.apps is not None + assert project.apps["app1"].bus_name == bus_name + else: + error = ".*'_invalid!' is not a valid bus name" + with pytest.raises(errors.ProjectValidationError, match=error): + Project.unmarshal(data) + + def test_app_completer(self, app_yaml_data): + data = app_yaml_data(completer="test-completer") + project = Project.unmarshal(data) + assert project.apps is not None + assert project.apps["app1"].completer == "test-completer" + + def test_app_stop_command(self, app_yaml_data): + data = app_yaml_data(stop_command="test-stop-command") + project = Project.unmarshal(data) + assert project.apps is not None + assert project.apps["app1"].stop_command == "test-stop-command" + + def test_app_post_stop_command(self, app_yaml_data): + data = app_yaml_data(post_stop_command="test-post-stop-command") + project = Project.unmarshal(data) + assert project.apps is not None + assert project.apps["app1"].post_stop_command == "test-post-stop-command" + + @pytest.mark.parametrize( + "start_timeout", ["10", "10ns", "10us", "10ms", "10s", "10m"] + ) + def test_app_start_timeout_valid(self, start_timeout, app_yaml_data): + data = app_yaml_data(start_timeout=start_timeout) + project = Project.unmarshal(data) + assert project.apps is not None + assert project.apps["app1"].start_timeout == start_timeout + + @pytest.mark.parametrize( + "start_timeout", + ["10 s", "10 seconds", "1:00", "invalid"], + ) + def test_app_start_timeout_invalid(self, start_timeout, app_yaml_data): + data = app_yaml_data(start_timeout=start_timeout) + + error = f".*'{start_timeout}' is not a valid time value" + with pytest.raises(errors.ProjectValidationError, match=error): + Project.unmarshal(data) + + @pytest.mark.parametrize( + "stop_timeout", ["10", "10ns", "10us", "10ms", "10s", "10m"] + ) + def test_app_stop_timeout_valid(self, stop_timeout, app_yaml_data): + data = app_yaml_data(stop_timeout=stop_timeout) + project = Project.unmarshal(data) + assert project.apps is not None + assert project.apps["app1"].stop_timeout == stop_timeout + + @pytest.mark.parametrize( + "stop_timeout", + ["10 s", "10 seconds", "1:00", "invalid"], + ) + def test_app_stop_timeout_invalid(self, stop_timeout, app_yaml_data): + data = app_yaml_data(stop_timeout=stop_timeout) + + error = f".*'{stop_timeout}' is not a valid time value" + with pytest.raises(errors.ProjectValidationError, match=error): + Project.unmarshal(data) + + @pytest.mark.parametrize( + "watchdog_timeout", ["10", "10ns", "10us", "10ms", "10s", "10m"] + ) + def test_app_watchdog_timeout_valid(self, watchdog_timeout, app_yaml_data): + data = app_yaml_data(watchdog_timeout=watchdog_timeout) + project = Project.unmarshal(data) + assert project.apps is not None + assert project.apps["app1"].watchdog_timeout == watchdog_timeout + + @pytest.mark.parametrize( + "watchdog_timeout", + ["10 s", "10 seconds", "1:00", "invalid"], + ) + def test_app_watchdog_timeout_invalid(self, watchdog_timeout, app_yaml_data): + data = app_yaml_data(watchdog_timeout=watchdog_timeout) + + error = f".*'{watchdog_timeout}' is not a valid time value" + with pytest.raises(errors.ProjectValidationError, match=error): + Project.unmarshal(data) + + def test_app_reload_command(self, app_yaml_data): + data = app_yaml_data(reload_command="test-reload-command") + project = Project.unmarshal(data) + assert project.apps is not None + assert project.apps["app1"].reload_command == "test-reload-command" + + @pytest.mark.parametrize( + "restart_delay", ["10", "10ns", "10us", "10ms", "10s", "10m"] + ) + def test_app_restart_delay_valid(self, restart_delay, app_yaml_data): + data = app_yaml_data(restart_delay=restart_delay) + project = Project.unmarshal(data) + assert project.apps is not None + assert project.apps["app1"].restart_delay == restart_delay + + @pytest.mark.parametrize( + "restart_delay", + ["10 s", "10 seconds", "1:00", "invalid"], + ) + def test_app_restart_delay_invalid(self, restart_delay, app_yaml_data): + data = app_yaml_data(restart_delay=restart_delay) + + error = f".*'{restart_delay}' is not a valid time value" + with pytest.raises(errors.ProjectValidationError, match=error): + Project.unmarshal(data) + + def test_app_timer(self, app_yaml_data): + data = app_yaml_data(timer="test-timer") + project = Project.unmarshal(data) + assert project.apps is not None + assert project.apps["app1"].timer == "test-timer" + @pytest.mark.parametrize( "daemon", ["simple", "forking", "oneshot", "notify", "dbus", "_invalid"], ) - def test_app_daemon(self, daemon, yaml_data): - data = yaml_data(apps={"app1": {"command": "/bin/true", "daemon": daemon}}) + def test_app_daemon(self, daemon, app_yaml_data): + data = app_yaml_data(daemon=daemon) if daemon != "_invalid": project = Project.unmarshal(data) @@ -407,11 +578,61 @@ def test_app_daemon(self, daemon, yaml_data): with pytest.raises(errors.ProjectValidationError, match=error): Project.unmarshal(data) + @pytest.mark.parametrize( + "after", + [ + "i am a string", + ["i", "am", "a", "list"], + ], + ) + def test_app_after(self, after, app_yaml_data): + data = app_yaml_data(after=after) + + if after == "i am a string": + error = ".*value is not a valid list" + with pytest.raises(errors.ProjectValidationError, match=error): + Project.unmarshal(data) + else: + project = Project.unmarshal(data) + assert project.apps is not None + assert project.apps["app1"].after == after + + def test_app_duplicate_after(self, app_yaml_data): + data = app_yaml_data(after=["duplicate", "duplicate"]) + + error = ".*duplicate entries in 'after' not permitted" + with pytest.raises(errors.ProjectValidationError, match=error): + Project.unmarshal(data) + + @pytest.mark.parametrize( + "before", + [ + "i am a string", + ["i", "am", "a", "list"], + ], + ) + def test_app_before(self, before, app_yaml_data): + data = app_yaml_data(before=before) + + if before == "i am a string": + error = ".*value is not a valid list" + with pytest.raises(errors.ProjectValidationError, match=error): + Project.unmarshal(data) + else: + project = Project.unmarshal(data) + assert project.apps is not None + assert project.apps["app1"].before == before + + def test_app_duplicate_before(self, app_yaml_data): + data = app_yaml_data(before=["duplicate", "duplicate"]) + + error = ".*duplicate entries in 'before' not permitted" + with pytest.raises(errors.ProjectValidationError, match=error): + Project.unmarshal(data) + @pytest.mark.parametrize("refresh_mode", ["endure", "restart", "_invalid"]) - def test_app_refresh_mode(self, refresh_mode, yaml_data): - data = yaml_data( - apps={"app1": {"command": "/bin/true", "refresh-mode": refresh_mode}} - ) + def test_app_refresh_mode(self, refresh_mode, app_yaml_data): + data = app_yaml_data(refresh_mode=refresh_mode) if refresh_mode != "_invalid": project = Project.unmarshal(data) @@ -436,10 +657,8 @@ def test_app_refresh_mode(self, refresh_mode, yaml_data): "_invalid", ], ) - def test_app_stop_mode(self, stop_mode, yaml_data): - data = yaml_data( - apps={"app1": {"command": "/bin/true", "stop-mode": stop_mode}} - ) + def test_app_stop_mode(self, stop_mode, app_yaml_data): + data = app_yaml_data(stop_mode=stop_mode) if stop_mode != "_invalid": project = Project.unmarshal(data) @@ -463,12 +682,8 @@ def test_app_stop_mode(self, stop_mode, yaml_data): "_invalid", ], ) - def test_app_restart_condition(self, restart_condition, yaml_data): - data = yaml_data( - apps={ - "app1": {"command": "/bin/true", "restart-condition": restart_condition} - } - ) + def test_app_restart_condition(self, restart_condition, app_yaml_data): + data = app_yaml_data(restart_condition=restart_condition) if restart_condition != "_invalid": project = Project.unmarshal(data) @@ -480,10 +695,8 @@ def test_app_restart_condition(self, restart_condition, yaml_data): Project.unmarshal(data) @pytest.mark.parametrize("install_mode", ["enable", "disable", "_invalid"]) - def test_app_install_mode(self, install_mode, yaml_data): - data = yaml_data( - apps={"app1": {"command": "/bin/true", "install-mode": install_mode}} - ) + def test_app_install_mode(self, install_mode, app_yaml_data): + data = app_yaml_data(install_mode=install_mode) if install_mode != "_invalid": project = Project.unmarshal(data) @@ -494,6 +707,39 @@ def test_app_install_mode(self, install_mode, yaml_data): with pytest.raises(errors.ProjectValidationError, match=error): Project.unmarshal(data) + def test_app_valid_aliases(self, app_yaml_data): + data = app_yaml_data(aliases=["i", "am", "a", "list"]) + + project = Project.unmarshal(data) + assert project.apps is not None + assert project.apps["app1"].aliases == ["i", "am", "a", "list"] + + @pytest.mark.parametrize( + "aliases", + [ + "i am a string", + ["_invalid!"], + ], + ) + def test_app_invalid_aliases(self, aliases, app_yaml_data): + data = app_yaml_data(aliases=aliases) + + if isinstance(aliases, list): + error = f".*'{aliases[0]}' is not a valid alias" + with pytest.raises(errors.ProjectValidationError, match=error): + Project.unmarshal(data) + else: + error = ".*value is not a valid list" + with pytest.raises(errors.ProjectValidationError, match=error): + Project.unmarshal(data) + + def test_app_duplicate_aliases(self, app_yaml_data): + data = app_yaml_data(aliases=["duplicate", "duplicate"]) + + error = ".*duplicate entries in 'aliases' not permitted" + with pytest.raises(errors.ProjectValidationError, match=error): + Project.unmarshal(data) + @pytest.mark.parametrize( "environment", [ @@ -501,10 +747,8 @@ def test_app_install_mode(self, install_mode, yaml_data): {"FIRST_VARIABLE": "foo", "SECOND_VARIABLE": "bar"}, ], ) - def test_app_environment_valid(self, environment, yaml_data): - data = yaml_data( - apps={"app1": {"command": "/bin/true", "environment": environment}} - ) + def test_app_environment_valid(self, environment, app_yaml_data): + data = app_yaml_data(environment=environment) project = Project.unmarshal(data) assert project.apps is not None assert project.apps["app1"].environment == environment @@ -517,14 +761,85 @@ def test_app_environment_valid(self, environment, yaml_data): [{"i": "am"}, {"a": "list"}, {"of": "dictionaries"}], ], ) - def test_app_environment_invalid(self, environment, yaml_data): - data = yaml_data( - apps={"app1": {"command": "/bin/true", "environment": environment}} - ) + def test_app_environment_invalid(self, environment, app_yaml_data): + data = app_yaml_data(environment=environment) + + error = ".*value is not a valid dict" + with pytest.raises(errors.ProjectValidationError, match=error): + Project.unmarshal(data) + + @pytest.mark.parametrize("adapter", ["none", "full", "_invalid"]) + def test_app_adapter(self, adapter, app_yaml_data): + data = app_yaml_data(adapter=adapter) + + if adapter != "_invalid": + project = Project.unmarshal(data) + assert project.apps is not None + assert project.apps["app1"].adapter == adapter + else: + error = ".*unexpected value; permitted: 'none', 'full'" + with pytest.raises(errors.ProjectValidationError, match=error): + Project.unmarshal(data) + + @pytest.mark.parametrize( + "command_chain", + [ + "i am a string", + ["_invalid!"], + ["snap/command-chain/snapcraft-runner"], + ["i", "am", "a", "list"], + ], + ) + def test_app_command_chain(self, command_chain, app_yaml_data): + data = app_yaml_data(command_chain=command_chain) + + if command_chain == "i am a string": + error = ".*value is not a valid list" + with pytest.raises(errors.ProjectValidationError, match=error): + Project.unmarshal(data) + elif command_chain == ["_invalid!"]: + error = f".*'{command_chain[0]}' is not a valid command chain" + with pytest.raises(errors.ProjectValidationError, match=error): + Project.unmarshal(data) + else: + project = Project.unmarshal(data) + assert project.apps is not None + assert project.apps["app1"].command_chain == command_chain + + @pytest.mark.parametrize("listen_stream", [1, 100, 65535, "/tmp/mysocket.sock"]) + def test_app_sockets_valid_listen_stream(self, listen_stream, socket_yaml_data): + data = socket_yaml_data(listen_stream=listen_stream) + + project = Project.unmarshal(data) + assert project.apps is not None + assert project.apps["app1"].sockets is not None + assert project.apps["app1"].sockets["socket1"].listen_stream == listen_stream + + @pytest.mark.parametrize("listen_stream", [-1, 0, 65536]) + def test_app_sockets_invalid_listen_stream(self, listen_stream, socket_yaml_data): + data = socket_yaml_data(listen_stream=listen_stream) - error = re.escape( - "Bad snapcraft.yaml content:\n" - "- value is not a valid dict (in field 'apps.app1.environment')" - ) + error = f".*{listen_stream} is not an integer between 1 and 65535" with pytest.raises(errors.ProjectValidationError, match=error): Project.unmarshal(data) + + def test_app_sockets_missing_listen_stream(self, socket_yaml_data): + data = socket_yaml_data() + + error = ".*field 'listen-stream' required" + with pytest.raises(errors.ProjectValidationError, match=error): + Project.unmarshal(data) + + @pytest.mark.parametrize("socket_mode", [1, "_invalid"]) + def test_app_sockets_valid_socket_mode(self, socket_mode, socket_yaml_data): + data = socket_yaml_data(listen_stream="test", socket_mode=socket_mode) + + if socket_mode != "_invalid": + project = Project.unmarshal(data) + assert project.apps is not None + assert project.apps["app1"].sockets is not None + assert project.apps["app1"].sockets["socket1"].socket_mode == socket_mode + else: + error = ".*value is not a valid integer" + with pytest.raises(errors.ProjectValidationError, match=error): + Project.unmarshal(data) From 59cf3f488e9615262b9485f8c81ee74ad2187dcf Mon Sep 17 00:00:00 2001 From: Claudio Matsuoka Date: Thu, 24 Feb 2022 18:59:11 -0300 Subject: [PATCH 048/167] projects: add grammar validation Validate snapcraft.yaml using pydantic validation models supplied by craft-grammar before processing grammar-aware properties. Signed-off-by: Claudio Matsuoka --- snapcraft/parts/lifecycle.py | 5 +- snapcraft/projects.py | 31 +++++++ tests/unit/test_projects.py | 152 ++++++++++++++++++++++++++++++++++- 3 files changed, 185 insertions(+), 3 deletions(-) diff --git a/snapcraft/parts/lifecycle.py b/snapcraft/parts/lifecycle.py index f4a9be7f5c..3efb8fd750 100644 --- a/snapcraft/parts/lifecycle.py +++ b/snapcraft/parts/lifecycle.py @@ -27,7 +27,7 @@ from snapcraft import errors, pack from snapcraft.meta import snap_yaml from snapcraft.parts import PartsLifecycle -from snapcraft.projects import Project +from snapcraft.projects import GrammarAwareProject, Project from . import grammar @@ -69,6 +69,9 @@ def run(command_name: str, parsed_args: "argparse.Namespace") -> None: resolution="To start a new project, use `snapcraft init`", ) + # validate project grammar + GrammarAwareProject.validate_grammar(yaml_data) + # only execute the new codebase from core22 onwards if yaml_data.get("base") != "core22": raise errors.LegacyFallback("base is not core22") diff --git a/snapcraft/projects.py b/snapcraft/projects.py index 982fd7d351..9a6a8ab2da 100644 --- a/snapcraft/projects.py +++ b/snapcraft/projects.py @@ -23,6 +23,7 @@ from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Union import pydantic +from craft_grammar.models import GrammarSingleEntryDictList, GrammarStr, GrammarStrList from pydantic import conlist, constr from snapcraft import repo @@ -349,6 +350,36 @@ def unmarshal(cls, data: Dict[str, Any]) -> "Project": return project +class _GrammarAwareModel(pydantic.BaseModel): + class Config: + """Default configuration for grammar-aware models.""" + + validate_assignment = True + extra = "allow" # this is required to verify only grammar-aware parts + alias_generator = lambda s: s.replace("_", "-") # noqa: E731 + allow_population_by_field_name = True + + +class _GrammarAwarePart(_GrammarAwareModel): + source: Optional[GrammarStr] + build_environment: Optional[GrammarSingleEntryDictList] + build_packages: Optional[GrammarStrList] + stage_packages: Optional[GrammarStrList] + build_snaps: Optional[GrammarStrList] + stage_snaps: Optional[GrammarStrList] + + +class GrammarAwareProject(_GrammarAwareModel): + """Project definition containing grammar-aware components.""" + + parts: Dict[str, _GrammarAwarePart] + + @classmethod + def validate_grammar(cls, data: Dict[str, Any]) -> None: + """Ensure grammar-enabled entries are syntactically valid.""" + cls(**data) + + def _format_pydantic_errors(errors, *, file_name: str = "snapcraft.yaml"): """Format errors. diff --git a/tests/unit/test_projects.py b/tests/unit/test_projects.py index 656656ced3..f7a1dfa492 100644 --- a/tests/unit/test_projects.py +++ b/tests/unit/test_projects.py @@ -16,10 +16,11 @@ from typing import Any, Dict +import pydantic import pytest from snapcraft import errors -from snapcraft.projects import Project +from snapcraft.projects import GrammarAwareProject, Project @pytest.fixture @@ -35,7 +36,7 @@ def _project_yaml_data( "description": "description", "grade": "stable", "confinement": "strict", - "parts": [], + "parts": {}, **kwargs, } @@ -843,3 +844,150 @@ def test_app_sockets_valid_socket_mode(self, socket_mode, socket_yaml_data): error = ".*value is not a valid integer" with pytest.raises(errors.ProjectValidationError, match=error): Project.unmarshal(data) + + +class TestGrammarValidation: + """Basic grammar validation testing.""" + + def test_grammar_trivial(self, project_yaml_data): + data = project_yaml_data( + parts={ + "p1": { + "plugin": "nil", + } + } + ) + GrammarAwareProject.validate_grammar(data) + + def test_grammar_without_grammar(self, project_yaml_data): + data = project_yaml_data( + parts={ + "p1": { + "plugin": "nil", + "sources": ".", + "build-environment": [ + {"FOO": "1"}, + {"BAR": "2"}, + ], + "build-packages": ["a", "b"], + "build-snaps": ["d", "e"], + "stage-packages": ["foo", "bar"], + "stage-snaps": ["baz", "quux"], + } + } + ) + GrammarAwareProject.validate_grammar(data) + + def test_grammar_simple(self, project_yaml_data): + data = project_yaml_data( + parts={ + "p1": { + "plugin": "nil", + "sources": [ + {"on arm64": "this"}, + {"else": "that"}, + ], + "build-environment": [ + { + "on amd64": [ + {"FOO": "1"}, + {"BAR": "2"}, + ] + }, + ], + "build-packages": [{"to arm64,amd64": ["a", "b"]}, "else fail"], + "build-snaps": [ + {"on somearch": ["d", "e"]}, + ], + "stage-packages": [ + "pkg1", + "pkg2", + {"to somearch": ["foo", "bar"]}, + ], + "stage-snaps": [ + {"on arch to otherarch": ["baz", "quux"]}, + ], + } + } + ) + GrammarAwareProject.validate_grammar(data) + + def test_grammar_recursive(self, project_yaml_data): + data = project_yaml_data( + parts={ + "p1": { + "plugin": "nil", + "sources": [ + {"on arm64": [{"to amd64": "this"}, "else fail"]}, + {"else": "that"}, + ], + } + } + ) + GrammarAwareProject.validate_grammar(data) + + def test_grammar_try(self, project_yaml_data): + data = project_yaml_data( + parts={ + "p1": { + "plugin": "nil", + "source": [ + {"try": "this"}, + {"else": "that"}, + ], + } + } + ) + + with pytest.raises(pydantic.ValidationError) as raised: + GrammarAwareProject.validate_grammar(data) + + err = raised.value.errors() + assert len(err) == 1 + assert err[0]["loc"] == ("parts", "p1", "source") + assert err[0]["type"] == "value_error" + assert ( + err[0]["msg"] == "'try' was removed from grammar, use 'on ' instead" + ) + + def test_grammar_type_error(self, project_yaml_data): + data = project_yaml_data( + parts={ + "p1": { + "plugin": "nil", + "source": [ + {"on amd64": [25]}, + ], + } + } + ) + + with pytest.raises(pydantic.ValidationError) as raised: + GrammarAwareProject.validate_grammar(data) + + err = raised.value.errors() + assert len(err) == 1 + assert err[0]["loc"] == ("parts", "p1", "source") + assert err[0]["type"] == "type_error" + assert err[0]["msg"] == "value must be a string: [25]" + + def test_grammar_syntax_error(self, project_yaml_data): + data = project_yaml_data( + parts={ + "p1": { + "plugin": "nil", + "source": [ + {"on amd64,,arm64": "foo"}, + ], + } + } + ) + + with pytest.raises(pydantic.ValidationError) as raised: + GrammarAwareProject.validate_grammar(data) + + err = raised.value.errors() + assert len(err) == 1 + assert err[0]["loc"] == ("parts", "p1", "source") + assert err[0]["type"] == "value_error" + assert err[0]["msg"] == "syntax error in 'on' selector" From 685ad4b52a4f8653baaaa9d9b3f35138dd5ad63b Mon Sep 17 00:00:00 2001 From: Claudio Matsuoka Date: Thu, 24 Feb 2022 19:09:46 -0300 Subject: [PATCH 049/167] requirements: update dependencies Signed-off-by: Claudio Matsuoka --- requirements-devel.txt | 21 +++++++++++---------- requirements.txt | 18 +++++++++--------- 2 files changed, 20 insertions(+), 19 deletions(-) diff --git a/requirements-devel.txt b/requirements-devel.txt index 260c19a050..090e3aad16 100644 --- a/requirements-devel.txt +++ b/requirements-devel.txt @@ -5,15 +5,16 @@ catkin-pkg==0.4.24 certifi==2021.10.8 cffi==1.15.0 chardet==4.0.0 -charset-normalizer==2.0.11 -click==8.0.3 +charset-normalizer==2.0.12 +click==8.0.4 codespell==2.1.0 -coverage==6.3.1 +coverage==6.3.2 craft-cli==0.2.0 +craft-grammar==1.1.1 craft-parts==1.1.2 cryptography==3.4 Deprecated==1.2.13 -distro==1.6.0 +distro==1.7.0 docutils==0.18.1 entrypoints==0.3 extras==1.0.0 @@ -23,7 +24,7 @@ gnupg==2.3.1 httplib2==0.20.4 hupper==1.10.3 idna==3.3 -importlib-metadata==4.10.1 +importlib-metadata==4.11.2 iniconfig==1.1.1 isort==5.10.1 jeepney==0.7.1 @@ -33,7 +34,7 @@ launchpadlib==1.10.16 lazr.restfulclient==0.14.4 lazr.uri==1.0.6 lazy-object-proxy==1.7.1 -lxml==4.7.1 +lxml==4.8.0 macaroonbakery==1.3.1 mccabe==0.6.1 mypy==0.931 @@ -47,7 +48,7 @@ pbr==5.8.1 pexpect==4.8.0 plaster==1.0 plaster-pastedeploy==0.7 -platformdirs==2.5.0 +platformdirs==2.5.1 pluggy==1.0.0 progressbar==2.5 protobuf==3.19.4 @@ -70,7 +71,7 @@ pymacaroons==0.13.0 pyparsing==3.0.7 pyramid==2.0 pyRFC3339==1.1 -pytest==7.0.0 +pytest==7.0.1 pytest-cov==3.0.0 pytest-mock==3.7.0 pytest-subprocess==1.4.1 @@ -92,7 +93,7 @@ snowballstemmer==2.2.0 tabulate==0.8.9 testscenarios==0.5.0 testtools==2.5.0 -tinydb==4.6.1 +tinydb==4.7.0 toml==0.10.2 tomli==2.0.1 translationstring==1.4 @@ -100,7 +101,7 @@ types-Deprecated==1.2.5 types-PyYAML==6.0.4 types-setuptools==57.4.9 typing-utils==0.1.0 -typing_extensions==4.0.1 +typing_extensions==4.1.1 urllib3==1.26.8 venusian==3.0.0 wadllib==1.3.6 diff --git a/requirements.txt b/requirements.txt index c6a4a42162..c0dfb9dfe1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,31 +3,31 @@ catkin-pkg==0.4.24 certifi==2021.10.8 cffi==1.15.0 chardet==4.0.0 -charset-normalizer==2.0.11 -click==8.0.3 +charset-normalizer==2.0.12 +click==8.0.4 craft-cli==0.2.0 -craft-grammar==1.0.0 +craft-grammar==1.1.1 craft-parts==1.1.2 cryptography==3.4 Deprecated==1.2.13 -distro==1.6.0 +distro==1.7.0 docutils==0.18.1 gnupg==2.3.1 httplib2==0.20.4 idna==3.3 -importlib-metadata==4.10.1 +importlib-metadata==4.11.2 jeepney==0.7.1 jsonschema==2.5.1 keyring==23.5.0 launchpadlib==1.10.16 lazr.restfulclient==0.14.4 lazr.uri==1.0.6 -lxml==4.7.1 +lxml==4.8.0 macaroonbakery==1.3.1 mypy-extensions==0.4.3 oauthlib==3.2.0 overrides==6.1.0 -platformdirs==2.5.0 +platformdirs==2.5.1 progressbar==2.5 protobuf==3.19.4 psutil==5.9.0 @@ -54,11 +54,11 @@ semver==3.0.0.dev3 simplejson==3.17.6 six==1.16.0 tabulate==0.8.9 -tinydb==4.6.1 +tinydb==4.7.0 toml==0.10.2 types-Deprecated==1.2.5 typing-utils==0.1.0 -typing_extensions==4.0.1 +typing_extensions==4.1.1 urllib3==1.26.8 wadllib==1.3.6 wrapt==1.13.3 From 6512b7cdddc89cf6abe182e9323dff376b60bc61 Mon Sep 17 00:00:00 2001 From: Callahan Kovacs Date: Tue, 1 Mar 2022 15:14:12 +0100 Subject: [PATCH 050/167] meta: support plugs Signed-off-by: Callahan Kovacs --- snapcraft/meta/snap_yaml.py | 11 ++++++ snapcraft/projects.py | 29 +++++++++++++++- tests/unit/meta/test_snap_yaml.py | 22 ++++++++++++ tests/unit/test_projects.py | 57 ++++++++++++++++++++++++++++++- 4 files changed, 117 insertions(+), 2 deletions(-) diff --git a/snapcraft/meta/snap_yaml.py b/snapcraft/meta/snap_yaml.py index afd82fac4b..6727944904 100644 --- a/snapcraft/meta/snap_yaml.py +++ b/snapcraft/meta/snap_yaml.py @@ -33,6 +33,15 @@ class Socket(YamlModel): socket_mode: Optional[int] +class ContentPlug(YamlModel): + """snap.yaml content plug entry.""" + + content: Optional[str] + interface: str + target: str + default_provider: Optional[str] + + class SnapApp(YamlModel): """Snap.yaml app entry. @@ -104,6 +113,7 @@ class SnapMetadata(YamlModel): confinement: str grade: str environment: Optional[Dict[str, Any]] + plugs: Optional[Dict[str, Union[ContentPlug, Any]]] def write(project: Project, prime_dir: Path, *, arch: str): @@ -171,6 +181,7 @@ def write(project: Project, prime_dir: Path, *, arch: str): confinement=project.confinement, grade=project.grade, environment=project.environment, + plugs=project.plugs, ) yaml.add_representer(str, _repr_str, Dumper=yaml.SafeDumper) diff --git a/snapcraft/projects.py b/snapcraft/projects.py index 9a6a8ab2da..a45a7c98ae 100644 --- a/snapcraft/projects.py +++ b/snapcraft/projects.py @@ -202,6 +202,15 @@ class Architecture(ProjectModel): build_to: Optional[Union[str, UniqueStrList]] +class ContentPlug(ProjectModel): + """Snapcraft project content plug definition.""" + + content: Optional[str] + interface: str + target: str + default_provider: Optional[str] + + class Project(ProjectModel): """Snapcraft project definition. @@ -237,12 +246,30 @@ class Project(ProjectModel): hooks: Optional[Dict[str, Hook]] passthrough: Optional[Dict[str, Any]] apps: Optional[Dict[str, App]] - plugs: Optional[Dict[str, Dict[str, str]]] # TODO: add plug name validation + plugs: Optional[Dict[str, Union[ContentPlug, Any]]] slots: Optional[Dict[str, Dict[str, str]]] # TODO: add slot name validation parts: Dict[str, Any] # parts are handled by craft-parts epoch: Optional[str] environment: Optional[Dict[str, Any]] + @pydantic.validator("plugs") + @classmethod + def _validate_plugs(cls, plugs): + if plugs is not None: + for plug_name, plug in plugs.items(): + if ( + isinstance(plug, dict) + and plug.get("interface") == "content" + and not plug.get("target") + ): + raise ValueError( + f"ContentPlug '{plug_name}' must have a 'target' parameter." + ) + if isinstance(plug, list): + raise ValueError(f"Plug '{plug_name}' cannot be a list.") + + return plugs + @pydantic.root_validator(pre=True) @classmethod def _validate_mandatory_version(cls, values): diff --git a/tests/unit/meta/test_snap_yaml.py b/tests/unit/meta/test_snap_yaml.py index ce948e0001..a4cb427c3c 100644 --- a/tests/unit/meta/test_snap_yaml.py +++ b/tests/unit/meta/test_snap_yaml.py @@ -146,6 +146,17 @@ def complex_project(): test-socket-2: listen_stream: 100 socket_mode: 1 + plugs: + empty-plug: + string-plug: home + dict-plug: + string-parameter: foo + bool-parameter: True + content-interface: + interface: content + target: test-target + content: test-content + default-provider: test-provider """ ) data = yaml.safe_load(snapcraft_yaml) @@ -219,5 +230,16 @@ def test_complex_snap_yaml(complex_project, new_dir): grade: stable environment: GLOBAL_VARIABLE: test-global-variable + plugs: + empty-plug: null + string-plug: home + dict-plug: + string-parameter: foo + bool-parameter: true + content-interface: + content: test-content + interface: content + target: test-target + default_provider: test-provider """ ) diff --git a/tests/unit/test_projects.py b/tests/unit/test_projects.py index f7a1dfa492..4357dfae9b 100644 --- a/tests/unit/test_projects.py +++ b/tests/unit/test_projects.py @@ -20,7 +20,9 @@ import pytest from snapcraft import errors -from snapcraft.projects import GrammarAwareProject, Project +from snapcraft.projects import ContentPlug, GrammarAwareProject, Project + +# pylint: disable=too-many-lines @pytest.fixture @@ -405,6 +407,59 @@ def test_project_environment_invalid(self, environment, project_yaml_data): with pytest.raises(errors.ProjectValidationError, match=error): Project.unmarshal(project_yaml_data(environment=environment)) + @pytest.mark.parametrize( + "plugs", + [ + {"empty-plug": None}, + {"string-plug": "home"}, + {"dict-plug": {"string-parameter": "foo", "bool-parameter": True}}, + ], + ) + def test_project_plugs_valid(self, plugs, project_yaml_data): + project = Project.unmarshal(project_yaml_data(plugs=plugs)) + assert project.plugs == plugs + + @pytest.mark.parametrize( + "plugs", + [ + "i am a string", + ["i", "am", "a", "list"], + [{"i": "am"}, {"a": "list"}, {"of": "dictionaries"}], + ], + ) + def test_project_plugs_invalid(self, plugs, project_yaml_data): + error = ".*value is not a valid dict" + with pytest.raises(errors.ProjectValidationError, match=error): + Project.unmarshal(project_yaml_data(plugs=plugs)) + + def test_project_content_plugs_valid(self, project_yaml_data): + content_plug_data = { + "content-interface": { + "interface": "content", + "target": "test-target", + "content": "test-content", + "default-provider": "test-provider", + } + } + content_plug = ContentPlug(**content_plug_data["content-interface"]) + + project = Project.unmarshal(project_yaml_data(plugs=content_plug_data)) + assert project.plugs is not None + assert project.plugs["content-interface"] == content_plug + + def test_project_content_plugs_missing_target(self, project_yaml_data): + content_plug = { + "content-interface": { + "interface": "content", + "content": "test-content", + "default-provider": "test-provider", + } + } + error = ".*'content-interface' must have a 'target' parameter" + + with pytest.raises(errors.ProjectValidationError, match=error): + Project.unmarshal(project_yaml_data(plugs=content_plug)) + class TestAppValidation: """Validate apps.""" From 00eed7dc92e57e1d4de82411132f53d55c936474 Mon Sep 17 00:00:00 2001 From: Callahan Kovacs Date: Thu, 3 Mar 2022 16:54:26 +0100 Subject: [PATCH 051/167] meta: add support for hooks Signed-off-by: Callahan Kovacs --- snapcraft/meta/snap_yaml.py | 14 ++------ snapcraft/projects.py | 41 +++++++++++++++------- tests/unit/meta/test_snap_yaml.py | 27 ++++++++++++++ tests/unit/test_projects.py | 58 ++++++++++++++++++++++++++++++- 4 files changed, 116 insertions(+), 24 deletions(-) diff --git a/snapcraft/meta/snap_yaml.py b/snapcraft/meta/snap_yaml.py index 6727944904..c29e213e1e 100644 --- a/snapcraft/meta/snap_yaml.py +++ b/snapcraft/meta/snap_yaml.py @@ -33,15 +33,6 @@ class Socket(YamlModel): socket_mode: Optional[int] -class ContentPlug(YamlModel): - """snap.yaml content plug entry.""" - - content: Optional[str] - interface: str - target: str - default_provider: Optional[str] - - class SnapApp(YamlModel): """Snap.yaml app entry. @@ -95,7 +86,6 @@ class SnapMetadata(YamlModel): https://snapcraft.io/docs/snap-format for details. TODO: implement adopt-info (CRAFT-803) - TODO: implement hooks (CRAFT-808) """ name: str @@ -113,7 +103,8 @@ class SnapMetadata(YamlModel): confinement: str grade: str environment: Optional[Dict[str, Any]] - plugs: Optional[Dict[str, Union[ContentPlug, Any]]] + plugs: Optional[Dict[str, Any]] + hooks: Optional[Dict[str, Any]] def write(project: Project, prime_dir: Path, *, arch: str): @@ -182,6 +173,7 @@ def write(project: Project, prime_dir: Path, *, arch: str): grade=project.grade, environment=project.environment, plugs=project.plugs, + hooks=project.hooks, ) yaml.add_representer(str, _repr_str, Dumper=yaml.SafeDumper) diff --git a/snapcraft/projects.py b/snapcraft/projects.py index a45a7c98ae..8e5304ede0 100644 --- a/snapcraft/projects.py +++ b/snapcraft/projects.py @@ -54,6 +54,19 @@ class Config: # pylint: disable=too-few-public-methods # fmt: on +def _validate_command_chain(command_chains: Optional[List[str]]) -> Optional[List[str]]: + """Validate command_chain.""" + if command_chains is not None: + for command_chain in command_chains: + if not re.match(r"^[A-Za-z0-9/._#:$-]*$", command_chain): + raise ValueError( + f"{command_chain!r} is not a valid command chain. Command chain entries must " + "be strings, and can only use ASCII alphanumeric characters and the following " + "special characters: / . _ # : $ -" + ) + return command_chains + + class Socket(ProjectModel): """Snapcraft app socket definition.""" @@ -162,15 +175,7 @@ def _validate_time(cls, timeval): @pydantic.validator("command_chain") @classmethod def _validate_command_chain(cls, command_chains): - for command_chain in command_chains: - if not re.match(r"^[A-Za-z0-9/._#:$-]*$", command_chain): - raise ValueError( - f"{command_chain!r} is not a valid command chain. Command chain entries must " - "be strings, and can only use ASCII alphanumeric characters and the following " - "special characters: / . _ # : $ -" - ) - - return command_chains + return _validate_command_chain(command_chains) @pydantic.validator("aliases") @classmethod @@ -189,11 +194,23 @@ def _validate_aliases(cls, aliases): class Hook(ProjectModel): """Snapcraft project hook definition.""" - command_chain: List[str] = [] - environment: List[Dict[str, str]] = [] - plugs: UniqueStrList = [] + command_chain: Optional[List[str]] + environment: Optional[Dict[str, Any]] + plugs: Optional[UniqueStrList] passthrough: Optional[Dict[str, Any]] + @pydantic.validator("command_chain") + @classmethod + def _validate_command_chain(cls, command_chains): + return _validate_command_chain(command_chains) + + @pydantic.validator("plugs") + @classmethod + def _validate_plugs(cls, plugs): + if not plugs: + raise ValueError("'plugs' field cannot be empty.") + return plugs + class Architecture(ProjectModel): """Snapcraft project architecture definition.""" diff --git a/tests/unit/meta/test_snap_yaml.py b/tests/unit/meta/test_snap_yaml.py index a4cb427c3c..1ef89657b2 100644 --- a/tests/unit/meta/test_snap_yaml.py +++ b/tests/unit/meta/test_snap_yaml.py @@ -146,6 +146,7 @@ def complex_project(): test-socket-2: listen_stream: 100 socket_mode: 1 + plugs: empty-plug: string-plug: home @@ -157,6 +158,19 @@ def complex_project(): target: test-target content: test-content default-provider: test-provider + + hooks: + configure: + command-chain: ["test"] + environment: + test-variable-1: "test" + test-variable-2: "test" + plugs: + - home + - network + install: + environment: + environment-var-1: "test" """ ) data = yaml.safe_load(snapcraft_yaml) @@ -241,5 +255,18 @@ def test_complex_snap_yaml(complex_project, new_dir): interface: content target: test-target default_provider: test-provider + hooks: + configure: + command_chain: + - test + environment: + test-variable-1: test + test-variable-2: test + plugs: + - home + - network + install: + environment: + environment-var-1: test """ ) diff --git a/tests/unit/test_projects.py b/tests/unit/test_projects.py index 4357dfae9b..140d262abf 100644 --- a/tests/unit/test_projects.py +++ b/tests/unit/test_projects.py @@ -20,7 +20,7 @@ import pytest from snapcraft import errors -from snapcraft.projects import ContentPlug, GrammarAwareProject, Project +from snapcraft.projects import ContentPlug, GrammarAwareProject, Hook, Project # pylint: disable=too-many-lines @@ -461,6 +461,62 @@ def test_project_content_plugs_missing_target(self, project_yaml_data): Project.unmarshal(project_yaml_data(plugs=content_plug)) +class TestHookValidation: + """Validate hooks.""" + + @pytest.mark.parametrize( + "hooks", + [ + {"configure": {}}, + { + "configure": { + "command-chain": ["test-1", "test-2"], + "build-environment": { + "FIRST_VARIABLE": "test-3", + "SECOND_VARIABLE": "test-4", + }, + "plugs": ["home", "network"], + } + }, + ], + ) + def test_project_hooks_valid(self, hooks, project_yaml_data): + configure_hook_data = Hook(**hooks["configure"]) + project = Project.unmarshal(project_yaml_data(hooks=hooks)) + + assert project.hooks is not None + assert project.hooks["configure"] == configure_hook_data + + def test_project_hooks_command_chain_invalid(self, project_yaml_data): + hook = {"configure": {"command-chain": ["_invalid!"]}} + error = "'_invalid!' is not a valid command chain" + + with pytest.raises(errors.ProjectValidationError, match=error): + Project.unmarshal(project_yaml_data(hooks=hook)) + + @pytest.mark.parametrize( + "environment", + [ + "i am a string", + ["i", "am", "a", "list"], + [{"i": "am"}, {"a": "list"}, {"of": "dictionaries"}], + ], + ) + def test_project_hooks_environment_invalid(self, environment, project_yaml_data): + hooks = {"configure": {"environment": environment}} + + error = ".*value is not a valid dict" + with pytest.raises(errors.ProjectValidationError, match=error): + Project.unmarshal(project_yaml_data(hooks=hooks)) + + def test_project_hooks_plugs_empty(self, project_yaml_data): + hook = {"configure": {"plugs": []}} + error = ".*'plugs' field cannot be empty" + + with pytest.raises(errors.ProjectValidationError, match=error): + Project.unmarshal(project_yaml_data(hooks=hook)) + + class TestAppValidation: """Validate apps.""" From 889850a9e6f14742ecf30dcd00578206d02ac539 Mon Sep 17 00:00:00 2001 From: Claudio Matsuoka Date: Thu, 3 Mar 2022 13:58:28 +0100 Subject: [PATCH 052/167] requirements: update dependencies Signed-off-by: Claudio Matsuoka --- requirements-devel.txt | 5 +++-- requirements.txt | 3 ++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/requirements-devel.txt b/requirements-devel.txt index 090e3aad16..a388e264b4 100644 --- a/requirements-devel.txt +++ b/requirements-devel.txt @@ -11,7 +11,8 @@ codespell==2.1.0 coverage==6.3.2 craft-cli==0.2.0 craft-grammar==1.1.1 -craft-parts==1.1.2 +craft-parts==1.3.0 +craft-providers==1.0.5 cryptography==3.4 Deprecated==1.2.13 distro==1.7.0 @@ -99,7 +100,7 @@ tomli==2.0.1 translationstring==1.4 types-Deprecated==1.2.5 types-PyYAML==6.0.4 -types-setuptools==57.4.9 +types-setuptools==57.4.10 typing-utils==0.1.0 typing_extensions==4.1.1 urllib3==1.26.8 diff --git a/requirements.txt b/requirements.txt index c0dfb9dfe1..65f3446106 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,7 +7,8 @@ charset-normalizer==2.0.12 click==8.0.4 craft-cli==0.2.0 craft-grammar==1.1.1 -craft-parts==1.1.2 +craft-parts==1.3.0 +craft-providers==1.0.5 cryptography==3.4 Deprecated==1.2.13 distro==1.7.0 From 8a06cdbbd3d69da683ac62f3b573d49145adefb7 Mon Sep 17 00:00:00 2001 From: Claudio Matsuoka Date: Thu, 3 Mar 2022 13:56:51 +0100 Subject: [PATCH 053/167] providers: integrate craft-providers support Run lifecycle commands inside a build provider using lxd or multipass. Note: multipass images for multipass will be fixed when base: devel is implemented Signed-off-by: Claudio Matsuoka --- pyproject.toml | 3 +- setup.py | 1 + snapcraft/cli.py | 3 +- snapcraft/commands/lifecycle.py | 17 +- snapcraft/pack.py | 10 +- snapcraft/parts/lifecycle.py | 49 +++++- snapcraft/providers/__init__.py | 25 +++ snapcraft/providers/_buildd.py | 118 ++++++++++++++ snapcraft/providers/_get_provider.py | 56 +++++++ snapcraft/providers/_logs.py | 51 ++++++ snapcraft/providers/_lxd.py | 206 +++++++++++++++++++++++++ snapcraft/providers/_multipass.py | 182 ++++++++++++++++++++++ snapcraft/providers/_provider.py | 125 +++++++++++++++ snapcraft/utils.py | 87 +++++++++++ tests/unit/cli/test_default_command.py | 39 ++++- tests/unit/cli/test_lifecycle.py | 91 ++++++++++- tests/unit/parts/test_lifecycle.py | 116 +++++++++++++- tests/unit/test_pack.py | 17 +- 18 files changed, 1170 insertions(+), 26 deletions(-) create mode 100644 snapcraft/providers/__init__.py create mode 100644 snapcraft/providers/_buildd.py create mode 100644 snapcraft/providers/_get_provider.py create mode 100644 snapcraft/providers/_logs.py create mode 100644 snapcraft/providers/_lxd.py create mode 100644 snapcraft/providers/_multipass.py create mode 100644 snapcraft/providers/_provider.py create mode 100644 snapcraft/utils.py diff --git a/pyproject.toml b/pyproject.toml index bbe92e37f0..3a5f4af0a7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,7 +31,8 @@ ensure_newline_before_comments = true line_length = 88 [tool.pylint.messages_control] -disable = "too-few-public-methods,fixme,use-implicit-booleaness-not-comparison" +# duplicate-code can't be disabled locally: https://github.com/PyCQA/pylint/issues/214 +disable = "too-few-public-methods,fixme,use-implicit-booleaness-not-comparison,duplicate-code" [tool.pylint.format] max-attributes = 15 diff --git a/setup.py b/setup.py index ab9851e4d7..ed0fa3f7d9 100755 --- a/setup.py +++ b/setup.py @@ -95,6 +95,7 @@ def recursive_data_files(directory, install_directory): "craft-cli", "craft-grammar", "craft-parts", + "craft-providers", "cryptography==3.4", "gnupg", "jsonschema==2.5.1", diff --git a/snapcraft/cli.py b/snapcraft/cli.py index 0b18e90769..b1c1298612 100644 --- a/snapcraft/cli.py +++ b/snapcraft/cli.py @@ -61,8 +61,7 @@ def run(): legacy.legacy_run() # set lib loggers to debug level so that all messages are sent to Emitter - # TODO: add craft_providers - for lib_name in ("craft_parts",): + for lib_name in ("craft_parts", "craft_providers"): logger = logging.getLogger(lib_name) logger.setLevel(logging.DEBUG) diff --git a/snapcraft/commands/lifecycle.py b/snapcraft/commands/lifecycle.py index 127a9a7aeb..3cedd14b44 100644 --- a/snapcraft/commands/lifecycle.py +++ b/snapcraft/commands/lifecycle.py @@ -35,8 +35,21 @@ class _LifecycleCommand(BaseCommand, abc.ABC): @overrides def fill_parser(self, parser: "argparse.ArgumentParser") -> None: - # TODO: add arguments for all step commands and pack - pass + parser.add_argument( + "--destructive-mode", + action="store_true", + help="Build in the current host (implies `--provider=host`)", + ) + parser.add_argument( + "--provider", + choices=["host", "lxd", "multipass"], + help="The build provider to use", + ) + parser.add_argument( + "--use-lxd", + action="store_true", + help="Use LXD to build (implies `--provider=lxd`)", + ) @overrides def run(self, parsed_args): diff --git a/snapcraft/pack.py b/snapcraft/pack.py index ff4ba07c05..e7e075cdca 100644 --- a/snapcraft/pack.py +++ b/snapcraft/pack.py @@ -62,7 +62,13 @@ def pack_snap( emit.progress("Creating snap package...") try: - subprocess.run(command, capture_output=True, check=True) # type: ignore + subprocess.run( + command, capture_output=True, check=True, universal_newlines=True + ) # type: ignore except subprocess.CalledProcessError as err: - raise errors.SnapcraftError(f"Cannot pack snap file: {err!s}") + msg = f"Cannot pack snap file: {err!s}" + if err.stderr: + msg += f" ({err.stderr.strip()!s})" + raise errors.SnapcraftError(msg) + emit.message("Created snap package", intermediate=True) diff --git a/snapcraft/parts/lifecycle.py b/snapcraft/parts/lifecycle.py index 3efb8fd750..c7ca8ed491 100644 --- a/snapcraft/parts/lifecycle.py +++ b/snapcraft/parts/lifecycle.py @@ -16,18 +16,20 @@ """Parts lifecycle preparation and execution.""" +import subprocess from pathlib import Path -from typing import TYPE_CHECKING, Any, Dict +from typing import TYPE_CHECKING, Any, Dict, cast import yaml import yaml.error from craft_cli import emit from craft_parts import infos -from snapcraft import errors, pack +from snapcraft import errors, pack, providers, utils from snapcraft.meta import snap_yaml from snapcraft.parts import PartsLifecycle from snapcraft.projects import GrammarAwareProject, Project +from snapcraft.providers import capture_logs_from_instance from . import grammar @@ -100,12 +102,19 @@ def _run_command( assets_dir: Path, parsed_args: "argparse.Namespace", ) -> None: + destructive_mode = parsed_args.destructive_mode or parsed_args.provider == "host" + managed_mode = utils.is_managed_mode() - # TODO: check destructive and managed modes and run in provider - _ = parsed_args + if not managed_mode and not destructive_mode: + _run_in_provider(project, command_name, parsed_args) + return + + if managed_mode: + work_dir = utils.get_managed_environment_home_path() + else: + work_dir = Path("work").absolute() step_name = "prime" if command_name == "pack" else command_name - work_dir = Path("work").absolute() lifecycle = PartsLifecycle( project.parts, @@ -144,6 +153,36 @@ def _load_yaml(filename: Path) -> Dict[str, Any]: raise errors.SnapcraftError(f"YAML parsing error: {err!s}") from err +def _run_in_provider(project: Project, command_name: str, parsed_args: "argparse.Namespace"): + """Pack image in provider instance.""" + provider = "lxd" if parsed_args.use_lxd else parsed_args.provider + + emit.progress("Checking build provider availability") + provider = providers.get_provider(provider) + provider.ensure_provider_is_available() + + cmd = ["snapcraft", command_name] + + if hasattr(parsed_args, "parts"): + cmd.extend(parsed_args.parts) + + output_dir = utils.get_managed_environment_project_path() + + emit.progress("Launching build provider") + with provider.launched_environment( + project_name=project.name, project_path=Path().absolute(), base=cast(str, project.base) + ) as instance: + try: + instance.execute_run( + cmd, check=True, cwd=output_dir, + ) + except subprocess.CalledProcessError as err: + capture_logs_from_instance(instance) + raise providers.ProviderError( + f"Failed to pack image '{project.name}:{project.version}'." + ) from err + + # TODO Needs exposure from craft-parts. def _get_arch() -> str: machine = infos._get_host_architecture() # pylint: disable=protected-access diff --git a/snapcraft/providers/__init__.py b/snapcraft/providers/__init__.py new file mode 100644 index 0000000000..488be0f246 --- /dev/null +++ b/snapcraft/providers/__init__.py @@ -0,0 +1,25 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright 2021-2022 Canonical Ltd. +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License version 3 as published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program. If not, see . + +"""Build provider support.""" + +from ._buildd import SnapcraftBuilddBaseConfiguration # noqa: F401 +from ._get_provider import get_provider # noqa: F401 +from ._logs import capture_logs_from_instance # noqa: F401 +from ._lxd import LXDProvider # noqa: F401 +from ._multipass import MultipassProvider # noqa: F401 +from ._provider import Provider # noqa: F401 +from ._provider import ProviderError # noqa: F401 diff --git a/snapcraft/providers/_buildd.py b/snapcraft/providers/_buildd.py new file mode 100644 index 0000000000..c86e98f9b3 --- /dev/null +++ b/snapcraft/providers/_buildd.py @@ -0,0 +1,118 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright 2021-2022 Canonical Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +"""Buildd-related helpers for Snapcraft.""" + +import enum +import sys +from typing import Optional + +from craft_providers import Executor, bases +from craft_providers.actions import snap_installer + +from snapcraft import utils + + +# FIXME: update in craft-providers +class _BuilddBaseAlias(enum.Enum): + """Mappings for supported buildd images.""" + + JAMMY = "22.04" + + +BASE_TO_BUILDD_IMAGE_ALIAS = { + # "core22": bases.BuilddBaseAlias.JAMMY, + "core22": _BuilddBaseAlias.JAMMY, +} + + +class SnapcraftBuilddBaseConfiguration(bases.BuilddBase): + """Base configuration for Snapcraft. + + :cvar compatibility_tag: Tag/Version for variant of build configuration and + setup. Any change to this version would indicate that prior [versioned] + instances are incompatible and must be cleaned. As such, any new value + should be unique to old values (e.g. incrementing). Snapcraft extends + the buildd tag to include its own version indicator (.0) and namespace + ("snapcraft"). + """ + + compatibility_tag: str = f"snapcraft-{bases.BuilddBase.compatibility_tag}.0" + + @staticmethod + def _setup_snapcraft(*, executor: Executor) -> None: + """Install Snapcraft in target environment. + + On Linux, the default behavior is to inject the host snap into the target + environment. + + On other platforms, the Snapcraft snap is installed from the Snap Store. + + When installing the snap from the Store, we check if the user specifies a + channel, using SNAPCRAFT_INSTALL_SNAP_CHANNEL=. If unspecified, + we use the "stable" channel on the default track. + + On Linux, the user may specify this environment variable to force Snapcraft + to install the snap from the Store rather than inject the host snap. + + :raises BaseConfigurationError: on error. + """ + snap_channel = utils.get_managed_environment_snap_channel() + if snap_channel is None and sys.platform != "linux": + snap_channel = "stable" + + if snap_channel: + try: + snap_installer.install_from_store( + executor=executor, + snap_name="snapcraft", + channel=snap_channel, + classic=True, + ) + except snap_installer.SnapInstallationError as error: + raise bases.BaseConfigurationError( + "Failed to install snapcraft snap from store channel " + f"{snap_channel!r} into target environment." + ) from error + else: + try: + snap_installer.inject_from_host( + executor=executor, snap_name="snapcraft", classic=True + ) + except snap_installer.SnapInstallationError as error: + raise bases.BaseConfigurationError( + "Failed to inject host snapcraft snap into target environment." + ) from error + + def setup( + self, + *, + executor: Executor, + retry_wait: float = 0.25, + timeout: Optional[float] = None, + ) -> None: + """Prepare base instance for use by the application. + + :param executor: Executor for target container. + :param retry_wait: Duration to sleep() between status checks (if + required). + :param timeout: Timeout in seconds. + + :raises BaseCompatibilityError: if instance is incompatible. + :raises BaseConfigurationError: on other unexpected error. + """ + super().setup(executor=executor, retry_wait=retry_wait, timeout=timeout) + self._setup_snapcraft(executor=executor) diff --git a/snapcraft/providers/_get_provider.py b/snapcraft/providers/_get_provider.py new file mode 100644 index 0000000000..dcba6683ca --- /dev/null +++ b/snapcraft/providers/_get_provider.py @@ -0,0 +1,56 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright 2021-2022 Canonical Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +"""Build environment provider support for snapcraft.""" + +import sys +from typing import Optional + +from ._lxd import LXDProvider +from ._multipass import MultipassProvider +from ._provider import Provider + + +def get_provider(provider: Optional[str] = None) -> Provider: + """Get the configured or appropriate provider for the host OS. + + If platform is not Linux, use Multipass. + + If platform is Linux: + (1) use provider specified in the function argument, + (2) use provider specified with snap configuration if running + as snap, + (3) default to platform default (LXD on Linux). + + :return: Provider instance. + """ + if provider is None: + provider = _get_platform_default_provider() + + if provider == "lxd": + return LXDProvider() + + if provider == "multipass": + return MultipassProvider() + + raise RuntimeError(f"Unsupported provider specified: {provider!r}.") + + +def _get_platform_default_provider() -> str: + if sys.platform == "linux": + return "lxd" + + return "multipass" diff --git a/snapcraft/providers/_logs.py b/snapcraft/providers/_logs.py new file mode 100644 index 0000000000..56161fdcbe --- /dev/null +++ b/snapcraft/providers/_logs.py @@ -0,0 +1,51 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright 2021-2022 Canonical Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +"""Build environment provider support for snapcraft.""" + +import pathlib +import tempfile + +from craft_cli import emit +from craft_providers import Executor + +from snapcraft.utils import get_managed_environment_log_path + + +def capture_logs_from_instance(instance: Executor) -> None: + """Retrieve logs from instance. + + :param instance: Instance to retrieve logs from. + + :returns: String of logs. + """ + # Get a temporary file path. + with tempfile.NamedTemporaryFile(delete=False, prefix="snapcraft-") as tmp_file: + local_log_path = pathlib.Path(tmp_file.name) + + instance_log_path = get_managed_environment_log_path() + + try: + instance.pull_file(source=instance_log_path, destination=local_log_path) + except FileNotFoundError: + emit.trace("No logs found in instance.") + return + + emit.trace("Logs captured from managed instance:") + with local_log_path.open("rt", encoding="utf8") as logfile: + for line in logfile: + emit.trace(":: " + line.rstrip()) + local_log_path.unlink() diff --git a/snapcraft/providers/_lxd.py b/snapcraft/providers/_lxd.py new file mode 100644 index 0000000000..3add220c21 --- /dev/null +++ b/snapcraft/providers/_lxd.py @@ -0,0 +1,206 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright 2021-2022 Canonical Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +"""LXD build environment provider support for Snapcraft.""" + +import contextlib +import logging +import os +import pathlib +from typing import Generator, List + +from craft_providers import Executor, bases, lxd + +from snapcraft.utils import confirm_with_user, get_managed_environment_project_path + +from ._buildd import BASE_TO_BUILDD_IMAGE_ALIAS, SnapcraftBuilddBaseConfiguration +from ._provider import Provider, ProviderError + +logger = logging.getLogger(__name__) + +# FIXME: use final 22.04 image +_BASE_IMAGE = {"core22": "ubuntu-daily:22.04"} + + +class LXDProvider(Provider): + """LXD build environment provider. + + :param lxc: Optional lxc client to use. + :param lxd_project: LXD project to use (default is snapcraft). + :param lxd_remote: LXD remote to use (default is local). + """ + + def __init__( + self, + *, + lxc: lxd.LXC = lxd.LXC(), + lxd_project: str = "snapcraft", + lxd_remote: str = "local", + ) -> None: + self.lxc = lxc + self.lxd_project = lxd_project + self.lxd_remote = lxd_remote + + def clean_project_environments( + self, *, project_name: str, project_path: pathlib.Path + ) -> List[str]: + """Clean up any build environments created for project. + + :param project_name: Name of project. + + :returns: List of containers deleted. + """ + deleted: List[str] = [] + + # Nothing to do if provider is not installed. + if not self.is_provider_available(): + return deleted + + inode = str(project_path.stat().st_ino) + + try: + names = self.lxc.list_names( + project=self.lxd_project, remote=self.lxd_remote + ) + except lxd.LXDError as error: + raise ProviderError(str(error)) from error + + for name in names: + if name == f"snapcraft-{project_name}-{inode}": + logger.debug("Deleting container %r.", name) + try: + self.lxc.delete( + instance_name=name, + force=True, + project=self.lxd_project, + remote=self.lxd_remote, + ) + except lxd.LXDError as error: + raise ProviderError(str(error)) from error + deleted.append(name) + else: + logger.debug("Not deleting container %r.", name) + + return deleted + + @classmethod + def ensure_provider_is_available(cls) -> None: + """Ensure provider is available, prompting the user to install it if required. + + :raises ProviderError: if provider is not available. + """ + if not lxd.is_installed(): + if confirm_with_user( + "LXD is required, but not installed. Do you wish to install LXD " + "and configure it with the defaults?", + default=False, + ): + try: + lxd.install() + except lxd.LXDInstallationError as error: + raise ProviderError( + "Failed to install LXD. Visit https://snapcraft.io/lxd for " + "instructions on how to install the LXD snap for your distribution", + ) from error + else: + raise ProviderError( + "LXD is required, but not installed. Visit https://snapcraft.io/lxd " + "for instructions on how to install the LXD snap for your distribution", + ) + + try: + lxd.ensure_lxd_is_ready() + except lxd.LXDError as error: + raise ProviderError(str(error)) from error + + @classmethod + def is_provider_available(cls) -> bool: + """Check if provider is installed and available for use. + + :returns: True if installed. + """ + return lxd.is_installed() + + @contextlib.contextmanager + def launched_environment( + self, + *, + project_name: str, + project_path: pathlib.Path, + base: str, + ) -> Generator[Executor, None, None]: + """Launch environment for specified base. + + :param project_name: Name of project. + :param project_path: Path to project. + :param base: Base to create. + """ + alias = BASE_TO_BUILDD_IMAGE_ALIAS[base] + + instance_name = self.get_instance_name( + project_name=project_name, + project_path=project_path, + ) + + base_image = _BASE_IMAGE[base] + if ":" in base_image: + image_remote, image_name = base_image.split(":", 1) + else: + try: + image_remote = lxd.configure_buildd_image_remote() + image_name = base_image + except lxd.LXDError as error: + raise ProviderError(str(error)) from error + + environment = self.get_command_environment() + + base_configuration = SnapcraftBuilddBaseConfiguration( + alias=alias, # type: ignore + environment=environment, + hostname=instance_name, + ) + + try: + instance = lxd.launch( + name=instance_name, + base_configuration=base_configuration, + image_name=image_name, + image_remote=image_remote, + auto_clean=True, + auto_create_project=True, + map_user_uid=True, + uid=os.stat(project_path).st_uid, + use_snapshots=True, + project=self.lxd_project, + remote=self.lxd_remote, + ) + except (bases.BaseConfigurationError, lxd.LXDError) as error: + raise ProviderError(str(error)) from error + + # Mount project. + instance.mount( + host_source=project_path, target=get_managed_environment_project_path() + ) + + try: + yield instance + finally: + # Ensure to unmount everything and stop instance upon completion. + try: + instance.unmount_all() + instance.stop() + except lxd.LXDError as error: + raise ProviderError(str(error)) from error diff --git a/snapcraft/providers/_multipass.py b/snapcraft/providers/_multipass.py new file mode 100644 index 0000000000..007750f201 --- /dev/null +++ b/snapcraft/providers/_multipass.py @@ -0,0 +1,182 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright 2021-2022 Canonical Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +"""Multipass build environment provider for Snapcraft.""" + +import contextlib +import logging +import pathlib +from typing import Generator, List + +from craft_providers import Executor, bases, multipass +from craft_providers.multipass.errors import MultipassError + +from snapcraft.utils import confirm_with_user, get_managed_environment_project_path + +from ._buildd import BASE_TO_BUILDD_IMAGE_ALIAS, SnapcraftBuilddBaseConfiguration +from ._provider import Provider, ProviderError + +logger = logging.getLogger(__name__) + + +class MultipassProvider(Provider): + """Multipass build environment provider. + + :param multipass: Optional Multipass client to use. + """ + + def __init__( + self, + instance: multipass.Multipass = multipass.Multipass(), + ) -> None: + self.multipass = instance + + def clean_project_environments( + self, *, project_name: str, project_path: pathlib.Path + ) -> List[str]: + """Clean up any build environments created for project. + + :param project_name: Name of the project. + :param project_path: Directory of the project. + + :returns: List of containers deleted. + """ + deleted: List[str] = [] + + # Nothing to do if provider is not installed. + if not self.is_provider_available(): + return deleted + + inode = project_path.stat().st_ino + + try: + names = self.multipass.list() + except multipass.MultipassError as error: + raise ProviderError(str(error)) from error + + for name in names: + if name == f"snapcraft-{project_name}-{inode}": + logger.debug("Deleting Multipass VM %r.", name) + try: + self.multipass.delete( + instance_name=name, + purge=True, + ) + except multipass.MultipassError as error: + raise ProviderError(str(error)) from error + + deleted.append(name) + else: + logger.debug("Not deleting Multipass VM %r.", name) + + return deleted + + @classmethod + def ensure_provider_is_available(cls) -> None: + """Ensure provider is available, prompting the user to install it if required. + + :raises ProviderError: if provider is not available. + """ + if not multipass.is_installed(): + if confirm_with_user( + "Multipass is required, but not installed. Do you wish to install Multipass " + "and configure it with the defaults?", + default=False, + ): + try: + multipass.install() + except multipass.MultipassInstallationError as error: + raise ProviderError( + "Failed to install Multipass. Visit https://multipass.run/ for " + "instructions on installing Multipass for your operating system.", + ) from error + else: + raise ProviderError( + "Multipass is required, but not installed. Visit https://multipass.run/ for " + "instructions on installing Multipass for your operating system.", + ) + + try: + multipass.ensure_multipass_is_ready() + except multipass.MultipassError as error: + raise ProviderError(str(error)) from error + + @classmethod + def is_provider_available(cls) -> bool: + """Check if provider is installed and available for use. + + :returns: True if installed. + """ + return multipass.is_installed() + + @contextlib.contextmanager + def launched_environment( + self, + *, + project_name: str, + project_path: pathlib.Path, + base: str, + ) -> Generator[Executor, None, None]: + """Launch environment for specified base. + + :param project_name: Name of the project. + :param project_path: Path to project. + :param base: Base to create. + """ + alias = BASE_TO_BUILDD_IMAGE_ALIAS[base] + + instance_name = self.get_instance_name( + project_name=project_name, + project_path=project_path, + ) + + environment = self.get_command_environment() + base_configuration = SnapcraftBuilddBaseConfiguration( + alias=alias, # type: ignore + environment=environment, + hostname=instance_name, + ) + + try: + instance = multipass.launch( + name=instance_name, + base_configuration=base_configuration, + image_name=f"snapcraft:{base}", + cpus=2, + disk_gb=64, + mem_gb=2, + auto_clean=True, + ) + except (bases.BaseConfigurationError, MultipassError) as error: + raise ProviderError(str(error)) from error + + try: + # Mount project. + instance.mount( + host_source=project_path, target=get_managed_environment_project_path() + ) + except MultipassError as error: + raise ProviderError(str(error)) from error + + try: + yield instance + finally: + # Ensure to unmount everything and stop instance upon completion. + try: + instance.unmount_all() + instance.stop() + except MultipassError as error: + raise ProviderError(str(error)) from error diff --git a/snapcraft/providers/_provider.py b/snapcraft/providers/_provider.py new file mode 100644 index 0000000000..8c056651fb --- /dev/null +++ b/snapcraft/providers/_provider.py @@ -0,0 +1,125 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright 2021-2022 Canonical Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +"""Build environment provider support for snapcraft.""" + +import contextlib +import os +import pathlib +from abc import ABC, abstractmethod +from typing import Dict, Generator, List, Optional, Tuple, Union + +from craft_providers import Executor, bases + +from snapcraft.errors import SnapcraftError + + +class ProviderError(SnapcraftError): + """Error in provider operation.""" + + +class Provider(ABC): + """Snapcraft's build environment provider.""" + + @abstractmethod + def clean_project_environments( + self, *, project_name: str, project_path: pathlib.Path + ) -> List[str]: + """Clean up any environments created for project. + + :param project_name: Name of project. + + :returns: List of containers deleted. + """ + + @classmethod + @abstractmethod + def ensure_provider_is_available(cls) -> None: + """Ensure provider is available, prompting the user to install it if required. + + :raises ProviderError: if provider is not available. + """ + + @staticmethod + def get_command_environment() -> Dict[str, Optional[str]]: + """Construct the required environment.""" + env = bases.buildd.default_command_environment() + env["SNAPCRAFT_MANAGED_MODE"] = "1" + + # Pass-through host environment that target may need. + for env_key in ["http_proxy", "https_proxy", "no_proxy"]: + if env_key in os.environ: + env[env_key] = os.environ[env_key] + + return env + + @staticmethod + def get_instance_name( + *, + project_name: str, + project_path: pathlib.Path, + ) -> str: + """Formulate the name for an instance using each of the given parameters. + + Incorporate each of the parameters into the name to come up with a + predictable naming schema that avoids name collisions across multiple + projects. + + :param project_name: Name of the project. + :param project_path: Directory of the project. + """ + return "-".join(["snapcraft", project_name, str(project_path.stat().st_ino)]) + + @classmethod + def is_base_available(cls, base: str) -> Tuple[bool, Union[str, None]]: + """Check if provider can provide an environment matching given base. + + :param base: Base to check. + + :returns: Tuple of bool indicating whether it is a match, with optional + reason if not a match. + """ + if base not in ["ubuntu:18.04", "ubuntu:20.04"]: + return ( + False, + f"Base {base!r} is not supported (must be 'ubuntu:18.04' or 'ubuntu:20.04')", + ) + + return True, None + + @classmethod + @abstractmethod + def is_provider_available(cls) -> bool: + """Check if provider is installed and available for use. + + :returns: True if installed. + """ + + @abstractmethod + @contextlib.contextmanager + def launched_environment( + self, + *, + project_name: str, + project_path: pathlib.Path, + base: str, + ) -> Generator[Executor, None, None]: + """Launch environment for specified base. + + :param project_name: Name of the project. + :param project_path: Path to the project. + :param base: Base to create. + """ diff --git a/snapcraft/utils.py b/snapcraft/utils.py new file mode 100644 index 0000000000..830b316ba8 --- /dev/null +++ b/snapcraft/utils.py @@ -0,0 +1,87 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright 2021-2022 Canonical Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +"""Utilities for snapcraft.""" + +import distutils.util +import logging +import os +import pathlib +import sys +from collections import namedtuple +from typing import Optional + +logger = logging.getLogger(__name__) + +OSPlatform = namedtuple("OSPlatform", "system release machine") + + +def is_managed_mode(): + """Check if snapcraft is running in a managed environment.""" + managed_flag = os.getenv("SNAPCRAFT_MANAGED_MODE", "n") + return distutils.util.strtobool(managed_flag) == 1 + + +def get_managed_environment_home_path(): + """Path for home when running in managed environment.""" + return pathlib.Path("/root") + + +def get_managed_environment_project_path(): + """Path for project when running in managed environment.""" + return get_managed_environment_home_path() / "project" + + +def get_managed_environment_log_path(): + """Path for log when running in managed environment.""" + return pathlib.Path("/tmp/snapcraft.log") + + +def get_managed_environment_snap_channel() -> Optional[str]: + """User-specified channel to use when installing Snapcraft snap from Snap Store. + + :returns: Channel string if specified, else None. + """ + return os.getenv("SNAPCRAFT_INSTALL_SNAP_CHANNEL") + + +def confirm_with_user(prompt, default=False) -> bool: + """Query user for yes/no answer. + + If stdin is not a tty, the default value is returned. + + If user returns an empty answer, the default value is returned. + returns default value. + + :returns: True if answer starts with [yY], False if answer starts with [nN], + otherwise the default. + """ + if is_managed_mode(): + raise RuntimeError("confirmation not yet supported in managed-mode") + + if not sys.stdin.isatty(): + return default + + choices = " [Y/n]: " if default else " [y/N]: " + + reply = str(input(prompt + choices)).lower().strip() + if reply and reply[0] == "y": + return True + + if reply and reply[0] == "n": + return False + + return default diff --git a/tests/unit/cli/test_default_command.py b/tests/unit/cli/test_default_command.py index af7f24f314..f1457737bd 100644 --- a/tests/unit/cli/test_default_command.py +++ b/tests/unit/cli/test_default_command.py @@ -28,7 +28,34 @@ def test_default_command(mocker): mock_pack_cmd = mocker.patch("snapcraft.commands.lifecycle.PackCommand.run") cli.run() assert mock_pack_cmd.mock_calls == [ - call(argparse.Namespace(directory=None, output=None)) + call( + argparse.Namespace( + directory=None, + output=None, + destructive_mode=False, + use_lxd=False, + provider=None, + ) + ) + ] + + +def test_default_command_parameters(mocker): + mocker.patch.object( + sys, "argv", ["cmd", "--destructive-mode", "--use-lxd", "--provider=lxd"] + ) + mock_pack_cmd = mocker.patch("snapcraft.commands.lifecycle.PackCommand.run") + cli.run() + assert mock_pack_cmd.mock_calls == [ + call( + argparse.Namespace( + directory=None, + output=None, + destructive_mode=True, + use_lxd=True, + provider="lxd", + ) + ) ] @@ -38,5 +65,13 @@ def test_default_command_output(mocker, option): mock_pack_cmd = mocker.patch("snapcraft.commands.lifecycle.PackCommand.run") cli.run() assert mock_pack_cmd.mock_calls == [ - call(argparse.Namespace(directory=None, output="name")) + call( + argparse.Namespace( + directory=None, + output="name", + destructive_mode=False, + use_lxd=False, + provider=None, + ) + ) ] diff --git a/tests/unit/cli/test_lifecycle.py b/tests/unit/cli/test_lifecycle.py index a5b6d1afcd..5ce6379e8f 100644 --- a/tests/unit/cli/test_lifecycle.py +++ b/tests/unit/cli/test_lifecycle.py @@ -36,7 +36,13 @@ def test_lifecycle_command(cmd, run_method, mocker): mocker.patch.object(sys, "argv", ["cmd", cmd]) mock_lifecycle_cmd = mocker.patch(run_method) cli.run() - assert mock_lifecycle_cmd.mock_calls == [call(argparse.Namespace(parts=[]))] + assert mock_lifecycle_cmd.mock_calls == [ + call( + argparse.Namespace( + parts=[], destructive_mode=False, use_lxd=False, provider=None + ) + ) + ] @pytest.mark.parametrize( @@ -49,20 +55,75 @@ def test_lifecycle_command(cmd, run_method, mocker): ], ) def test_lifecycle_command_arguments(cmd, run_method, mocker): - mocker.patch.object(sys, "argv", ["cmd", cmd, "part1", "part2"]) + mocker.patch.object( + sys, + "argv", + [ + "cmd", + cmd, + "--destructive-mode", + "--use-lxd", + "--provider=lxd", + "part1", + "part2", + ], + ) mock_lifecycle_cmd = mocker.patch(run_method) cli.run() assert mock_lifecycle_cmd.mock_calls == [ - call(argparse.Namespace(parts=["part1", "part2"])) + call( + argparse.Namespace( + parts=["part1", "part2"], + destructive_mode=True, + use_lxd=True, + provider="lxd", + ) + ) ] +@pytest.mark.parametrize("provider", ["host", "lxd", "multipass"]) +def test_lifecycle_command_provider(mocker, provider): + mocker.patch.object(sys, "argv", ["cmd", "pack", "--provider=" + provider]) + mock_pack_cmd = mocker.patch("snapcraft.commands.lifecycle.PackCommand.run") + cli.run() + assert mock_pack_cmd.mock_calls == [ + call( + argparse.Namespace( + directory=None, + output=None, + destructive_mode=False, + use_lxd=False, + provider=provider, + ) + ) + ] + + +def test_lifecycle_command_provider_invalid(mocker): + mocker.patch.object(sys, "argv", ["cmd", "pack", "--provider=foo"]) + mock_pack_cmd = mocker.patch("snapcraft.commands.lifecycle.PackCommand.run") + assert mock_pack_cmd.mock_calls == [] + + def test_lifecycle_command_pack(mocker): - mocker.patch.object(sys, "argv", ["cmd", "pack"]) + mocker.patch.object( + sys, + "argv", + ["cmd", "pack", "--destructive-mode", "--use-lxd", "--provider=lxd"], + ) mock_pack_cmd = mocker.patch("snapcraft.commands.lifecycle.PackCommand.run") cli.run() assert mock_pack_cmd.mock_calls == [ - call(argparse.Namespace(directory=None, output=None)) + call( + argparse.Namespace( + directory=None, + output=None, + destructive_mode=True, + use_lxd=True, + provider="lxd", + ) + ) ] @@ -72,7 +133,15 @@ def test_lifecycle_command_pack_output(mocker, option): mock_pack_cmd = mocker.patch("snapcraft.commands.lifecycle.PackCommand.run") cli.run() assert mock_pack_cmd.mock_calls == [ - call(argparse.Namespace(directory=None, output="name")) + call( + argparse.Namespace( + directory=None, + output="name", + destructive_mode=False, + use_lxd=False, + provider=None, + ) + ) ] @@ -81,5 +150,13 @@ def test_lifecycle_command_pack_directory(mocker): mock_pack_cmd = mocker.patch("snapcraft.commands.lifecycle.PackCommand.run") cli.run() assert mock_pack_cmd.mock_calls == [ - call(argparse.Namespace(directory="name", output=None)) + call( + argparse.Namespace( + directory="name", + output=None, + destructive_mode=False, + use_lxd=False, + provider=None, + ) + ) ] diff --git a/tests/unit/parts/test_lifecycle.py b/tests/unit/parts/test_lifecycle.py index 2fb36dd1f7..ed68ec4bf1 100644 --- a/tests/unit/parts/test_lifecycle.py +++ b/tests/unit/parts/test_lifecycle.py @@ -124,7 +124,7 @@ def test_snapcraft_yaml_load(new_dir, snapcraft_yaml, filename, mocker): "pull", project=project, assets_dir=assets_dir, - parsed_args=argparse.Namespace(parts=["part1"]) + parsed_args=argparse.Namespace(parts=["part1"]), ), ] @@ -169,7 +169,12 @@ def test_lifecycle_run_command_step(cmd, step, snapcraft_yaml, new_dir, mocker): pack_mock = mocker.patch("snapcraft.pack.pack_snap") parts_lifecycle._run_command( - cmd, project=project, assets_dir=Path(), parsed_args=argparse.Namespace() + cmd, + project=project, + assets_dir=Path(), + parsed_args=argparse.Namespace( + destructive_mode=True, use_lxd=False, provider=None + ), ) assert run_mock.mock_calls == [call(step)] @@ -186,10 +191,115 @@ def test_lifecycle_run_command_pack(snapcraft_yaml, new_dir, mocker): "pack", project=project, assets_dir=Path(), - parsed_args=argparse.Namespace(directory=None, output=None), + parsed_args=argparse.Namespace( + directory=None, + output=None, + destructive_mode=True, + use_lxd=False, + provider=None, + ), ) assert run_mock.mock_calls == [call("prime")] assert pack_mock.mock_calls == [ call(new_dir / "work/prime", output=None, compression="xz") ] + + +def test_lifecycle_pack_destructive_mode(snapcraft_yaml, new_dir, mocker): + project = Project.unmarshal(snapcraft_yaml(base="core22")) + run_in_provider_mock = mocker.patch("snapcraft.parts.lifecycle._run_in_provider") + run_mock = mocker.patch("snapcraft.parts.PartsLifecycle.run") + pack_mock = mocker.patch("snapcraft.pack.pack_snap") + mocker.patch("snapcraft.meta.snap_yaml.write") + mocker.patch("snapcraft.utils.is_managed_mode", return_value=True) + mocker.patch( + "snapcraft.utils.get_managed_environment_home_path", + return_value=new_dir / "home", + ) + + parts_lifecycle._run_command( + "pack", + project=project, + assets_dir=Path(), + parsed_args=argparse.Namespace( + directory=None, + output=None, + destructive_mode=True, + use_lxd=False, + provider=None, + ), + ) + + assert run_in_provider_mock.mock_calls == [] + assert run_mock.mock_calls == [call("prime")] + assert pack_mock.mock_calls == [ + call(new_dir / "home/prime", output=None, compression="xz") + ] + + +def test_lifecycle_pack_managed(snapcraft_yaml, new_dir, mocker): + project = Project.unmarshal(snapcraft_yaml(base="core22")) + run_in_provider_mock = mocker.patch("snapcraft.parts.lifecycle._run_in_provider") + run_mock = mocker.patch("snapcraft.parts.PartsLifecycle.run") + pack_mock = mocker.patch("snapcraft.pack.pack_snap") + mocker.patch("snapcraft.meta.snap_yaml.write") + mocker.patch("snapcraft.utils.is_managed_mode", return_value=True) + mocker.patch( + "snapcraft.utils.get_managed_environment_home_path", + return_value=new_dir / "home", + ) + + parts_lifecycle._run_command( + "pack", + project=project, + assets_dir=Path(), + parsed_args=argparse.Namespace( + directory=None, + output=None, + destructive_mode=False, + use_lxd=False, + provider=None, + ), + ) + + assert run_in_provider_mock.mock_calls == [] + assert run_mock.mock_calls == [call("prime")] + assert pack_mock.mock_calls == [ + call(new_dir / "home/prime", output=None, compression="xz") + ] + + +def test_lifecycle_pack_not_managed(snapcraft_yaml, new_dir, mocker): + project = Project.unmarshal(snapcraft_yaml(base="core22")) + run_in_provider_mock = mocker.patch("snapcraft.parts.lifecycle._run_in_provider") + run_mock = mocker.patch("snapcraft.parts.PartsLifecycle.run") + mocker.patch("snapcraft.utils.is_managed_mode", return_value=False) + + parts_lifecycle._run_command( + "pack", + project=project, + assets_dir=Path(), + parsed_args=argparse.Namespace( + directory=None, + output=None, + destructive_mode=False, + use_lxd=False, + provider=None, + ), + ) + + assert run_mock.mock_calls == [] + assert run_in_provider_mock.mock_calls == [ + call( + project, + "pack", + argparse.Namespace( + directory=None, + output=None, + destructive_mode=False, + use_lxd=False, + provider=None, + ), + ) + ] diff --git a/tests/unit/test_pack.py b/tests/unit/test_pack.py index 9653e3055d..87158bc2b3 100644 --- a/tests/unit/test_pack.py +++ b/tests/unit/test_pack.py @@ -26,7 +26,12 @@ def test_pack_snap(mocker, new_dir): mock_run = mocker.patch("subprocess.run") pack.pack_snap(new_dir, output=None) assert mock_run.mock_calls == [ - call(["snap", "pack", new_dir], capture_output=True, check=True) + call( + ["snap", "pack", new_dir], + capture_output=True, + check=True, + universal_newlines=True, + ) ] @@ -34,7 +39,12 @@ def test_pack_snap_compression_none(mocker, new_dir): mock_run = mocker.patch("subprocess.run") pack.pack_snap(new_dir, output=None, compression=None) assert mock_run.mock_calls == [ - call(["snap", "pack", new_dir], capture_output=True, check=True) + call( + ["snap", "pack", new_dir], + capture_output=True, + check=True, + universal_newlines=True, + ) ] @@ -46,6 +56,7 @@ def test_pack_snap_compression(mocker, new_dir): ["snap", "pack", "--compression", "zz", new_dir], capture_output=True, check=True, + universal_newlines=True, ) ] @@ -58,6 +69,7 @@ def test_pack_snap_output_file(mocker, new_dir): ["snap", "pack", "--filename", "foo", new_dir, "/tmp"], capture_output=True, check=True, + universal_newlines=True, ) ] @@ -70,6 +82,7 @@ def test_pack_snap_output_dir(mocker, new_dir): ["snap", "pack", new_dir, str(new_dir)], capture_output=True, check=True, + universal_newlines=True, ) ] From 204aba89ba3ecbf5f740ed3987a84dd0ab78df15 Mon Sep 17 00:00:00 2001 From: Claudio Matsuoka Date: Fri, 4 Mar 2022 11:25:41 +0100 Subject: [PATCH 054/167] tests: also run core22 spread tests in destructive mode Signed-off-by: Claudio Matsuoka --- tests/spread/general/core22/task.yaml | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/tests/spread/general/core22/task.yaml b/tests/spread/general/core22/task.yaml index 873cf3fcc8..465d4e4dd3 100644 --- a/tests/spread/general/core22/task.yaml +++ b/tests/spread/general/core22/task.yaml @@ -25,11 +25,21 @@ restore: | execute: | cd "$SNAP" - # Build what we have. if [ "$SPREAD_SYSTEM" = "ubuntu-20.04-64" ]; then - snapcraft --verbose + # No jammy for this ppa yet + if [ "$(basename "$SNAP")" != "test-apt-ppa" ]; then + # Build what we have. + snapcraft --verbose --use-lxd - # And verify the snap runs as expected. + # And verify the snap runs as expected. + snap install "${SNAP}"_1.0_*.snap --dangerous + snap_executable="${SNAP}.test-ppa" + [ "$("${snap_executable}")" = "hello!" ] + fi + + # Do it again in destructive mode + snap remove "${SNAP}" + snapcraft --verbose --destructive-mode snap install "${SNAP}"_1.0_*.snap --dangerous snap_executable="${SNAP}.test-ppa" [ "$("${snap_executable}")" = "hello!" ] From 507959d701de461ab9a5d29194837fa5207d1534 Mon Sep 17 00:00:00 2001 From: Callahan Kovacs Date: Mon, 14 Mar 2022 12:29:48 -0500 Subject: [PATCH 055/167] tests: update spread url Signed-off-by: Callahan Kovacs --- TESTING.md | 2 +- runtests.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/TESTING.md b/TESTING.md index cbd0b6ddc5..6683153d07 100644 --- a/TESTING.md +++ b/TESTING.md @@ -155,7 +155,7 @@ It's possible to select only one of the suites using `--test-name`, for example: To run them, first, download the spread binary: - curl -s -O https://niemeyer.s3.amazonaws.com/spread-amd64.tar.gz && tar xzvf spread-amd64.tar.gz + curl -s -O https://storage.googleapis.com/snapd-spread-tests/spread/spread-amd64.tar.gz && tar xzvf spread-amd64.tar.gz Then, you can run them using a local LXD as the backend with: diff --git a/runtests.sh b/runtests.sh index 7e657f23ab..b17489f685 100755 --- a/runtests.sh +++ b/runtests.sh @@ -39,7 +39,7 @@ run_snapcraft_tests(){ run_spread(){ TMP_SPREAD="$(mktemp -d)" - curl -s https://niemeyer.s3.amazonaws.com/spread-amd64.tar.gz | tar xzv -C "$TMP_SPREAD" + curl -s https://storage.googleapis.com/snapd-spread-tests/spread/spread-amd64.tar.gz | tar xzv -C "$TMP_SPREAD" if [[ "$#" -eq 0 ]]; then "$TMP_SPREAD/spread" -v lxd: From d82e2f29d546f70f530bfda2ec3eb59adc03e3cb Mon Sep 17 00:00:00 2001 From: Claudio Matsuoka Date: Tue, 15 Mar 2022 14:07:08 -0300 Subject: [PATCH 056/167] parts: use current dir as work dir Having parts, stage and prime in the current directory is a design requirement. This change only affects destructive mode builds. Signed-off-by: Claudio Matsuoka --- snapcraft/parts/lifecycle.py | 2 +- tests/unit/parts/test_lifecycle.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/snapcraft/parts/lifecycle.py b/snapcraft/parts/lifecycle.py index c7ca8ed491..14d19af271 100644 --- a/snapcraft/parts/lifecycle.py +++ b/snapcraft/parts/lifecycle.py @@ -112,7 +112,7 @@ def _run_command( if managed_mode: work_dir = utils.get_managed_environment_home_path() else: - work_dir = Path("work").absolute() + work_dir = Path.cwd() step_name = "prime" if command_name == "pack" else command_name diff --git a/tests/unit/parts/test_lifecycle.py b/tests/unit/parts/test_lifecycle.py index ed68ec4bf1..0916ee918b 100644 --- a/tests/unit/parts/test_lifecycle.py +++ b/tests/unit/parts/test_lifecycle.py @@ -202,7 +202,7 @@ def test_lifecycle_run_command_pack(snapcraft_yaml, new_dir, mocker): assert run_mock.mock_calls == [call("prime")] assert pack_mock.mock_calls == [ - call(new_dir / "work/prime", output=None, compression="xz") + call(new_dir / "prime", output=None, compression="xz") ] From 1b18e46d3e04c110822198b0917225f9860bc0a9 Mon Sep 17 00:00:00 2001 From: Claudio Matsuoka Date: Tue, 15 Mar 2022 14:19:40 -0300 Subject: [PATCH 057/167] parts: adjust message level Signed-off-by: Claudio Matsuoka --- snapcraft/parts/lifecycle.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/snapcraft/parts/lifecycle.py b/snapcraft/parts/lifecycle.py index 14d19af271..2ada1cd86c 100644 --- a/snapcraft/parts/lifecycle.py +++ b/snapcraft/parts/lifecycle.py @@ -157,7 +157,7 @@ def _run_in_provider(project: Project, command_name: str, parsed_args: "argparse """Pack image in provider instance.""" provider = "lxd" if parsed_args.use_lxd else parsed_args.provider - emit.progress("Checking build provider availability") + emit.trace("Checking build provider availability") provider = providers.get_provider(provider) provider.ensure_provider_is_available() From 94f5b97eae09272e6c70028bc75a83e3cded7bc7 Mon Sep 17 00:00:00 2001 From: Claudio Matsuoka Date: Tue, 15 Mar 2022 14:59:56 -0300 Subject: [PATCH 058/167] commands: remove provider argument Remove --provider argument from lifecycle commands, and ensure --destructive-mode and --use-lxd are mutually exclusive. Note that --provider=name must still work in legacy snapcraft. Signed-off-by: Claudio Matsuoka --- snapcraft/commands/lifecycle.py | 21 ++--- snapcraft/parts/lifecycle.py | 12 +-- tests/unit/cli/test_default_command.py | 25 ++++-- tests/unit/cli/test_lifecycle.py | 113 ++++++++++++++++++++++--- tests/unit/parts/test_lifecycle.py | 60 ++++++++++--- 5 files changed, 184 insertions(+), 47 deletions(-) diff --git a/snapcraft/commands/lifecycle.py b/snapcraft/commands/lifecycle.py index 3cedd14b44..5338ec48c6 100644 --- a/snapcraft/commands/lifecycle.py +++ b/snapcraft/commands/lifecycle.py @@ -17,8 +17,8 @@ """Snapcraft lifecycle commands.""" import abc +import argparse import textwrap -from typing import TYPE_CHECKING from craft_cli import BaseCommand, emit from overrides import overrides @@ -26,30 +26,25 @@ from snapcraft import pack from snapcraft.parts import lifecycle as parts_lifecycle -if TYPE_CHECKING: - import argparse - class _LifecycleCommand(BaseCommand, abc.ABC): """Run lifecycle-related commands.""" @overrides def fill_parser(self, parser: "argparse.ArgumentParser") -> None: - parser.add_argument( + group = parser.add_mutually_exclusive_group() + group.add_argument( "--destructive-mode", action="store_true", - help="Build in the current host (implies `--provider=host`)", + help="Build in the current host", ) - parser.add_argument( - "--provider", - choices=["host", "lxd", "multipass"], - help="The build provider to use", - ) - parser.add_argument( + group.add_argument( "--use-lxd", action="store_true", - help="Use LXD to build (implies `--provider=lxd`)", + help="Use LXD to build", ) + # --provider is only available in legacy + parser.add_argument("--provider", help=argparse.SUPPRESS) @overrides def run(self, parsed_args): diff --git a/snapcraft/parts/lifecycle.py b/snapcraft/parts/lifecycle.py index 2ada1cd86c..7233a7099f 100644 --- a/snapcraft/parts/lifecycle.py +++ b/snapcraft/parts/lifecycle.py @@ -78,6 +78,10 @@ def run(command_name: str, parsed_args: "argparse.Namespace") -> None: if yaml_data.get("base") != "core22": raise errors.LegacyFallback("base is not core22") + # argument --provider is only supported by legacy snapcraft + if parsed_args.provider: + raise errors.SnapcraftError("Option --provider is not supported.") + # TODO: apply extensions # yaml_data = apply_extensions(yaml_data) @@ -102,10 +106,9 @@ def _run_command( assets_dir: Path, parsed_args: "argparse.Namespace", ) -> None: - destructive_mode = parsed_args.destructive_mode or parsed_args.provider == "host" managed_mode = utils.is_managed_mode() - if not managed_mode and not destructive_mode: + if not managed_mode and not parsed_args.destructive_mode: _run_in_provider(project, command_name, parsed_args) return @@ -155,10 +158,9 @@ def _load_yaml(filename: Path) -> Dict[str, Any]: def _run_in_provider(project: Project, command_name: str, parsed_args: "argparse.Namespace"): """Pack image in provider instance.""" - provider = "lxd" if parsed_args.use_lxd else parsed_args.provider - emit.trace("Checking build provider availability") - provider = providers.get_provider(provider) + provider_name = "lxd" if parsed_args.use_lxd else None + provider = providers.get_provider(provider_name) provider.ensure_provider_is_available() cmd = ["snapcraft", command_name] diff --git a/tests/unit/cli/test_default_command.py b/tests/unit/cli/test_default_command.py index f1457737bd..848d2d301b 100644 --- a/tests/unit/cli/test_default_command.py +++ b/tests/unit/cli/test_default_command.py @@ -40,10 +40,8 @@ def test_default_command(mocker): ] -def test_default_command_parameters(mocker): - mocker.patch.object( - sys, "argv", ["cmd", "--destructive-mode", "--use-lxd", "--provider=lxd"] - ) +def test_default_command_destructive_mode(mocker): + mocker.patch.object(sys, "argv", ["cmd", "--destructive-mode"]) mock_pack_cmd = mocker.patch("snapcraft.commands.lifecycle.PackCommand.run") cli.run() assert mock_pack_cmd.mock_calls == [ @@ -52,8 +50,25 @@ def test_default_command_parameters(mocker): directory=None, output=None, destructive_mode=True, + use_lxd=False, + provider=None, + ) + ) + ] + + +def test_default_command_use_lxd(mocker): + mocker.patch.object(sys, "argv", ["cmd", "--use-lxd"]) + mock_pack_cmd = mocker.patch("snapcraft.commands.lifecycle.PackCommand.run") + cli.run() + assert mock_pack_cmd.mock_calls == [ + call( + argparse.Namespace( + directory=None, + output=None, + destructive_mode=False, use_lxd=True, - provider="lxd", + provider=None, ) ) ] diff --git a/tests/unit/cli/test_lifecycle.py b/tests/unit/cli/test_lifecycle.py index 5ce6379e8f..5ad5e05bd6 100644 --- a/tests/unit/cli/test_lifecycle.py +++ b/tests/unit/cli/test_lifecycle.py @@ -55,6 +55,40 @@ def test_lifecycle_command(cmd, run_method, mocker): ], ) def test_lifecycle_command_arguments(cmd, run_method, mocker): + mocker.patch.object( + sys, + "argv", + [ + "cmd", + cmd, + "part1", + "part2", + ], + ) + mock_lifecycle_cmd = mocker.patch(run_method) + cli.run() + assert mock_lifecycle_cmd.mock_calls == [ + call( + argparse.Namespace( + parts=["part1", "part2"], + destructive_mode=False, + use_lxd=False, + provider=None, + ) + ) + ] + + +@pytest.mark.parametrize( + "cmd,run_method", + [ + ("pull", "snapcraft.commands.lifecycle.PullCommand.run"), + ("build", "snapcraft.commands.lifecycle.BuildCommand.run"), + ("stage", "snapcraft.commands.lifecycle.StageCommand.run"), + ("prime", "snapcraft.commands.lifecycle.PrimeCommand.run"), + ], +) +def test_lifecycle_command_arguments_destructive_mode(cmd, run_method, mocker): mocker.patch.object( sys, "argv", @@ -62,8 +96,6 @@ def test_lifecycle_command_arguments(cmd, run_method, mocker): "cmd", cmd, "--destructive-mode", - "--use-lxd", - "--provider=lxd", "part1", "part2", ], @@ -75,16 +107,54 @@ def test_lifecycle_command_arguments(cmd, run_method, mocker): argparse.Namespace( parts=["part1", "part2"], destructive_mode=True, + use_lxd=False, + provider=None, + ) + ) + ] + + +@pytest.mark.parametrize( + "cmd,run_method", + [ + ("pull", "snapcraft.commands.lifecycle.PullCommand.run"), + ("build", "snapcraft.commands.lifecycle.BuildCommand.run"), + ("stage", "snapcraft.commands.lifecycle.StageCommand.run"), + ("prime", "snapcraft.commands.lifecycle.PrimeCommand.run"), + ], +) +def test_lifecycle_command_arguments_use_lxd(cmd, run_method, mocker): + mocker.patch.object( + sys, + "argv", + [ + "cmd", + cmd, + "--use-lxd", + "part1", + "part2", + ], + ) + mock_lifecycle_cmd = mocker.patch(run_method) + cli.run() + assert mock_lifecycle_cmd.mock_calls == [ + call( + argparse.Namespace( + parts=["part1", "part2"], + destructive_mode=False, use_lxd=True, - provider="lxd", + provider=None, ) ) ] -@pytest.mark.parametrize("provider", ["host", "lxd", "multipass"]) -def test_lifecycle_command_provider(mocker, provider): - mocker.patch.object(sys, "argv", ["cmd", "pack", "--provider=" + provider]) +def test_lifecycle_command_pack(mocker): + mocker.patch.object( + sys, + "argv", + ["cmd", "pack"], + ) mock_pack_cmd = mocker.patch("snapcraft.commands.lifecycle.PackCommand.run") cli.run() assert mock_pack_cmd.mock_calls == [ @@ -94,23 +164,38 @@ def test_lifecycle_command_provider(mocker, provider): output=None, destructive_mode=False, use_lxd=False, - provider=provider, + provider=None, ) ) ] -def test_lifecycle_command_provider_invalid(mocker): - mocker.patch.object(sys, "argv", ["cmd", "pack", "--provider=foo"]) +def test_lifecycle_command_pack_destructive_mode(mocker): + mocker.patch.object( + sys, + "argv", + ["cmd", "pack", "--destructive-mode"], + ) mock_pack_cmd = mocker.patch("snapcraft.commands.lifecycle.PackCommand.run") - assert mock_pack_cmd.mock_calls == [] + cli.run() + assert mock_pack_cmd.mock_calls == [ + call( + argparse.Namespace( + directory=None, + output=None, + destructive_mode=True, + use_lxd=False, + provider=None, + ) + ) + ] -def test_lifecycle_command_pack(mocker): +def test_lifecycle_command_pack_use_lxd(mocker): mocker.patch.object( sys, "argv", - ["cmd", "pack", "--destructive-mode", "--use-lxd", "--provider=lxd"], + ["cmd", "pack", "--use-lxd"], ) mock_pack_cmd = mocker.patch("snapcraft.commands.lifecycle.PackCommand.run") cli.run() @@ -119,9 +204,9 @@ def test_lifecycle_command_pack(mocker): argparse.Namespace( directory=None, output=None, - destructive_mode=True, + destructive_mode=False, use_lxd=True, - provider="lxd", + provider=None, ) ) ] diff --git a/tests/unit/parts/test_lifecycle.py b/tests/unit/parts/test_lifecycle.py index 0916ee918b..e3913d1d8f 100644 --- a/tests/unit/parts/test_lifecycle.py +++ b/tests/unit/parts/test_lifecycle.py @@ -110,7 +110,12 @@ def test_snapcraft_yaml_load(new_dir, snapcraft_yaml, filename, mocker): yaml_data = snapcraft_yaml(base="core22", filename=filename) run_command_mock = mocker.patch("snapcraft.parts.lifecycle._run_command") - parts_lifecycle.run("pull", argparse.Namespace(parts=["part1"])) + parts_lifecycle.run( + "pull", + argparse.Namespace( + parts=["part1"], destructive_mode=True, use_lxd=False, provider=None + ), + ) project = Project.unmarshal(yaml_data) @@ -124,7 +129,9 @@ def test_snapcraft_yaml_load(new_dir, snapcraft_yaml, filename, mocker): "pull", project=project, assets_dir=assets_dir, - parsed_args=argparse.Namespace(parts=["part1"]), + parsed_args=argparse.Namespace( + parts=["part1"], destructive_mode=True, use_lxd=False, provider=None + ), ), ] @@ -153,6 +160,46 @@ def test_legacy_base_not_core22(new_dir, snapcraft_yaml): assert str(raised.value) == "base is not core22" +@pytest.mark.parametrize("cmd", ["pull", "build", "stage", "prime", "pack"]) +def test_lifecycle_run_provider(cmd, snapcraft_yaml, new_dir, mocker): + """Option --provider is not supported in core22.""" + snapcraft_yaml(base="core22") + run_mock = mocker.patch("snapcraft.parts.PartsLifecycle.run") + + with pytest.raises(errors.SnapcraftError) as raised: + parts_lifecycle.run( + cmd, + parsed_args=argparse.Namespace( + destructive_mode=False, + use_lxd=False, + provider="some", + ), + ) + + assert run_mock.mock_calls == [] + assert str(raised.value) == "Option --provider is not supported." + + +@pytest.mark.parametrize("cmd", ["pull", "build", "stage", "prime"]) +def test_lifecycle_legacy_run_provider(cmd, snapcraft_yaml, new_dir, mocker): + """Option --provider is supported by legacy.""" + snapcraft_yaml(base="core20") + run_mock = mocker.patch("snapcraft.parts.PartsLifecycle.run") + + with pytest.raises(errors.LegacyFallback) as raised: + parts_lifecycle.run( + cmd, + parsed_args=argparse.Namespace( + destructive_mode=False, + use_lxd=False, + provider="some", + ), + ) + + assert run_mock.mock_calls == [] + assert str(raised.value) == "base is not core22" + + @pytest.mark.parametrize( "cmd,step", [ @@ -172,9 +219,7 @@ def test_lifecycle_run_command_step(cmd, step, snapcraft_yaml, new_dir, mocker): cmd, project=project, assets_dir=Path(), - parsed_args=argparse.Namespace( - destructive_mode=True, use_lxd=False, provider=None - ), + parsed_args=argparse.Namespace(destructive_mode=True, use_lxd=False), ) assert run_mock.mock_calls == [call(step)] @@ -196,7 +241,6 @@ def test_lifecycle_run_command_pack(snapcraft_yaml, new_dir, mocker): output=None, destructive_mode=True, use_lxd=False, - provider=None, ), ) @@ -227,7 +271,6 @@ def test_lifecycle_pack_destructive_mode(snapcraft_yaml, new_dir, mocker): output=None, destructive_mode=True, use_lxd=False, - provider=None, ), ) @@ -259,7 +302,6 @@ def test_lifecycle_pack_managed(snapcraft_yaml, new_dir, mocker): output=None, destructive_mode=False, use_lxd=False, - provider=None, ), ) @@ -285,7 +327,6 @@ def test_lifecycle_pack_not_managed(snapcraft_yaml, new_dir, mocker): output=None, destructive_mode=False, use_lxd=False, - provider=None, ), ) @@ -299,7 +340,6 @@ def test_lifecycle_pack_not_managed(snapcraft_yaml, new_dir, mocker): output=None, destructive_mode=False, use_lxd=False, - provider=None, ), ) ] From 9d8dd34df3de60422fa95aadb73d4104c611a84d Mon Sep 17 00:00:00 2001 From: Claudio Matsuoka Date: Tue, 15 Mar 2022 18:50:45 -0300 Subject: [PATCH 059/167] parts: change launch instance message Changed message to avoid mentioning build providers (or build instances since build is also a step name). Message output is still broken, need support in craft-cli for message pausing. Signed-off-by: Claudio Matsuoka --- snapcraft/parts/lifecycle.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/snapcraft/parts/lifecycle.py b/snapcraft/parts/lifecycle.py index 7233a7099f..cfb992f3c1 100644 --- a/snapcraft/parts/lifecycle.py +++ b/snapcraft/parts/lifecycle.py @@ -170,11 +170,13 @@ def _run_in_provider(project: Project, command_name: str, parsed_args: "argparse output_dir = utils.get_managed_environment_project_path() - emit.progress("Launching build provider") + # FIXME: pause emitter when executing instance (needs craft-cli support) + emit.progress("Launching instance...") with provider.launched_environment( project_name=project.name, project_path=Path().absolute(), base=cast(str, project.base) ) as instance: try: + emit.message("Launched instance", intermediate=True) instance.execute_run( cmd, check=True, cwd=output_dir, ) From 2dce6f9cfbc508a1cc58f48df5b0a1885cdc9afe Mon Sep 17 00:00:00 2001 From: Claudio Matsuoka Date: Wed, 16 Mar 2022 19:21:46 -0300 Subject: [PATCH 060/167] providers: update craft-providers Signed-off-by: Claudio Matsuoka --- requirements-devel.txt | 2 +- requirements.txt | 2 +- snapcraft/providers/_buildd.py | 15 ++++----------- 3 files changed, 6 insertions(+), 13 deletions(-) diff --git a/requirements-devel.txt b/requirements-devel.txt index a388e264b4..8e1e1be6b0 100644 --- a/requirements-devel.txt +++ b/requirements-devel.txt @@ -12,7 +12,7 @@ coverage==6.3.2 craft-cli==0.2.0 craft-grammar==1.1.1 craft-parts==1.3.0 -craft-providers==1.0.5 +craft-providers==1.1.0 cryptography==3.4 Deprecated==1.2.13 distro==1.7.0 diff --git a/requirements.txt b/requirements.txt index 65f3446106..85fd02d77d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,7 +8,7 @@ click==8.0.4 craft-cli==0.2.0 craft-grammar==1.1.1 craft-parts==1.3.0 -craft-providers==1.0.5 +craft-providers==1.1.0 cryptography==3.4 Deprecated==1.2.13 distro==1.7.0 diff --git a/snapcraft/providers/_buildd.py b/snapcraft/providers/_buildd.py index c86e98f9b3..6a4bba5078 100644 --- a/snapcraft/providers/_buildd.py +++ b/snapcraft/providers/_buildd.py @@ -16,7 +16,6 @@ """Buildd-related helpers for Snapcraft.""" -import enum import sys from typing import Optional @@ -25,17 +24,8 @@ from snapcraft import utils - -# FIXME: update in craft-providers -class _BuilddBaseAlias(enum.Enum): - """Mappings for supported buildd images.""" - - JAMMY = "22.04" - - BASE_TO_BUILDD_IMAGE_ALIAS = { - # "core22": bases.BuilddBaseAlias.JAMMY, - "core22": _BuilddBaseAlias.JAMMY, + "core22": bases.BuilddBaseAlias.JAMMY, } @@ -74,6 +64,9 @@ def _setup_snapcraft(*, executor: Executor) -> None: if snap_channel is None and sys.platform != "linux": snap_channel = "stable" + # FIXME: don't reinstall snapcraft if already installed. + # See https://github.com/canonical/craft-providers/issues/91 + if snap_channel: try: snap_installer.install_from_store( From b704c72aace1c797490be810aee05679a724226c Mon Sep 17 00:00:00 2001 From: Claudio Matsuoka Date: Wed, 16 Mar 2022 20:38:08 -0300 Subject: [PATCH 061/167] providers: use predictable logname in instance If running on a managed instance, use a predictable log file name that can be used by the external process to retrieve log data. Signed-off-by: Claudio Matsuoka --- snapcraft/cli.py | 15 +++++++++++++-- snapcraft/parts/lifecycle.py | 4 +--- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/snapcraft/cli.py b/snapcraft/cli.py index b1c1298612..85374562e4 100644 --- a/snapcraft/cli.py +++ b/snapcraft/cli.py @@ -23,7 +23,7 @@ import craft_cli from craft_cli import ArgumentParsingError, EmitterMode, emit -from snapcraft import __version__, errors +from snapcraft import __version__, errors, utils from snapcraft_legacy.cli import legacy from . import commands @@ -65,7 +65,17 @@ def run(): logger = logging.getLogger(lib_name) logger.setLevel(logging.DEBUG) - emit.init(EmitterMode.NORMAL, "snapcraft", f"Starting Snapcraft {__version__}") + emit_args = { + "mode": EmitterMode.NORMAL, + "appname": "snapcraft", + "greeting": f"Starting Snapcraft {__version__}", + } + + if utils.is_managed_mode(): + emit_args["log_filepath"] = utils.get_managed_environment_log_path() + + emit.init(**emit_args) + dispatcher = craft_cli.Dispatcher( "snapcraft", COMMAND_GROUPS, @@ -88,3 +98,4 @@ def run(): legacy.legacy_run() except errors.SnapcraftError as err: emit.error(err) + sys.exit(1) diff --git a/snapcraft/parts/lifecycle.py b/snapcraft/parts/lifecycle.py index cfb992f3c1..a318e94891 100644 --- a/snapcraft/parts/lifecycle.py +++ b/snapcraft/parts/lifecycle.py @@ -177,9 +177,7 @@ def _run_in_provider(project: Project, command_name: str, parsed_args: "argparse ) as instance: try: emit.message("Launched instance", intermediate=True) - instance.execute_run( - cmd, check=True, cwd=output_dir, - ) + instance.execute_run(cmd, check=True, cwd=output_dir) except subprocess.CalledProcessError as err: capture_logs_from_instance(instance) raise providers.ProviderError( From e56ab59ae0a68b06f8bec3e5448111027452b9a6 Mon Sep 17 00:00:00 2001 From: Claudio Matsuoka Date: Thu, 17 Mar 2022 11:44:27 -0300 Subject: [PATCH 062/167] tests: fix url scheme in flutter spread test Github doesn't accept unauthenticated git protocol accesses, use https instead. Signed-off-by: Claudio Matsuoka --- .../plugins/v1/flutter/snaps/flutter-hello/src/pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/spread/plugins/v1/flutter/snaps/flutter-hello/src/pubspec.yaml b/tests/spread/plugins/v1/flutter/snaps/flutter-hello/src/pubspec.yaml index c7fcefc688..9bec7d010e 100644 --- a/tests/spread/plugins/v1/flutter/snaps/flutter-hello/src/pubspec.yaml +++ b/tests/spread/plugins/v1/flutter/snaps/flutter-hello/src/pubspec.yaml @@ -32,7 +32,7 @@ dependencies: url_launcher_fde: git: - url: git://github.com/google/flutter-desktop-embedding.git + url: https://github.com/google/flutter-desktop-embedding.git path: plugins/flutter_plugins/url_launcher_fde ref: b0794faf2c000576515aee56ca6bb5bee64cece4 From 8f6619c67cd1d887483be006d2d011c7f0d53727 Mon Sep 17 00:00:00 2001 From: Sergio Schvezov Date: Tue, 15 Mar 2022 21:42:25 -0300 Subject: [PATCH 063/167] extensions: support core22 This adds some variations to the API and changes where things are processed when compared to the original implementation. For integration it is rather hard to test until there is an extension implementation to test with, so the current code only makes sure that use of apply_extensions does not break the current implementation. Signed-off-by: Sergio Schvezov --- setup.cfg | 2 +- snapcraft/errors.py | 4 + snapcraft/extensions/__init__.py | 30 +++ snapcraft/extensions/_extension.py | 120 ++++++++++++ snapcraft/extensions/_utils.py | 136 ++++++++++++++ snapcraft/extensions/registry.py | 69 +++++++ snapcraft/parts/lifecycle.py | 7 +- tests/unit/extensions/__init__.py | 15 ++ tests/unit/extensions/conftest.py | 156 ++++++++++++++++ tests/unit/extensions/test_extensions.py | 224 +++++++++++++++++++++++ tests/unit/extensions/test_registry.py | 42 +++++ tests/unit/extensions/test_utils.py | 82 +++++++++ 12 files changed, 882 insertions(+), 5 deletions(-) create mode 100644 snapcraft/extensions/__init__.py create mode 100644 snapcraft/extensions/_extension.py create mode 100644 snapcraft/extensions/_utils.py create mode 100644 snapcraft/extensions/registry.py create mode 100644 tests/unit/extensions/__init__.py create mode 100644 tests/unit/extensions/conftest.py create mode 100644 tests/unit/extensions/test_extensions.py create mode 100644 tests/unit/extensions/test_registry.py create mode 100644 tests/unit/extensions/test_utils.py diff --git a/setup.cfg b/setup.cfg index 8bd336269e..d0e82780c3 100644 --- a/setup.cfg +++ b/setup.cfg @@ -26,7 +26,7 @@ exclude = prime [mypy] -python_version = 3.6 +python_version = 3.8 ignore_missing_imports = True follow_imports = silent diff --git a/snapcraft/errors.py b/snapcraft/errors.py index 112fa3903c..014ef1061d 100644 --- a/snapcraft/errors.py +++ b/snapcraft/errors.py @@ -38,5 +38,9 @@ class ProjectValidationError(SnapcraftError): """Error validatiing snapcraft.yaml.""" +class ExtensionError(SnapcraftError): + """Error during parts processing.""" + + class LegacyFallback(Exception): """Fall back to legacy snapcraft implementation.""" diff --git a/snapcraft/extensions/__init__.py b/snapcraft/extensions/__init__.py new file mode 100644 index 0000000000..512ccdcac2 --- /dev/null +++ b/snapcraft/extensions/__init__.py @@ -0,0 +1,30 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright 2022 Canonical Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +"""Extension processor and related utilities.""" + +from ._extension import Extension +from ._utils import apply_extensions +from .registry import get_extension_class, get_extension_names, register, unregister + +__all__ = [ + "Extension", + "get_extension_class", + "get_extension_names", + "apply_extensions", + "register", + "unregister", +] diff --git a/snapcraft/extensions/_extension.py b/snapcraft/extensions/_extension.py new file mode 100644 index 0000000000..c6c52a3413 --- /dev/null +++ b/snapcraft/extensions/_extension.py @@ -0,0 +1,120 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright 2018-2022 Canonical Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import abc +import os +from typing import Any, Dict, Optional, Tuple, final + +from craft_cli import emit + +from snapcraft import errors + + +class Extension(abc.ABC): + """Extension is the class from which all extensions inherit. + + Extensions have the ability to add snippets to apps, parts, and indeed add new parts + to a given snapcraft.yaml. + + :param yaml_data: Loaded snapcraft.yaml data. + :param arch: the host architecture. + :param target_arch: the target architecture. + """ + + def __init__( + self, *, yaml_data: Dict[str, Any], arch: str, target_arch: str + ) -> None: + """Create a new Extension.""" + self.yaml_data = yaml_data + self.arch = arch + self.target_arch = target_arch + + @staticmethod + @abc.abstractmethod + def get_supported_bases() -> Tuple[str, ...]: + """Return a tuple of supported bases.""" + + @staticmethod + @abc.abstractmethod + def get_supported_confinement() -> Tuple[str, ...]: + """Return a tuple of supported confinement settings.""" + + @staticmethod + @abc.abstractmethod + def is_experimental(base: Optional[str]) -> bool: + """Return whether or not this extension is unstable for given base.""" + + @abc.abstractmethod + def get_root_snippet(self) -> Dict[str, Any]: + """Return the root snippet to apply.""" + + @abc.abstractmethod + def get_app_snippet(self) -> Dict[str, Any]: + """Return the app snippet to apply.""" + + @abc.abstractmethod + def get_part_snippet(self) -> Dict[str, Any]: + """Return the part snippet to apply to existing parts.""" + + @abc.abstractmethod + def get_parts_snippet(self) -> Dict[str, Any]: + """Return the parts to add to parts.""" + + @final + def validate(self, extension_name: str): + """Validate that the extension can be used with the current project. + + :param extension_name: the name of the extension being parsed. + :raises errors.ExtensionError: if the extension is incompatible with the project. + """ + base: str = self.yaml_data["base"] + confinement: Optional[str] = self.yaml_data.get("confinement") + + if self.is_experimental(base) and not os.getenv( + "SNAPCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS" + ): + raise errors.ExtensionError( + f"Extension is experimental: {extension_name!r}", + docs_url="https://snapcraft.io/docs/supported-extensions", + ) + elif self.is_experimental(base): + emit.message( + f"*EXPERIMENTAL* extension {extension_name!r} enabled", + intermediate=True, + ) + + if base not in self.get_supported_bases(): + raise errors.ExtensionError( + f"Extension {extension_name!r} does not support base: {base!r}" + ) + + if ( + confinement is not None + and confinement not in self.get_supported_confinement() + ): + raise errors.ExtensionError( + f"Extension {extension_name!r} does not support confinement {confinement!r}" + ) + + invalid_parts = [ + p + for p in self.get_parts_snippet() + if not p.startswith(f"{extension_name}/") + ] + if invalid_parts: + raise ValueError( + f"Extension has invalid part names: {invalid_parts!r}. Format is /" + ) diff --git a/snapcraft/extensions/_utils.py b/snapcraft/extensions/_utils.py new file mode 100644 index 0000000000..eab6544af8 --- /dev/null +++ b/snapcraft/extensions/_utils.py @@ -0,0 +1,136 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright 2017-2022 Canonical Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import collections +import contextlib +import copy +from typing import Any, Dict, List, Set + +from ._extension import Extension +from .registry import get_extension_class + + +def apply_extensions( + yaml_data: Dict[str, Any], *, arch: str, target_arch: str +) -> Dict[str, Any]: + """Apply all extensions. + + :param dict yaml_data: Loaded, unprocessed snapcraft.yaml + :param arch: the host architecture. + :param target_arch: the target architecture. + :returns: Modified snapcraft.yaml data with extensions applied + """ + # Don't modify the dict passed in + yaml_data = copy.deepcopy(yaml_data) + + # Mapping of extension names to set of app names to which the extension needs to be + # applied. + declared_extensions: Dict[str, Set[str]] = collections.defaultdict(set) + + for app_name, app_definition in yaml_data.get("apps", dict()).items(): + extension_names = app_definition.get("extensions", []) + + for extension_name in extension_names: + declared_extensions[extension_name].add(app_name) + + # Now that we've saved the app -> extension relationship, remove the property + # from this app's declaration in the YAML. + with contextlib.suppress(KeyError): + del yaml_data["apps"][app_name]["extensions"] + + # Process extensions in a consistent order + for extension_name in sorted(declared_extensions.keys()): + extension_class = get_extension_class(extension_name) + extension = extension_class( + yaml_data=copy.deepcopy(yaml_data), arch=arch, target_arch=target_arch + ) + extension.validate(extension_name=extension_name) + _apply_extension(yaml_data, declared_extensions[extension_name], extension) + + return yaml_data + + +def _apply_extension( + yaml_data: Dict[str, Any], + app_names: Set[str], + extension: Extension, +) -> None: + # Apply the root components of the extension (if any) + root_extension = extension.get_root_snippet() + for property_name, property_value in root_extension.items(): + yaml_data[property_name] = _apply_extension_property( + yaml_data.get(property_name), property_value + ) + + # Apply the app-specific components of the extension (if any) + app_extension = extension.get_app_snippet() + for app_name in app_names: + app_definition = yaml_data["apps"][app_name] + for property_name, property_value in app_extension.items(): + app_definition[property_name] = _apply_extension_property( + app_definition.get(property_name), property_value + ) + + # Next, apply the part-specific components + part_extension = extension.get_part_snippet() + parts = yaml_data["parts"] + for part_name, part_definition in parts.items(): + for property_name, property_value in part_extension.items(): + part_definition[property_name] = _apply_extension_property( + part_definition.get(property_name), property_value + ) + + # Finally, add any parts specified in the extension + for part_name, part_definition in extension.get_parts_snippet().items(): + parts[part_name] = part_definition + + +def _apply_extension_property(existing_property: Any, extension_property: Any) -> Any: + if existing_property: + # If the property is not scalar, merge them + if isinstance(existing_property, list) and isinstance(extension_property, list): + merged = extension_property + existing_property + + # If the lists are just strings, remove duplicates. + if all(isinstance(item, str) for item in merged): + return _remove_list_duplicates(merged) + + return merged + + elif isinstance(existing_property, dict) and isinstance( + extension_property, dict + ): + for key, value in extension_property.items(): + existing_property[key] = _apply_extension_property( + existing_property.get(key), value + ) + return existing_property + return existing_property + + return extension_property + + +def _remove_list_duplicates(seq: List[str]) -> List[str]: + """De-dupe string list maintaining ordering.""" + seen: Set[str] = set() + deduped: List[str] = list() + + for item in seq: + if item not in seen: + seen.add(item) + deduped.append(item) + + return deduped diff --git a/snapcraft/extensions/registry.py b/snapcraft/extensions/registry.py new file mode 100644 index 0000000000..77864b12a3 --- /dev/null +++ b/snapcraft/extensions/registry.py @@ -0,0 +1,69 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright 2018-2022 Canonical Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +"""Extension registry.""" + +from typing import Dict, List, Type + +from snapcraft import errors + +from ._extension import Extension + +ExtensionType = Type[Extension] + +_EXTENSIONS: Dict[str, ExtensionType] = {} + + +def get_extension_names() -> List[str]: + """Obtain a extension class given the name. + + :param name: The extension name. + :return: The list of available extensions. + :raises ExtensionError: If the extension name is invalid. + """ + return list(_EXTENSIONS.keys()) + + +def get_extension_class(extension_name: str) -> ExtensionType: + """Obtain a extension class given the name. + + :param name: The extension name. + :return: The extension class. + :raises ExtensionError: If the extension name is invalid. + """ + try: + return _EXTENSIONS[extension_name] + except KeyError as key_error: + raise errors.ExtensionError( + f"Extension {extension_name!r} does not exist" + ) from key_error + + +def register(extension_name: str, extension_class: ExtensionType) -> None: + """Register extension. + + :param extension_name: the name to register. + :param extension_class: the Extension implementation. + """ + _EXTENSIONS[extension_name] = extension_class + + +def unregister(extension_name: str) -> None: + """Unregister extension_name. + + :raises KeyError: if extension_name is not registered. + """ + del _EXTENSIONS[extension_name] diff --git a/snapcraft/parts/lifecycle.py b/snapcraft/parts/lifecycle.py index a318e94891..d929922129 100644 --- a/snapcraft/parts/lifecycle.py +++ b/snapcraft/parts/lifecycle.py @@ -25,7 +25,7 @@ from craft_cli import emit from craft_parts import infos -from snapcraft import errors, pack, providers, utils +from snapcraft import errors, extensions, pack, providers, utils from snapcraft.meta import snap_yaml from snapcraft.parts import PartsLifecycle from snapcraft.projects import GrammarAwareProject, Project @@ -82,11 +82,10 @@ def run(command_name: str, parsed_args: "argparse.Namespace") -> None: if parsed_args.provider: raise errors.SnapcraftError("Option --provider is not supported.") - # TODO: apply extensions - # yaml_data = apply_extensions(yaml_data) - # TODO: support for target_arch arch = _get_arch() + yaml_data = extensions.apply_extensions(yaml_data, arch=arch, target_arch=arch) + if "parts" in yaml_data: yaml_data["parts"] = grammar.process_parts( parts_yaml_data=yaml_data["parts"], arch=arch, target_arch=arch diff --git a/tests/unit/extensions/__init__.py b/tests/unit/extensions/__init__.py new file mode 100644 index 0000000000..e4721fbbe3 --- /dev/null +++ b/tests/unit/extensions/__init__.py @@ -0,0 +1,15 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright 2022 Canonical Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . diff --git a/tests/unit/extensions/conftest.py b/tests/unit/extensions/conftest.py new file mode 100644 index 0000000000..4c3eba54d8 --- /dev/null +++ b/tests/unit/extensions/conftest.py @@ -0,0 +1,156 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright 2022 Canonical Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + + +from typing import Any, Dict, Optional, Tuple + +import pytest + +from snapcraft import extensions + + +@pytest.fixture +def fake_extension(): + """Basic extension.""" + + class ExtensionImpl(extensions.Extension): + @staticmethod + def get_supported_bases() -> Tuple[str, ...]: + return ("core22",) + + @staticmethod + def get_supported_confinement() -> Tuple[str, ...]: + return ("strict",) + + @staticmethod + def is_experimental(base: Optional[str] = None) -> bool: + return False + + def get_root_snippet(self) -> Dict[str, Any]: + return {"grade": "fake-grade"} + + def get_app_snippet(self) -> Dict[str, Any]: + return {"plugs": ["fake-plug"]} + + def get_part_snippet(self) -> Dict[str, Any]: + return {"after": ["fake-extension/fake-part"]} + + def get_parts_snippet(self) -> Dict[str, Any]: + return {"fake-extension/fake-part": {"plugin": "nil"}} + + extensions.register("fake-extension", ExtensionImpl) + yield ExtensionImpl + extensions.unregister("fake-extension") + + +@pytest.fixture +def fake_extension_extra(): + """A variation of fake_extension with some conflicts and new code.""" + + class ExtensionImpl(extensions.Extension): + @staticmethod + def get_supported_bases() -> Tuple[str, ...]: + return ("core22",) + + @staticmethod + def get_supported_confinement() -> Tuple[str, ...]: + return ("strict",) + + @staticmethod + def is_experimental(base: Optional[str] = None) -> bool: + return False + + def get_root_snippet(self) -> Dict[str, Any]: + return {} + + def get_app_snippet(self) -> Dict[str, Any]: + return {"plugs": ["fake-plug", "fake-plug-extra"]} + + def get_part_snippet(self) -> Dict[str, Any]: + return {"after": ["fake-extension-extra/fake-part"]} + + def get_parts_snippet(self) -> Dict[str, Any]: + return {"fake-extension-extra/fake-part": {"plugin": "nil"}} + + extensions.register("fake-extension-extra", ExtensionImpl) + yield ExtensionImpl + extensions.unregister("fake-extension-extra") + + +@pytest.fixture +def fake_extension_invalid_parts(): + class ExtensionImpl(extensions.Extension): + @staticmethod + def get_supported_bases() -> Tuple[str, ...]: + return ("core22",) + + @staticmethod + def get_supported_confinement() -> Tuple[str, ...]: + return ("strict",) + + @staticmethod + def is_experimental(base: Optional[str] = None) -> bool: + return False + + def get_root_snippet(self) -> Dict[str, Any]: + return {"grade": "fake-grade"} + + def get_app_snippet(self) -> Dict[str, Any]: + return {"plugs": ["fake-plug"]} + + def get_part_snippet(self) -> Dict[str, Any]: + return {"after": ["fake-extension/fake-part"]} + + def get_parts_snippet(self) -> Dict[str, Any]: + return {"fake-part": {"plugin": "nil"}, "fake-part-2": {"plugin": "nil"}} + + extensions.register("fake-extension-invalid-parts", ExtensionImpl) + yield ExtensionImpl + extensions.unregister("fake-extension-invalid-parts") + + +@pytest.fixture +def fake_extension_experimental(): + """Basic extension.""" + + class ExtensionImpl(extensions.Extension): + @staticmethod + def get_supported_bases() -> Tuple[str, ...]: + return ("core22",) + + @staticmethod + def get_supported_confinement() -> Tuple[str, ...]: + return ("strict",) + + @staticmethod + def is_experimental(base: Optional[str] = None) -> bool: + return True + + def get_root_snippet(self) -> Dict[str, Any]: + return {} + + def get_app_snippet(self) -> Dict[str, Any]: + return {} + + def get_part_snippet(self) -> Dict[str, Any]: + return {} + + def get_parts_snippet(self) -> Dict[str, Any]: + return {} + + extensions.register("fake-extension-experimental", ExtensionImpl) + yield ExtensionImpl + extensions.unregister("fake-extension-experimental") diff --git a/tests/unit/extensions/test_extensions.py b/tests/unit/extensions/test_extensions.py new file mode 100644 index 0000000000..34f765a6c3 --- /dev/null +++ b/tests/unit/extensions/test_extensions.py @@ -0,0 +1,224 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright 2022 Canonical Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + + +import pytest + +from snapcraft import errors, extensions + + +@pytest.mark.usefixtures("fake_extension") +def test_apply_extension(): + yaml_data = { + "name": "fake-snap", + "summary": "fake summary", + "description": "fake description", + "base": "core22", + "apps": { + "fake-command": { + "command": "bin/fake-command", + "plugs": ["my-fake-plug"], + "extensions": ["fake-extension"], + } + }, + "parts": {"fake-part": {"source": ".", "plugin": "dump"}}, + } + + assert extensions.apply_extensions( + yaml_data, arch="amd64", target_arch="amd64" + ) == { + "name": "fake-snap", + "summary": "fake summary", + "description": "fake description", + "base": "core22", + "grade": "fake-grade", + "apps": { + "fake-command": { + "command": "bin/fake-command", + "plugs": ["fake-plug", "my-fake-plug"], + } + }, + "parts": { + "fake-part": { + "source": ".", + "plugin": "dump", + "after": ["fake-extension/fake-part"], + }, + "fake-extension/fake-part": {"plugin": "nil"}, + }, + } + + +@pytest.mark.usefixtures("fake_extension") +@pytest.mark.usefixtures("fake_extension_extra") +def test_apply_multiple_extensions(): + yaml_data = { + "name": "fake-snap", + "summary": "fake summary", + "description": "fake description", + "base": "core22", + "apps": { + "fake-command": { + "command": "bin/fake-command", + "plugs": ["my-fake-plug"], + "extensions": ["fake-extension", "fake-extension-extra"], + } + }, + "parts": {"fake-part": {"source": ".", "plugin": "dump"}}, + } + + assert extensions.apply_extensions( + yaml_data, arch="amd64", target_arch="amd64" + ) == { + "name": "fake-snap", + "summary": "fake summary", + "description": "fake description", + "base": "core22", + "grade": "fake-grade", + "apps": { + "fake-command": { + "command": "bin/fake-command", + "plugs": ["fake-plug", "fake-plug-extra", "my-fake-plug"], + } + }, + "parts": { + "fake-part": { + "source": ".", + "plugin": "dump", + "after": ["fake-extension-extra/fake-part", "fake-extension/fake-part"], + }, + "fake-extension/fake-part": { + "plugin": "nil", + "after": ["fake-extension-extra/fake-part"], + }, + "fake-extension-extra/fake-part": {"plugin": "nil"}, + }, + } + + +@pytest.mark.usefixtures("fake_extension") +def test_apply_extension_wrong_base(): + yaml_data = { + "base": "core20", + "apps": { + "fake-command": { + "command": "bin/fake-command", + "extensions": ["fake-extension"], + } + }, + } + + with pytest.raises(errors.ExtensionError) as raised: + extensions.apply_extensions(yaml_data, arch="amd64", target_arch="amd64") + + assert ( + str(raised.value) + == "Extension 'fake-extension' does not support base: 'core20'" + ) + + +@pytest.mark.usefixtures("fake_extension") +def test_apply_extension_wrong_confinement(): + yaml_data = { + "base": "core22", + "confinement": "classic", + "apps": { + "fake-command": { + "command": "bin/fake-command", + "extensions": ["fake-extension"], + } + }, + } + + with pytest.raises(errors.ExtensionError) as raised: + extensions.apply_extensions(yaml_data, arch="amd64", target_arch="amd64") + + assert ( + str(raised.value) + == "Extension 'fake-extension' does not support confinement 'classic'" + ) + + +@pytest.mark.usefixtures("fake_extension_invalid_parts") +def test_apply_extension_invalid_parts(): + # This is a Snapcraft developer error. + yaml_data = { + "base": "core22", + "apps": { + "fake-command": { + "command": "bin/fake-command", + "extensions": ["fake-extension-invalid-parts"], + } + }, + } + + with pytest.raises(ValueError) as raised: + extensions.apply_extensions(yaml_data, arch="amd64", target_arch="amd64") + + assert ( + str(raised.value) + == "Extension has invalid part names: ['fake-part', 'fake-part-2']. Format is /" + ) + + +@pytest.mark.usefixtures("fake_extension_experimental") +def test_apply_extension_experimental(): + yaml_data = { + "base": "core22", + "apps": { + "fake-command": { + "command": "bin/fake-command", + "extensions": ["fake-extension-experimental"], + } + }, + } + + with pytest.raises(errors.ExtensionError) as raised: + extensions.apply_extensions(yaml_data, arch="amd64", target_arch="amd64") + + assert ( + str(raised.value) == "Extension is experimental: 'fake-extension-experimental'" + ) + assert raised.value.docs_url == "https://snapcraft.io/docs/supported-extensions" + + +@pytest.mark.usefixtures("fake_extension_experimental") +def test_apply_extension_experimental_with_environment(emitter, monkeypatch): + monkeypatch.setenv("SNAPCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS", "1") + + yaml_data = { + "base": "core22", + "apps": { + "fake-command": { + "command": "bin/fake-command", + "extensions": ["fake-extension-experimental"], + } + }, + "parts": { + "fake-part": { + "source": ".", + "plugin": "dump", + "after": ["fake-extension-extra/fake-part", "fake-extension/fake-part"], + }, + }, + } + + # Should not raise. + extensions.apply_extensions(yaml_data, arch="amd64", target_arch="amd64") + + emitter.assert_recorded( + ["*EXPERIMENTAL* extension 'fake-extension-experimental' enabled"] + ) diff --git a/tests/unit/extensions/test_registry.py b/tests/unit/extensions/test_registry.py new file mode 100644 index 0000000000..256945b562 --- /dev/null +++ b/tests/unit/extensions/test_registry.py @@ -0,0 +1,42 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright 2022 Canonical Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import pytest + +from snapcraft import errors, extensions + + +@pytest.mark.usefixtures("fake_extension") +@pytest.mark.usefixtures("fake_extension_extra") +@pytest.mark.usefixtures("fake_extension_experimental") +def test_get_extension_names(): + assert extensions.get_extension_names() == [ + "fake-extension-experimental", + "fake-extension-extra", + "fake-extension", + ] + + +def test_get_extension_class(fake_extension): + assert extensions.get_extension_class("fake-extension") == fake_extension + + +def test_get_extesion_class_not_found(): + # This is a developer error. + with pytest.raises(errors.ExtensionError) as raised: + extensions.get_extension_class("fake-extension-not-found") + + assert str(raised.value) == "Extension 'fake-extension-not-found' does not exist" diff --git a/tests/unit/extensions/test_utils.py b/tests/unit/extensions/test_utils.py new file mode 100644 index 0000000000..d279759c2c --- /dev/null +++ b/tests/unit/extensions/test_utils.py @@ -0,0 +1,82 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright 2022 Canonical Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + + +import pytest + +from snapcraft.extensions._utils import _apply_extension_property + + +@pytest.mark.parametrize( + "existing_property,extension_property,expected_value", + [ + # prepend + ( + ["item3", "item4", "item5"], + ["item1", "item2"], + ["item1", "item2", "item3", "item4", "item5"], + ), + # empty extension + (["item3", "item4", "item5"], [], ["item3", "item4", "item5"]), + # empty property + ([], ["item1", "item2"], ["item1", "item2"]), + # duplicate items keeps first found + ( + ["item3", "item4", "item1"], + ["item1", "item2"], + ["item1", "item2", "item3", "item4"], + ), + # non scalar + ( + [{"k2": "v2"}], + [{"k1": "v1"}], + [{"k1": "v1"}, {"k2": "v2"}], + ), + ], +) +def test_apply_property_list(existing_property, extension_property, expected_value): + assert ( + _apply_extension_property(existing_property, extension_property) + == expected_value + ) + + +@pytest.mark.parametrize( + "existing_property,extension_property,expected_value", + [ + # add + ( + {"k1": "v1", "k2": "v2", "k3": "v3"}, + {"k4": "v4", "k5": "v5"}, + {"k1": "v1", "k2": "v2", "k3": "v3", "k4": "v4", "k5": "v5"}, + ), + # conflicts keeps existing property + ( + {"k1": "v1", "k2": "v2", "k3": "v3"}, + {"k3": "nv3", "k4": "v4"}, + {"k1": "v1", "k2": "v2", "k3": "v3", "k4": "v4"}, + ), + # empty property + ({}, {"k4": "v4", "k5": "v5"}, {"k4": "v4", "k5": "v5"}), + ], +) +def test_apply_property_dictionary( + existing_property, extension_property, expected_value +): + assert ( + _apply_extension_property(existing_property, extension_property) + == expected_value + ) From a40a3431465b47ad4628f1bc7c4fc0e4b8858640 Mon Sep 17 00:00:00 2001 From: Sergio Schvezov Date: Thu, 24 Mar 2022 21:43:11 -0300 Subject: [PATCH 064/167] parts: raise error when duplicate keys are used - Move yaml loading to a new module in the part package: yaml_utils - Forward port the safe loading implementation from legacy LP: #1942217 Signed-off-by: Sergio Schvezov --- snapcraft/parts/lifecycle.py | 36 ++++-------- snapcraft/parts/yaml_utils.py | 82 ++++++++++++++++++++++++++ tests/unit/parts/test_yaml_utils.py | 89 +++++++++++++++++++++++++++++ 3 files changed, 182 insertions(+), 25 deletions(-) create mode 100644 snapcraft/parts/yaml_utils.py create mode 100644 tests/unit/parts/test_yaml_utils.py diff --git a/snapcraft/parts/lifecycle.py b/snapcraft/parts/lifecycle.py index d929922129..139672c0f5 100644 --- a/snapcraft/parts/lifecycle.py +++ b/snapcraft/parts/lifecycle.py @@ -18,10 +18,8 @@ import subprocess from pathlib import Path -from typing import TYPE_CHECKING, Any, Dict, cast +from typing import TYPE_CHECKING, cast -import yaml -import yaml.error from craft_cli import emit from craft_parts import infos @@ -31,7 +29,7 @@ from snapcraft.projects import GrammarAwareProject, Project from snapcraft.providers import capture_logs_from_instance -from . import grammar +from . import grammar, yaml_utils if TYPE_CHECKING: import argparse @@ -62,8 +60,15 @@ def run(command_name: str, parsed_args: "argparse.Namespace") -> None: if project_file.parent.name == "snap": assets_dir = project_file.parent - yaml_data = _load_yaml(project_file) - break + try: + with open(project_file, encoding="utf-8") as yaml_file: + yaml_data = yaml_utils.load(yaml_file) + break + except OSError as err: + msg = err.strerror + if err.filename: + msg = f"{msg}: {err.filename!r}." + raise errors.SnapcraftError(msg) from err else: raise errors.SnapcraftError( "Could not find snap/snapcraft.yaml. Are you sure you are in the " @@ -136,25 +141,6 @@ def _run_command( ) -def _load_yaml(filename: Path) -> Dict[str, Any]: - """Load and parse a YAML-formatted file. - - :param filename: The YAML file to load. - - :raises SnapcraftError: if loading didn't succeed. - """ - try: - with open(filename, encoding="utf-8") as yaml_file: - return yaml.safe_load(yaml_file) - except OSError as err: - msg = err.strerror - if err.filename: - msg = f"{msg}: {err.filename!r}." - raise errors.SnapcraftError(msg) from err - except yaml.error.YAMLError as err: - raise errors.SnapcraftError(f"YAML parsing error: {err!s}") from err - - def _run_in_provider(project: Project, command_name: str, parsed_args: "argparse.Namespace"): """Pack image in provider instance.""" emit.trace("Checking build provider availability") diff --git a/snapcraft/parts/yaml_utils.py b/snapcraft/parts/yaml_utils.py new file mode 100644 index 0000000000..8892012356 --- /dev/null +++ b/snapcraft/parts/yaml_utils.py @@ -0,0 +1,82 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright 2022 Canonical Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +"""YAML utilities for Snapcraft.""" + +from typing import Any, Dict, TextIO + +import yaml +import yaml.error + +from snapcraft import errors + + +def _check_duplicate_keys(node): + mappings = set() + + for key_node, _ in node.value: + try: + if key_node.value in mappings: + raise yaml.constructor.ConstructorError( + "while constructing a mapping", + node.start_mark, + f"found duplicate key {key_node.value!r}", + node.start_mark, + ) + mappings.add(key_node.value) + except TypeError: + # Ignore errors for malformed inputs that will be caught later. + pass + + +def _dict_constructor(loader, node): + _check_duplicate_keys(node) + + # Necessary in order to make yaml merge tags work + loader.flatten_mapping(node) + value = loader.construct_pairs(node) + + try: + return dict(value) + except TypeError as type_error: + raise yaml.constructor.ConstructorError( + "while constructing a mapping", + node.start_mark, + "found unhashable key", + node.start_mark, + ) from type_error + + +class _SafeLoader(yaml.SafeLoader): # pylint: disable=too-many-ancestors + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + self.add_constructor( + yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG, _dict_constructor + ) + + +def load(filestream: TextIO) -> Dict[str, Any]: + """Load and parse a YAML-formatted file. + + :param filename: The YAML file to load. + + :raises SnapcraftError: if loading didn't succeed. + """ + try: + return yaml.load(filestream, Loader=_SafeLoader) + except yaml.error.YAMLError as err: + raise errors.SnapcraftError(f"YAML parsing error: {err!s}") from err diff --git a/tests/unit/parts/test_yaml_utils.py b/tests/unit/parts/test_yaml_utils.py new file mode 100644 index 0000000000..5046c69dab --- /dev/null +++ b/tests/unit/parts/test_yaml_utils.py @@ -0,0 +1,89 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright 2022 Canonical Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + + +import io +from textwrap import dedent + +import pytest + +from snapcraft import errors +from snapcraft.parts import yaml_utils + + +def test_yaml_load(): + assert ( + yaml_utils.load( + io.StringIO( + dedent( + """\ + entry: + sub-entry: + - list1 + - list2 + scalar: scalar-value + """ + ) + ) + ) + == { + "entry": { + "sub-entry": ["list1", "list2"], + }, + "scalar": "scalar-value", + } + ) + + +def test_yaml_load_duplicates_errors(): + with pytest.raises(errors.SnapcraftError) as raised: + yaml_utils.load( + io.StringIO( + dedent( + """\ + entry: value1 + entry: value2 + """ + ) + ) + ) + + assert str(raised.value) == dedent( + """\ + YAML parsing error: while constructing a mapping + found duplicate key 'entry' + in "", line 1, column 1""" + ) + + +def test_yaml_load_unhashable_errors(): + with pytest.raises(errors.SnapcraftError) as raised: + yaml_utils.load( + io.StringIO( + dedent( + """\ + entry: {{value}} + """ + ) + ) + ) + + assert str(raised.value) == dedent( + """\ + YAML parsing error: while constructing a mapping + found unhashable key + in "", line 1, column 8""" + ) From 50296d8665d58a16964f05a74ded2f4038bb76fe Mon Sep 17 00:00:00 2001 From: Claudio Matsuoka Date: Tue, 29 Mar 2022 17:39:33 -0300 Subject: [PATCH 065/167] linters: address issues and enable pylint in ci Signed-off-by: Claudio Matsuoka --- .github/workflows/tests.yaml | 10 +++++++--- snapcraft/extensions/_extension.py | 8 ++++++-- snapcraft/extensions/_utils.py | 10 +++++----- tests/unit/extensions/conftest.py | 8 ++++++++ tests/unit/extensions/test_extensions.py | 6 +++--- 5 files changed, 29 insertions(+), 13 deletions(-) diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index b62bdc79db..70268ea226 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -43,9 +43,6 @@ jobs: run: | source ${HOME}/.venv/snapcraft/bin/activate make test-mypy - - name: Run shellcheck - run: | - make test-shellcheck - name: Run pydocstyle run: | source ${HOME}/.venv/snapcraft/bin/activate @@ -56,6 +53,13 @@ jobs: sudo snap install --classic node sudo snap install --classic pyright make test-pyright + - name: Run pylint + run: | + source ${HOME}/.venv/snapcraft/bin/activate + make test-pylint + - name: Run shellcheck + run: | + make test-shellcheck tests: strategy: diff --git a/snapcraft/extensions/_extension.py b/snapcraft/extensions/_extension.py index c6c52a3413..2a34670814 100644 --- a/snapcraft/extensions/_extension.py +++ b/snapcraft/extensions/_extension.py @@ -14,6 +14,8 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +"""Extension base class definition.""" + import abc import os from typing import Any, Dict, Optional, Tuple, final @@ -90,7 +92,8 @@ def validate(self, extension_name: str): f"Extension is experimental: {extension_name!r}", docs_url="https://snapcraft.io/docs/supported-extensions", ) - elif self.is_experimental(base): + + if self.is_experimental(base): emit.message( f"*EXPERIMENTAL* extension {extension_name!r} enabled", intermediate=True, @@ -116,5 +119,6 @@ def validate(self, extension_name: str): ] if invalid_parts: raise ValueError( - f"Extension has invalid part names: {invalid_parts!r}. Format is /" + f"Extension has invalid part names: {invalid_parts!r}. " + "Format is /" ) diff --git a/snapcraft/extensions/_utils.py b/snapcraft/extensions/_utils.py index eab6544af8..8022963c78 100644 --- a/snapcraft/extensions/_utils.py +++ b/snapcraft/extensions/_utils.py @@ -14,6 +14,8 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +"""Extension application helpers.""" + import collections import contextlib import copy @@ -40,7 +42,7 @@ def apply_extensions( # applied. declared_extensions: Dict[str, Set[str]] = collections.defaultdict(set) - for app_name, app_definition in yaml_data.get("apps", dict()).items(): + for app_name, app_definition in yaml_data.get("apps", {}).items(): extension_names = app_definition.get("extensions", []) for extension_name in extension_names: @@ -110,9 +112,7 @@ def _apply_extension_property(existing_property: Any, extension_property: Any) - return merged - elif isinstance(existing_property, dict) and isinstance( - extension_property, dict - ): + if isinstance(existing_property, dict) and isinstance(extension_property, dict): for key, value in extension_property.items(): existing_property[key] = _apply_extension_property( existing_property.get(key), value @@ -126,7 +126,7 @@ def _apply_extension_property(existing_property: Any, extension_property: Any) - def _remove_list_duplicates(seq: List[str]) -> List[str]: """De-dupe string list maintaining ordering.""" seen: Set[str] = set() - deduped: List[str] = list() + deduped: List[str] = [] for item in seq: if item not in seen: diff --git a/tests/unit/extensions/conftest.py b/tests/unit/extensions/conftest.py index 4c3eba54d8..a335543170 100644 --- a/tests/unit/extensions/conftest.py +++ b/tests/unit/extensions/conftest.py @@ -27,6 +27,8 @@ def fake_extension(): """Basic extension.""" class ExtensionImpl(extensions.Extension): + """The test extension implementation.""" + @staticmethod def get_supported_bases() -> Tuple[str, ...]: return ("core22",) @@ -61,6 +63,8 @@ def fake_extension_extra(): """A variation of fake_extension with some conflicts and new code.""" class ExtensionImpl(extensions.Extension): + """The test extension implementation.""" + @staticmethod def get_supported_bases() -> Tuple[str, ...]: return ("core22",) @@ -93,6 +97,8 @@ def get_parts_snippet(self) -> Dict[str, Any]: @pytest.fixture def fake_extension_invalid_parts(): class ExtensionImpl(extensions.Extension): + """The test extension implementation.""" + @staticmethod def get_supported_bases() -> Tuple[str, ...]: return ("core22",) @@ -127,6 +133,8 @@ def fake_extension_experimental(): """Basic extension.""" class ExtensionImpl(extensions.Extension): + """The test extension implementation.""" + @staticmethod def get_supported_bases() -> Tuple[str, ...]: return ("core22",) diff --git a/tests/unit/extensions/test_extensions.py b/tests/unit/extensions/test_extensions.py index 34f765a6c3..063ca16b0f 100644 --- a/tests/unit/extensions/test_extensions.py +++ b/tests/unit/extensions/test_extensions.py @@ -168,9 +168,9 @@ def test_apply_extension_invalid_parts(): with pytest.raises(ValueError) as raised: extensions.apply_extensions(yaml_data, arch="amd64", target_arch="amd64") - assert ( - str(raised.value) - == "Extension has invalid part names: ['fake-part', 'fake-part-2']. Format is /" + assert str(raised.value) == ( + "Extension has invalid part names: ['fake-part', 'fake-part-2']. " + "Format is /" ) From 2d70a5ba7304c7b84e8d3f4eab2ceb8e114093de Mon Sep 17 00:00:00 2001 From: Claudio Matsuoka Date: Tue, 29 Mar 2022 20:28:39 -0300 Subject: [PATCH 066/167] linters: run linting tools without virtualenv Signed-off-by: Claudio Matsuoka --- .github/workflows/tests.yaml | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 70268ea226..383f9699d6 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -19,43 +19,33 @@ jobs: run: | sudo apt update sudo apt install -y python3-pip python3-venv libapt-pkg-dev libyaml-dev xdelta3 shellcheck - python3 -m venv ${HOME}/.venv/snapcraft - source ${HOME}/.venv/snapcraft/bin/activate pip install -U -r requirements.txt -r requirements-devel.txt pip install . - name: Run black run: | - source ${HOME}/.venv/snapcraft/bin/activate make test-black - name: Run codespell run: | - source ${HOME}/.venv/snapcraft/bin/activate make test-codespell - name: Run flake8 run: | - source ${HOME}/.venv/snapcraft/bin/activate make test-flake8 - name: Run isort run: | - source ${HOME}/.venv/snapcraft/bin/activate make test-isort - name: Run mypy run: | - source ${HOME}/.venv/snapcraft/bin/activate make test-mypy - name: Run pydocstyle run: | - source ${HOME}/.venv/snapcraft/bin/activate make test-pydocstyle - name: Run pyright run: | - source ${HOME}/.venv/snapcraft/bin/activate sudo snap install --classic node sudo snap install --classic pyright make test-pyright - name: Run pylint run: | - source ${HOME}/.venv/snapcraft/bin/activate make test-pylint - name: Run shellcheck run: | From 4f8659b507ff3d048d74c98f9f8835dfd2b23c36 Mon Sep 17 00:00:00 2001 From: Claudio Matsuoka Date: Tue, 29 Mar 2022 20:36:35 -0300 Subject: [PATCH 067/167] requirements: unpin pylint and update dependencies Signed-off-by: Claudio Matsuoka --- requirements-devel.txt | 33 +++++++++++++++++---------------- requirements.txt | 18 +++++++++--------- setup.py | 2 +- 3 files changed, 27 insertions(+), 26 deletions(-) diff --git a/requirements-devel.txt b/requirements-devel.txt index 8e1e1be6b0..2f776a8dd7 100644 --- a/requirements-devel.txt +++ b/requirements-devel.txt @@ -1,20 +1,21 @@ -astroid==2.8.6 +astroid==2.11.2 attrs==21.4.0 -black==22.1.0 +black==22.3.0 catkin-pkg==0.4.24 certifi==2021.10.8 cffi==1.15.0 chardet==4.0.0 charset-normalizer==2.0.12 -click==8.0.4 +click==8.1.0 codespell==2.1.0 coverage==6.3.2 -craft-cli==0.2.0 +craft-cli==0.3.1 craft-grammar==1.1.1 -craft-parts==1.3.0 +craft-parts==1.4.0 craft-providers==1.1.0 cryptography==3.4 Deprecated==1.2.13 +dill==0.3.4 distro==1.7.0 docutils==0.18.1 entrypoints==0.3 @@ -25,7 +26,7 @@ gnupg==2.3.1 httplib2==0.20.4 hupper==1.10.3 idna==3.3 -importlib-metadata==4.11.2 +importlib-metadata==4.11.3 iniconfig==1.1.1 isort==5.10.1 jeepney==0.7.1 @@ -38,7 +39,7 @@ lazy-object-proxy==1.7.1 lxml==4.8.0 macaroonbakery==1.3.1 mccabe==0.6.1 -mypy==0.931 +mypy==0.942 mypy-extensions==0.4.3 oauthlib==3.2.0 overrides==6.1.0 @@ -59,12 +60,12 @@ py==1.11.0 pycodestyle==2.5.0 pycparser==2.21 pydantic==1.9.0 -pydantic-yaml==0.6.1 +pydantic-yaml==0.6.3 pydocstyle==6.1.1 pyelftools==0.28 pyflakes==2.1.1 pyftpdlib==1.5.6 -pylint==2.11.1 +pylint==2.13.3 pylint-fixme-info==1.0.3 pylint-pytest==1.1.2 pylxd==2.3.1 @@ -72,13 +73,13 @@ pymacaroons==0.13.0 pyparsing==3.0.7 pyramid==2.0 pyRFC3339==1.1 -pytest==7.0.1 +pytest==7.1.1 pytest-cov==3.0.0 pytest-mock==3.7.0 pytest-subprocess==1.4.1 python-dateutil==2.8.2 python-debian==0.1.43 -pytz==2021.3 +pytz==2022.1 pyxdg==0.27 PyYAML==5.3 raven==6.10.0 @@ -87,7 +88,7 @@ requests-toolbelt==0.9.1 requests-unixsocket==0.3.0 SecretStorage==3.3.1 semantic-version==2.9.0 -semver==3.0.0.dev3 +semver==2.13.0 simplejson==3.17.6 six==1.16.0 snowballstemmer==2.2.0 @@ -99,15 +100,15 @@ toml==0.10.2 tomli==2.0.1 translationstring==1.4 types-Deprecated==1.2.5 -types-PyYAML==6.0.4 -types-setuptools==57.4.10 +types-PyYAML==6.0.5 +types-setuptools==57.4.11 typing-utils==0.1.0 typing_extensions==4.1.1 -urllib3==1.26.8 +urllib3==1.26.9 venusian==3.0.0 wadllib==1.3.6 WebOb==1.8.7 -wrapt==1.13.3 +wrapt==1.14.0 ws4py==0.5.1 zipp==3.7.0 zope.deprecation==4.4.0 diff --git a/requirements.txt b/requirements.txt index 85fd02d77d..65c856c561 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,10 +4,10 @@ certifi==2021.10.8 cffi==1.15.0 chardet==4.0.0 charset-normalizer==2.0.12 -click==8.0.4 -craft-cli==0.2.0 +click==8.1.0 +craft-cli==0.3.1 craft-grammar==1.1.1 -craft-parts==1.3.0 +craft-parts==1.4.0 craft-providers==1.1.0 cryptography==3.4 Deprecated==1.2.13 @@ -16,7 +16,7 @@ docutils==0.18.1 gnupg==2.3.1 httplib2==0.20.4 idna==3.3 -importlib-metadata==4.11.2 +importlib-metadata==4.11.3 jeepney==0.7.1 jsonschema==2.5.1 keyring==23.5.0 @@ -34,7 +34,7 @@ protobuf==3.19.4 psutil==5.9.0 pycparser==2.21 pydantic==1.9.0 -pydantic-yaml==0.6.1 +pydantic-yaml==0.6.3 pyelftools==0.28 pylxd==2.3.1 pymacaroons==0.13.0 @@ -42,7 +42,7 @@ pyparsing==3.0.7 pyRFC3339==1.1 python-dateutil==2.8.2 python-debian==0.1.43 -pytz==2021.3 +pytz==2022.1 pyxdg==0.27 PyYAML==5.3 raven==6.10.0 @@ -51,7 +51,7 @@ requests-toolbelt==0.9.1 requests-unixsocket==0.3.0 SecretStorage==3.3.1 semantic-version==2.9.0 -semver==3.0.0.dev3 +semver==2.13.0 simplejson==3.17.6 six==1.16.0 tabulate==0.8.9 @@ -60,9 +60,9 @@ toml==0.10.2 types-Deprecated==1.2.5 typing-utils==0.1.0 typing_extensions==4.1.1 -urllib3==1.26.8 +urllib3==1.26.9 wadllib==1.3.6 -wrapt==1.13.3 +wrapt==1.14.0 ws4py==0.5.1 zipp==3.7.0 python-apt @ https://launchpad.net/ubuntu/+archive/primary/+sourcefiles/python-apt/2.0.0ubuntu0.20.04.6/python-apt_2.0.0ubuntu0.20.04.6.tar.xz; sys.platform == "linux" diff --git a/setup.py b/setup.py index ed0fa3f7d9..ccb790aede 100755 --- a/setup.py +++ b/setup.py @@ -74,7 +74,7 @@ def recursive_data_files(directory, install_directory): "pycodestyle", "pydocstyle", "pyftpdlib", - "pylint<2.12.0", + "pylint", "pylint-fixme-info", "pylint-pytest", "pyramid", From 62d65df2aeb5e4e1a322b72bece868566d167714 Mon Sep 17 00:00:00 2001 From: Callahan Kovacs Date: Fri, 25 Mar 2022 14:17:00 -0500 Subject: [PATCH 068/167] spread: fix python-hello --- .../plugins/v2/snaps/python-hello-multiple-parts-staged/setup.py | 1 + .../spread/plugins/v2/snaps/python-hello-multiple-parts/setup.py | 1 + 2 files changed, 2 insertions(+) diff --git a/tests/spread/plugins/v2/snaps/python-hello-multiple-parts-staged/setup.py b/tests/spread/plugins/v2/snaps/python-hello-multiple-parts-staged/setup.py index 37482e25e9..910d4047ad 100644 --- a/tests/spread/plugins/v2/snaps/python-hello-multiple-parts-staged/setup.py +++ b/tests/spread/plugins/v2/snaps/python-hello-multiple-parts-staged/setup.py @@ -7,4 +7,5 @@ author_email="snapcraft@lists.snapcraft.io", description="A simple hello world in python", scripts=["hello"], + py_modules=["hello"], ) diff --git a/tests/spread/plugins/v2/snaps/python-hello-multiple-parts/setup.py b/tests/spread/plugins/v2/snaps/python-hello-multiple-parts/setup.py index 37482e25e9..910d4047ad 100644 --- a/tests/spread/plugins/v2/snaps/python-hello-multiple-parts/setup.py +++ b/tests/spread/plugins/v2/snaps/python-hello-multiple-parts/setup.py @@ -7,4 +7,5 @@ author_email="snapcraft@lists.snapcraft.io", description="A simple hello world in python", scripts=["hello"], + py_modules=["hello"], ) From af265360334a353bfd4466f9e2ddbc5c6f0d0937 Mon Sep 17 00:00:00 2001 From: Sergio Schvezov Date: Thu, 31 Mar 2022 15:54:43 -0300 Subject: [PATCH 069/167] parts: consider non core22 when loading yaml The yaml loader is shared, the strategy is to load twice, once with the plain safe loader to check for the base and raise if necessary, then load again with Snapcraft's new custom _SafeLoader. Signed-off-by: Sergio Schvezov --- snapcraft/parts/lifecycle.py | 4 ---- snapcraft/parts/yaml_utils.py | 10 ++++++++ tests/unit/parts/test_yaml_utils.py | 37 ++++++++++++++++++++++++++++- 3 files changed, 46 insertions(+), 5 deletions(-) diff --git a/snapcraft/parts/lifecycle.py b/snapcraft/parts/lifecycle.py index 139672c0f5..a88e0eb8ce 100644 --- a/snapcraft/parts/lifecycle.py +++ b/snapcraft/parts/lifecycle.py @@ -79,10 +79,6 @@ def run(command_name: str, parsed_args: "argparse.Namespace") -> None: # validate project grammar GrammarAwareProject.validate_grammar(yaml_data) - # only execute the new codebase from core22 onwards - if yaml_data.get("base") != "core22": - raise errors.LegacyFallback("base is not core22") - # argument --provider is only supported by legacy snapcraft if parsed_args.provider: raise errors.SnapcraftError("Option --provider is not supported.") diff --git a/snapcraft/parts/yaml_utils.py b/snapcraft/parts/yaml_utils.py index 8892012356..3bb8783397 100644 --- a/snapcraft/parts/yaml_utils.py +++ b/snapcraft/parts/yaml_utils.py @@ -75,7 +75,17 @@ def load(filestream: TextIO) -> Dict[str, Any]: :param filename: The YAML file to load. :raises SnapcraftError: if loading didn't succeed. + :raises LegacyFallback: if the project's base is not core22. """ + try: + # TODO: support for build-base. + if yaml.safe_load(filestream)["base"] != "core22": + raise errors.LegacyFallback("base is not core22") + except KeyError as key_error: + raise errors.LegacyFallback("no base defined") from key_error + except yaml.error.YAMLError as err: + raise errors.SnapcraftError(f"YAML parsing error: {err!s}") from err + filestream.seek(0) try: return yaml.load(filestream, Loader=_SafeLoader) except yaml.error.YAMLError as err: diff --git a/tests/unit/parts/test_yaml_utils.py b/tests/unit/parts/test_yaml_utils.py index 5046c69dab..bdebc590ed 100644 --- a/tests/unit/parts/test_yaml_utils.py +++ b/tests/unit/parts/test_yaml_utils.py @@ -30,6 +30,7 @@ def test_yaml_load(): io.StringIO( dedent( """\ + base: core22 entry: sub-entry: - list1 @@ -40,6 +41,7 @@ def test_yaml_load(): ) ) == { + "base": "core22", "entry": { "sub-entry": ["list1", "list2"], }, @@ -54,6 +56,7 @@ def test_yaml_load_duplicates_errors(): io.StringIO( dedent( """\ + base: core22 entry: value1 entry: value2 """ @@ -75,6 +78,7 @@ def test_yaml_load_unhashable_errors(): io.StringIO( dedent( """\ + base: core22 entry: {{value}} """ ) @@ -84,6 +88,37 @@ def test_yaml_load_unhashable_errors(): assert str(raised.value) == dedent( """\ YAML parsing error: while constructing a mapping + in "", line 2, column 8 found unhashable key - in "", line 1, column 8""" + in "", line 2, column 9""" ) + + +def test_yaml_load_not_core22_base(): + with pytest.raises(errors.LegacyFallback) as raised: + yaml_utils.load( + io.StringIO( + dedent( + """\ + base: core20 + """ + ) + ) + ) + + assert str(raised.value) == "base is not core22" + + +def test_yaml_load_no_base(): + with pytest.raises(errors.LegacyFallback) as raised: + yaml_utils.load( + io.StringIO( + dedent( + """\ + entry: foo + """ + ) + ) + ) + + assert str(raised.value) == "no base defined" From a46dd210eabc6aacf728950457a64a986df06998 Mon Sep 17 00:00:00 2001 From: Sergio Schvezov Date: Thu, 31 Mar 2022 20:26:28 -0300 Subject: [PATCH 070/167] parts: amend YAML error to mention snapcraft.yaml Signed-off-by: Sergio Schvezov --- snapcraft/parts/yaml_utils.py | 4 ++-- tests/unit/parts/test_lifecycle.py | 24 ------------------------ tests/unit/parts/test_yaml_utils.py | 4 ++-- 3 files changed, 4 insertions(+), 28 deletions(-) diff --git a/snapcraft/parts/yaml_utils.py b/snapcraft/parts/yaml_utils.py index 3bb8783397..87228128eb 100644 --- a/snapcraft/parts/yaml_utils.py +++ b/snapcraft/parts/yaml_utils.py @@ -84,9 +84,9 @@ def load(filestream: TextIO) -> Dict[str, Any]: except KeyError as key_error: raise errors.LegacyFallback("no base defined") from key_error except yaml.error.YAMLError as err: - raise errors.SnapcraftError(f"YAML parsing error: {err!s}") from err + raise errors.SnapcraftError(f"snapcraft.yaml parsing error: {err!s}") from err filestream.seek(0) try: return yaml.load(filestream, Loader=_SafeLoader) except yaml.error.YAMLError as err: - raise errors.SnapcraftError(f"YAML parsing error: {err!s}") from err + raise errors.SnapcraftError(f"snapcraft.yaml parsing error: {err!s}") from err diff --git a/tests/unit/parts/test_lifecycle.py b/tests/unit/parts/test_lifecycle.py index e3913d1d8f..bf4ccdc05d 100644 --- a/tests/unit/parts/test_lifecycle.py +++ b/tests/unit/parts/test_lifecycle.py @@ -136,30 +136,6 @@ def test_snapcraft_yaml_load(new_dir, snapcraft_yaml, filename, mocker): ] -def test_snapcraft_yaml_parse_error(new_dir, snapcraft_yaml, mocker): - """If snapcraft.yaml is not a valid yaml, raise an error.""" - snapcraft_yaml(base="invalid: true") - run_command_mock = mocker.patch("snapcraft.parts.lifecycle._run_command") - - with pytest.raises(errors.SnapcraftError) as raised: - parts_lifecycle.run("pull", argparse.Namespace(parts=["part1"])) - - assert str(raised.value) == ( - "YAML parsing error: mapping values are not allowed here\n" - ' in "snap/snapcraft.yaml", line 4, column 14' - ) - assert run_command_mock.mock_calls == [] - - -def test_legacy_base_not_core22(new_dir, snapcraft_yaml): - """Only core22 is processed by the new code, use legacy otherwise.""" - snapcraft_yaml(base="core20") - with pytest.raises(errors.LegacyFallback) as raised: - parts_lifecycle.run("pull", argparse.Namespace()) - - assert str(raised.value) == "base is not core22" - - @pytest.mark.parametrize("cmd", ["pull", "build", "stage", "prime", "pack"]) def test_lifecycle_run_provider(cmd, snapcraft_yaml, new_dir, mocker): """Option --provider is not supported in core22.""" diff --git a/tests/unit/parts/test_yaml_utils.py b/tests/unit/parts/test_yaml_utils.py index bdebc590ed..930fb5d8a1 100644 --- a/tests/unit/parts/test_yaml_utils.py +++ b/tests/unit/parts/test_yaml_utils.py @@ -66,7 +66,7 @@ def test_yaml_load_duplicates_errors(): assert str(raised.value) == dedent( """\ - YAML parsing error: while constructing a mapping + snapcraft.yaml parsing error: while constructing a mapping found duplicate key 'entry' in "", line 1, column 1""" ) @@ -87,7 +87,7 @@ def test_yaml_load_unhashable_errors(): assert str(raised.value) == dedent( """\ - YAML parsing error: while constructing a mapping + snapcraft.yaml parsing error: while constructing a mapping in "", line 2, column 8 found unhashable key in "", line 2, column 9""" From 0b0450d3d1def7d654937a96e9b5a75f57fb7e31 Mon Sep 17 00:00:00 2001 From: Claudio Matsuoka Date: Fri, 1 Apr 2022 17:47:06 -0300 Subject: [PATCH 071/167] requirements: update dependencies Signed-off-by: Claudio Matsuoka --- requirements-devel.txt | 12 ++++++------ requirements.txt | 10 +++++----- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/requirements-devel.txt b/requirements-devel.txt index 2f776a8dd7..1acd3f2df5 100644 --- a/requirements-devel.txt +++ b/requirements-devel.txt @@ -6,13 +6,13 @@ certifi==2021.10.8 cffi==1.15.0 chardet==4.0.0 charset-normalizer==2.0.12 -click==8.1.0 +click==8.1.2 codespell==2.1.0 coverage==6.3.2 -craft-cli==0.3.1 +craft-cli==0.4.0 craft-grammar==1.1.1 -craft-parts==1.4.0 -craft-providers==1.1.0 +craft-parts==1.4.2 +craft-providers==1.1.1 cryptography==3.4 Deprecated==1.2.13 dill==0.3.4 @@ -53,7 +53,7 @@ plaster-pastedeploy==0.7 platformdirs==2.5.1 pluggy==1.0.0 progressbar==2.5 -protobuf==3.19.4 +protobuf==3.20.0 psutil==5.9.0 ptyprocess==0.7.0 py==1.11.0 @@ -65,7 +65,7 @@ pydocstyle==6.1.1 pyelftools==0.28 pyflakes==2.1.1 pyftpdlib==1.5.6 -pylint==2.13.3 +pylint==2.13.4 pylint-fixme-info==1.0.3 pylint-pytest==1.1.2 pylxd==2.3.1 diff --git a/requirements.txt b/requirements.txt index 65c856c561..1bec40c02b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,11 +4,11 @@ certifi==2021.10.8 cffi==1.15.0 chardet==4.0.0 charset-normalizer==2.0.12 -click==8.1.0 -craft-cli==0.3.1 +click==8.1.2 +craft-cli==0.4.0 craft-grammar==1.1.1 -craft-parts==1.4.0 -craft-providers==1.1.0 +craft-parts==1.4.2 +craft-providers==1.1.1 cryptography==3.4 Deprecated==1.2.13 distro==1.7.0 @@ -30,7 +30,7 @@ oauthlib==3.2.0 overrides==6.1.0 platformdirs==2.5.1 progressbar==2.5 -protobuf==3.19.4 +protobuf==3.20.0 psutil==5.9.0 pycparser==2.21 pydantic==1.9.0 From 851d82b37e6e3bc0c9f501d1e03e5e94e7f20a34 Mon Sep 17 00:00:00 2001 From: Claudio Matsuoka Date: Fri, 18 Mar 2022 18:31:26 -0300 Subject: [PATCH 072/167] parts: pause emitter and use output streams Fix verbosity levels in managed instances. Adjust execution to pause the emitter when running snapcraft in a managed instance and redirect output streams to prevent execution output leaks. Signed-off-by: Claudio Matsuoka --- snapcraft/parts/lifecycle.py | 19 ++++++++++++++----- snapcraft/parts/parts.py | 3 ++- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/snapcraft/parts/lifecycle.py b/snapcraft/parts/lifecycle.py index a88e0eb8ce..25d995adb5 100644 --- a/snapcraft/parts/lifecycle.py +++ b/snapcraft/parts/lifecycle.py @@ -20,7 +20,7 @@ from pathlib import Path from typing import TYPE_CHECKING, cast -from craft_cli import emit +from craft_cli import EmitterMode, emit from craft_parts import infos from snapcraft import errors, extensions, pack, providers, utils @@ -149,16 +149,25 @@ def _run_in_provider(project: Project, command_name: str, parsed_args: "argparse if hasattr(parsed_args, "parts"): cmd.extend(parsed_args.parts) + if emit.get_mode() == EmitterMode.VERBOSE: + cmd.append("--verbose") + elif emit.get_mode() == EmitterMode.QUIET: + cmd.append("--quiet") + elif emit.get_mode() == EmitterMode.TRACE: + cmd.append("--trace") + output_dir = utils.get_managed_environment_project_path() - # FIXME: pause emitter when executing instance (needs craft-cli support) emit.progress("Launching instance...") with provider.launched_environment( - project_name=project.name, project_path=Path().absolute(), base=cast(str, project.base) + project_name=project.name, + project_path=Path().absolute(), + base=cast(str, project.base), ) as instance: try: - emit.message("Launched instance", intermediate=True) - instance.execute_run(cmd, check=True, cwd=output_dir) + with emit.pause(): + instance.execute_run(cmd, check=True, cwd=output_dir) + capture_logs_from_instance(instance) except subprocess.CalledProcessError as err: capture_logs_from_instance(instance) raise providers.ProviderError( diff --git a/snapcraft/parts/parts.py b/snapcraft/parts/parts.py index 1970fddcf8..9490855a44 100644 --- a/snapcraft/parts/parts.py +++ b/snapcraft/parts/parts.py @@ -120,7 +120,8 @@ def run(self, step_name: str) -> None: for action in actions: message = _action_message(action) emit.progress(f"Executing parts lifecycle: {message}") - aex.execute(action) + with emit.open_stream("Executing action") as stream: + aex.execute(action, stdout=stream, stderr=stream) emit.message("Executed parts lifecycle", intermediate=True) except RuntimeError as err: From b2c708a99aadcfcf9e2580767edf2a1d567568d7 Mon Sep 17 00:00:00 2001 From: Claudio Matsuoka Date: Wed, 30 Mar 2022 17:42:45 -0300 Subject: [PATCH 073/167] parts: define and consume project variables Project variables are variables that can be set by craftctl in parts adopting external metadata. A list of project variables are passed to the parts lifecycle manager. After the lifecycle execution ends their values are retrieved and added to the snap metadata file. Signed-off-by: Claudio Matsuoka --- pyproject.toml | 1 + snap/snapcraft.yaml | 5 ++++- snapcraft/commands/lifecycle.py | 7 +++++- snapcraft/meta/snap_yaml.py | 6 ++--- snapcraft/parts/lifecycle.py | 35 ++++++++++++++++++++++++++++-- snapcraft/parts/parts.py | 20 +++++++++++++++-- snapcraft/projects.py | 7 ++++-- tests/unit/meta/test_snap_yaml.py | 22 ++++++++++++++----- tests/unit/parts/test_lifecycle.py | 26 +++++++++++++++++----- tests/unit/parts/test_parts.py | 32 ++++++++++++++++++++++----- tests/unit/test_projects.py | 1 + 11 files changed, 134 insertions(+), 28 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 3a5f4af0a7..57d20cb985 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,6 +36,7 @@ disable = "too-few-public-methods,fixme,use-implicit-booleaness-not-comparison,d [tool.pylint.format] max-attributes = 15 +max-args = 6 good-names = "id" [tool.pylint.MASTER] diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index f0ea19cd1c..5000dafd10 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -15,7 +15,7 @@ assumes: apps: snapcraft: environment: - PATH: "/snap/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" + PATH: "$SNAP/libexec/snapcraft:/snap/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" # https://github.com/lxc/pylxd/pull/361 PYLXD_WARNINGS: "none" command: bin/python $SNAP/bin/snapcraft @@ -134,4 +134,7 @@ parts: [ -n "$(echo $version | grep "+git")" ] && grade=devel || grade=stable snapcraftctl set-grade "$grade" ln -sf ../usr/bin/python3.8 $SNAPCRAFT_PART_INSTALL/bin/python3 + mkdir -p $SNAPCRAFT_PART_INSTALL/libexec/snapcraft + mv $SNAPCRAFT_PART_INSTALL/bin/craftctl $SNAPCRAFT_PART_INSTALL/libexec/snapcraft/ + sed -i -e '1 s|^#!/.*|#!/snap/snapcraft/current/bin/python|' $SNAPCRAFT_PART_INSTALL/libexec/snapcraft/craftctl after: [snapcraft-libs] diff --git a/snapcraft/commands/lifecycle.py b/snapcraft/commands/lifecycle.py index 5338ec48c6..e43c685abb 100644 --- a/snapcraft/commands/lifecycle.py +++ b/snapcraft/commands/lifecycle.py @@ -43,6 +43,7 @@ def fill_parser(self, parser: "argparse.ArgumentParser") -> None: action="store_true", help="Use LXD to build", ) + # --provider is only available in legacy parser.add_argument("--provider", help=argparse.SUPPRESS) @@ -63,7 +64,11 @@ class _LifecycleStepCommand(_LifecycleCommand): def fill_parser(self, parser: "argparse.ArgumentParser") -> None: super().fill_parser(parser) parser.add_argument( - "parts", metavar="parts", type=str, nargs="*", help="Parts to process" + "parts", + metavar="part-name", + type=str, + nargs="*", + help="Optional list of parts to process", ) diff --git a/snapcraft/meta/snap_yaml.py b/snapcraft/meta/snap_yaml.py index c29e213e1e..dd7f8455ff 100644 --- a/snapcraft/meta/snap_yaml.py +++ b/snapcraft/meta/snap_yaml.py @@ -107,7 +107,7 @@ class SnapMetadata(YamlModel): hooks: Optional[Dict[str, Any]] -def write(project: Project, prime_dir: Path, *, arch: str): +def write(project: Project, prime_dir: Path, *, arch: str, version: str, grade: str): """Create a snap.yaml file.""" meta_dir = prime_dir / "meta" meta_dir.mkdir(parents=True, exist_ok=True) @@ -159,7 +159,7 @@ def write(project: Project, prime_dir: Path, *, arch: str): snap_metadata = SnapMetadata( name=project.name, title=project.title, - version=project.version, # type: ignore + version=version, summary=project.summary, description=project.description, license=project.license, @@ -170,7 +170,7 @@ def write(project: Project, prime_dir: Path, *, arch: str): epoch=project.epoch, apps=snap_apps, confinement=project.confinement, - grade=project.grade, + grade=grade, environment=project.environment, plugs=project.plugs, hooks=project.hooks, diff --git a/snapcraft/parts/lifecycle.py b/snapcraft/parts/lifecycle.py index 25d995adb5..80ed07a6ea 100644 --- a/snapcraft/parts/lifecycle.py +++ b/snapcraft/parts/lifecycle.py @@ -118,16 +118,45 @@ def _run_command( work_dir = Path.cwd() step_name = "prime" if command_name == "pack" else command_name + part_names = getattr(parsed_args, "parts", []) lifecycle = PartsLifecycle( project.parts, work_dir=work_dir, assets_dir=assets_dir, package_repositories=project.package_repositories, + part_names=part_names, + adopt_info=project.adopt_info, + project_vars={ + "version": project.version or "", + "grade": project.grade, + } ) lifecycle.run(step_name) - snap_yaml.write(project, lifecycle.prime_dir, arch=lifecycle.target_arch) + # Generate snap.yaml + project_vars = lifecycle.project_vars + if step_name == "prime" and not part_names: + version = project_vars["version"] + if not version: + raise errors.SnapcraftError("snap version cannot be empty") + + # FIXME: refactor craft-parts to define validators for project variables + grade = project_vars["grade"] + if grade not in ("stable", "devel"): + raise errors.SnapcraftError( + f"invalid grade {grade!r}, must be either 'stable' or 'devel'" + ) + + emit.progress("Generating snap metadata...") + snap_yaml.write( + project, + lifecycle.prime_dir, + arch=lifecycle.target_arch, + version=version, + grade=grade, + ) + emit.message("Generated snap metadata", intermediate=True) if command_name == "pack": pack.pack_snap( @@ -137,7 +166,9 @@ def _run_command( ) -def _run_in_provider(project: Project, command_name: str, parsed_args: "argparse.Namespace"): +def _run_in_provider( + project: Project, command_name: str, parsed_args: "argparse.Namespace" +): """Pack image in provider instance.""" emit.trace("Checking build provider availability") provider_name = "lxd" if parsed_args.use_lxd else None diff --git a/snapcraft/parts/parts.py b/snapcraft/parts/parts.py index 9490855a44..3349d7eb47 100644 --- a/snapcraft/parts/parts.py +++ b/snapcraft/parts/parts.py @@ -17,7 +17,7 @@ """Craft-parts lifecycle wrapper.""" import pathlib -from typing import Any, Dict, List +from typing import Any, Dict, List, Optional import craft_parts from craft_cli import emit @@ -41,6 +41,7 @@ class PartsLifecycle: :param all_parts: A dictionary containing the parts defined in the project. :param work_dir: The working directory for parts processing. :param assets_dir: The directory containing project assets. + :param adopt_info: The name of the part containing metadata do adopt. :raises PartsLifecycleError: On error initializing the parts lifecycle. """ @@ -52,9 +53,13 @@ def __init__( work_dir: pathlib.Path, assets_dir: pathlib.Path, package_repositories: List[Dict[str, Any]], + part_names: Optional[List[str]], + adopt_info: Optional[str], + project_vars: Dict[str, str], ): self._assets_dir = assets_dir self._package_repositories = package_repositories + self._part_names = part_names emit.progress("Initializing parts lifecycle") @@ -74,6 +79,8 @@ def __init__( work_dir=work_dir, cache_dir=cache_dir, ignore_local_sources=["*.snap"], + project_vars_part_name=adopt_info, + project_vars=project_vars, ) except craft_parts.PartsError as err: raise errors.PartsLifecycleError(str(err)) from err @@ -88,6 +95,14 @@ def target_arch(self) -> str: """Return the parts project target architecture.""" return self._lcm.project_info.target_arch + @property + def project_vars(self) -> Dict[str, str]: + """Return the value of project variable ``version``.""" + return { + "version": self._lcm.project_info.get_project_var("version"), + "grade": self._lcm.project_info.get_project_var("grade"), + } + def run(self, step_name: str) -> None: """Run the parts lifecycle. @@ -101,7 +116,7 @@ def run(self, step_name: str) -> None: raise RuntimeError(f"Invalid target step {step_name!r}") try: - actions = self._lcm.plan(target_step) + actions = self._lcm.plan(target_step, part_names=self._part_names) emit.progress("Installing package repositories...") @@ -122,6 +137,7 @@ def run(self, step_name: str) -> None: emit.progress(f"Executing parts lifecycle: {message}") with emit.open_stream("Executing action") as stream: aex.execute(action, stdout=stream, stderr=stream) + emit.message(f"Executed: {message}", intermediate=True) emit.message("Executed parts lifecycle", intermediate=True) except RuntimeError as err: diff --git a/snapcraft/projects.py b/snapcraft/projects.py index 8e5304ede0..27a56bc4f8 100644 --- a/snapcraft/projects.py +++ b/snapcraft/projects.py @@ -235,7 +235,6 @@ class Project(ProjectModel): XXX: Not implemented in this version - system-usernames - - adopt-info (after adding craftctl support to craft-parts) """ name: constr(max_length=40) # type: ignore @@ -267,6 +266,7 @@ class Project(ProjectModel): slots: Optional[Dict[str, Dict[str, str]]] # TODO: add slot name validation parts: Dict[str, Any] # parts are handled by craft-parts epoch: Optional[str] + adopt_info: Optional[str] environment: Optional[Dict[str, Any]] @pydantic.validator("plugs") @@ -326,7 +326,10 @@ def _validate_name(cls, name): @pydantic.validator("version") @classmethod - def _validate_version(cls, version): + def _validate_version(cls, version, values): + if not version and "adopt_info" not in values: + raise ValueError("Version must be declared if not adopting metadata") + if not re.match(r"^[a-zA-Z0-9](?:[a-zA-Z0-9:.+~-]*[a-zA-Z0-9+~])?$", version): raise ValueError( "Snap versions consist of upper- and lower-case alphanumeric characters, " diff --git a/tests/unit/meta/test_snap_yaml.py b/tests/unit/meta/test_snap_yaml.py index 1ef89657b2..235bfb2231 100644 --- a/tests/unit/meta/test_snap_yaml.py +++ b/tests/unit/meta/test_snap_yaml.py @@ -55,7 +55,13 @@ def simple_project(): def test_simple_snap_yaml(simple_project, new_dir): - snap_yaml.write(simple_project, prime_dir=Path(new_dir), arch="arch") + snap_yaml.write( + simple_project, + prime_dir=Path(new_dir), + arch="arch", + version="1.30", + grade="stable", + ) yaml_file = Path("meta/snap.yaml") assert yaml_file.is_file() @@ -63,7 +69,7 @@ def test_simple_snap_yaml(simple_project, new_dir): assert content == textwrap.dedent( """\ name: mytest - version: 1.29.3 + version: '1.30' summary: Single-line elevator pitch for your amazing snap description: | This is my-snap's description. You have a paragraph or two to tell the @@ -178,7 +184,13 @@ def complex_project(): def test_complex_snap_yaml(complex_project, new_dir): - snap_yaml.write(complex_project, prime_dir=Path(new_dir), arch="arch") + snap_yaml.write( + complex_project, + prime_dir=Path(new_dir), + arch="arch", + version="1.30", + grade="devel", + ) yaml_file = Path("meta/snap.yaml") assert yaml_file.is_file() @@ -186,7 +198,7 @@ def test_complex_snap_yaml(complex_project, new_dir): assert content == textwrap.dedent( """\ name: mytest - version: 1.29.3 + version: '1.30' summary: Single-line elevator pitch for your amazing snap description: | This is my-snap's description. You have a paragraph or two to tell the @@ -241,7 +253,7 @@ def test_complex_snap_yaml(complex_project, new_dir): listen_stream: 100 socket_mode: 1 confinement: strict - grade: stable + grade: devel environment: GLOBAL_VARIABLE: test-global-variable plugs: diff --git a/tests/unit/parts/test_lifecycle.py b/tests/unit/parts/test_lifecycle.py index bf4ccdc05d..e0781e48e8 100644 --- a/tests/unit/parts/test_lifecycle.py +++ b/tests/unit/parts/test_lifecycle.py @@ -18,7 +18,7 @@ import textwrap from pathlib import Path from typing import Any, Dict -from unittest.mock import call +from unittest.mock import PropertyMock, call import pytest @@ -92,6 +92,15 @@ def write_file( yield write_file +@pytest.fixture +def project_vars(mocker): + yield mocker.patch( + "snapcraft.parts.PartsLifecycle.project_vars", + new_callable=PropertyMock, + return_value={"version": "0.1", "grade": "stable"}, + ) + + def test_config_not_found(new_dir): """If snapcraft.yaml is not found, raise an error.""" with pytest.raises(errors.SnapcraftError) as raised: @@ -185,7 +194,7 @@ def test_lifecycle_legacy_run_provider(cmd, snapcraft_yaml, new_dir, mocker): ("prime", "prime"), ], ) -def test_lifecycle_run_command_step(cmd, step, snapcraft_yaml, new_dir, mocker): +def test_lifecycle_run_command_step(cmd, step, snapcraft_yaml, project_vars, new_dir, mocker): project = Project.unmarshal(snapcraft_yaml(base="core22")) run_mock = mocker.patch("snapcraft.parts.PartsLifecycle.run") mocker.patch("snapcraft.meta.snap_yaml.write") @@ -195,14 +204,14 @@ def test_lifecycle_run_command_step(cmd, step, snapcraft_yaml, new_dir, mocker): cmd, project=project, assets_dir=Path(), - parsed_args=argparse.Namespace(destructive_mode=True, use_lxd=False), + parsed_args=argparse.Namespace(destructive_mode=True, use_lxd=False, parts=[]), ) assert run_mock.mock_calls == [call(step)] assert pack_mock.mock_calls == [] -def test_lifecycle_run_command_pack(snapcraft_yaml, new_dir, mocker): +def test_lifecycle_run_command_pack(snapcraft_yaml, project_vars, new_dir, mocker): project = Project.unmarshal(snapcraft_yaml(base="core22")) run_mock = mocker.patch("snapcraft.parts.PartsLifecycle.run") mocker.patch("snapcraft.meta.snap_yaml.write") @@ -217,6 +226,7 @@ def test_lifecycle_run_command_pack(snapcraft_yaml, new_dir, mocker): output=None, destructive_mode=True, use_lxd=False, + parts=[], ), ) @@ -226,7 +236,7 @@ def test_lifecycle_run_command_pack(snapcraft_yaml, new_dir, mocker): ] -def test_lifecycle_pack_destructive_mode(snapcraft_yaml, new_dir, mocker): +def test_lifecycle_pack_destructive_mode(snapcraft_yaml, project_vars, new_dir, mocker): project = Project.unmarshal(snapcraft_yaml(base="core22")) run_in_provider_mock = mocker.patch("snapcraft.parts.lifecycle._run_in_provider") run_mock = mocker.patch("snapcraft.parts.PartsLifecycle.run") @@ -247,6 +257,7 @@ def test_lifecycle_pack_destructive_mode(snapcraft_yaml, new_dir, mocker): output=None, destructive_mode=True, use_lxd=False, + parts=[], ), ) @@ -257,7 +268,7 @@ def test_lifecycle_pack_destructive_mode(snapcraft_yaml, new_dir, mocker): ] -def test_lifecycle_pack_managed(snapcraft_yaml, new_dir, mocker): +def test_lifecycle_pack_managed(snapcraft_yaml, project_vars, new_dir, mocker): project = Project.unmarshal(snapcraft_yaml(base="core22")) run_in_provider_mock = mocker.patch("snapcraft.parts.lifecycle._run_in_provider") run_mock = mocker.patch("snapcraft.parts.PartsLifecycle.run") @@ -278,6 +289,7 @@ def test_lifecycle_pack_managed(snapcraft_yaml, new_dir, mocker): output=None, destructive_mode=False, use_lxd=False, + parts=[], ), ) @@ -303,6 +315,7 @@ def test_lifecycle_pack_not_managed(snapcraft_yaml, new_dir, mocker): output=None, destructive_mode=False, use_lxd=False, + parts=[], ), ) @@ -316,6 +329,7 @@ def test_lifecycle_pack_not_managed(snapcraft_yaml, new_dir, mocker): output=None, destructive_mode=False, use_lxd=False, + parts=[], ), ) ] diff --git a/tests/unit/parts/test_parts.py b/tests/unit/parts/test_parts.py index d2282935b9..c3895620c6 100644 --- a/tests/unit/parts/test_parts.py +++ b/tests/unit/parts/test_parts.py @@ -32,7 +32,13 @@ def parts_data(): @pytest.mark.parametrize("step_name", ["pull", "overlay", "build", "stage", "prime"]) def test_parts_lifecycle_run(parts_data, step_name, new_dir, emitter): lifecycle = PartsLifecycle( - parts_data, work_dir=new_dir, assets_dir=new_dir, package_repositories=[] + parts_data, + work_dir=new_dir, + assets_dir=new_dir, + part_names=[], + package_repositories=[], + adopt_info=None, + project_vars={"version": "1", "grade": "stable"}, ) lifecycle.run(step_name) assert lifecycle.prime_dir == Path(new_dir, "prime") @@ -42,7 +48,13 @@ def test_parts_lifecycle_run(parts_data, step_name, new_dir, emitter): def test_parts_lifecycle_run_bad_step(parts_data, new_dir): lifecycle = PartsLifecycle( - parts_data, work_dir=new_dir, assets_dir=new_dir, package_repositories=[] + parts_data, + work_dir=new_dir, + assets_dir=new_dir, + part_names=[], + package_repositories=[], + adopt_info=None, + project_vars={"version": "1", "grade": "stable"}, ) with pytest.raises(RuntimeError) as raised: lifecycle.run("invalid") @@ -51,7 +63,13 @@ def test_parts_lifecycle_run_bad_step(parts_data, new_dir): def test_parts_lifecycle_run_internal_error(parts_data, new_dir, mocker): lifecycle = PartsLifecycle( - parts_data, work_dir=new_dir, assets_dir=new_dir, package_repositories=[] + parts_data, + work_dir=new_dir, + assets_dir=new_dir, + part_names=[], + package_repositories=[], + adopt_info=None, + project_vars={"version": "1", "grade": "stable"}, ) mocker.patch("craft_parts.LifecycleManager.plan", side_effect=RuntimeError("crash")) with pytest.raises(RuntimeError) as raised: @@ -64,11 +82,13 @@ def test_parts_lifecycle_run_parts_error(new_dir): {"p1": {"plugin": "dump", "source": "foo"}}, work_dir=new_dir, assets_dir=new_dir, + part_names=[], package_repositories=[], + adopt_info=None, + project_vars={"version": "1", "grade": "stable"}, ) with pytest.raises(errors.PartsLifecycleError) as raised: lifecycle.run("prime") - assert ( - str(raised.value) - == "Failed to pull source: unable to determine source type of 'foo'." + assert str(raised.value) == ( + "Failed to pull source: unable to determine source type of 'foo'." ) diff --git a/tests/unit/test_projects.py b/tests/unit/test_projects.py index 140d262abf..f51fed5fb5 100644 --- a/tests/unit/test_projects.py +++ b/tests/unit/test_projects.py @@ -92,6 +92,7 @@ def test_project_defaults(self, project_yaml_data): assert project.slots is None assert project.epoch is None assert project.environment is None + assert project.adopt_info is None def test_app_defaults(self, project_yaml_data): data = project_yaml_data(apps={"app1": {"command": "/bin/true"}}) From f0d1e358734e6b0bead913a724d28e3f852a9458 Mon Sep 17 00:00:00 2001 From: Claudio Matsuoka Date: Thu, 31 Mar 2022 13:37:41 -0300 Subject: [PATCH 074/167] tests: update core22 spread tests Signed-off-by: Claudio Matsuoka --- snapcraft/commands/lifecycle.py | 1 - .../spread/general/core22/craftctl/task.yaml | 30 ++++++++++ .../craftctl/test-craftctl-default/Makefile | 19 ++++++ .../craftctl/test-craftctl-default/hello.c | 6 ++ .../test-craftctl-default/snap/snapcraft.yaml | 28 +++++++++ .../test-craftctl-get-set/snap/snapcraft.yaml | 26 +++++++++ .../general/core22/environment/task.yaml | 58 +++++++++++++++++++ .../test-variables/snap/snapcraft.yaml | 19 ++++++ .../{ => package-repositories}/task.yaml | 0 .../snap/keys/FC42E99D.asc | 0 .../snap/snapcraft.yaml | 0 .../test-apt-key-name/snap/keys/FC42E99D.asc | 0 .../test-apt-key-name/snap/snapcraft.yaml | 0 .../test-apt-keyserver/snap/snapcraft.yaml | 0 .../test-apt-path/snap/snapcraft.yaml | 0 .../test-apt-ppa/snap/snapcraft.yaml | 0 .../scriptlet-failures/snap/snapcraft.yaml | 17 ++++++ .../general/core22/scriptlets/task.yaml | 28 +++++++++ 18 files changed, 231 insertions(+), 1 deletion(-) create mode 100644 tests/spread/general/core22/craftctl/task.yaml create mode 100644 tests/spread/general/core22/craftctl/test-craftctl-default/Makefile create mode 100644 tests/spread/general/core22/craftctl/test-craftctl-default/hello.c create mode 100644 tests/spread/general/core22/craftctl/test-craftctl-default/snap/snapcraft.yaml create mode 100644 tests/spread/general/core22/craftctl/test-craftctl-get-set/snap/snapcraft.yaml create mode 100644 tests/spread/general/core22/environment/task.yaml create mode 100644 tests/spread/general/core22/environment/test-variables/snap/snapcraft.yaml rename tests/spread/general/core22/{ => package-repositories}/task.yaml (100%) rename tests/spread/general/core22/{ => package-repositories}/test-apt-key-fingerprint/snap/keys/FC42E99D.asc (100%) rename tests/spread/general/core22/{ => package-repositories}/test-apt-key-fingerprint/snap/snapcraft.yaml (100%) rename tests/spread/general/core22/{ => package-repositories}/test-apt-key-name/snap/keys/FC42E99D.asc (100%) rename tests/spread/general/core22/{ => package-repositories}/test-apt-key-name/snap/snapcraft.yaml (100%) rename tests/spread/general/core22/{ => package-repositories}/test-apt-keyserver/snap/snapcraft.yaml (100%) rename tests/spread/general/core22/{ => package-repositories}/test-apt-path/snap/snapcraft.yaml (100%) rename tests/spread/general/core22/{ => package-repositories}/test-apt-ppa/snap/snapcraft.yaml (100%) create mode 100644 tests/spread/general/core22/scriptlets/scriptlet-failures/snap/snapcraft.yaml create mode 100644 tests/spread/general/core22/scriptlets/task.yaml diff --git a/snapcraft/commands/lifecycle.py b/snapcraft/commands/lifecycle.py index e43c685abb..7ea4da5af7 100644 --- a/snapcraft/commands/lifecycle.py +++ b/snapcraft/commands/lifecycle.py @@ -43,7 +43,6 @@ def fill_parser(self, parser: "argparse.ArgumentParser") -> None: action="store_true", help="Use LXD to build", ) - # --provider is only available in legacy parser.add_argument("--provider", help=argparse.SUPPRESS) diff --git a/tests/spread/general/core22/craftctl/task.yaml b/tests/spread/general/core22/craftctl/task.yaml new file mode 100644 index 0000000000..21e05dfe3b --- /dev/null +++ b/tests/spread/general/core22/craftctl/task.yaml @@ -0,0 +1,30 @@ +summary: Test craftctl commands on core22 + +environment: + SNAP/test_craftctl_default: test-craftctl-default + SNAP/test_craftctl_get_set: test-craftctl-get-set + +prepare: | + #shellcheck source=tests/spread/tools/snapcraft-yaml.sh + . "$TOOLS_DIR/snapcraft-yaml.sh" + # set_base "$SNAP/snap/snapcraft.yaml" + snap install core22 --edge + +restore: | + cd "$SNAP" + rm -f ./*.snap + rm -Rf work + + #shellcheck source=tests/spread/tools/snapcraft-yaml.sh + . "$TOOLS_DIR/snapcraft-yaml.sh" + restore_yaml "snap/snapcraft.yaml" + +execute: | + cd "$SNAP" + + if [ "$SPREAD_SYSTEM" = "ubuntu-20.04-64" ]; then + snapcraft --verbose --destructive-mode + TESTBIN="${SNAP##*test-}" + snap install craftctl-*.snap --dangerous + $TESTBIN | grep hello + fi diff --git a/tests/spread/general/core22/craftctl/test-craftctl-default/Makefile b/tests/spread/general/core22/craftctl/test-craftctl-default/Makefile new file mode 100644 index 0000000000..aa47d9c2bc --- /dev/null +++ b/tests/spread/general/core22/craftctl/test-craftctl-default/Makefile @@ -0,0 +1,19 @@ +CC = gcc +CFLAGS = -O2 -Wall +LD = gcc +LDFLAGS = +OBJS = hello.o +BIN = hello + +.c.o: + $(CC) -c $(CFLAGS) -o$*.o $< + +$(BIN): $(OBJS) + $(LD) -o $@ $(OBJS) + +install: + mkdir -p $(DESTDIR)/usr/bin + install -m755 $(BIN) $(DESTDIR)/usr/bin + +clean: + rm -f $(OBJS) diff --git a/tests/spread/general/core22/craftctl/test-craftctl-default/hello.c b/tests/spread/general/core22/craftctl/test-craftctl-default/hello.c new file mode 100644 index 0000000000..7583bb3313 --- /dev/null +++ b/tests/spread/general/core22/craftctl/test-craftctl-default/hello.c @@ -0,0 +1,6 @@ +#include + +int main() +{ + printf("hello\n"); +} diff --git a/tests/spread/general/core22/craftctl/test-craftctl-default/snap/snapcraft.yaml b/tests/spread/general/core22/craftctl/test-craftctl-default/snap/snapcraft.yaml new file mode 100644 index 0000000000..1dbb998f5d --- /dev/null +++ b/tests/spread/general/core22/craftctl/test-craftctl-default/snap/snapcraft.yaml @@ -0,0 +1,28 @@ +name: craftctl-default +version: '1.0' +summary: test +description: test +grade: stable +confinement: strict +base: core22 + +apps: + craftctl-default: + command: usr/bin/hello + +parts: + hello: + plugin: make + source: . + override-pull: | + echo "This is the pull step" + craftctl default + override-build: | + echo "This is the build step" + craftctl default + override-stage: | + echo "This is the stage step" + craftctl default + override-prime: | + echo "This is the prime step" + craftctl default diff --git a/tests/spread/general/core22/craftctl/test-craftctl-get-set/snap/snapcraft.yaml b/tests/spread/general/core22/craftctl/test-craftctl-get-set/snap/snapcraft.yaml new file mode 100644 index 0000000000..0e17298a5a --- /dev/null +++ b/tests/spread/general/core22/craftctl/test-craftctl-get-set/snap/snapcraft.yaml @@ -0,0 +1,26 @@ +name: craftctl-get-set +summary: test +description: test +grade: devel +confinement: strict +base: core22 +adopt-info: hello + +apps: + craftctl-get-set: + command: hello.sh + +parts: + hello: + plugin: nil + override-pull: | + echo -e "#!/usr/bin/env bash\necho hello" > hello.sh + chmod +x hello.sh + craftctl get grade | grep devel + craftctl set version="22" + craftctl set grade=stable + override-build: | + craftctl get version | grep 22 + craftctl get grade | grep stable + echo "This is the build step" + cp hello.sh "$CRAFT_PART_INSTALL"/ diff --git a/tests/spread/general/core22/environment/task.yaml b/tests/spread/general/core22/environment/task.yaml new file mode 100644 index 0000000000..116730bac7 --- /dev/null +++ b/tests/spread/general/core22/environment/task.yaml @@ -0,0 +1,58 @@ +summary: Test scriptlets variables on core22 + +environment: + SNAP/test_variables: test-variables + +prepare: | + #shellcheck source=tests/spread/tools/snapcraft-yaml.sh + . "$TOOLS_DIR/snapcraft-yaml.sh" + # set_base "$SNAP/snap/snapcraft.yaml" + snap install core22 --edge + +restore: | + cd "$SNAP" + rm -f ./*.snap + rm -Rf work + + #shellcheck source=tests/spread/tools/snapcraft-yaml.sh + . "$TOOLS_DIR/snapcraft-yaml.sh" + restore_yaml "snap/snapcraft.yaml" + +execute: | + cd "$SNAP" + + check_vars() { + file="$1" + echo "==== $file ====" + cat "$file" + for exp in \ + "^CRAFT_ARCH_TRIPLET=x86_64-linux-gnu$" \ + "^CRAFT_TARGET_ARCH=amd64$" \ + "^CRAFT_PARALLEL_BUILD_COUNT=[0-9]\+$" \ + "^CRAFT_PROJECT_DIR=/root/project$" \ + "^CRAFT_PART_NAME=hello$" \ + "^CRAFT_PART_SRC=/root/parts/hello/src$" \ + "^CRAFT_PART_SRC_WORK=/root/parts/hello/src$" \ + "^CRAFT_PART_BUILD=/root/parts/hello/build$" \ + "^CRAFT_PART_BUILD_WORK=/root/parts/hello/build$" \ + "^CRAFT_PART_INSTALL=/root/parts/hello/install$" \ + "^CRAFT_OVERLAY=/root/overlay/overlay$" \ + "^CRAFT_STAGE=/root/stage$" \ + "^CRAFT_PRIME=/root/prime$"; do + grep -q "$exp" < "$file" + done + } + + if [ "$SPREAD_SYSTEM" = "ubuntu-20.04-64" ]; then + snapcraft pull + check_vars pull.txt + + snapcraft build + check_vars build.txt + + snapcraft stage + check_vars stage.txt + + snapcraft prime + check_vars prime.txt + fi diff --git a/tests/spread/general/core22/environment/test-variables/snap/snapcraft.yaml b/tests/spread/general/core22/environment/test-variables/snap/snapcraft.yaml new file mode 100644 index 0000000000..6bca17edfa --- /dev/null +++ b/tests/spread/general/core22/environment/test-variables/snap/snapcraft.yaml @@ -0,0 +1,19 @@ +name: variables +version: "1" +summary: test +description: test +grade: devel +confinement: strict +base: core22 + +parts: + hello: + plugin: nil + override-pull: | + env > $CRAFT_PROJECT_DIR/pull.txt + override-build: | + env > $CRAFT_PROJECT_DIR/build.txt + override-stage: | + env > $CRAFT_PROJECT_DIR/stage.txt + override-prime: | + env > $CRAFT_PROJECT_DIR/prime.txt diff --git a/tests/spread/general/core22/task.yaml b/tests/spread/general/core22/package-repositories/task.yaml similarity index 100% rename from tests/spread/general/core22/task.yaml rename to tests/spread/general/core22/package-repositories/task.yaml diff --git a/tests/spread/general/core22/test-apt-key-fingerprint/snap/keys/FC42E99D.asc b/tests/spread/general/core22/package-repositories/test-apt-key-fingerprint/snap/keys/FC42E99D.asc similarity index 100% rename from tests/spread/general/core22/test-apt-key-fingerprint/snap/keys/FC42E99D.asc rename to tests/spread/general/core22/package-repositories/test-apt-key-fingerprint/snap/keys/FC42E99D.asc diff --git a/tests/spread/general/core22/test-apt-key-fingerprint/snap/snapcraft.yaml b/tests/spread/general/core22/package-repositories/test-apt-key-fingerprint/snap/snapcraft.yaml similarity index 100% rename from tests/spread/general/core22/test-apt-key-fingerprint/snap/snapcraft.yaml rename to tests/spread/general/core22/package-repositories/test-apt-key-fingerprint/snap/snapcraft.yaml diff --git a/tests/spread/general/core22/test-apt-key-name/snap/keys/FC42E99D.asc b/tests/spread/general/core22/package-repositories/test-apt-key-name/snap/keys/FC42E99D.asc similarity index 100% rename from tests/spread/general/core22/test-apt-key-name/snap/keys/FC42E99D.asc rename to tests/spread/general/core22/package-repositories/test-apt-key-name/snap/keys/FC42E99D.asc diff --git a/tests/spread/general/core22/test-apt-key-name/snap/snapcraft.yaml b/tests/spread/general/core22/package-repositories/test-apt-key-name/snap/snapcraft.yaml similarity index 100% rename from tests/spread/general/core22/test-apt-key-name/snap/snapcraft.yaml rename to tests/spread/general/core22/package-repositories/test-apt-key-name/snap/snapcraft.yaml diff --git a/tests/spread/general/core22/test-apt-keyserver/snap/snapcraft.yaml b/tests/spread/general/core22/package-repositories/test-apt-keyserver/snap/snapcraft.yaml similarity index 100% rename from tests/spread/general/core22/test-apt-keyserver/snap/snapcraft.yaml rename to tests/spread/general/core22/package-repositories/test-apt-keyserver/snap/snapcraft.yaml diff --git a/tests/spread/general/core22/test-apt-path/snap/snapcraft.yaml b/tests/spread/general/core22/package-repositories/test-apt-path/snap/snapcraft.yaml similarity index 100% rename from tests/spread/general/core22/test-apt-path/snap/snapcraft.yaml rename to tests/spread/general/core22/package-repositories/test-apt-path/snap/snapcraft.yaml diff --git a/tests/spread/general/core22/test-apt-ppa/snap/snapcraft.yaml b/tests/spread/general/core22/package-repositories/test-apt-ppa/snap/snapcraft.yaml similarity index 100% rename from tests/spread/general/core22/test-apt-ppa/snap/snapcraft.yaml rename to tests/spread/general/core22/package-repositories/test-apt-ppa/snap/snapcraft.yaml diff --git a/tests/spread/general/core22/scriptlets/scriptlet-failures/snap/snapcraft.yaml b/tests/spread/general/core22/scriptlets/scriptlet-failures/snap/snapcraft.yaml new file mode 100644 index 0000000000..bec55a0e4d --- /dev/null +++ b/tests/spread/general/core22/scriptlets/scriptlet-failures/snap/snapcraft.yaml @@ -0,0 +1,17 @@ +name: craftctl-build-failure +base: core22 +version: '0.1' +summary: Fail on snapcraftctl build +description: | + Failing with purpose. + +grade: devel +confinement: strict + +parts: + my-part: + plugin: make + source: . + override-build: | + craftctl set version && echo "should have failed set version" + craftctl default && echo "should have failed build" diff --git a/tests/spread/general/core22/scriptlets/task.yaml b/tests/spread/general/core22/scriptlets/task.yaml new file mode 100644 index 0000000000..13c8b4fc33 --- /dev/null +++ b/tests/spread/general/core22/scriptlets/task.yaml @@ -0,0 +1,28 @@ +summary: Validate scriptlet failures + +environment: + SNAP_DIR/scriptlet_failures: scriptlet-failures + +prepare: | + #shellcheck source=tests/spread/tools/snapcraft-yaml.sh + . "$TOOLS_DIR/snapcraft-yaml.sh" + # set_base "$SNAP_DIR/snap/snapcraft.yaml" + snap install core22 --edge + +restore: | + cd "$SNAP_DIR" + snapcraft clean + rm -f ./*.snap + + #shellcheck source=tests/spread/tools/snapcraft-yaml.sh + . "$TOOLS_DIR/snapcraft-yaml.sh" + restore_yaml "snap/snapcraft.yaml" + +execute: | + if [ "$SPREAD_SYSTEM" = "ubuntu-20.04-64" ]; then + cd "$SNAP_DIR" + snapcraft_log="$(snapcraft build 2>&1 || true)" + + echo "${snapcraft_log}" | NOMATCH "^should have failed set-version" + echo "${snapcraft_log}" | NOMATCH "^should have failed build" + fi From 39d42857d4ae69f9043b35f0ecae9b3c663071d7 Mon Sep 17 00:00:00 2001 From: Sergio Schvezov Date: Thu, 7 Apr 2022 17:43:06 -0300 Subject: [PATCH 075/167] commands: list-extensions and extensions alias Bring in the extensions available from the new code base and then mesh them with the ones from legacy. Use inheritance to support the alias of extensions to list-extensions. Signed-off-by: Sergio Schvezov --- requirements-devel.txt | 1 + setup.cfg | 2 +- setup.py | 1 + snapcraft/cli.py | 8 + snapcraft/commands/__init__.py | 16 +- snapcraft/commands/extensions.py | 95 ++++++++++ tests/unit/commands/test_list_extensions.py | 75 ++++++++ tests/unit/conftest.py | 181 ++++++++++++++++++++ 8 files changed, 376 insertions(+), 3 deletions(-) create mode 100644 snapcraft/commands/extensions.py create mode 100644 tests/unit/commands/test_list_extensions.py diff --git a/requirements-devel.txt b/requirements-devel.txt index 2f776a8dd7..64810a98b3 100644 --- a/requirements-devel.txt +++ b/requirements-devel.txt @@ -102,6 +102,7 @@ translationstring==1.4 types-Deprecated==1.2.5 types-PyYAML==6.0.5 types-setuptools==57.4.11 +types-tabulate==0.8.6 typing-utils==0.1.0 typing_extensions==4.1.1 urllib3==1.26.9 diff --git a/setup.cfg b/setup.cfg index d0e82780c3..a80dbe21d5 100644 --- a/setup.cfg +++ b/setup.cfg @@ -39,4 +39,4 @@ ignore = E203,E501 # D203 1 blank line required before class docstring (reason: pep257 default) # D213 Multi-line docstring summary should start at the second line (reason: pep257 default) ignore = D107, D203, D213 - +ignore_decorators = overrides diff --git a/setup.py b/setup.py index ccb790aede..2ae1a13e48 100755 --- a/setup.py +++ b/setup.py @@ -84,6 +84,7 @@ def recursive_data_files(directory, install_directory): "pytest-subprocess", "types-PyYAML", "types-setuptools", + "types-tabulate", ] if sys.platform == "win32": diff --git a/snapcraft/cli.py b/snapcraft/cli.py index 85374562e4..841d864135 100644 --- a/snapcraft/cli.py +++ b/snapcraft/cli.py @@ -39,6 +39,14 @@ commands.PackCommand, ], ), + craft_cli.CommandGroup( + "Extensions", + [ + commands.ListExtensionsCommand, + # hidden command, alias to list-extensions. + commands.ExtensionsCommand, + ], + ), craft_cli.CommandGroup("Other", [commands.VersionCommand]), ] diff --git a/snapcraft/commands/__init__.py b/snapcraft/commands/__init__.py index 9a2da35c83..983490a647 100644 --- a/snapcraft/commands/__init__.py +++ b/snapcraft/commands/__init__.py @@ -16,11 +16,23 @@ """Snapcraft commands.""" -from .lifecycle import ( # noqa: F401 +from .extensions import ExtensionsCommand, ListExtensionsCommand +from .lifecycle import ( BuildCommand, PackCommand, PrimeCommand, PullCommand, StageCommand, ) -from .version import VersionCommand # noqa: F401 +from .version import VersionCommand + +__all__ = [ + "BuildCommand", + "PackCommand", + "PrimeCommand", + "PullCommand", + "StageCommand", + "ExtensionsCommand", + "ListExtensionsCommand", + "VersionCommand", +] diff --git a/snapcraft/commands/extensions.py b/snapcraft/commands/extensions.py new file mode 100644 index 0000000000..0ec75d25e6 --- /dev/null +++ b/snapcraft/commands/extensions.py @@ -0,0 +1,95 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright 2022 Canonical Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +"""Snapcraft lifecycle commands.""" + +import abc +import textwrap +from typing import Dict, List + +import tabulate +from craft_cli import BaseCommand, emit +from overrides import overrides +from pydantic import BaseModel + +from snapcraft import extensions +from snapcraft_legacy.internal.project_loader import ( + find_extension, + supported_extension_names, +) + + +class ExtensionModel(BaseModel): + """Extension model for presentation.""" + + name: str + bases: List[str] + + def marshal(self) -> Dict[str, str]: + """Marshal model into a dictionary for presentation.""" + return { + "Extension name": self.name, + "Supported bases": ", ".join(sorted(self.bases)), + } + + +class ListExtensionsCommand(BaseCommand, abc.ABC): + """A command to list the available extensions.""" + + name = "list-extensions" + help_msg = "List available extensions" + overview = textwrap.dedent( + """ + List the available extensions and the bases it can work on. + """ + ) + + @overrides + def run(self, parsed_args): + extension_presentation: Dict[str, ExtensionModel] = {} + + # New extensions. + for extension_name in extensions.registry.get_extension_names(): + extension_class = extensions.registry.get_extension_class(extension_name) + extension_bases = list(extension_class.get_supported_bases()) + extension_presentation[extension_name] = ExtensionModel( + name=extension_name, bases=extension_bases + ) + + # Extensions from snapcraft_legacy. + for extension_name in supported_extension_names(): + extension_class = find_extension(extension_name) + extension_name = extension_name.replace("_", "-") + extension_bases = list(extension_class.get_supported_bases()) + if extension_name in extension_presentation: + extension_presentation[extension_name].bases += extension_bases + else: + extension_presentation[extension_name] = ExtensionModel( + name=extension_name, bases=extension_bases + ) + + printable_extensions = sorted( + [v.marshal() for v in extension_presentation.values()], + key=lambda d: d["Extension name"], + ) + emit.message(tabulate.tabulate(printable_extensions, headers="keys")) + + +class ExtensionsCommand(ListExtensionsCommand, abc.ABC): + """A command alias to list the available extensions.""" + + name = "extensions" + hidden = True diff --git a/tests/unit/commands/test_list_extensions.py b/tests/unit/commands/test_list_extensions.py new file mode 100644 index 0000000000..2ea72a60b5 --- /dev/null +++ b/tests/unit/commands/test_list_extensions.py @@ -0,0 +1,75 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright 2022 Canonical Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from argparse import Namespace +from textwrap import dedent + +import pytest + +from snapcraft.commands import ExtensionsCommand, ListExtensionsCommand + + +@pytest.mark.usefixtures("fake_extension") +@pytest.mark.parametrize("command", [ListExtensionsCommand, ExtensionsCommand]) +def test_command(emitter, command): + cmd = command(None) + cmd.run(Namespace()) + emitter.assert_recorded( + [ + dedent( + """\ + Extension name Supported bases + ---------------- ----------------- + fake-extension core22 + flutter-beta core18 + flutter-dev core18 + flutter-master core18 + flutter-stable core18 + gnome-3-28 core18 + gnome-3-34 core18 + gnome-3-38 core20 + kde-neon core18, core20 + ros1-noetic core20 + ros2-foxy core20""" + ) + ] + ) + + +@pytest.mark.usefixtures("fake_extension_name_from_legacy") +@pytest.mark.parametrize("command", [ListExtensionsCommand, ExtensionsCommand]) +def test_command_extension_dups(emitter, command): + cmd = command(None) + cmd.run(Namespace()) + emitter.assert_recorded( + [ + dedent( + """\ + Extension name Supported bases + ---------------- ----------------- + flutter-beta core18 + flutter-dev core18 + flutter-master core18 + flutter-stable core18 + gnome-3-28 core18 + gnome-3-34 core18 + gnome-3-38 core20 + kde-neon core18, core20 + ros1-noetic core20 + ros2-foxy core20, core22""" + ) + ] + ) diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py index d320059d6c..92f2375a0d 100644 --- a/tests/unit/conftest.py +++ b/tests/unit/conftest.py @@ -17,10 +17,13 @@ import os import tempfile from pathlib import Path +from typing import Any, Dict, Optional, Tuple import pytest from craft_cli import messages +from snapcraft import extensions + # XXX: This can be removed once testing fixtures are provided by craft-cli. class RecordingEmitter: @@ -98,3 +101,181 @@ def emitter(monkeypatch): monkeypatch.setattr(messages.emit, "trace", lambda text: rec.record("trace", text)) return rec + + +@pytest.fixture +def fake_extension(): + """Basic extension.""" + + class ExtensionImpl(extensions.Extension): + """The test extension implementation.""" + + @staticmethod + def get_supported_bases() -> Tuple[str, ...]: + return ("core22",) + + @staticmethod + def get_supported_confinement() -> Tuple[str, ...]: + return ("strict",) + + @staticmethod + def is_experimental(base: Optional[str] = None) -> bool: + return False + + def get_root_snippet(self) -> Dict[str, Any]: + return {"grade": "fake-grade"} + + def get_app_snippet(self) -> Dict[str, Any]: + return {"plugs": ["fake-plug"]} + + def get_part_snippet(self) -> Dict[str, Any]: + return {"after": ["fake-extension/fake-part"]} + + def get_parts_snippet(self) -> Dict[str, Any]: + return {"fake-extension/fake-part": {"plugin": "nil"}} + + extensions.register("fake-extension", ExtensionImpl) + yield ExtensionImpl + extensions.unregister("fake-extension") + + +@pytest.fixture +def fake_extension_extra(): + """A variation of fake_extension with some conflicts and new code.""" + + class ExtensionImpl(extensions.Extension): + """The test extension implementation.""" + + @staticmethod + def get_supported_bases() -> Tuple[str, ...]: + return ("core22",) + + @staticmethod + def get_supported_confinement() -> Tuple[str, ...]: + return ("strict",) + + @staticmethod + def is_experimental(base: Optional[str] = None) -> bool: + return False + + def get_root_snippet(self) -> Dict[str, Any]: + return {} + + def get_app_snippet(self) -> Dict[str, Any]: + return {"plugs": ["fake-plug", "fake-plug-extra"]} + + def get_part_snippet(self) -> Dict[str, Any]: + return {"after": ["fake-extension-extra/fake-part"]} + + def get_parts_snippet(self) -> Dict[str, Any]: + return {"fake-extension-extra/fake-part": {"plugin": "nil"}} + + extensions.register("fake-extension-extra", ExtensionImpl) + yield ExtensionImpl + extensions.unregister("fake-extension-extra") + + +@pytest.fixture +def fake_extension_invalid_parts(): + class ExtensionImpl(extensions.Extension): + """The test extension implementation.""" + + @staticmethod + def get_supported_bases() -> Tuple[str, ...]: + return ("core22",) + + @staticmethod + def get_supported_confinement() -> Tuple[str, ...]: + return ("strict",) + + @staticmethod + def is_experimental(base: Optional[str] = None) -> bool: + return False + + def get_root_snippet(self) -> Dict[str, Any]: + return {"grade": "fake-grade"} + + def get_app_snippet(self) -> Dict[str, Any]: + return {"plugs": ["fake-plug"]} + + def get_part_snippet(self) -> Dict[str, Any]: + return {"after": ["fake-extension/fake-part"]} + + def get_parts_snippet(self) -> Dict[str, Any]: + return {"fake-part": {"plugin": "nil"}, "fake-part-2": {"plugin": "nil"}} + + extensions.register("fake-extension-invalid-parts", ExtensionImpl) + yield ExtensionImpl + extensions.unregister("fake-extension-invalid-parts") + + +@pytest.fixture +def fake_extension_experimental(): + """Basic extension.""" + + class ExtensionImpl(extensions.Extension): + """The test extension implementation.""" + + @staticmethod + def get_supported_bases() -> Tuple[str, ...]: + return ("core22",) + + @staticmethod + def get_supported_confinement() -> Tuple[str, ...]: + return ("strict",) + + @staticmethod + def is_experimental(base: Optional[str] = None) -> bool: + return True + + def get_root_snippet(self) -> Dict[str, Any]: + return {} + + def get_app_snippet(self) -> Dict[str, Any]: + return {} + + def get_part_snippet(self) -> Dict[str, Any]: + return {} + + def get_parts_snippet(self) -> Dict[str, Any]: + return {} + + extensions.register("fake-extension-experimental", ExtensionImpl) + yield ExtensionImpl + extensions.unregister("fake-extension-experimental") + + +@pytest.fixture +def fake_extension_name_from_legacy(): + """A fake_extension variant with a name collision with legacy.""" + + class ExtensionImpl(extensions.Extension): + """The test extension implementation.""" + + @staticmethod + def get_supported_bases() -> Tuple[str, ...]: + return ("core22",) + + @staticmethod + def get_supported_confinement() -> Tuple[str, ...]: + return ("strict",) + + @staticmethod + def is_experimental(base: Optional[str] = None) -> bool: + return False + + def get_root_snippet(self) -> Dict[str, Any]: + return {} + + def get_app_snippet(self) -> Dict[str, Any]: + return {"plugs": ["fake-plug", "fake-plug-extra"]} + + def get_part_snippet(self) -> Dict[str, Any]: + return {"after": ["fake-extension-extra/fake-part"]} + + def get_parts_snippet(self) -> Dict[str, Any]: + return {"fake-extension-extra/fake-part": {"plugin": "nil"}} + + extensions.register("ros2-foxy", ExtensionImpl) + yield ExtensionImpl + extensions.unregister("ros2-foxy") From f01c06024664c295dd07af37dfbb6b0f079e058c Mon Sep 17 00:00:00 2001 From: Sergio Schvezov Date: Thu, 7 Apr 2022 19:48:28 -0300 Subject: [PATCH 076/167] parts: refactor lifecycle processing for reuse Extract yaml processing into its own method so it can be reused when expanding extensions. Signed-off-by: Sergio Schvezov --- snapcraft/parts/lifecycle.py | 105 +++++++++++++++++++++-------------- 1 file changed, 64 insertions(+), 41 deletions(-) diff --git a/snapcraft/parts/lifecycle.py b/snapcraft/parts/lifecycle.py index 80ed07a6ea..2c202e9b6c 100644 --- a/snapcraft/parts/lifecycle.py +++ b/snapcraft/parts/lifecycle.py @@ -17,8 +17,9 @@ """Parts lifecycle preparation and execution.""" import subprocess +from dataclasses import dataclass from pathlib import Path -from typing import TYPE_CHECKING, cast +from typing import TYPE_CHECKING, Any, Dict, cast from craft_cli import EmitterMode, emit from craft_parts import infos @@ -35,54 +36,54 @@ import argparse -_PROJECT_FILES = [ - Path("snapcraft.yaml"), - Path("snap/snapcraft.yaml"), - Path("build-aux/snap/snapcraft.yaml"), - Path(".snapcraft.yaml"), +@dataclass +class _SnapProject: + project_file: Path + assets_dir: Path = Path("snap") + + +_SNAP_PROJECT_FILES = [ + _SnapProject(project_file=Path("snapcraft.yaml")), + _SnapProject(project_file=Path("snap/snapcraft.yaml")), + _SnapProject( + project_file=Path("build-aux/snap/snapcraft.yaml"), + assets_dir=Path("build-aux/snap"), + ), + _SnapProject(project_file=Path(".snapcraft.yaml")), ] -def run(command_name: str, parsed_args: "argparse.Namespace") -> None: - """Run the parts lifecycle. +def _get_snap_project() -> _SnapProject: + for snap_project in _SNAP_PROJECT_FILES: + if snap_project.project_file.exists(): + return snap_project - :raises SnapcraftError: if the step name is invalid, or the project - yaml file cannot be loaded. - :raises LegacyFallback: if the project's base is not core22. + raise errors.SnapcraftError( + "Could not find snap/snapcraft.yaml. Are you sure you are in the " + "right directory?", + resolution="To start a new project, use `snapcraft init`", + ) + + +def process_yaml(project_file: Path) -> Dict[str, Any]: + """Process the yaml from project file. + + :raises SnapcraftError: if the project yaml file cannot be loaded. """ - emit.trace(f"command: {command_name}, arguments: {parsed_args}") yaml_data = {} - assets_dir = Path("snap") - - for project_file in _PROJECT_FILES: - if project_file.is_file(): - - if project_file.parent.name == "snap": - assets_dir = project_file.parent - - try: - with open(project_file, encoding="utf-8") as yaml_file: - yaml_data = yaml_utils.load(yaml_file) - break - except OSError as err: - msg = err.strerror - if err.filename: - msg = f"{msg}: {err.filename!r}." - raise errors.SnapcraftError(msg) from err - else: - raise errors.SnapcraftError( - "Could not find snap/snapcraft.yaml. Are you sure you are in the " - "right directory?", - resolution="To start a new project, use `snapcraft init`", - ) + + try: + with open(project_file, encoding="utf-8") as yaml_file: + yaml_data = yaml_utils.load(yaml_file) + except OSError as err: + msg = err.strerror + if err.filename: + msg = f"{msg}: {err.filename!r}." + raise errors.SnapcraftError(msg) from err # validate project grammar GrammarAwareProject.validate_grammar(yaml_data) - # argument --provider is only supported by legacy snapcraft - if parsed_args.provider: - raise errors.SnapcraftError("Option --provider is not supported.") - # TODO: support for target_arch arch = _get_arch() yaml_data = extensions.apply_extensions(yaml_data, arch=arch, target_arch=arch) @@ -92,10 +93,32 @@ def run(command_name: str, parsed_args: "argparse.Namespace") -> None: parts_yaml_data=yaml_data["parts"], arch=arch, target_arch=arch ) + return yaml_data + + +def run(command_name: str, parsed_args: "argparse.Namespace") -> None: + """Run the parts lifecycle. + + :raises SnapcraftError: if the step name is invalid, or the project + yaml file cannot be loaded. + :raises LegacyFallback: if the project's base is not core22. + """ + emit.trace(f"command: {command_name}, arguments: {parsed_args}") + + snap_project = _get_snap_project() + yaml_data = process_yaml(snap_project.project_file) + + # argument --provider is only supported by legacy snapcraft + if parsed_args.provider: + raise errors.SnapcraftError("Option --provider is not supported.") + project = Project.unmarshal(yaml_data) _run_command( - command_name, project=project, assets_dir=assets_dir, parsed_args=parsed_args + command_name, + project=project, + assets_dir=snap_project.assets_dir, + parsed_args=parsed_args, ) @@ -130,7 +153,7 @@ def _run_command( project_vars={ "version": project.version or "", "grade": project.grade, - } + }, ) lifecycle.run(step_name) From ca1a94c749cbbd55086ab5e481e3a12f31955f4b Mon Sep 17 00:00:00 2001 From: Sergio Schvezov Date: Thu, 7 Apr 2022 20:18:39 -0300 Subject: [PATCH 077/167] command: expand-extensions Signed-off-by: Sergio Schvezov --- snapcraft/cli.py | 1 + snapcraft/commands/__init__.py | 7 +- snapcraft/commands/extensions.py | 22 ++++++ snapcraft/parts/lifecycle.py | 8 +- tests/unit/commands/test_expand_extensions.py | 78 +++++++++++++++++++ 5 files changed, 113 insertions(+), 3 deletions(-) create mode 100644 tests/unit/commands/test_expand_extensions.py diff --git a/snapcraft/cli.py b/snapcraft/cli.py index 841d864135..bf5f18d8be 100644 --- a/snapcraft/cli.py +++ b/snapcraft/cli.py @@ -45,6 +45,7 @@ commands.ListExtensionsCommand, # hidden command, alias to list-extensions. commands.ExtensionsCommand, + commands.ExpandExtensionsCommand, ], ), craft_cli.CommandGroup("Other", [commands.VersionCommand]), diff --git a/snapcraft/commands/__init__.py b/snapcraft/commands/__init__.py index 983490a647..b4599f8392 100644 --- a/snapcraft/commands/__init__.py +++ b/snapcraft/commands/__init__.py @@ -16,7 +16,11 @@ """Snapcraft commands.""" -from .extensions import ExtensionsCommand, ListExtensionsCommand +from .extensions import ( + ExpandExtensionsCommand, + ExtensionsCommand, + ListExtensionsCommand, +) from .lifecycle import ( BuildCommand, PackCommand, @@ -28,6 +32,7 @@ __all__ = [ "BuildCommand", + "ExpandExtensionsCommand", "PackCommand", "PrimeCommand", "PullCommand", diff --git a/snapcraft/commands/extensions.py b/snapcraft/commands/extensions.py index 0ec75d25e6..6c00e2cccb 100644 --- a/snapcraft/commands/extensions.py +++ b/snapcraft/commands/extensions.py @@ -21,11 +21,13 @@ from typing import Dict, List import tabulate +import yaml from craft_cli import BaseCommand, emit from overrides import overrides from pydantic import BaseModel from snapcraft import extensions +from snapcraft.parts.lifecycle import get_snap_project, process_yaml from snapcraft_legacy.internal.project_loader import ( find_extension, supported_extension_names, @@ -93,3 +95,23 @@ class ExtensionsCommand(ListExtensionsCommand, abc.ABC): name = "extensions" hidden = True + + +class ExpandExtensionsCommand(BaseCommand, abc.ABC): + """A command to expand the yaml from extensions.""" + + name = "expand-extensions" + help_msg = "Expand extensions in snapcraft.yaml" + overview = textwrap.dedent( + """ + Extensions defined under apps in snapcraft.yaml will be + expanded and shown as output. + """ + ) + + @overrides + def run(self, parsed_args): + snap_project = get_snap_project() + yaml_data = process_yaml(snap_project.project_file) + + emit.message(yaml.safe_dump(yaml_data, indent=4, sort_keys=False)) diff --git a/snapcraft/parts/lifecycle.py b/snapcraft/parts/lifecycle.py index 2c202e9b6c..46d202c49f 100644 --- a/snapcraft/parts/lifecycle.py +++ b/snapcraft/parts/lifecycle.py @@ -53,7 +53,11 @@ class _SnapProject: ] -def _get_snap_project() -> _SnapProject: +def get_snap_project() -> _SnapProject: + """Find the snapcraft.yaml to load. + + :raises SnapcraftError: if the project yaml file cannot be found. + """ for snap_project in _SNAP_PROJECT_FILES: if snap_project.project_file.exists(): return snap_project @@ -105,7 +109,7 @@ def run(command_name: str, parsed_args: "argparse.Namespace") -> None: """ emit.trace(f"command: {command_name}, arguments: {parsed_args}") - snap_project = _get_snap_project() + snap_project = get_snap_project() yaml_data = process_yaml(snap_project.project_file) # argument --provider is only supported by legacy snapcraft diff --git a/tests/unit/commands/test_expand_extensions.py b/tests/unit/commands/test_expand_extensions.py new file mode 100644 index 0000000000..3b7efc6ad0 --- /dev/null +++ b/tests/unit/commands/test_expand_extensions.py @@ -0,0 +1,78 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright 2022 Canonical Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from argparse import Namespace +from pathlib import Path +from textwrap import dedent + +import pytest + +from snapcraft.commands import ExpandExtensionsCommand + + +@pytest.mark.usefixtures("fake_extension") +def test_command(new_dir, emitter): + with Path("snapcraft.yaml").open("w") as yaml_file: + print( + dedent( + """\ + name: test-name + version: "0.1" + summary: testing extensions + description: expand a fake extension + base: core22 + + apps: + app1: + command: app1 + extensions: [fake-extension] + + parts: + part1: + plugin: nil + """ + ), + file=yaml_file, + ) + + cmd = ExpandExtensionsCommand(None) + cmd.run(Namespace()) + emitter.assert_recorded( + [ + dedent( + """\ + name: test-name + version: '0.1' + summary: testing extensions + description: expand a fake extension + base: core22 + apps: + app1: + command: app1 + plugs: + - fake-plug + parts: + part1: + plugin: nil + after: + - fake-extension/fake-part + fake-extension/fake-part: + plugin: nil + grade: fake-grade + """ + ) + ] + ) From abcafbfab4a51826defe6a63a5135571bfca9134 Mon Sep 17 00:00:00 2001 From: Claudio Matsuoka Date: Thu, 7 Apr 2022 19:22:42 -0300 Subject: [PATCH 078/167] commands, parts: add clean command Add command to clean parts. The build instance will be removed if no parts are specified and not running in destructive or managed modes. Signed-off-by: Claudio Matsuoka --- snapcraft/cli.py | 1 + snapcraft/commands/__init__.py | 2 + snapcraft/commands/lifecycle.py | 13 +++ snapcraft/parts/lifecycle.py | 32 +++++- snapcraft/parts/parts.py | 14 +++ snapcraft/providers/_lxd.py | 7 +- tests/unit/commands/test_lifecycle.py | 2 + tests/unit/parts/test_lifecycle.py | 138 +++++++++++++++++++++++++- tests/unit/parts/test_parts.py | 28 ++++++ 9 files changed, 228 insertions(+), 9 deletions(-) diff --git a/snapcraft/cli.py b/snapcraft/cli.py index bf5f18d8be..b557a3b799 100644 --- a/snapcraft/cli.py +++ b/snapcraft/cli.py @@ -32,6 +32,7 @@ craft_cli.CommandGroup( "Lifecycle", [ + commands.CleanCommand, commands.PullCommand, commands.BuildCommand, commands.StageCommand, diff --git a/snapcraft/commands/__init__.py b/snapcraft/commands/__init__.py index b4599f8392..bb0dbf1553 100644 --- a/snapcraft/commands/__init__.py +++ b/snapcraft/commands/__init__.py @@ -23,6 +23,7 @@ ) from .lifecycle import ( BuildCommand, + CleanCommand, PackCommand, PrimeCommand, PullCommand, @@ -32,6 +33,7 @@ __all__ = [ "BuildCommand", + "CleanCommand", "ExpandExtensionsCommand", "PackCommand", "PrimeCommand", diff --git a/snapcraft/commands/lifecycle.py b/snapcraft/commands/lifecycle.py index 7ea4da5af7..4075a60dd9 100644 --- a/snapcraft/commands/lifecycle.py +++ b/snapcraft/commands/lifecycle.py @@ -166,3 +166,16 @@ def run(self, parsed_args): pack.pack_snap(parsed_args.directory, output=parsed_args.output) else: super().run(parsed_args) + + +class CleanCommand(_LifecycleStepCommand): + """Remove a part's assets.""" + + name = "clean" + help_msg = "Remove a part's assets" + overview = textwrap.dedent( + """ + Clean up artifacts belonging to parts. If no parts are specified, + remove the managed snap packing environment (VM or container). + """ + ) diff --git a/snapcraft/parts/lifecycle.py b/snapcraft/parts/lifecycle.py index 46d202c49f..3c5d216464 100644 --- a/snapcraft/parts/lifecycle.py +++ b/snapcraft/parts/lifecycle.py @@ -134,9 +134,13 @@ def _run_command( parsed_args: "argparse.Namespace", ) -> None: managed_mode = utils.is_managed_mode() + part_names = getattr(parsed_args, "parts", None) if not managed_mode and not parsed_args.destructive_mode: - _run_in_provider(project, command_name, parsed_args) + if command_name == "clean" and not part_names: + _clean_provider(project, parsed_args) + else: + _run_in_provider(project, command_name, parsed_args) return if managed_mode: @@ -145,7 +149,6 @@ def _run_command( work_dir = Path.cwd() step_name = "prime" if command_name == "pack" else command_name - part_names = getattr(parsed_args, "parts", []) lifecycle = PartsLifecycle( project.parts, @@ -159,6 +162,10 @@ def _run_command( "grade": project.grade, }, ) + if command_name == "clean": + lifecycle.clean(part_names=part_names) + return + lifecycle.run(step_name) # Generate snap.yaml @@ -193,9 +200,26 @@ def _run_command( ) +def _clean_provider(project: Project, parsed_args: "argparse.Namespace") -> None: + """Clean the provider environment. + + :param project: The project to clean. + """ + emit.trace("Clean build provider") + provider_name = "lxd" if parsed_args.use_lxd else None + provider = providers.get_provider(provider_name) + instance_names = provider.clean_project_environments( + project_name=project.name, project_path=Path().absolute() + ) + if instance_names: + emit.message(f"Removed instance: {', '.join(instance_names)}") + else: + emit.message("No instances to remove") + + def _run_in_provider( project: Project, command_name: str, parsed_args: "argparse.Namespace" -): +) -> None: """Pack image in provider instance.""" emit.trace("Checking build provider availability") provider_name = "lxd" if parsed_args.use_lxd else None @@ -229,7 +253,7 @@ def _run_in_provider( except subprocess.CalledProcessError as err: capture_logs_from_instance(instance) raise providers.ProviderError( - f"Failed to pack image '{project.name}:{project.version}'." + f"Failed to execute {command_name} in instance." ) from err diff --git a/snapcraft/parts/parts.py b/snapcraft/parts/parts.py index 3349d7eb47..11df8cfbb4 100644 --- a/snapcraft/parts/parts.py +++ b/snapcraft/parts/parts.py @@ -150,6 +150,20 @@ def run(self, step_name: str) -> None: except Exception as err: raise errors.PartsLifecycleError(str(err)) from err + def clean(self, *, part_names: Optional[List[str]] = None) -> None: + """Remove lifecycle artifacts. + + :param part_names: The names of the parts to clean. If not + specified, all parts will be cleaned. + """ + if part_names: + message = "Cleaning parts: " + ", ".join(part_names) + else: + message = "Cleaning all parts" + + emit.message(message, intermediate=True) + self._lcm.clean(part_names=part_names) + def _action_message(action: craft_parts.Action) -> str: msg = { diff --git a/snapcraft/providers/_lxd.py b/snapcraft/providers/_lxd.py index 3add220c21..b571a25d4c 100644 --- a/snapcraft/providers/_lxd.py +++ b/snapcraft/providers/_lxd.py @@ -69,7 +69,10 @@ def clean_project_environments( if not self.is_provider_available(): return deleted - inode = str(project_path.stat().st_ino) + instance_name = self.get_instance_name( + project_name=project_name, + project_path=project_path, + ) try: names = self.lxc.list_names( @@ -79,7 +82,7 @@ def clean_project_environments( raise ProviderError(str(error)) from error for name in names: - if name == f"snapcraft-{project_name}-{inode}": + if name == instance_name: logger.debug("Deleting container %r.", name) try: self.lxc.delete( diff --git a/tests/unit/commands/test_lifecycle.py b/tests/unit/commands/test_lifecycle.py index d7d5864de9..6d4902bade 100644 --- a/tests/unit/commands/test_lifecycle.py +++ b/tests/unit/commands/test_lifecycle.py @@ -21,6 +21,7 @@ from snapcraft.commands.lifecycle import ( BuildCommand, + CleanCommand, PackCommand, PrimeCommand, PullCommand, @@ -35,6 +36,7 @@ ("build", BuildCommand), ("stage", StageCommand), ("prime", PrimeCommand), + ("clean", CleanCommand), ], ) def test_lifecycle_command(cmd_name, cmd_class, mocker): diff --git a/tests/unit/parts/test_lifecycle.py b/tests/unit/parts/test_lifecycle.py index e0781e48e8..7a6a366181 100644 --- a/tests/unit/parts/test_lifecycle.py +++ b/tests/unit/parts/test_lifecycle.py @@ -145,7 +145,7 @@ def test_snapcraft_yaml_load(new_dir, snapcraft_yaml, filename, mocker): ] -@pytest.mark.parametrize("cmd", ["pull", "build", "stage", "prime", "pack"]) +@pytest.mark.parametrize("cmd", ["pull", "build", "stage", "prime", "pack", "clean"]) def test_lifecycle_run_provider(cmd, snapcraft_yaml, new_dir, mocker): """Option --provider is not supported in core22.""" snapcraft_yaml(base="core22") @@ -165,7 +165,7 @@ def test_lifecycle_run_provider(cmd, snapcraft_yaml, new_dir, mocker): assert str(raised.value) == "Option --provider is not supported." -@pytest.mark.parametrize("cmd", ["pull", "build", "stage", "prime"]) +@pytest.mark.parametrize("cmd", ["pull", "build", "stage", "prime", "clean"]) def test_lifecycle_legacy_run_provider(cmd, snapcraft_yaml, new_dir, mocker): """Option --provider is supported by legacy.""" snapcraft_yaml(base="core20") @@ -194,7 +194,9 @@ def test_lifecycle_legacy_run_provider(cmd, snapcraft_yaml, new_dir, mocker): ("prime", "prime"), ], ) -def test_lifecycle_run_command_step(cmd, step, snapcraft_yaml, project_vars, new_dir, mocker): +def test_lifecycle_run_command_step( + cmd, step, snapcraft_yaml, project_vars, new_dir, mocker +): project = Project.unmarshal(snapcraft_yaml(base="core22")) run_mock = mocker.patch("snapcraft.parts.PartsLifecycle.run") mocker.patch("snapcraft.meta.snap_yaml.write") @@ -333,3 +335,133 @@ def test_lifecycle_pack_not_managed(snapcraft_yaml, new_dir, mocker): ), ) ] + + +def test_lifecycle_run_command_clean(snapcraft_yaml, project_vars, new_dir, mocker): + """Clean provider project when called without parts.""" + project = Project.unmarshal(snapcraft_yaml(base="core22")) + clean_mock = mocker.patch( + "snapcraft.providers.LXDProvider.clean_project_environments", + return_value=["instance-name"], + ) + + parts_lifecycle._run_command( + "clean", + project=project, + assets_dir=Path(), + parsed_args=argparse.Namespace( + directory=None, + output=None, + destructive_mode=False, + use_lxd=False, + parts=None, + ), + ) + + assert clean_mock.mock_calls == [call(project_name="mytest", project_path=new_dir)] + + +def test_lifecycle_clean_destructive_mode( + snapcraft_yaml, project_vars, new_dir, mocker +): + """Clean local project if called in destructive mode.""" + project = Project.unmarshal(snapcraft_yaml(base="core22")) + clean_mock = mocker.patch("snapcraft.parts.PartsLifecycle.clean") + + parts_lifecycle._run_command( + "clean", + project=project, + assets_dir=Path(), + parsed_args=argparse.Namespace( + directory=None, + output=None, + destructive_mode=True, + use_lxd=False, + parts=None, + ), + ) + + assert clean_mock.mock_calls == [call(part_names=None)] + + +def test_lifecycle_clean_part_names(snapcraft_yaml, project_vars, new_dir, mocker): + """Clean project inside provider if called with part names.""" + project = Project.unmarshal(snapcraft_yaml(base="core22")) + run_in_provider_mock = mocker.patch("snapcraft.parts.lifecycle._run_in_provider") + + parts_lifecycle._run_command( + "clean", + project=project, + assets_dir=Path(), + parsed_args=argparse.Namespace( + directory=None, + output=None, + destructive_mode=False, + use_lxd=False, + parts=["part1"], + ), + ) + + assert run_in_provider_mock.mock_calls == [ + call( + project, + "clean", + argparse.Namespace( + directory=None, + output=None, + destructive_mode=False, + use_lxd=False, + parts=["part1"], + ), + ) + ] + + +def test_lifecycle_clean_part_names_destructive_mode( + snapcraft_yaml, project_vars, new_dir, mocker +): + """Clean local project if called in destructive mode.""" + project = Project.unmarshal(snapcraft_yaml(base="core22")) + clean_mock = mocker.patch("snapcraft.parts.PartsLifecycle.clean") + + parts_lifecycle._run_command( + "clean", + project=project, + assets_dir=Path(), + parsed_args=argparse.Namespace( + directory=None, + output=None, + destructive_mode=True, + use_lxd=False, + parts=["part1"], + ), + ) + + assert clean_mock.mock_calls == [call(part_names=["part1"])] + + +def test_lifecycle_clean_managed(snapcraft_yaml, project_vars, new_dir, mocker): + project = Project.unmarshal(snapcraft_yaml(base="core22")) + run_in_provider_mock = mocker.patch("snapcraft.parts.lifecycle._run_in_provider") + clean_mock = mocker.patch("snapcraft.parts.PartsLifecycle.clean") + mocker.patch("snapcraft.utils.is_managed_mode", return_value=True) + mocker.patch( + "snapcraft.utils.get_managed_environment_home_path", + return_value=new_dir / "home", + ) + + parts_lifecycle._run_command( + "clean", + project=project, + assets_dir=Path(), + parsed_args=argparse.Namespace( + directory=None, + output=None, + destructive_mode=False, + use_lxd=False, + parts=["part1"], + ), + ) + + assert run_in_provider_mock.mock_calls == [] + assert clean_mock.mock_calls == [call(part_names=["part1"])] diff --git a/tests/unit/parts/test_parts.py b/tests/unit/parts/test_parts.py index c3895620c6..e82dabeb4f 100644 --- a/tests/unit/parts/test_parts.py +++ b/tests/unit/parts/test_parts.py @@ -92,3 +92,31 @@ def test_parts_lifecycle_run_parts_error(new_dir): assert str(raised.value) == ( "Failed to pull source: unable to determine source type of 'foo'." ) + + +def test_parts_lifecycle_clean(parts_data, new_dir, emitter): + lifecycle = PartsLifecycle( + parts_data, + work_dir=new_dir, + assets_dir=new_dir, + part_names=[], + package_repositories=[], + adopt_info=None, + project_vars={"version": "1", "grade": "stable"}, + ) + lifecycle.clean(part_names=None) + emitter.assert_recorded(["Cleaning all parts"]) + + +def test_parts_lifecycle_clean_parts(parts_data, new_dir, emitter): + lifecycle = PartsLifecycle( + parts_data, + work_dir=new_dir, + assets_dir=new_dir, + part_names=[], + package_repositories=[], + adopt_info=None, + project_vars={"version": "1", "grade": "stable"}, + ) + lifecycle.clean(part_names=["p1"]) + emitter.assert_recorded(["Cleaning parts: p1"]) From e7a1f7d1ef4e26d6493411b4a8384152f037d40c Mon Sep 17 00:00:00 2001 From: Claudio Matsuoka Date: Thu, 7 Apr 2022 22:41:49 -0300 Subject: [PATCH 079/167] tests: add clean command spread test Signed-off-by: Claudio Matsuoka --- .../general/core22/clean/snap/snapcraft.yaml | 14 +++++ tests/spread/general/core22/clean/task.yaml | 60 +++++++++++++++++++ 2 files changed, 74 insertions(+) create mode 100644 tests/spread/general/core22/clean/snap/snapcraft.yaml create mode 100644 tests/spread/general/core22/clean/task.yaml diff --git a/tests/spread/general/core22/clean/snap/snapcraft.yaml b/tests/spread/general/core22/clean/snap/snapcraft.yaml new file mode 100644 index 0000000000..028287c079 --- /dev/null +++ b/tests/spread/general/core22/clean/snap/snapcraft.yaml @@ -0,0 +1,14 @@ +name: clean +version: '1.0' +summary: test +description: test +grade: stable +confinement: strict +base: core22 + +parts: + part1: + plugin: nil + + part2: + plugin: nil diff --git a/tests/spread/general/core22/clean/task.yaml b/tests/spread/general/core22/clean/task.yaml new file mode 100644 index 0000000000..707ad9b39c --- /dev/null +++ b/tests/spread/general/core22/clean/task.yaml @@ -0,0 +1,60 @@ +summary: Test craftctl commands on core22 + +environment: + +prepare: | + #shellcheck source=tests/spread/tools/snapcraft-yaml.sh + . "$TOOLS_DIR/snapcraft-yaml.sh" + # set_base "$SNAP/snap/snapcraft.yaml" + snap install core22 --edge + +restore: | + rm -f ./*.snap + rm -Rf work + + #shellcheck source=tests/spread/tools/snapcraft-yaml.sh + . "$TOOLS_DIR/snapcraft-yaml.sh" + restore_yaml "snap/snapcraft.yaml" + +execute: | + snapcraft pack + snapcraft clean part1 + lxc --project=snapcraft list | grep snapcraft-clean + + snapcraft pack 2>&1 | tee output.txt + + grep "Executing parts lifecycle: pull part1" < output.txt + grep "Executing parts lifecycle: skip pull part2 (already ran)" < output.txt + grep "Executing parts lifecycle: overlay part1" < output.txt + grep "Executing parts lifecycle: skip overlay part2 (already ran)" < output.txt + grep "Executing parts lifecycle: build part1" < output.txt + grep "Executing parts lifecycle: skip build part2 (already ran)" < output.txt + grep "Executing parts lifecycle: stage part1" < output.txt + grep "Executing parts lifecycle: skip stage part2 (already ran)" < output.txt + grep "Executing parts lifecycle: prime part1" < output.txt + grep "Executing parts lifecycle: skip prime part2 (already ran)" < output.txt + + snapcraft clean + if lxc --project=snapcraft list | grep snapcraft-clean; then + echo "instance not removed" + exit 1 + fi + + # also try it in destructive mode + test ! -d parts && test ! -d stage && test ! -d prime + + snapcraft pack --destructive-mode + + test -d parts && test -d stage && test -d prime + test ! -z "$(ls -A parts/part1/state)" + test ! -z "$(ls -A parts/part1/state)" + + snapcraft clean --destructive-mode part1 + + test -d parts && test -d stage && test -d prime + test -z "$(ls -A parts/part1/state)" + test ! -z "$(ls -A parts/part2/state)" + + snapcraft clean --destructive-mode + + test ! -d parts && test ! -d stage && test ! -d prime From 8fa477fabac4f13d8d4198e82ac14c951a7c0a11 Mon Sep 17 00:00:00 2001 From: Claudio Matsuoka Date: Fri, 8 Apr 2022 09:30:07 -0300 Subject: [PATCH 080/167] requirements: resolve conflict and update dependencies Signed-off-by: Claudio Matsuoka --- requirements-devel.txt | 17 ++++++++--------- requirements.txt | 6 +++--- setup.py | 3 ++- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/requirements-devel.txt b/requirements-devel.txt index 4bffda7d91..4ade23aa06 100644 --- a/requirements-devel.txt +++ b/requirements-devel.txt @@ -12,16 +12,15 @@ coverage==6.3.2 craft-cli==0.4.0 craft-grammar==1.1.1 craft-parts==1.4.2 -craft-providers==1.1.1 +craft-providers==1.2.0 cryptography==3.4 Deprecated==1.2.13 dill==0.3.4 distro==1.7.0 docutils==0.18.1 -entrypoints==0.3 extras==1.0.0 fixtures==3.0.0 -flake8==3.7.9 +flake8==4.0.1 gnupg==2.3.1 httplib2==0.20.4 hupper==1.10.3 @@ -29,7 +28,7 @@ idna==3.3 importlib-metadata==4.11.3 iniconfig==1.1.1 isort==5.10.1 -jeepney==0.7.1 +jeepney==0.8.0 jsonschema==2.5.1 keyring==23.5.0 launchpadlib==1.10.16 @@ -57,15 +56,15 @@ protobuf==3.20.0 psutil==5.9.0 ptyprocess==0.7.0 py==1.11.0 -pycodestyle==2.5.0 +pycodestyle==2.8.0 pycparser==2.21 pydantic==1.9.0 pydantic-yaml==0.6.3 pydocstyle==6.1.1 pyelftools==0.28 -pyflakes==2.1.1 +pyflakes==2.4.0 pyftpdlib==1.5.6 -pylint==2.13.4 +pylint==2.13.5 pylint-fixme-info==1.0.3 pylint-pytest==1.1.2 pylxd==2.3.1 @@ -101,7 +100,7 @@ tomli==2.0.1 translationstring==1.4 types-Deprecated==1.2.5 types-PyYAML==6.0.5 -types-setuptools==57.4.11 +types-setuptools==57.4.12 types-tabulate==0.8.6 typing-utils==0.1.0 typing_extensions==4.1.1 @@ -111,7 +110,7 @@ wadllib==1.3.6 WebOb==1.8.7 wrapt==1.14.0 ws4py==0.5.1 -zipp==3.7.0 +zipp==3.8.0 zope.deprecation==4.4.0 zope.interface==5.4.0 python-apt @ https://launchpad.net/ubuntu/+archive/primary/+sourcefiles/python-apt/2.0.0ubuntu0.20.04.6/python-apt_2.0.0ubuntu0.20.04.6.tar.xz; sys.platform == "linux" diff --git a/requirements.txt b/requirements.txt index 1bec40c02b..36ab873865 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,7 +8,7 @@ click==8.1.2 craft-cli==0.4.0 craft-grammar==1.1.1 craft-parts==1.4.2 -craft-providers==1.1.1 +craft-providers==1.2.0 cryptography==3.4 Deprecated==1.2.13 distro==1.7.0 @@ -17,7 +17,7 @@ gnupg==2.3.1 httplib2==0.20.4 idna==3.3 importlib-metadata==4.11.3 -jeepney==0.7.1 +jeepney==0.8.0 jsonschema==2.5.1 keyring==23.5.0 launchpadlib==1.10.16 @@ -64,7 +64,7 @@ urllib3==1.26.9 wadllib==1.3.6 wrapt==1.14.0 ws4py==0.5.1 -zipp==3.7.0 +zipp==3.8.0 python-apt @ https://launchpad.net/ubuntu/+archive/primary/+sourcefiles/python-apt/2.0.0ubuntu0.20.04.6/python-apt_2.0.0ubuntu0.20.04.6.tar.xz; sys.platform == "linux" PyNaCl==1.4.0; sys.platform != "linux" PyNaCl @ https://files.pythonhosted.org/packages/61/ab/2ac6dea8489fa713e2b4c6c5b549cc962dd4a842b5998d9e80cf8440b7cd/PyNaCl-1.3.0.tar.gz; sys.platform == "linux" diff --git a/setup.py b/setup.py index 2ae1a13e48..18cd579796 100755 --- a/setup.py +++ b/setup.py @@ -59,11 +59,12 @@ def recursive_data_files(directory, install_directory): scripts = [] dev_requires = [ + "mccabe<0.7.0", # to resolve version conflict "black", "codespell", "coverage", "flake8", - "pyflakes==2.1.1", + "pyflakes", "fixtures", "isort", "mccabe", From 3225305665efe92f785de6748af83926999d6ab1 Mon Sep 17 00:00:00 2001 From: Claudio Matsuoka Date: Fri, 8 Apr 2022 16:27:38 -0300 Subject: [PATCH 081/167] providers: override instance warmup If a new injection is required it should also be performed on instance warm-up. Note that snaps that are already installed won't be reinstalled. Signed-off-by: Claudio Matsuoka --- snapcraft/providers/_buildd.py | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/snapcraft/providers/_buildd.py b/snapcraft/providers/_buildd.py index 6a4bba5078..902488b4a6 100644 --- a/snapcraft/providers/_buildd.py +++ b/snapcraft/providers/_buildd.py @@ -64,8 +64,8 @@ def _setup_snapcraft(*, executor: Executor) -> None: if snap_channel is None and sys.platform != "linux": snap_channel = "stable" - # FIXME: don't reinstall snapcraft if already installed. - # See https://github.com/canonical/craft-providers/issues/91 + # Snaps that are already installed won't be reinstalled. + # See https://github.com/canonical/craft-providers/issues/91 if snap_channel: try: @@ -100,8 +100,7 @@ def setup( """Prepare base instance for use by the application. :param executor: Executor for target container. - :param retry_wait: Duration to sleep() between status checks (if - required). + :param retry_wait: Duration to sleep() between status checks (if required). :param timeout: Timeout in seconds. :raises BaseCompatibilityError: if instance is incompatible. @@ -109,3 +108,25 @@ def setup( """ super().setup(executor=executor, retry_wait=retry_wait, timeout=timeout) self._setup_snapcraft(executor=executor) + + def warmup( + self, + *, + executor: Executor, + retry_wait: float = 0.25, + timeout: Optional[float] = None, + ) -> None: + """Prepare a previously created and setup instance for use by the application. + + In addition to the guarantees provided by buildd: + - snapcraft installed + + :param executor: Executor for target container. + :param retry_wait: Duration to sleep() between status checks (if required). + :param timeout: Timeout in seconds. + + :raises BaseCompatibilityError: if instance is incompatible. + :raises BaseConfigurationError: on other unexpected error. + """ + super().warmup(executor=executor, retry_wait=retry_wait, timeout=timeout) + self._setup_snapcraft(executor=executor) From 9cd53e4e41ba4b38e72d731db2eabe0dc358d960 Mon Sep 17 00:00:00 2001 From: Claudio Matsuoka Date: Fri, 8 Apr 2022 16:44:34 -0300 Subject: [PATCH 082/167] providers: add overrides decorator to setup methods Signed-off-by: Claudio Matsuoka --- snapcraft/providers/_buildd.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/snapcraft/providers/_buildd.py b/snapcraft/providers/_buildd.py index 902488b4a6..a58c69e525 100644 --- a/snapcraft/providers/_buildd.py +++ b/snapcraft/providers/_buildd.py @@ -21,6 +21,7 @@ from craft_providers import Executor, bases from craft_providers.actions import snap_installer +from overrides import overrides from snapcraft import utils @@ -90,6 +91,7 @@ def _setup_snapcraft(*, executor: Executor) -> None: "Failed to inject host snapcraft snap into target environment." ) from error + @overrides def setup( self, *, @@ -109,6 +111,7 @@ def setup( super().setup(executor=executor, retry_wait=retry_wait, timeout=timeout) self._setup_snapcraft(executor=executor) + @overrides def warmup( self, *, From 468051ea52dce881dcb7a74e7817c297f50ec12c Mon Sep 17 00:00:00 2001 From: Claudio Matsuoka Date: Sat, 9 Apr 2022 20:56:21 -0300 Subject: [PATCH 083/167] cli: fix help command legacy fallback Handle craft-cli's `ProvideHelpException` error to display the same message displayed when invoked using the `--help` option. Signed-off-by: Claudio Matsuoka --- snapcraft/cli.py | 6 ++--- tests/unit/cli/test_help.py | 48 +++++++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 3 deletions(-) create mode 100644 tests/unit/cli/test_help.py diff --git a/snapcraft/cli.py b/snapcraft/cli.py index b557a3b799..9617bf87f4 100644 --- a/snapcraft/cli.py +++ b/snapcraft/cli.py @@ -21,7 +21,7 @@ import sys import craft_cli -from craft_cli import ArgumentParsingError, EmitterMode, emit +from craft_cli import ArgumentParsingError, EmitterMode, ProvideHelpException, emit from snapcraft import __version__, errors, utils from snapcraft_legacy.cli import legacy @@ -89,7 +89,7 @@ def run(): dispatcher = craft_cli.Dispatcher( "snapcraft", COMMAND_GROUPS, - summary="What's the app about", + summary="Package, distribute, and update snaps for Linux and IoT", extra_global_args=GLOBAL_ARGS, default_command=commands.PackCommand, ) @@ -102,7 +102,7 @@ def run(): dispatcher.load_command(None) dispatcher.run() emit.ended_ok() - except (errors.LegacyFallback, ArgumentParsingError) as err: + except (ProvideHelpException, errors.LegacyFallback, ArgumentParsingError) as err: emit.trace(f"run legacy implementation: {err!s}") emit.ended_ok() legacy.legacy_run() diff --git a/tests/unit/cli/test_help.py b/tests/unit/cli/test_help.py new file mode 100644 index 0000000000..36d63f2af5 --- /dev/null +++ b/tests/unit/cli/test_help.py @@ -0,0 +1,48 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright 2022 Canonical Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import sys +from unittest.mock import call + +import pytest + +from snapcraft import cli + + +def test_help_command(mocker): + mocker.patch.object(sys, "argv", ["cmd", "help"]) + mock_dispatcher_run = mocker.patch("craft_cli.dispatcher.Dispatcher.run") + mock_legacy_run = mocker.patch("snapcraft_legacy.cli.legacy.legacy_run") + + cli.run() + + assert mock_dispatcher_run.mock_calls == [] + assert mock_legacy_run.mock_calls == [call()] + + +@pytest.mark.parametrize("arg", ["-h", "--help"]) +def test_help_option(mocker, arg): + mocker.patch.object(sys, "argv", ["cmd", arg]) + mock_dispatcher_run = mocker.patch("craft_cli.dispatcher.Dispatcher.run") + mock_legacy_run = mocker.patch( + "snapcraft_legacy.cli.legacy.legacy_run", side_effect=SystemExit + ) + + with pytest.raises(SystemExit): + cli.run() + + assert mock_dispatcher_run.mock_calls == [] + assert mock_legacy_run.mock_calls == [call()] From 3e863c22b1f01e1e06173ef5d6d4c6bd073bbbd3 Mon Sep 17 00:00:00 2001 From: Claudio Matsuoka Date: Mon, 11 Apr 2022 18:47:48 -0300 Subject: [PATCH 084/167] commands: add snap command Allow usage of the legacy command `snap` to create a snap package, and fix passing the output parameter to the managed instance. Signed-off-by: Claudio Matsuoka --- snapcraft/cli.py | 4 +-- snapcraft/commands/__init__.py | 2 ++ snapcraft/commands/lifecycle.py | 34 ++++++++++++++++++++---- snapcraft/pack.py | 3 +++ snapcraft/parts/lifecycle.py | 7 +++-- tests/unit/commands/test_lifecycle.py | 37 ++++++++++++++++++++++++--- tests/unit/parts/test_lifecycle.py | 26 +++++++++++-------- 7 files changed, 90 insertions(+), 23 deletions(-) diff --git a/snapcraft/cli.py b/snapcraft/cli.py index 9617bf87f4..74cc611104 100644 --- a/snapcraft/cli.py +++ b/snapcraft/cli.py @@ -38,14 +38,14 @@ commands.StageCommand, commands.PrimeCommand, commands.PackCommand, + commands.SnapCommand, # hidden (legacy compatibility) ], ), craft_cli.CommandGroup( "Extensions", [ commands.ListExtensionsCommand, - # hidden command, alias to list-extensions. - commands.ExtensionsCommand, + commands.ExtensionsCommand, # hidden (alias to list-extensions) commands.ExpandExtensionsCommand, ], ), diff --git a/snapcraft/commands/__init__.py b/snapcraft/commands/__init__.py index bb0dbf1553..9d5c4e8fef 100644 --- a/snapcraft/commands/__init__.py +++ b/snapcraft/commands/__init__.py @@ -27,6 +27,7 @@ PackCommand, PrimeCommand, PullCommand, + SnapCommand, StageCommand, ) from .version import VersionCommand @@ -38,6 +39,7 @@ "PackCommand", "PrimeCommand", "PullCommand", + "SnapCommand", "StageCommand", "ExtensionsCommand", "ListExtensionsCommand", diff --git a/snapcraft/commands/lifecycle.py b/snapcraft/commands/lifecycle.py index 4075a60dd9..a752f5f186 100644 --- a/snapcraft/commands/lifecycle.py +++ b/snapcraft/commands/lifecycle.py @@ -127,15 +127,14 @@ class PrimeCommand(_LifecycleStepCommand): class PackCommand(_LifecycleCommand): - """Prepare the final payload for packing.""" + """Pack the final snap payload.""" name = "pack" - help_msg = "Build artifacts defined for a part" + help_msg = "Create the snap package" overview = textwrap.dedent( """ - Prepare the final payload to be packed as a snap. If part names are - specified only those parts will be primed. The default is to prime - all parts. + Process parts and create a snap file containing the project payload. + If a directory is specified, pack its contents instead. """ ) @@ -168,6 +167,31 @@ def run(self, parsed_args): super().run(parsed_args) +class SnapCommand(_LifecycleCommand): + """Pack the final snap payload. This is a legacy compatibility command.""" + + name = "snap" + help_msg = "Build artifacts defined for a part" + hidden = True + overview = textwrap.dedent( + """ + Process parts and create a snap file containing the project payload. + """ + ) + + @overrides + def fill_parser(self, parser: "argparse.ArgumentParser") -> None: + """Add arguments specific to the pack command.""" + super().fill_parser(parser) + parser.add_argument( + "-o", + "--output", + metavar="filename", + type=str, + help="Path to the resulting snap", + ) + + class CleanCommand(_LifecycleStepCommand): """Remove a part's assets.""" diff --git a/snapcraft/pack.py b/snapcraft/pack.py index e7e075cdca..bc8807a1da 100644 --- a/snapcraft/pack.py +++ b/snapcraft/pack.py @@ -33,6 +33,8 @@ def pack_snap( :param output: Snap file name or directory. :param compression: Compression type to use, None for defaults. """ + emit.trace(f"pack_snap: output={output!r}, compression={compression!r}") + output_file = None output_dir = None @@ -61,6 +63,7 @@ def pack_snap( command.append(output_dir) emit.progress("Creating snap package...") + emit.trace(f"Pack command: {command}") try: subprocess.run( command, capture_output=True, check=True, universal_newlines=True diff --git a/snapcraft/parts/lifecycle.py b/snapcraft/parts/lifecycle.py index 3c5d216464..23f465ad6f 100644 --- a/snapcraft/parts/lifecycle.py +++ b/snapcraft/parts/lifecycle.py @@ -148,7 +148,7 @@ def _run_command( else: work_dir = Path.cwd() - step_name = "prime" if command_name == "pack" else command_name + step_name = "prime" if command_name in ("pack", "snap") else command_name lifecycle = PartsLifecycle( project.parts, @@ -192,7 +192,7 @@ def _run_command( ) emit.message("Generated snap metadata", intermediate=True) - if command_name == "pack": + if command_name in ("pack", "snap"): pack.pack_snap( lifecycle.prime_dir, output=parsed_args.output, @@ -231,6 +231,9 @@ def _run_in_provider( if hasattr(parsed_args, "parts"): cmd.extend(parsed_args.parts) + if getattr(parsed_args, "output", None): + cmd.extend(["--output", parsed_args.output]) + if emit.get_mode() == EmitterMode.VERBOSE: cmd.append("--verbose") elif emit.get_mode() == EmitterMode.QUIET: diff --git a/tests/unit/commands/test_lifecycle.py b/tests/unit/commands/test_lifecycle.py index 6d4902bade..06bab102f3 100644 --- a/tests/unit/commands/test_lifecycle.py +++ b/tests/unit/commands/test_lifecycle.py @@ -25,6 +25,7 @@ PackCommand, PrimeCommand, PullCommand, + SnapCommand, StageCommand, ) @@ -48,13 +49,43 @@ def test_lifecycle_command(cmd_name, cmd_class, mocker): ] -def test_pack_command(mocker): +@pytest.mark.parametrize( + "cmd_name,cmd_class", + [ + ("pack", PackCommand), + ("snap", SnapCommand), + ], +) +def test_pack_command(mocker, cmd_name, cmd_class): lifecycle_run_mock = mocker.patch("snapcraft.parts.lifecycle.run") - cmd = PackCommand(None) + cmd = cmd_class(None) cmd.run(argparse.Namespace(directory=None, output=None, compression=None)) assert lifecycle_run_mock.mock_calls == [ - call("pack", argparse.Namespace(directory=None, output=None, compression=None)) + call( + cmd_name, argparse.Namespace(directory=None, output=None, compression=None) + ) + ] + + +@pytest.mark.parametrize( + "cmd_name,cmd_class", + [ + ("pack", PackCommand), + ("snap", SnapCommand), + ], +) +def test_pack_command_with_output(mocker, cmd_name, cmd_class): + lifecycle_run_mock = mocker.patch("snapcraft.parts.lifecycle.run") + pack_mock = mocker.patch("snapcraft.pack.pack_snap") + cmd = cmd_class(None) + cmd.run(argparse.Namespace(directory=None, output="output", compression=None)) + assert lifecycle_run_mock.mock_calls == [ + call( + cmd_name, + argparse.Namespace(compression=None, directory=None, output="output"), + ) ] + assert pack_mock.mock_calls == [] def test_pack_command_with_directory(mocker): diff --git a/tests/unit/parts/test_lifecycle.py b/tests/unit/parts/test_lifecycle.py index 7a6a366181..3102ecf3ce 100644 --- a/tests/unit/parts/test_lifecycle.py +++ b/tests/unit/parts/test_lifecycle.py @@ -145,7 +145,7 @@ def test_snapcraft_yaml_load(new_dir, snapcraft_yaml, filename, mocker): ] -@pytest.mark.parametrize("cmd", ["pull", "build", "stage", "prime", "pack", "clean"]) +@pytest.mark.parametrize("cmd", ["pull", "build", "stage", "prime", "pack", "snap", "clean"]) def test_lifecycle_run_provider(cmd, snapcraft_yaml, new_dir, mocker): """Option --provider is not supported in core22.""" snapcraft_yaml(base="core22") @@ -165,7 +165,7 @@ def test_lifecycle_run_provider(cmd, snapcraft_yaml, new_dir, mocker): assert str(raised.value) == "Option --provider is not supported." -@pytest.mark.parametrize("cmd", ["pull", "build", "stage", "prime", "clean"]) +@pytest.mark.parametrize("cmd", ["pull", "build", "stage", "prime", "snap", "clean"]) def test_lifecycle_legacy_run_provider(cmd, snapcraft_yaml, new_dir, mocker): """Option --provider is supported by legacy.""" snapcraft_yaml(base="core20") @@ -213,14 +213,15 @@ def test_lifecycle_run_command_step( assert pack_mock.mock_calls == [] -def test_lifecycle_run_command_pack(snapcraft_yaml, project_vars, new_dir, mocker): +@pytest.mark.parametrize("cmd", ["pack", "snap"]) +def test_lifecycle_run_command_pack(cmd, snapcraft_yaml, project_vars, new_dir, mocker): project = Project.unmarshal(snapcraft_yaml(base="core22")) run_mock = mocker.patch("snapcraft.parts.PartsLifecycle.run") mocker.patch("snapcraft.meta.snap_yaml.write") pack_mock = mocker.patch("snapcraft.pack.pack_snap") parts_lifecycle._run_command( - "pack", + cmd, project=project, assets_dir=Path(), parsed_args=argparse.Namespace( @@ -238,7 +239,8 @@ def test_lifecycle_run_command_pack(snapcraft_yaml, project_vars, new_dir, mocke ] -def test_lifecycle_pack_destructive_mode(snapcraft_yaml, project_vars, new_dir, mocker): +@pytest.mark.parametrize("cmd", ["pack", "snap"]) +def test_lifecycle_pack_destructive_mode(cmd, snapcraft_yaml, project_vars, new_dir, mocker): project = Project.unmarshal(snapcraft_yaml(base="core22")) run_in_provider_mock = mocker.patch("snapcraft.parts.lifecycle._run_in_provider") run_mock = mocker.patch("snapcraft.parts.PartsLifecycle.run") @@ -251,7 +253,7 @@ def test_lifecycle_pack_destructive_mode(snapcraft_yaml, project_vars, new_dir, ) parts_lifecycle._run_command( - "pack", + cmd, project=project, assets_dir=Path(), parsed_args=argparse.Namespace( @@ -270,7 +272,8 @@ def test_lifecycle_pack_destructive_mode(snapcraft_yaml, project_vars, new_dir, ] -def test_lifecycle_pack_managed(snapcraft_yaml, project_vars, new_dir, mocker): +@pytest.mark.parametrize("cmd", ["pack", "snap"]) +def test_lifecycle_pack_managed(cmd, snapcraft_yaml, project_vars, new_dir, mocker): project = Project.unmarshal(snapcraft_yaml(base="core22")) run_in_provider_mock = mocker.patch("snapcraft.parts.lifecycle._run_in_provider") run_mock = mocker.patch("snapcraft.parts.PartsLifecycle.run") @@ -283,7 +286,7 @@ def test_lifecycle_pack_managed(snapcraft_yaml, project_vars, new_dir, mocker): ) parts_lifecycle._run_command( - "pack", + cmd, project=project, assets_dir=Path(), parsed_args=argparse.Namespace( @@ -302,14 +305,15 @@ def test_lifecycle_pack_managed(snapcraft_yaml, project_vars, new_dir, mocker): ] -def test_lifecycle_pack_not_managed(snapcraft_yaml, new_dir, mocker): +@pytest.mark.parametrize("cmd", ["pack", "snap"]) +def test_lifecycle_pack_not_managed(cmd, snapcraft_yaml, new_dir, mocker): project = Project.unmarshal(snapcraft_yaml(base="core22")) run_in_provider_mock = mocker.patch("snapcraft.parts.lifecycle._run_in_provider") run_mock = mocker.patch("snapcraft.parts.PartsLifecycle.run") mocker.patch("snapcraft.utils.is_managed_mode", return_value=False) parts_lifecycle._run_command( - "pack", + cmd, project=project, assets_dir=Path(), parsed_args=argparse.Namespace( @@ -325,7 +329,7 @@ def test_lifecycle_pack_not_managed(snapcraft_yaml, new_dir, mocker): assert run_in_provider_mock.mock_calls == [ call( project, - "pack", + cmd, argparse.Namespace( directory=None, output=None, From 0be91cfd9ca41dd7219b0946228f3beab20df565 Mon Sep 17 00:00:00 2001 From: Claudio Matsuoka Date: Mon, 11 Apr 2022 20:15:01 -0300 Subject: [PATCH 085/167] tests: add pack and snap spread tests Signed-off-by: Claudio Matsuoka --- .../core22/packing/snap/snapcraft.yaml | 16 ++++++++ tests/spread/general/core22/packing/task.yaml | 38 +++++++++++++++++++ 2 files changed, 54 insertions(+) create mode 100644 tests/spread/general/core22/packing/snap/snapcraft.yaml create mode 100644 tests/spread/general/core22/packing/task.yaml diff --git a/tests/spread/general/core22/packing/snap/snapcraft.yaml b/tests/spread/general/core22/packing/snap/snapcraft.yaml new file mode 100644 index 0000000000..3222b1c8e5 --- /dev/null +++ b/tests/spread/general/core22/packing/snap/snapcraft.yaml @@ -0,0 +1,16 @@ +name: my-snap-name +version: '0.1' +summary: Single-line elevator pitch for your amazing snap # 79 char long summary +description: | + This is my-snap's description. You have a paragraph or two to tell the + most important story about your snap. Keep it under 100 words though, + we live in tweetspace and your description wants to look good in the snap + store. +base: core22 + +grade: devel +confinement: devmode + +parts: + my-part: + plugin: nil diff --git a/tests/spread/general/core22/packing/task.yaml b/tests/spread/general/core22/packing/task.yaml new file mode 100644 index 0000000000..00dcac93df --- /dev/null +++ b/tests/spread/general/core22/packing/task.yaml @@ -0,0 +1,38 @@ +summary: Validate scriptlet failures + +environment: + CMD/pack: pack + CMD/pack_output: pack -o output.snap + CMD/pack_output_subdir: pack --output subdir/output.snap + CMD/snap: snap + CMD/snap_output: snap --output output.snap + CMD/snap_output_subdir: snap -o subdir/output.snap + CMD/default: "" + CMD/default_output: -o output.snap + CMD/default_output_subdir: --output subdir/output.snap + +prepare: | + #shellcheck source=tests/spread/tools/snapcraft-yaml.sh + . "$TOOLS_DIR/snapcraft-yaml.sh" + # set_base "$SNAP_DIR/snap/snapcraft.yaml" + snap install core22 --edge + +restore: | + snapcraft clean + rm -Rf subdir ./*.snap + + #shellcheck source=tests/spread/tools/snapcraft-yaml.sh + . "$TOOLS_DIR/snapcraft-yaml.sh" + restore_yaml "snap/snapcraft.yaml" + +execute: | + # shellcheck disable=SC2086 + snapcraft $CMD + + if echo "$CMD" | grep subdir/output; then + test -f subdir/output.snap + elif echo "$CMD" | grep output; then + test -f output.snap + else + test -f my-snap-name_0.1_*.snap + fi From d859c65d09cf3676ab7b2ca53a33fa620996c834 Mon Sep 17 00:00:00 2001 From: Claudio Matsuoka Date: Tue, 12 Apr 2022 08:13:33 -0300 Subject: [PATCH 086/167] commands: add deprecation notice for snap command Signed-off-by: Claudio Matsuoka --- snapcraft/parts/lifecycle.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/snapcraft/parts/lifecycle.py b/snapcraft/parts/lifecycle.py index 23f465ad6f..333ade46a1 100644 --- a/snapcraft/parts/lifecycle.py +++ b/snapcraft/parts/lifecycle.py @@ -136,6 +136,11 @@ def _run_command( managed_mode = utils.is_managed_mode() part_names = getattr(parsed_args, "parts", None) + if not managed_mode and command_name == "snap": + emit.message( + "The 'snap' command is deprecated, use 'pack' instead.", intermediate=True + ) + if not managed_mode and not parsed_args.destructive_mode: if command_name == "clean" and not part_names: _clean_provider(project, parsed_args) From 8e93ad795a8330a441e95ab09aeb9f6133603304 Mon Sep 17 00:00:00 2001 From: Claudio Matsuoka Date: Tue, 12 Apr 2022 11:32:12 -0300 Subject: [PATCH 087/167] commands: update help messages and docstrings Signed-off-by: Claudio Matsuoka --- snapcraft/commands/lifecycle.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/snapcraft/commands/lifecycle.py b/snapcraft/commands/lifecycle.py index a752f5f186..f8a124afb9 100644 --- a/snapcraft/commands/lifecycle.py +++ b/snapcraft/commands/lifecycle.py @@ -133,8 +133,9 @@ class PackCommand(_LifecycleCommand): help_msg = "Create the snap package" overview = textwrap.dedent( """ - Process parts and create a snap file containing the project payload. - If a directory is specified, pack its contents instead. + Process parts and create a snap file containing the project payload + with the provided metadata. If a directory is specified, pack its + contents instead. """ ) @@ -171,11 +172,12 @@ class SnapCommand(_LifecycleCommand): """Pack the final snap payload. This is a legacy compatibility command.""" name = "snap" - help_msg = "Build artifacts defined for a part" + help_msg = "Create a snap" hidden = True overview = textwrap.dedent( """ - Process parts and create a snap file containing the project payload. + Process parts and create a snap file containing the project payload + with the provided metadata. """ ) From f209841fd86b69e03de6883102c82e0fd1fbf56a Mon Sep 17 00:00:00 2001 From: Claudio Matsuoka Date: Tue, 12 Apr 2022 11:57:40 -0300 Subject: [PATCH 088/167] commands: show message if lxd is requested on linux Display a message if lxd is requested in a platform where it's already the default build provider. Signed-off-by: Claudio Matsuoka --- snapcraft/parts/lifecycle.py | 5 +++++ snapcraft/providers/__init__.py | 2 +- snapcraft/providers/_get_provider.py | 8 ++++++-- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/snapcraft/parts/lifecycle.py b/snapcraft/parts/lifecycle.py index 333ade46a1..8c795fbd13 100644 --- a/snapcraft/parts/lifecycle.py +++ b/snapcraft/parts/lifecycle.py @@ -141,6 +141,11 @@ def _run_command( "The 'snap' command is deprecated, use 'pack' instead.", intermediate=True ) + if parsed_args.use_lxd and providers.get_platform_default_provider() == "lxd": + emit.message( + "LXD is used by default on this platform.", intermediate=True + ) + if not managed_mode and not parsed_args.destructive_mode: if command_name == "clean" and not part_names: _clean_provider(project, parsed_args) diff --git a/snapcraft/providers/__init__.py b/snapcraft/providers/__init__.py index 488be0f246..1172c31ac1 100644 --- a/snapcraft/providers/__init__.py +++ b/snapcraft/providers/__init__.py @@ -17,7 +17,7 @@ """Build provider support.""" from ._buildd import SnapcraftBuilddBaseConfiguration # noqa: F401 -from ._get_provider import get_provider # noqa: F401 +from ._get_provider import get_platform_default_provider, get_provider # noqa: F401 from ._logs import capture_logs_from_instance # noqa: F401 from ._lxd import LXDProvider # noqa: F401 from ._multipass import MultipassProvider # noqa: F401 diff --git a/snapcraft/providers/_get_provider.py b/snapcraft/providers/_get_provider.py index dcba6683ca..a623068b20 100644 --- a/snapcraft/providers/_get_provider.py +++ b/snapcraft/providers/_get_provider.py @@ -38,7 +38,7 @@ def get_provider(provider: Optional[str] = None) -> Provider: :return: Provider instance. """ if provider is None: - provider = _get_platform_default_provider() + provider = get_platform_default_provider() if provider == "lxd": return LXDProvider() @@ -49,7 +49,11 @@ def get_provider(provider: Optional[str] = None) -> Provider: raise RuntimeError(f"Unsupported provider specified: {provider!r}.") -def _get_platform_default_provider() -> str: +def get_platform_default_provider() -> str: + """Obtain the default provider for the host platform. + + :return: Default provider name. + """ if sys.platform == "linux": return "lxd" From 7b49c1eaed1ed9992ccee1ea7e9b778338886715 Mon Sep 17 00:00:00 2001 From: Sergio Schvezov Date: Thu, 14 Apr 2022 14:40:50 -0300 Subject: [PATCH 089/167] utils: in-house strtobool implementation distutils is mostly going away in Python 3.12 Signed-off-by: Sergio Schvezov --- snapcraft/utils.py | 22 +++++++++-- tests/unit/test_utils.py | 83 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 102 insertions(+), 3 deletions(-) create mode 100644 tests/unit/test_utils.py diff --git a/snapcraft/utils.py b/snapcraft/utils.py index 830b316ba8..2428dfd36a 100644 --- a/snapcraft/utils.py +++ b/snapcraft/utils.py @@ -16,7 +16,6 @@ """Utilities for snapcraft.""" -import distutils.util import logging import os import pathlib @@ -29,10 +28,27 @@ OSPlatform = namedtuple("OSPlatform", "system release machine") -def is_managed_mode(): +def strtobool(value: str) -> bool: + """Convert a string representation of truth to true (1) or false (0). + + :param value: a True value of 'y', 'yes', 't', 'true', 'on', and '1' + or a False value of 'n', 'no', 'f', 'false', 'off', and '0'. + :raises ValueError: if `value` is not a valid boolean value. + """ + parsed_value = value.lower() + + if parsed_value in ("y", "yes", "t", "true", "on", "1"): + return True + if parsed_value in ("n", "no", "f", "false", "off", "0"): + return False + + raise ValueError(f"Invalid boolean value of {value!r}") + + +def is_managed_mode() -> bool: """Check if snapcraft is running in a managed environment.""" managed_flag = os.getenv("SNAPCRAFT_MANAGED_MODE", "n") - return distutils.util.strtobool(managed_flag) == 1 + return strtobool(managed_flag) def get_managed_environment_home_path(): diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py new file mode 100644 index 0000000000..f9d6801d87 --- /dev/null +++ b/tests/unit/test_utils.py @@ -0,0 +1,83 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright 2022 Canonical Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + + +import pytest + +from snapcraft import utils + + +@pytest.mark.parametrize( + "value", + [ + "y", + "Y", + "yes", + "YES", + "Yes", + "t", + "T", + "true", + "TRUE", + "True", + "On", + "ON", + "oN", + "1", + ], +) +def test_strtobool_true(value: str): + assert utils.strtobool(value) is True + + +@pytest.mark.parametrize( + "value", + [ + "n", + "N", + "no", + "NO", + "No", + "f", + "F", + "false", + "FALSE", + "False", + "off", + "OFF", + "oFF", + "0", + ], +) +def test_strtobool_false(value: str): + assert utils.strtobool(value) is False + + +@pytest.mark.parametrize( + "value", + [ + "not", + "yup", + "negative", + "positive", + "whatever", + "2", + "3", + ], +) +def test_strtobool_value_error(value: str): + with pytest.raises(ValueError): + utils.strtobool(value) From 1d7c2c12ddfc74bf0fe4cc7e48c5e25d90c709b8 Mon Sep 17 00:00:00 2001 From: Sergio Schvezov Date: Fri, 15 Apr 2022 18:53:02 -0300 Subject: [PATCH 090/167] storeapi: export SnapAPI Use it where no auth is required to avoid future integration issues with craft-store. Signed-off-by: Sergio Schvezov --- .../internal/build_providers/_snap.py | 2 +- .../internal/lifecycle/_runner.py | 2 +- snapcraft_legacy/storeapi/__init__.py | 22 ++++++++++++++----- snapcraft_legacy/storeapi/_snap_api.py | 8 +++++-- 4 files changed, 24 insertions(+), 10 deletions(-) diff --git a/snapcraft_legacy/internal/build_providers/_snap.py b/snapcraft_legacy/internal/build_providers/_snap.py index 3950bf3dab..a75ded5fe2 100644 --- a/snapcraft_legacy/internal/build_providers/_snap.py +++ b/snapcraft_legacy/internal/build_providers/_snap.py @@ -198,7 +198,7 @@ def _set_data(self) -> None: install_cmd = ["snap", op.name.lower()] snap_channel = _get_snap_channel(self.snap_name) - store_snap_info = storeapi.StoreClient().snap.get_info(self.snap_name) + store_snap_info = storeapi.SnapAPI().get_info(self.snap_name) snap_channel_map = store_snap_info.get_channel_mapping( risk=snap_channel.risk, track=snap_channel.track ) diff --git a/snapcraft_legacy/internal/lifecycle/_runner.py b/snapcraft_legacy/internal/lifecycle/_runner.py index 139eb9d30d..8643a101bf 100644 --- a/snapcraft_legacy/internal/lifecycle/_runner.py +++ b/snapcraft_legacy/internal/lifecycle/_runner.py @@ -46,7 +46,7 @@ def _get_required_grade(*, base: Optional[str], arch: str) -> str: # We use storeapi instead of repo.snaps so this can work under Docker # and related environments. try: - base_info = storeapi.StoreClient().snap.get_info(base) + base_info = storeapi.SnapAPI().get_info(base) base_info.get_channel_mapping(risk="stable", arch=arch) except storeapi.errors.SnapNotFoundError: return "devel" diff --git a/snapcraft_legacy/storeapi/__init__.py b/snapcraft_legacy/storeapi/__init__.py index 19156d408b..49cad85269 100644 --- a/snapcraft_legacy/storeapi/__init__.py +++ b/snapcraft_legacy/storeapi/__init__.py @@ -1,6 +1,6 @@ # -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- # -# Copyright 2016-2017, 2020-2021 Canonical Ltd +# Copyright 2016-2017, 2020-2022 Canonical Ltd # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License version 3 as @@ -16,12 +16,22 @@ import logging -from . import errors # noqa: F401 isort:skip -from . import channels # noqa: F401 isort:skip -from . import status # noqa: F401 isort:skip -from . import http_clients # noqa: F401 isort: skip +from . import errors # isort:skip +from . import channels # isort:skip +from . import status # isort:skip +from . import http_clients # isort: skip logger = logging.getLogger(__name__) -from ._store_client import StoreClient # noqa +from ._store_client import StoreClient +from ._snap_api import SnapAPI + +__all__ = [ + "errors", + "channels", + "status", + "http_clients", + "SnapAPI", + "StoreClient", +] diff --git a/snapcraft_legacy/storeapi/_snap_api.py b/snapcraft_legacy/storeapi/_snap_api.py index a405946b99..868610d58c 100644 --- a/snapcraft_legacy/storeapi/_snap_api.py +++ b/snapcraft_legacy/storeapi/_snap_api.py @@ -16,11 +16,13 @@ import logging import os -from typing import Dict +from typing import Dict, Optional from urllib.parse import urljoin import requests +from snapcraft_legacy.storeapi import http_clients + from . import constants, errors from ._requests import Requests from .info import SnapInfo @@ -36,7 +38,9 @@ class SnapAPI(Requests): at http://api.snapcraft.io/docs/. """ - def __init__(self, client): + def __init__(self, client: Optional[http_clients.Client] = None): + if client is None: + client = http_clients.Client() self._client = client self._root_url = os.environ.get("STORE_API_URL", constants.STORE_API_URL) From 51cfdda658e42973faae4add2b53accaf08fc21f Mon Sep 17 00:00:00 2001 From: Sergio Schvezov Date: Sun, 17 Apr 2022 11:22:12 -0300 Subject: [PATCH 091/167] storeapi: environment variable for auth selection Auth selection, Ubuntu Login SSO or Candid, is now donw through an environment variable. This cleans up the internals and simplifies the API. It also allows us to move away from it when the time comes to maybe OAUTH. This code removes support for the --experimental-login option in commands that used to support it with an error message for the new mechanism. Signed-off-by: Sergio Schvezov --- snapcraft_legacy/_store.py | 98 ++++++++++++---------- snapcraft_legacy/cli/assertions.py | 11 ++- snapcraft_legacy/cli/store.py | 66 ++++++++------- snapcraft_legacy/storeapi/__init__.py | 2 + snapcraft_legacy/storeapi/_store_client.py | 35 ++++---- snapcraft_legacy/storeapi/constants.py | 7 ++ tests/spread/general/store/task.yaml | 11 +-- 7 files changed, 127 insertions(+), 103 deletions(-) diff --git a/snapcraft_legacy/_store.py b/snapcraft_legacy/_store.py index 0e430e61b4..4ae6faa662 100644 --- a/snapcraft_legacy/_store.py +++ b/snapcraft_legacy/_store.py @@ -169,7 +169,7 @@ def _try_login( email: str, password: str, *, - store: storeapi.StoreClient, + store_client: storeapi.StoreClient, save: bool = True, packages: Iterable[Dict[str, str]] = None, acls: Iterable[str] = None, @@ -178,7 +178,7 @@ def _try_login( config_fd: TextIO = None, ) -> None: try: - store.login( + store_client.login( email=email, password=password, packages=packages, @@ -193,7 +193,7 @@ def _try_login( echo.wrapped(storeapi.constants.TWO_FACTOR_WARNING) except storeapi.http_clients.errors.StoreTwoFactorAuthenticationRequired: one_time_password = echo.prompt("Second-factor auth") - store.login( + store_client.login( email=email, password=password, otp=one_time_password, @@ -205,54 +205,64 @@ def _try_login( save=save, ) - # Continue if agreement and namespace conditions are met. - _check_dev_agreement_and_namespace_statuses(store) + +def _prompt_login() -> Tuple[str, str]: + echo.wrapped("Enter your Ubuntu One e-mail address and password.") + echo.wrapped( + "If you do not have an Ubuntu One account, you can create one " + "at https://snapcraft.io/account" + ) + email = echo.prompt("Email") + if os.getenv("SNAPCRAFT_TEST_INPUT"): + # Integration tests do not work well with hidden input. + echo.warning("Password will be visible.") + hide_input = False + else: + hide_input = True + password = echo.prompt("Password", hide_input=hide_input) + + return (email, password) def login( *, - store: storeapi.StoreClient, + store_client: storeapi.StoreClient, packages: Iterable[Dict[str, str]] = None, save: bool = True, acls: Iterable[str] = None, channels: Iterable[str] = None, expires: str = None, config_fd: TextIO = None, -) -> bool: - if not store: - store = storeapi.StoreClient() - - email = "" - password = "" - - if not config_fd: - echo.wrapped("Enter your Ubuntu One e-mail address and password.") - echo.wrapped( - "If you do not have an Ubuntu One account, you can create one " - "at https://snapcraft.io/account" +) -> None: + if store_client.use_candid() is True: + store_client.login( + acls=acls, + channels=channels, + packages=packages, + expires=expires, + config_fd=config_fd, ) - email = echo.prompt("Email") - if os.getenv("SNAPCRAFT_TEST_INPUT"): - # Integration tests do not work well with hidden input. - echo.warning("Password will be visible.") - hide_input = False + else: + if config_fd: + email = "" + password = "" else: - hide_input = True - password = echo.prompt("Password", hide_input=hide_input) - - _try_login( - email, - password, - store=store, - packages=packages, - acls=acls, - channels=channels, - expires=expires, - config_fd=config_fd, - save=save, - ) + email, password = _prompt_login() + + _try_login( + email, + password, + store_client=store_client, + packages=packages, + acls=acls, + channels=channels, + expires=expires, + config_fd=config_fd, + save=save, + ) - return True + # Continue if agreement and namespace conditions are met. + _check_dev_agreement_and_namespace_statuses(store_client) def _login_wrapper(method): @@ -261,7 +271,7 @@ def login_decorator(self, *args, **kwargs): return method(self, *args, **kwargs) except storeapi.http_clients.errors.InvalidCredentialsError: print("You are required to login before continuing.") - login(store=self) + login(store_client=self) return method(self, *args, **kwargs) return login_decorator @@ -545,15 +555,11 @@ def _maybe_prompt_for_key(name): return _select_key(keys) -def register_key(name, use_candid: bool = False) -> None: +def register_key(name) -> None: key = _maybe_prompt_for_key(name) - store_client = StoreClientCLI(use_candid=use_candid) - # TODO: remove coupling. - if isinstance(store_client.auth_client, storeapi.http_clients.CandidClient): - store_client.login(acls=["modify_account_key"], save=False) - else: - login(store=store_client, acls=["modify_account_key"], save=False) + store_client = StoreClientCLI() + login(store_client=store_client, acls=["modify_account_key"], save=False) logger.info("Registering key ...") account_info = store_client.get_account_information() diff --git a/snapcraft_legacy/cli/assertions.py b/snapcraft_legacy/cli/assertions.py index 7016d68638..5020480aba 100644 --- a/snapcraft_legacy/cli/assertions.py +++ b/snapcraft_legacy/cli/assertions.py @@ -27,7 +27,7 @@ import snapcraft_legacy from snapcraft_legacy._store import StoreClientCLI -from snapcraft_legacy import yaml_utils +from snapcraft_legacy import storeapi, yaml_utils from . import echo @@ -85,7 +85,11 @@ def create_key(key_name: str) -> None: ) def register_key(key_name: str, experimental_login: bool) -> None: """Register a key with the store to sign assertions.""" - snapcraft_legacy.register_key(key_name, use_candid=experimental_login) + if experimental_login: + raise click.BadArgumentUsage( + f"Set {storeapi.constants.ENVIRONMENT_STORE_AUTH}=candid instead" + ) + snapcraft_legacy.register_key(key_name) @assertionscli.command("sign-build") @@ -149,7 +153,8 @@ def list_validation_sets(name, sequence): """ store_client = StoreClientCLI() asserted_validation_sets = store_client.get_validation_sets( - name=name, sequence=sequence, + name=name, + sequence=sequence, ) if not asserted_validation_sets.assertions and (name or sequence): diff --git a/snapcraft_legacy/cli/store.py b/snapcraft_legacy/cli/store.py index 418fbff5b9..414bf078fa 100644 --- a/snapcraft_legacy/cli/store.py +++ b/snapcraft_legacy/cli/store.py @@ -31,7 +31,6 @@ from snapcraft_legacy import formatting_utils, storeapi from snapcraft_legacy._store import StoreClientCLI from snapcraft_legacy.storeapi import metrics as metrics_module -from snapcraft_legacy.storeapi.constants import DEFAULT_SERIES from . import echo from ._channel_map import get_tabulated_channel_map @@ -447,10 +446,12 @@ def close(snap_name, channels): account_info = store.get_account_information() try: - snap_id = account_info["snaps"][DEFAULT_SERIES][snap_name]["snap-id"] + snap_id = account_info["snaps"][storeapi.constants.DEFAULT_SERIES][snap_name][ + "snap-id" + ] except KeyError: raise storeapi.errors.StoreChannelClosingPermissionError( - snap_name, DEFAULT_SERIES + snap_name, storeapi.constants.DEFAULT_SERIES ) # Returned closed_channels cannot be trusted as it returns risks. @@ -706,6 +707,12 @@ def export_login( snapcraft export-login --expires="2019-01-01T00:00:00" exported """ + if experimental_login: + raise click.BadOptionUsage( + "--experimental-login", + "--experimental-login no longer supported. " + f"Set {storeapi.constants.ENVIRONMENT_STORE_AUTH}=candid instead", + ) snap_list = None channel_list = None @@ -714,7 +721,9 @@ def export_login( if snaps: snap_list = [] for package in snaps.split(","): - snap_list.append({"name": package, "series": DEFAULT_SERIES}) + snap_list.append( + {"name": package, "series": storeapi.constants.DEFAULT_SERIES} + ) if channels: channel_list = channels.split(",") @@ -722,24 +731,15 @@ def export_login( if acls: acl_list = acls.split(",") - store_client = storeapi.StoreClient(use_candid=experimental_login) - if store_client.use_candid: - store_client.login( - packages=snap_list, - channels=channel_list, - acls=acl_list, - expires=expires, - save=False, - ) - else: - snapcraft_legacy.login( - store=store_client, - packages=snap_list, - channels=channel_list, - acls=acl_list, - expires=expires, - save=False, - ) + store_client = storeapi.StoreClient() + snapcraft_legacy.login( + store_client=store_client, + packages=snap_list, + channels=channel_list, + acls=acl_list, + expires=expires, + save=False, + ) # Support a login_file of '-', which indicates a desire to print to stdout if login_file.strip() == "-": @@ -811,14 +811,17 @@ def login(login_file, experimental_login: bool): If you do not have an Ubuntu One account, you can create one at https://snapcraft.io/account """ - store_client = storeapi.StoreClient(use_candid=experimental_login) - if store_client.use_candid: - store_client.login(config_fd=login_file, save=True) - else: - snapcraft_legacy.login(store=store_client, config_fd=login_file) + if experimental_login: + raise click.BadOptionUsage( + "--experimental-login", + "--experimental-login no longer supported. " + f"Set {storeapi.constants.ENVIRONMENT_STORE_AUTH}=candid instead", + ) - print() + store_client = storeapi.StoreClient() + snapcraft_legacy.login(store_client=store_client, config_fd=login_file) + print() if login_file: try: human_acls = _human_readable_acls(store_client) @@ -963,13 +966,14 @@ def list_tracks(snap_name: str) -> None: required=True, ) def metrics(snap_name: str, name: str, start: str, end: str, format: str): - """Get metrics for . - """ + """Get metrics for .""" store = storeapi.StoreClient() account_info = store.get_account_information() try: - snap_id = account_info["snaps"][DEFAULT_SERIES][snap_name]["snap-id"] + snap_id = account_info["snaps"][storeapi.constants.DEFAULT_SERIES][snap_name][ + "snap-id" + ] except KeyError: echo.exit_error( brief="No permissions for snap.", diff --git a/snapcraft_legacy/storeapi/__init__.py b/snapcraft_legacy/storeapi/__init__.py index 49cad85269..7249b6a364 100644 --- a/snapcraft_legacy/storeapi/__init__.py +++ b/snapcraft_legacy/storeapi/__init__.py @@ -18,6 +18,7 @@ from . import errors # isort:skip from . import channels # isort:skip +from . import constants # isort:skip from . import status # isort:skip from . import http_clients # isort: skip @@ -30,6 +31,7 @@ __all__ = [ "errors", "channels", + "constants", "status", "http_clients", "SnapAPI", diff --git a/snapcraft_legacy/storeapi/_store_client.py b/snapcraft_legacy/storeapi/_store_client.py index 7888c3baa6..56bc7b770a 100644 --- a/snapcraft_legacy/storeapi/_store_client.py +++ b/snapcraft_legacy/storeapi/_store_client.py @@ -23,7 +23,7 @@ from snapcraft_legacy.internal.indicators import download_requests_stream -from . import _upload, errors, http_clients, metrics +from . import _upload, constants, errors, http_clients, metrics from ._dashboard_api import DashboardAPI from ._snap_api import SnapAPI from ._up_down_client import UpDownClient @@ -36,20 +36,10 @@ class StoreClient: """High-level client Snap resources.""" - @property - def use_candid(self) -> bool: - return isinstance(self.auth_client, http_clients.CandidClient) - - def __init__(self, use_candid: bool = False) -> None: - super().__init__() - + def __init__(self) -> None: self.client = http_clients.Client() - candid_has_credentials = http_clients.CandidClient.has_credentials() - logger.debug( - f"Candid forced: {use_candid}. Candid crendendials: {candid_has_credentials}." - ) - if use_candid or candid_has_credentials: + if self.use_candid() is True: self.auth_client: http_clients.AuthClient = http_clients.CandidClient() else: self.auth_client = http_clients.UbuntuOneAuthClient() @@ -58,6 +48,10 @@ def __init__(self, use_candid: bool = False) -> None: self.dashboard = DashboardAPI(self.auth_client) self._updown = UpDownClient(self.client) + @staticmethod + def use_candid() -> bool: + return os.getenv(constants.ENVIRONMENT_STORE_AUTH) == "candid" + def login( self, *, @@ -83,7 +77,10 @@ def login( ] macaroon = self.dashboard.get_macaroon( - acls=acls, packages=packages, channels=channels, expires=expires, + acls=acls, + packages=packages, + channels=channels, + expires=expires, ) self.auth_client.login(macaroon=macaroon, **kwargs) @@ -123,7 +120,10 @@ def register_key(self, account_key_request): def register(self, snap_name: str, is_private: bool = False, store_id: str = None): return self.dashboard.register( - snap_name, is_private=is_private, store_id=store_id, series=DEFAULT_SERIES, + snap_name, + is_private=is_private, + store_id=store_id, + series=DEFAULT_SERIES, ) def upload_precheck(self, snap_name): @@ -191,7 +191,10 @@ def get_snap_channel_map(self, *, snap_name: str) -> channel_map.ChannelMap: return self.dashboard.get_snap_channel_map(snap_name=snap_name) def get_metrics( - self, *, filters: List[metrics.MetricsFilter], snap_name: str, + self, + *, + filters: List[metrics.MetricsFilter], + snap_name: str, ) -> metrics.MetricsResults: return self.dashboard.get_metrics(filters=filters, snap_name=snap_name) diff --git a/snapcraft_legacy/storeapi/constants.py b/snapcraft_legacy/storeapi/constants.py index b032fead12..436c09175e 100644 --- a/snapcraft_legacy/storeapi/constants.py +++ b/snapcraft_legacy/storeapi/constants.py @@ -46,3 +46,10 @@ "We strongly recommend enabling multi-factor authentication: " "https://help.ubuntu.com/community/SSO/FAQs/2FA" ) + +ENVIRONMENT_STORE_AUTH = "SNAPCRAFT_STORE_AUTH" +"""Environment variable used to set an alterntive login method. + +The only setting that changes the behavior is `candid`, every +other value uses Ubuntu SSO. +""" diff --git a/tests/spread/general/store/task.yaml b/tests/spread/general/store/task.yaml index c1b16bc157..873fc9fd1d 100644 --- a/tests/spread/general/store/task.yaml +++ b/tests/spread/general/store/task.yaml @@ -4,8 +4,8 @@ manual: true environment: SNAP: dump-hello - SNAP_STORE_MACAROON/UBUNTU_ONE: "$(HOST: echo ${SNAP_STORE_MACAROON})" - SNAP_STORE_MACAROON/CANDID: "$(HOST: echo ${SNAP_STORE_CANDID_MACAROON})" + SNAP_STORE_MACAROON/ubuntu_one: "$(HOST: echo ${SNAP_STORE_MACAROON})" + SNAP_STORE_MACAROON/candid: "$(HOST: echo ${SNAP_STORE_CANDID_MACAROON})" STORE_DASHBOARD_URL: https://dashboard.staging.snapcraft.io/ STORE_API_URL: https://api.staging.snapcraft.io/ STORE_UPLOAD_URL: https://upload.apps.staging.ubuntu.com/ @@ -51,11 +51,8 @@ execute: | set +x echo "${SNAP_STORE_MACAROON}" > login set -x - if [ "${SPREAD_VARIANT}" = "CANDID" ]; then - snapcraft login --experimental-login --with login - else - snapcraft login --with login - fi + export SNAPCRAFT_STORE_AUTH="${SPREAD_VARIANT}" + snapcraft login --with login # Who Am I? snapcraft whoami From eea6b4a8206145fa56fc4394a210cdc2901f5b19 Mon Sep 17 00:00:00 2001 From: Sergio Schvezov Date: Mon, 18 Apr 2022 10:00:56 -0300 Subject: [PATCH 092/167] spread: remove git user config from pbr test This is not needed for spread under google, set up git author and email through environmnent variables for general system support (e.g.; Multipass). Signed-off-by: Sergio Schvezov --- spread.yaml | 4 ++++ tests/spread/plugins/v1/python/pbr/task.yaml | 3 +-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/spread.yaml b/spread.yaml index 7120b6f407..3e1b516320 100644 --- a/spread.yaml +++ b/spread.yaml @@ -28,6 +28,10 @@ environment: TOOLS_DIR: /snapcraft/tests/spread/tools + # Git environment for commits + GIT_AUTHOR_NAME: "Test User" + GIT_AUTHOR_EMAIL: "test-user@email.com" + backends: lxd: systems: diff --git a/tests/spread/plugins/v1/python/pbr/task.yaml b/tests/spread/plugins/v1/python/pbr/task.yaml index 520a6b456a..acc6fc9c28 100644 --- a/tests/spread/plugins/v1/python/pbr/task.yaml +++ b/tests/spread/plugins/v1/python/pbr/task.yaml @@ -17,8 +17,7 @@ prepare: | for python_dir in python2 python3; do pushd "$python_dir" git init - git config user.name "Test User" - git config user.email "test.user@example.com" + git config --global --add safe.directory "$(pwd)" git add . git commit -m "initial commit" popd From a53b099361e41ffadc9c57f7d5e8245e7a0aa4ac Mon Sep 17 00:00:00 2001 From: Sergio Schvezov Date: Mon, 18 Apr 2022 13:15:24 -0300 Subject: [PATCH 093/167] gha: setup alternative publication action If the new environment "secret" is set, trigger use of the new workflow. Signed-off-by: Sergio Schvezov --- .github/workflows/publish.yaml | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index f9a673d18a..48b73fa8e2 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -12,15 +12,16 @@ jobs: run: | # Secrets cannot be used in conditionals, so this is our dance: # https://github.com/actions/runner/issues/520 - if [[ -n "${{ secrets.STORE_LOGIN }}" ]]; then - echo "::set-output name=PUBLISH::true" - if [[ ${{ github.event_name }} == 'pull_request' ]]; then - echo "::set-output name=PUBLISH_BRANCH::edge/pr-${{ github.event.number }}" - else - echo "::set-output name=PUBLISH_BRANCH::" - fi + if [[ -n "${{ secrets.SNAPCRAFT_STORE_CREDENTIALS }}" ]]; then + echo "::set-output name=PUBLISH::env" + elif [[ -n "${{ secrets.STORE_LOGIN }}" ]]; then + echo "::set-output name=PUBLISH::legacy" else echo "::set-output name=PUBLISH::" + + if [[ ${{ github.event_name }} == 'pull_request' ]]; then + echo "::set-output name=PUBLISH_BRANCH::edge/pr-${{ github.event.number }}" + else echo "::set-output name=PUBLISH_BRANCH::" fi @@ -43,9 +44,18 @@ jobs: # Make sure it is installable. sudo snap install --dangerous --classic ${{ steps.build-snapcraft.outputs.snap }} - - if: steps.decisions.outputs.PUBLISH == 'true' && steps.decisions.outputs.PUBLISH_BRANCH != null + - if: steps.decisions.outputs.PUBLISH == 'legacy' && steps.decisions.outputs.PUBLISH_BRANCH != null uses: snapcore/action-publish@v1 with: store_login: ${{ secrets.STORE_LOGIN }} snap: ${{ steps.build-snapcraft.outputs.snap }} release: ${{ steps.decisions.outputs.PUBLISH_BRANCH }} + + - if: steps.decisions.outputs.PUBLISH == 'env' && steps.decisions.outputs.PUBLISH_BRANCH != null + # Use this until snapcore/action-publish#27 it is merged. + uses: sergiusens/action-publish + env: + SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_STORE_CREDENTIALS }} + with: + snap: ${{ steps.build-snapcraft.outputs.snap }} + release: ${{ steps.decisions.outputs.PUBLISH_BRANCH }} From 6ad5f7e56f0e7b1d1f68761376f358dd3a646f17 Mon Sep 17 00:00:00 2001 From: Callahan Kovacs Date: Mon, 18 Apr 2022 15:21:13 -0500 Subject: [PATCH 094/167] meta/snap.yaml: convert underscores to hyphens Signed-off-by: Callahan Kovacs --- snapcraft/meta/snap_yaml.py | 10 +++- tests/unit/meta/test_snap_yaml.py | 78 +++++++++++++++---------------- 2 files changed, 48 insertions(+), 40 deletions(-) diff --git a/snapcraft/meta/snap_yaml.py b/snapcraft/meta/snap_yaml.py index dd7f8455ff..1733894314 100644 --- a/snapcraft/meta/snap_yaml.py +++ b/snapcraft/meta/snap_yaml.py @@ -32,6 +32,12 @@ class Socket(YamlModel): listen_stream: Union[int, str] socket_mode: Optional[int] + class Config: # pylint: disable=too-few-public-methods + """Pydantic model configuration.""" + + allow_population_by_field_name = True + alias_generator = lambda s: s.replace("_", "-") # noqa: E731 + class SnapApp(YamlModel): """Snap.yaml app entry. @@ -177,7 +183,9 @@ def write(project: Project, prime_dir: Path, *, arch: str, version: str, grade: ) yaml.add_representer(str, _repr_str, Dumper=yaml.SafeDumper) - yaml_data = snap_metadata.yaml(exclude_none=True, sort_keys=False, width=1000) + yaml_data = snap_metadata.yaml( + by_alias=True, exclude_none=True, sort_keys=False, width=1000 + ) snap_yaml = meta_dir / "snap.yaml" snap_yaml.write_text(yaml_data) diff --git a/tests/unit/meta/test_snap_yaml.py b/tests/unit/meta/test_snap_yaml.py index 235bfb2231..fa897866ca 100644 --- a/tests/unit/meta/test_snap_yaml.py +++ b/tests/unit/meta/test_snap_yaml.py @@ -85,7 +85,7 @@ def test_simple_snap_yaml(simple_project, new_dir): apps: app1: command: bin/mytest - command_chain: + command-chain: - snap/command-chain/snapcraft-runner confinement: strict grade: stable @@ -121,37 +121,37 @@ def complex_project(): app1: command: bin/mytest autostart: test-app.desktop - common_id: test-common-id - bus_name: test-bus-name + common-id: test-common-id + bus-name: test-bus-name completer: test-completer - stop_command: test-stop-command - post_stop_command: test-post-stop-command - start_timeout: 1s - stop_timeout: 2s - watchdog_timeout: 3s - reload_command: test-reload-command - restart_delay: 4s + stop-command: test-stop-command + post-stop-command: test-post-stop-command + start-timeout: 1s + stop-timeout: 2s + watchdog-timeout: 3s + reload-command: test-reload-command + restart-delay: 4s timer: test-timer daemon: simple after: [test-after-1, test-after-2] before: [test-before-1, test-before-2] - refresh_mode: endure - stop_mode: sigterm - restart_condition: on-success - install_mode: enable + refresh-mode: endure + stop-mode: sigterm + restart-condition: on-success + install-mode: enable aliases: [test-alias-1, test-alias-2] environment: APP_VARIABLE: test-app-variable adapter: none - command_chain: + command-chain: - snap/command-chain/snapcraft-runner sockets: test-socket-1: - listen_stream: /tmp/test-socket.sock - socket_mode: 0 + listen-stream: /tmp/test-socket.sock + socket-mode: 0 test-socket-2: - listen_stream: 100 - socket_mode: 1 + listen-stream: 100 + socket-mode: 1 plugs: empty-plug: @@ -215,16 +215,16 @@ def test_complex_snap_yaml(complex_project, new_dir): app1: command: bin/mytest autostart: test-app.desktop - common_id: test-common-id - bus_name: test-bus-name + common-id: test-common-id + bus-name: test-bus-name completer: test-completer - stop_command: test-stop-command - post_stop_command: test-post-stop-command - start_timeout: 1s - stop_timeout: 2s - watchdog_timeout: 3s - reload_command: test-reload-command - restart_delay: 4s + stop-command: test-stop-command + post-stop-command: test-post-stop-command + start-timeout: 1s + stop-timeout: 2s + watchdog-timeout: 3s + reload-command: test-reload-command + restart-delay: 4s timer: test-timer daemon: simple after: @@ -233,25 +233,25 @@ def test_complex_snap_yaml(complex_project, new_dir): before: - test-before-1 - test-before-2 - refresh_mode: endure - stop_mode: sigterm - restart_condition: on-success - install_mode: enable + refresh-mode: endure + stop-mode: sigterm + restart-condition: on-success + install-mode: enable aliases: - test-alias-1 - test-alias-2 environment: APP_VARIABLE: test-app-variable adapter: none - command_chain: + command-chain: - snap/command-chain/snapcraft-runner sockets: test-socket-1: - listen_stream: /tmp/test-socket.sock - socket_mode: 0 + listen-stream: /tmp/test-socket.sock + socket-mode: 0 test-socket-2: - listen_stream: 100 - socket_mode: 1 + listen-stream: 100 + socket-mode: 1 confinement: strict grade: devel environment: @@ -266,10 +266,10 @@ def test_complex_snap_yaml(complex_project, new_dir): content: test-content interface: content target: test-target - default_provider: test-provider + default-provider: test-provider hooks: configure: - command_chain: + command-chain: - test environment: test-variable-1: test From a1c23f822c8fafac6652d446be4fb9d496678865 Mon Sep 17 00:00:00 2001 From: Callahan Kovacs Date: Mon, 18 Apr 2022 11:53:21 -0500 Subject: [PATCH 095/167] lifecycle: pass project name Signed-off-by: Callahan Kovacs --- snapcraft/parts/lifecycle.py | 1 + snapcraft/parts/parts.py | 2 ++ tests/unit/parts/test_parts.py | 6 ++++++ 3 files changed, 9 insertions(+) diff --git a/snapcraft/parts/lifecycle.py b/snapcraft/parts/lifecycle.py index 8c795fbd13..95e6c8ed3c 100644 --- a/snapcraft/parts/lifecycle.py +++ b/snapcraft/parts/lifecycle.py @@ -167,6 +167,7 @@ def _run_command( package_repositories=project.package_repositories, part_names=part_names, adopt_info=project.adopt_info, + project_name=project.name, project_vars={ "version": project.version or "", "grade": project.grade, diff --git a/snapcraft/parts/parts.py b/snapcraft/parts/parts.py index 11df8cfbb4..c3fd576f15 100644 --- a/snapcraft/parts/parts.py +++ b/snapcraft/parts/parts.py @@ -55,6 +55,7 @@ def __init__( package_repositories: List[Dict[str, Any]], part_names: Optional[List[str]], adopt_info: Optional[str], + project_name: str, project_vars: Dict[str, str], ): self._assets_dir = assets_dir @@ -79,6 +80,7 @@ def __init__( work_dir=work_dir, cache_dir=cache_dir, ignore_local_sources=["*.snap"], + project_name=project_name, project_vars_part_name=adopt_info, project_vars=project_vars, ) diff --git a/tests/unit/parts/test_parts.py b/tests/unit/parts/test_parts.py index e82dabeb4f..182189b862 100644 --- a/tests/unit/parts/test_parts.py +++ b/tests/unit/parts/test_parts.py @@ -38,6 +38,7 @@ def test_parts_lifecycle_run(parts_data, step_name, new_dir, emitter): part_names=[], package_repositories=[], adopt_info=None, + project_name="test-project", project_vars={"version": "1", "grade": "stable"}, ) lifecycle.run(step_name) @@ -54,6 +55,7 @@ def test_parts_lifecycle_run_bad_step(parts_data, new_dir): part_names=[], package_repositories=[], adopt_info=None, + project_name="test-project", project_vars={"version": "1", "grade": "stable"}, ) with pytest.raises(RuntimeError) as raised: @@ -69,6 +71,7 @@ def test_parts_lifecycle_run_internal_error(parts_data, new_dir, mocker): part_names=[], package_repositories=[], adopt_info=None, + project_name="test-project", project_vars={"version": "1", "grade": "stable"}, ) mocker.patch("craft_parts.LifecycleManager.plan", side_effect=RuntimeError("crash")) @@ -85,6 +88,7 @@ def test_parts_lifecycle_run_parts_error(new_dir): part_names=[], package_repositories=[], adopt_info=None, + project_name="test-project", project_vars={"version": "1", "grade": "stable"}, ) with pytest.raises(errors.PartsLifecycleError) as raised: @@ -102,6 +106,7 @@ def test_parts_lifecycle_clean(parts_data, new_dir, emitter): part_names=[], package_repositories=[], adopt_info=None, + project_name="test-project", project_vars={"version": "1", "grade": "stable"}, ) lifecycle.clean(part_names=None) @@ -116,6 +121,7 @@ def test_parts_lifecycle_clean_parts(parts_data, new_dir, emitter): part_names=[], package_repositories=[], adopt_info=None, + project_name="test-project", project_vars={"version": "1", "grade": "stable"}, ) lifecycle.clean(part_names=["p1"]) From 0b05dbfc2aa0a0d92505896bb3e0098dcf2ba951 Mon Sep 17 00:00:00 2001 From: Claudio Matsuoka Date: Tue, 19 Apr 2022 18:03:49 -0300 Subject: [PATCH 096/167] projects: adoptable fields are optional if adopt-info used Project fields `version`, `summary`, `description` and `grade` are allowed to be unspecified in project if `adopt-info` is used. The final values of such fields are still required and expected to be adopted from metadata, otherwise an error is raised. Signed-off-by: Claudio Matsuoka --- snapcraft/meta/snap_yaml.py | 8 ++--- snapcraft/parts/lifecycle.py | 57 +++++++++++++++++++++++------- snapcraft/projects.py | 33 ++++++++++------- tests/unit/meta/test_snap_yaml.py | 10 ++---- tests/unit/parts/test_lifecycle.py | 54 ++++++++++++++++++++++++++++ tests/unit/test_projects.py | 52 +++++++++++++++------------ 6 files changed, 155 insertions(+), 59 deletions(-) diff --git a/snapcraft/meta/snap_yaml.py b/snapcraft/meta/snap_yaml.py index dd7f8455ff..afec3435ce 100644 --- a/snapcraft/meta/snap_yaml.py +++ b/snapcraft/meta/snap_yaml.py @@ -107,7 +107,7 @@ class SnapMetadata(YamlModel): hooks: Optional[Dict[str, Any]] -def write(project: Project, prime_dir: Path, *, arch: str, version: str, grade: str): +def write(project: Project, prime_dir: Path, *, arch: str): """Create a snap.yaml file.""" meta_dir = prime_dir / "meta" meta_dir.mkdir(parents=True, exist_ok=True) @@ -159,9 +159,9 @@ def write(project: Project, prime_dir: Path, *, arch: str, version: str, grade: snap_metadata = SnapMetadata( name=project.name, title=project.title, - version=version, + version=project.version, summary=project.summary, - description=project.description, + description=project.description, # type: ignore license=project.license, type=project.type, architectures=[arch], @@ -170,7 +170,7 @@ def write(project: Project, prime_dir: Path, *, arch: str, version: str, grade: epoch=project.epoch, apps=snap_apps, confinement=project.confinement, - grade=grade, + grade=project.grade, # type: ignore environment=project.environment, plugs=project.plugs, hooks=project.hooks, diff --git a/snapcraft/parts/lifecycle.py b/snapcraft/parts/lifecycle.py index 8c795fbd13..ac6aae599f 100644 --- a/snapcraft/parts/lifecycle.py +++ b/snapcraft/parts/lifecycle.py @@ -21,6 +21,7 @@ from pathlib import Path from typing import TYPE_CHECKING, Any, Dict, cast +import pydantic from craft_cli import EmitterMode, emit from craft_parts import infos @@ -169,7 +170,7 @@ def _run_command( adopt_info=project.adopt_info, project_vars={ "version": project.version or "", - "grade": project.grade, + "grade": project.grade or "", }, ) if command_name == "clean": @@ -181,24 +182,13 @@ def _run_command( # Generate snap.yaml project_vars = lifecycle.project_vars if step_name == "prime" and not part_names: - version = project_vars["version"] - if not version: - raise errors.SnapcraftError("snap version cannot be empty") - - # FIXME: refactor craft-parts to define validators for project variables - grade = project_vars["grade"] - if grade not in ("stable", "devel"): - raise errors.SnapcraftError( - f"invalid grade {grade!r}, must be either 'stable' or 'devel'" - ) + _update_project_metadata(project, project_vars) emit.progress("Generating snap metadata...") snap_yaml.write( project, lifecycle.prime_dir, arch=lifecycle.target_arch, - version=version, - grade=grade, ) emit.message("Generated snap metadata", intermediate=True) @@ -210,6 +200,47 @@ def _run_command( ) +def _update_project_metadata(project: Project, project_vars: Dict[str, str]) -> None: + """Set project fields using corresponding adopted entries. + + :param project: The project to update. + :param project_vars: The variables updated during lifecycle execution. + + :raises SnapcraftError: If project update failed. + """ + # Update project variables + try: + if project_vars["version"]: + project.version = project_vars["version"] + if project_vars["grade"]: + project.grade = project_vars["grade"] # type: ignore + except pydantic.ValidationError as err: + _raise_formatted_validation_error(err) + raise errors.SnapcraftError(f"error setting variable: {err}") + + # Fields that must not end empty + for field in ("version", "grade", "summary", "description"): + if not getattr(project, field): + raise errors.SnapcraftError( + f"Field {field!r} was not adopted from metadata" + ) + +def _raise_formatted_validation_error(err: pydantic.ValidationError): + error_list = err.errors() + if len(error_list) != 1: + return + + error = error_list[0] + loc = error.get("loc") + msg = error.get("msg") + + if not (loc and msg) or not isinstance(loc, tuple): + return + + varname = ".".join((x for x in loc if isinstance(x, str))) + raise errors.SnapcraftError(f"error setting {varname}: {msg}") + + def _clean_provider(project: Project, parsed_args: "argparse.Namespace") -> None: """Clean the provider environment. diff --git a/snapcraft/projects.py b/snapcraft/projects.py index 27a56bc4f8..ab04130aaa 100644 --- a/snapcraft/projects.py +++ b/snapcraft/projects.py @@ -17,10 +17,7 @@ """Project file definition and helpers.""" import re - -# XXX: mypy doesn't like Literal -from typing import Literal # type: ignore -from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Union +from typing import TYPE_CHECKING, Any, Dict, List, Literal, Optional, Tuple, Union import pydantic from craft_grammar.models import GrammarSingleEntryDictList, GrammarStr, GrammarStrList @@ -39,7 +36,7 @@ class Config: # pylint: disable=too-few-public-methods validate_assignment = True extra = "allow" # FIXME: change to 'forbid' after model complete - allow_mutation = False + allow_mutation = True allow_population_by_field_name = True alias_generator = lambda s: s.replace("_", "-") # noqa: E731 @@ -248,14 +245,14 @@ class Project(ProjectModel): issues: Optional[Union[str, UniqueStrList]] source_code: Optional[str] website: Optional[str] - summary: constr(max_length=78) # type: ignore - description: str + summary: Optional[constr(max_length=78)] # type: ignore + description: Optional[str] type: Literal["app", "base", "gadget", "kernel", "snapd"] = "app" icon: Optional[str] confinement: Literal["classic", "devmode", "strict"] layout: Optional[Dict[str, Dict[str, Any]]] license: Optional[str] - grade: Literal["stable", "devel"] + grade: Optional[Literal["stable", "devel"]] architectures: List[Architecture] = [] assumes: UniqueStrList = [] package_repositories: List[Dict[str, Any]] = [] # handled by repo @@ -289,9 +286,10 @@ def _validate_plugs(cls, plugs): @pydantic.root_validator(pre=True) @classmethod - def _validate_mandatory_version(cls, values): - if "version" not in values and "adopt-info" not in values: - raise ValueError("Snap version is required if not using adopt-info") + def _validate_adoptable_fields(cls, values): + for field in ("version", "summary", "description", "grade"): + if field not in values and "adopt-info" not in values: + raise ValueError(f"Snap {field} is required if not using adopt-info") return values @pydantic.root_validator(pre=True) @@ -330,7 +328,9 @@ def _validate_version(cls, version, values): if not version and "adopt_info" not in values: raise ValueError("Version must be declared if not adopting metadata") - if not re.match(r"^[a-zA-Z0-9](?:[a-zA-Z0-9:.+~-]*[a-zA-Z0-9+~])?$", version): + if version and not re.match( + r"^[a-zA-Z0-9](?:[a-zA-Z0-9:.+~-]*[a-zA-Z0-9+~])?$", version + ): raise ValueError( "Snap versions consist of upper- and lower-case alphanumeric characters, " "as well as periods, colons, plus signs, tildes, and hyphens. They cannot " @@ -340,6 +340,15 @@ def _validate_version(cls, version, values): return version + @pydantic.validator("grade", "summary", "description") + @classmethod + def _validate_adoptable_field(cls, field_value, values, field): + if not field_value and "adopt_info" not in values: + raise ValueError( + f"{field.name.capitalize()} must be declared if not adopting metadata" + ) + return field_value + @pydantic.validator("build_base", always=True) @classmethod def _validate_build_base(cls, build_base, values): diff --git a/tests/unit/meta/test_snap_yaml.py b/tests/unit/meta/test_snap_yaml.py index 235bfb2231..d98257f80e 100644 --- a/tests/unit/meta/test_snap_yaml.py +++ b/tests/unit/meta/test_snap_yaml.py @@ -59,8 +59,6 @@ def test_simple_snap_yaml(simple_project, new_dir): simple_project, prime_dir=Path(new_dir), arch="arch", - version="1.30", - grade="stable", ) yaml_file = Path("meta/snap.yaml") assert yaml_file.is_file() @@ -69,7 +67,7 @@ def test_simple_snap_yaml(simple_project, new_dir): assert content == textwrap.dedent( """\ name: mytest - version: '1.30' + version: 1.29.3 summary: Single-line elevator pitch for your amazing snap description: | This is my-snap's description. You have a paragraph or two to tell the @@ -188,8 +186,6 @@ def test_complex_snap_yaml(complex_project, new_dir): complex_project, prime_dir=Path(new_dir), arch="arch", - version="1.30", - grade="devel", ) yaml_file = Path("meta/snap.yaml") assert yaml_file.is_file() @@ -198,7 +194,7 @@ def test_complex_snap_yaml(complex_project, new_dir): assert content == textwrap.dedent( """\ name: mytest - version: '1.30' + version: 1.29.3 summary: Single-line elevator pitch for your amazing snap description: | This is my-snap's description. You have a paragraph or two to tell the @@ -253,7 +249,7 @@ def test_complex_snap_yaml(complex_project, new_dir): listen_stream: 100 socket_mode: 1 confinement: strict - grade: devel + grade: stable environment: GLOBAL_VARIABLE: test-global-variable plugs: diff --git a/tests/unit/parts/test_lifecycle.py b/tests/unit/parts/test_lifecycle.py index 3102ecf3ce..9aa3761e86 100644 --- a/tests/unit/parts/test_lifecycle.py +++ b/tests/unit/parts/test_lifecycle.py @@ -341,6 +341,60 @@ def test_lifecycle_pack_not_managed(cmd, snapcraft_yaml, new_dir, mocker): ] +@pytest.mark.parametrize("cmd", ["pack", "snap"]) +def test_lifecycle_pack_metadata_error(cmd, snapcraft_yaml, new_dir, mocker): + project = Project.unmarshal(snapcraft_yaml(base="core22")) + run_mock = mocker.patch("snapcraft.parts.PartsLifecycle.run") + mocker.patch("snapcraft.utils.is_managed_mode", return_value=True) + mocker.patch( + "snapcraft.utils.get_managed_environment_home_path", + return_value=new_dir / "home", + ) + mocker.patch( + "snapcraft.parts.PartsLifecycle.project_vars", + new_callable=PropertyMock, + return_value={"version": "0.1", "grade": "invalid"}, # invalid value + ) + pack_mock = mocker.patch("snapcraft.pack.pack_snap") + mocker.patch("snapcraft.meta.snap_yaml.write") + + with pytest.raises(errors.SnapcraftError) as raised: + parts_lifecycle._run_command( + cmd, + project=project, + assets_dir=Path(), + parsed_args=argparse.Namespace( + directory=None, + output=None, + destructive_mode=False, + use_lxd=False, + parts=[], + ), + ) + + assert str(raised.value) == ( + "error setting grade: unexpected value; permitted: 'stable', 'devel'" + ) + assert run_mock.mock_calls == [call("prime")] + assert pack_mock.mock_calls == [] + + +@pytest.mark.parametrize("field", ["version", "summary", "description", "grade"]) +def test_lifecycle_metadata_empty(field, snapcraft_yaml): + """Adoptable fields shouldn't be empty after adoption.""" + yaml_data = snapcraft_yaml(base="core22") + yaml_data.pop(field) + yaml_data["adopt-info"] = "part" + project = Project.unmarshal(yaml_data) + + with pytest.raises(errors.SnapcraftError) as raised: + parts_lifecycle._update_project_metadata( + project, project_vars={"version": "", "grade": ""} + ) + + assert str(raised.value) == f"Field {field!r} was not adopted from metadata" + + def test_lifecycle_run_command_clean(snapcraft_yaml, project_vars, new_dir, mocker): """Clean provider project when called without parts.""" project = Project.unmarshal(snapcraft_yaml(base="core22")) diff --git a/tests/unit/test_projects.py b/tests/unit/test_projects.py index f51fed5fb5..3d125e2658 100644 --- a/tests/unit/test_projects.py +++ b/tests/unit/test_projects.py @@ -132,17 +132,7 @@ def test_app_defaults(self, project_yaml_data): class TestProjectValidation: """Validate top-level project items.""" - @pytest.mark.parametrize( - "field", - [ - "name", - "summary", - "description", - "grade", - "confinement", - "parts", - ], - ) + @pytest.mark.parametrize("field", ["name", "confinement", "parts"]) def test_mandatory_fields(self, field, project_yaml_data): data = project_yaml_data() data.pop(field) @@ -172,19 +162,27 @@ def test_mandatory_base(self, snap_type, requires_base, project_yaml_data): project = Project.unmarshal(data) assert project.base is None - def test_mandatory_version(self, project_yaml_data): + @pytest.mark.parametrize("field", ["version", "summary", "description", "grade"]) + def test_adoptable_fields(self, field, project_yaml_data): data = project_yaml_data() - data.pop("version") - error = "Snap version is required if not using adopt-info" + data.pop(field) + error = f"Snap {field} is required if not using adopt-info" with pytest.raises(errors.ProjectValidationError, match=error): Project.unmarshal(data) - def test_version_not_required(self, project_yaml_data): + @pytest.mark.parametrize("field", ["version", "summary", "description", "grade"]) + def test_adoptable_field_not_required(self, field, project_yaml_data): data = project_yaml_data() - data.pop("version") + data.pop(field) data["adopt-info"] = "part1" project = Project.unmarshal(data) - assert project.version is None + assert getattr(project, field) is None + + @pytest.mark.parametrize("field", ["version", "summary", "description", "grade"]) + def test_adoptable_field_assignment(self, field, project_yaml_data): + data = project_yaml_data() + project = Project.unmarshal(data) + setattr(project, field, None) @pytest.mark.parametrize( "name", @@ -277,8 +275,7 @@ def test_project_type(self, snap_type, project_yaml_data): Project.unmarshal(data) @pytest.mark.parametrize( - "confinement", - ["strict", "devmode", "classic", "_invalid"], + "confinement", ["strict", "devmode", "classic", "_invalid"] ) def test_project_confinement(self, confinement, project_yaml_data): data = project_yaml_data(confinement=confinement) @@ -291,10 +288,7 @@ def test_project_confinement(self, confinement, project_yaml_data): with pytest.raises(errors.ProjectValidationError, match=error): Project.unmarshal(data) - @pytest.mark.parametrize( - "grade", - ["devel", "stable", "_invalid"], - ) + @pytest.mark.parametrize("grade", ["devel", "stable", "_invalid"]) def test_project_grade(self, grade, project_yaml_data): data = project_yaml_data(grade=grade) @@ -306,6 +300,18 @@ def test_project_grade(self, grade, project_yaml_data): with pytest.raises(errors.ProjectValidationError, match=error): Project.unmarshal(data) + @pytest.mark.parametrize("grade", ["devel", "stable", "_invalid"]) + def test_project_grade_assignment(self, grade, project_yaml_data): + data = project_yaml_data() + + project = Project.unmarshal(data) + if grade != "_invalid": + project.grade = grade + else: + error = ".*unexpected value; permitted: 'stable', 'devel'" + with pytest.raises(pydantic.ValidationError, match=error): + project.grade = grade + def test_project_summary_valid(self, project_yaml_data): summary = "x" * 78 project = Project.unmarshal(project_yaml_data(summary=summary)) From 15b55daa8ad30f3f72e8369f3a0dd1128fe4f952 Mon Sep 17 00:00:00 2001 From: Sergio Schvezov Date: Tue, 19 Apr 2022 11:27:11 -0300 Subject: [PATCH 097/167] legacy storeapi: use craft-store - Replace the http_clients implementation in storeapi with craft-store. - Remove no longer necessary tests. - Adapt the code base to the slight API changes (with consideration of being entirely replaced as commands migrate out of legacy). - Remove logout calls for when not logged in. Signed-off-by: Sergio Schvezov --- .github/workflows/spread.yml | 6 +- appveyor.yml | 1 - requirements-devel.txt | 21 +- requirements.txt | 11 +- setup.py | 1 + snapcraft_legacy/_store.py | 130 +++-- snapcraft_legacy/cli/_errors.py | 7 +- snapcraft_legacy/cli/store.py | 78 ++- snapcraft_legacy/storeapi/__init__.py | 2 - snapcraft_legacy/storeapi/_dashboard_api.py | 412 +++++++------ snapcraft_legacy/storeapi/_metadata.py | 23 +- snapcraft_legacy/storeapi/_snap_api.py | 33 +- snapcraft_legacy/storeapi/_store_client.py | 90 ++- .../storeapi/{http_clients => }/agent.py | 0 snapcraft_legacy/storeapi/constants.py | 11 +- .../storeapi/http_clients/__init__.py | 25 - .../storeapi/http_clients/_candid_client.py | 160 ----- .../storeapi/http_clients/_config.py | 119 ---- .../storeapi/http_clients/_http_client.py | 89 --- .../http_clients/_ubuntu_sso_client.py | 233 -------- .../storeapi/http_clients/errors.py | 136 ----- tests/conftest.py | 11 + tests/legacy/fixture_setup/_fixtures.py | 18 +- tests/legacy/unit/commands/__init__.py | 49 +- .../commands/test_edit_validation_sets.py | 5 +- .../legacy/unit/commands/test_export_login.py | 95 +-- tests/legacy/unit/commands/test_gated.py | 6 - tests/legacy/unit/commands/test_list.py | 6 +- tests/legacy/unit/commands/test_list_keys.py | 7 +- .../unit/commands/test_list_revisions.py | 6 +- .../legacy/unit/commands/test_list_tracks.py | 6 +- .../commands/test_list_validation_sets.py | 5 +- tests/legacy/unit/commands/test_login.py | 129 +--- tests/legacy/unit/commands/test_metrics.py | 6 +- tests/legacy/unit/commands/test_register.py | 12 +- .../legacy/unit/commands/test_register_key.py | 29 +- tests/legacy/unit/commands/test_release.py | 5 +- .../unit/commands/test_set_default_track.py | 4 +- tests/legacy/unit/commands/test_sign_build.py | 111 ++-- tests/legacy/unit/commands/test_status.py | 5 +- tests/legacy/unit/commands/test_upload.py | 35 +- .../unit/commands/test_upload_metadata.py | 7 +- tests/legacy/unit/commands/test_validate.py | 2 - tests/legacy/unit/commands/test_whoami.py | 1 + .../legacy/unit/store/http_client/__init__.py | 0 .../store/http_client/test_candid_client.py | 342 ----------- .../unit/store/http_client/test_config.py | 112 ---- .../unit/store/http_client/test_errors.py | 138 ----- .../test_ubuntu_one_auth_client.py | 48 -- .../store/{http_client => }/test_agent.py | 2 +- tests/legacy/unit/store/test_store_client.py | 550 +----------------- tests/spread/general/store/task.yaml | 26 +- 52 files changed, 722 insertions(+), 2644 deletions(-) rename snapcraft_legacy/storeapi/{http_clients => }/agent.py (100%) delete mode 100644 snapcraft_legacy/storeapi/http_clients/__init__.py delete mode 100644 snapcraft_legacy/storeapi/http_clients/_candid_client.py delete mode 100644 snapcraft_legacy/storeapi/http_clients/_config.py delete mode 100644 snapcraft_legacy/storeapi/http_clients/_http_client.py delete mode 100644 snapcraft_legacy/storeapi/http_clients/_ubuntu_sso_client.py delete mode 100644 snapcraft_legacy/storeapi/http_clients/errors.py delete mode 100644 tests/legacy/unit/store/http_client/__init__.py delete mode 100644 tests/legacy/unit/store/http_client/test_candid_client.py delete mode 100644 tests/legacy/unit/store/http_client/test_config.py delete mode 100644 tests/legacy/unit/store/http_client/test_errors.py delete mode 100644 tests/legacy/unit/store/http_client/test_ubuntu_one_auth_client.py rename tests/legacy/unit/store/{http_client => }/test_agent.py (98%) diff --git a/.github/workflows/spread.yml b/.github/workflows/spread.yml index b2fde6fa43..ac7760c204 100644 --- a/.github/workflows/spread.yml +++ b/.github/workflows/spread.yml @@ -84,7 +84,7 @@ jobs: run: | # Secrets cannot be used in conditionals, so this is our dance: # https://github.com/actions/runner/issues/520 - if [[ -n "${{ secrets.SNAP_STORE_MACAROON }}" ]]; then + if [[ -n "${{ secrets.SNAPCRAFT_STORE_CREDENTIALS_STAGING }}" ]]; then echo "::set-output name=RUN::true" else echo "::set-output name=RUN::" @@ -107,8 +107,8 @@ jobs: name: Run spread env: SPREAD_GOOGLE_KEY: ${{ secrets.SPREAD_GOOGLE_KEY }} - SNAP_STORE_MACAROON: ${{ secrets.SNAP_STORE_MACAROON }} - SNAP_STORE_CANDID_MACAROON: ${{ secrets.SNAP_STORE_CANDID_MACAROON }} + SNAPCRAFT_STORE_CREDENTIALS_STAGING: "${{ secrets.SNAPCRAFT_STORE_CREDENTIALS_STAGING }}" + SNAPCRAFT_STORE_CREDENTIALS_STAGING_CANDID: "${{ secrets.SNAPCRAFT_STORE_CREDENTIALS_STAGING_CANDID }}" run: spread google:ubuntu-18.04-64:tests/spread/general/store - name: Discard spread workers diff --git a/appveyor.yml b/appveyor.yml index bc0ec1dc5c..2dd633fc06 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -58,7 +58,6 @@ build_script: test_script: - cmd: | echo "Smoke testing snapcraft.exe..." - dist\snapcraft.exe logout dist\snapcraft.exe version mkdir test cd test diff --git a/requirements-devel.txt b/requirements-devel.txt index 4ade23aa06..1276f35810 100644 --- a/requirements-devel.txt +++ b/requirements-devel.txt @@ -1,4 +1,4 @@ -astroid==2.11.2 +astroid==2.11.3 attrs==21.4.0 black==22.3.0 catkin-pkg==0.4.24 @@ -13,6 +13,7 @@ craft-cli==0.4.0 craft-grammar==1.1.1 craft-parts==1.4.2 craft-providers==1.2.0 +craft-store==2.1.0 cryptography==3.4 Deprecated==1.2.13 dill==0.3.4 @@ -49,7 +50,7 @@ pbr==5.8.1 pexpect==4.8.0 plaster==1.0 plaster-pastedeploy==0.7 -platformdirs==2.5.1 +platformdirs==2.5.2 pluggy==1.0.0 progressbar==2.5 protobuf==3.20.0 @@ -64,12 +65,12 @@ pydocstyle==6.1.1 pyelftools==0.28 pyflakes==2.4.0 pyftpdlib==1.5.6 -pylint==2.13.5 +pylint==2.13.7 pylint-fixme-info==1.0.3 pylint-pytest==1.1.2 pylxd==2.3.1 pymacaroons==0.13.0 -pyparsing==3.0.7 +pyparsing==3.0.8 pyramid==2.0 pyRFC3339==1.1 pytest==7.1.1 @@ -85,7 +86,7 @@ raven==6.10.0 requests==2.27.1 requests-toolbelt==0.9.1 requests-unixsocket==0.3.0 -SecretStorage==3.3.1 +SecretStorage==3.3.2 semantic-version==2.9.0 semver==2.13.0 simplejson==3.17.6 @@ -98,12 +99,12 @@ tinydb==4.7.0 toml==0.10.2 tomli==2.0.1 translationstring==1.4 -types-Deprecated==1.2.5 -types-PyYAML==6.0.5 -types-setuptools==57.4.12 -types-tabulate==0.8.6 +types-Deprecated==1.2.6 +types-PyYAML==6.0.6 +types-setuptools==57.4.14 +types-tabulate==0.8.7 typing-utils==0.1.0 -typing_extensions==4.1.1 +typing_extensions==4.2.0 urllib3==1.26.9 venusian==3.0.0 wadllib==1.3.6 diff --git a/requirements.txt b/requirements.txt index 36ab873865..5c5015aac5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,6 +9,7 @@ craft-cli==0.4.0 craft-grammar==1.1.1 craft-parts==1.4.2 craft-providers==1.2.0 +craft-store==2.1.0 cryptography==3.4 Deprecated==1.2.13 distro==1.7.0 @@ -28,7 +29,7 @@ macaroonbakery==1.3.1 mypy-extensions==0.4.3 oauthlib==3.2.0 overrides==6.1.0 -platformdirs==2.5.1 +platformdirs==2.5.2 progressbar==2.5 protobuf==3.20.0 psutil==5.9.0 @@ -38,7 +39,7 @@ pydantic-yaml==0.6.3 pyelftools==0.28 pylxd==2.3.1 pymacaroons==0.13.0 -pyparsing==3.0.7 +pyparsing==3.0.8 pyRFC3339==1.1 python-dateutil==2.8.2 python-debian==0.1.43 @@ -49,7 +50,7 @@ raven==6.10.0 requests==2.27.1 requests-toolbelt==0.9.1 requests-unixsocket==0.3.0 -SecretStorage==3.3.1 +SecretStorage==3.3.2 semantic-version==2.9.0 semver==2.13.0 simplejson==3.17.6 @@ -57,9 +58,9 @@ six==1.16.0 tabulate==0.8.9 tinydb==4.7.0 toml==0.10.2 -types-Deprecated==1.2.5 +types-Deprecated==1.2.6 typing-utils==0.1.0 -typing_extensions==4.1.1 +typing_extensions==4.2.0 urllib3==1.26.9 wadllib==1.3.6 wrapt==1.14.0 diff --git a/setup.py b/setup.py index 18cd579796..8981b28e05 100755 --- a/setup.py +++ b/setup.py @@ -98,6 +98,7 @@ def recursive_data_files(directory, install_directory): "craft-grammar", "craft-parts", "craft-providers", + "craft-store", "cryptography==3.4", "gnupg", "jsonschema==2.5.1", diff --git a/snapcraft_legacy/_store.py b/snapcraft_legacy/_store.py index 4ae6faa662..a40bd80928 100644 --- a/snapcraft_legacy/_store.py +++ b/snapcraft_legacy/_store.py @@ -23,12 +23,14 @@ import re import subprocess import tempfile -from datetime import datetime +from datetime import datetime, timedelta from pathlib import Path from subprocess import Popen -from typing import TYPE_CHECKING, Any, Dict, Iterable, List, Optional, TextIO, Tuple +from typing import TYPE_CHECKING, Any, Dict, List, Optional, Sequence, Tuple from urllib.parse import urljoin +import craft_store +import requests from tabulate import tabulate from snapcraft_legacy import storeapi, yaml_utils @@ -45,7 +47,11 @@ DeltaGenerationError, DeltaGenerationTooBigError, ) -from snapcraft_legacy.internal.errors import SnapDataExtractionError, ToolMissingError +from snapcraft_legacy.internal.errors import ( + SnapDataExtractionError, + SnapcraftEnvironmentError, + ToolMissingError, +) from snapcraft_legacy.storeapi.constants import DEFAULT_SERIES from snapcraft_legacy.storeapi.metrics import MetricsFilter, MetricsResults @@ -170,41 +176,38 @@ def _try_login( password: str, *, store_client: storeapi.StoreClient, - save: bool = True, - packages: Iterable[Dict[str, str]] = None, - acls: Iterable[str] = None, - channels: Iterable[str] = None, - expires: str = None, - config_fd: TextIO = None, -) -> None: + ttl: int, + acls: Optional[Sequence[str]] = None, + packages: Optional[Sequence[str]] = None, + channels: Optional[Sequence[str]] = None, +) -> str: try: - store_client.login( + credentials = store_client.login( email=email, password=password, - packages=packages, + ttl=ttl, acls=acls, + packages=packages, channels=channels, - expires=expires, - config_fd=config_fd, - save=save, ) - if not config_fd: - print() - echo.wrapped(storeapi.constants.TWO_FACTOR_WARNING) - except storeapi.http_clients.errors.StoreTwoFactorAuthenticationRequired: + print() + echo.wrapped(storeapi.constants.TWO_FACTOR_WARNING) + except craft_store.errors.StoreServerError as store_error: + if "twofactor-required" not in store_error.error_list: + raise one_time_password = echo.prompt("Second-factor auth") - store_client.login( + credentials = store_client.login( email=email, password=password, otp=one_time_password, + ttl=ttl, acls=acls, packages=packages, channels=channels, - expires=expires, - config_fd=config_fd, - save=save, ) + return credentials + def _prompt_login() -> Tuple[str, str]: echo.wrapped("Enter your Ubuntu One e-mail address and password.") @@ -227,52 +230,60 @@ def _prompt_login() -> Tuple[str, str]: def login( *, store_client: storeapi.StoreClient, - packages: Iterable[Dict[str, str]] = None, - save: bool = True, - acls: Iterable[str] = None, - channels: Iterable[str] = None, - expires: str = None, - config_fd: TextIO = None, -) -> None: + ttl: int = int(timedelta(days=365).total_seconds()), + acls: Optional[Sequence[str]] = None, + packages: Optional[Sequence[str]] = None, + channels: Optional[Sequence[str]] = None, +) -> str: if store_client.use_candid() is True: - store_client.login( + credentials = store_client.login( + ttl=ttl, acls=acls, channels=channels, packages=packages, - expires=expires, - config_fd=config_fd, ) else: - if config_fd: - email = "" - password = "" - else: - email, password = _prompt_login() + email, password = _prompt_login() - _try_login( + credentials = _try_login( email, password, store_client=store_client, + ttl=ttl, packages=packages, acls=acls, channels=channels, - expires=expires, - config_fd=config_fd, - save=save, ) # Continue if agreement and namespace conditions are met. _check_dev_agreement_and_namespace_statuses(store_client) + return credentials + def _login_wrapper(method): def login_decorator(self, *args, **kwargs): try: return method(self, *args, **kwargs) - except storeapi.http_clients.errors.InvalidCredentialsError: - print("You are required to login before continuing.") - login(store_client=self) - return method(self, *args, **kwargs) + except craft_store.errors.StoreServerError as store_error: + if ( + store_error.response.status_code == requests.codes.unauthorized + and not os.getenv(storeapi.constants.ENVIRONMENT_STORE_CREDENTIALS) + ): + self.logout() + echo.info("You are required to login before continuing.") + login(store_client=self) + return method(self, *args, **kwargs) + elif ( + store_error.response.status_code == requests.codes.unauthorized + and not os.getenv(storeapi.constants.ENVIRONMENT_STORE_CREDENTIALS) + ): + raise SnapcraftEnvironmentError( + "Provided credentials are no longer valid for the Snap Store. " + "Regenerate them and try again." + ) from store_error + else: + raise return login_decorator @@ -535,11 +546,14 @@ def create_key(name): enabled_names = { account_key["name"] for account_key in account_info["account_keys"] } - except storeapi.http_clients.errors.InvalidCredentialsError: - # Don't require a login here; if they don't have valid credentials, - # then they probably also don't have a key registered with the store - # yet. - enabled_names = set() + except craft_store.errors.StoreServerError as store_error: + if store_error.response.status_code == 401: + # Don't require a login here; if they don't have valid credentials, + # then they probably also don't have a key registered with the store + # yet. + enabled_names = set() + else: + raise if name in enabled_names: raise storeapi.errors.KeyAlreadyRegisteredError(name) subprocess.check_call(["snap", "create-key", name]) @@ -558,8 +572,12 @@ def _maybe_prompt_for_key(name): def register_key(name) -> None: key = _maybe_prompt_for_key(name) - store_client = StoreClientCLI() - login(store_client=store_client, acls=["modify_account_key"], save=False) + store_client = StoreClientCLI(ephemeral=True) + login( + store_client=store_client, + acls=["modify_account_key"], + ttl=int(timedelta(days=1).total_seconds()), + ) logger.info("Registering key ...") account_info = store_client.get_account_information() @@ -813,8 +831,8 @@ def _upload_delta( raise storeapi.errors.StoreDeltaApplicationError(str(e)) else: raise - except storeapi.http_clients.errors.StoreServerError as e: - raise storeapi.errors.StoreUploadError(snap_name, e.response) + except craft_store.errors.StoreServerError as store_error: + raise storeapi.errors.StoreUploadError(snap_name, store_error.response) finally: if os.path.isfile(delta_filename): try: @@ -937,7 +955,7 @@ def download( hash. :returns: A sha3_384 of the file that was or would have been downloaded. """ - return StoreClientCLI().download( + return StoreClientCLI.download( snap_name, risk=risk, track=track, diff --git a/snapcraft_legacy/cli/_errors.py b/snapcraft_legacy/cli/_errors.py index 48bcfdf0c1..7eb72c2ed5 100644 --- a/snapcraft_legacy/cli/_errors.py +++ b/snapcraft_legacy/cli/_errors.py @@ -23,6 +23,7 @@ from typing import Dict import click +import craft_store from raven import Client as RavenClient from raven.transport import RequestsHTTPTransport @@ -84,8 +85,10 @@ def _is_reportable_error(exc_info) -> bool: return exc_info[1].get_reportable() # Report non-snapcraft errors. - if not issubclass(exc_info[0], errors.SnapcraftError) and not isinstance( - exc_info[1], KeyboardInterrupt + if ( + not issubclass(exc_info[0], errors.SnapcraftError) + and not issubclass(exc_info[0], craft_store.errors.CraftStoreError) + and not isinstance(exc_info[1], KeyboardInterrupt) ): return True diff --git a/snapcraft_legacy/cli/store.py b/snapcraft_legacy/cli/store.py index 414bf078fa..b5d512e7d8 100644 --- a/snapcraft_legacy/cli/store.py +++ b/snapcraft_legacy/cli/store.py @@ -14,15 +14,18 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +import base64 +import contextlib import functools import json import operator import os import stat import sys -from datetime import date, timedelta +from datetime import date, datetime, timedelta from textwrap import dedent from typing import Dict, List, Optional, Set, Union +from urllib.parse import urlparse import click from tabulate import tabulate @@ -37,6 +40,11 @@ from ._metrics import convert_metrics_to_table from ._review import review_snap +_VALID_DATE_FORMATS = [ + "%Y-%m-%d", + "%Y-%m-%dT%H:%M:%SZ", +] + _MESSAGE_REGISTER_PRIVATE = dedent( """\ Even though this is private snap, you should think carefully about @@ -731,50 +739,62 @@ def export_login( if acls: acl_list = acls.split(",") - store_client = storeapi.StoreClient() - snapcraft_legacy.login( + if expires is not None: + for date_format in _VALID_DATE_FORMATS: + with contextlib.suppress(ValueError): + expiry_date = datetime.strptime(expires, date_format) + break + else: + valid_formats = formatting_utils.humanize_list(_VALID_DATE_FORMATS, "or") + raise click.BadParameter( + message=f"The expiry follow an ISO 8601 format ({valid_formats})" + ) + + ttl = int((expiry_date - datetime.now()).total_seconds()) + else: + # Default to 1y + ttl = int((datetime.now() + timedelta(days=365)).timestamp()) + + store_client = storeapi.StoreClient(ephemeral=True) + credentials = snapcraft_legacy.login( store_client=store_client, packages=snap_list, channels=channel_list, acls=acl_list, - expires=expires, - save=False, + ttl=ttl, ) # Support a login_file of '-', which indicates a desire to print to stdout if login_file.strip() == "-": echo.info("\nExported login starts on next line:") - store_client.export_login(config_fd=sys.stdout, encode=True) - print() + + echo.info(credentials) preamble = "Login successfully exported and printed above" - login_action = 'echo "" | snapcraft login --with -' + credentials_action = f"{storeapi.constants.ENVIRONMENT_STORE_CREDENTIALS}='' snapcraft " else: # This is sensitive-- it should only be accessible by the owner private_open = functools.partial(os.open, mode=0o600) - # mypy doesn't have the opener arg in its stub. Ignore its warning - with open(login_file, "w", opener=private_open) as f: # type: ignore - store_client.export_login(config_fd=f) + with open(login_file, "w", opener=private_open) as login_fd: + print(credentials, file=login_fd) # Now that the file has been written, we can just make it # owner-readable os.chmod(login_file, stat.S_IRUSR) - preamble = "Login successfully exported to {0!r}".format(login_file) - login_action = "snapcraft login --with {0}".format(login_file) + preamble = f"Login successfully exported to {login_file!r}" + credentials_action = f"{storeapi.constants.ENVIRONMENT_STORE_CREDENTIALS}=$(cat {login_file}) snapcraft " print() echo.info( dedent( - """\ - {}. This can now be used with + f"""\ + {preamble}. Any store action that now requires authentication can be used by running - {} + {credentials_action} - """.format( - preamble, login_action - ) + """ ) ) try: @@ -811,6 +831,13 @@ def login(login_file, experimental_login: bool): If you do not have an Ubuntu One account, you can create one at https://snapcraft.io/account """ + if login_file: + raise click.BadOptionUsage( + "--with", + "--with is no longer supported, export the auth to the environment " + f"variable {storeapi.constants.ENVIRONMENT_STORE_CREDENTIALS!r} instead", + ) + if experimental_login: raise click.BadOptionUsage( "--experimental-login", @@ -819,18 +846,9 @@ def login(login_file, experimental_login: bool): ) store_client = storeapi.StoreClient() - snapcraft_legacy.login(store_client=store_client, config_fd=login_file) + snapcraft_legacy.login(store_client=store_client) - print() - if login_file: - try: - human_acls = _human_readable_acls(store_client) - echo.info("Login successful. You now have these capabilities:\n") - echo.info(human_acls) - except NotImplementedError: - echo.info("Login successful.") - else: - echo.info("Login successful.") + echo.info("Login successful.") @storecli.command() diff --git a/snapcraft_legacy/storeapi/__init__.py b/snapcraft_legacy/storeapi/__init__.py index 7249b6a364..9b99cfed50 100644 --- a/snapcraft_legacy/storeapi/__init__.py +++ b/snapcraft_legacy/storeapi/__init__.py @@ -20,7 +20,6 @@ from . import channels # isort:skip from . import constants # isort:skip from . import status # isort:skip -from . import http_clients # isort: skip logger = logging.getLogger(__name__) @@ -33,7 +32,6 @@ "channels", "constants", "status", - "http_clients", "SnapAPI", "StoreClient", ] diff --git a/snapcraft_legacy/storeapi/_dashboard_api.py b/snapcraft_legacy/storeapi/_dashboard_api.py index 7075c6d26e..9a8d5b5488 100644 --- a/snapcraft_legacy/storeapi/_dashboard_api.py +++ b/snapcraft_legacy/storeapi/_dashboard_api.py @@ -16,14 +16,14 @@ import json import logging -import os -from typing import Any, Dict, Iterable, List, Optional +from typing import Any, Dict, List, Optional from urllib.parse import urlencode, urljoin +import craft_store import requests from simplejson.scanner import JSONDecodeError -from . import _metadata, constants, errors, http_clients, metrics +from . import _metadata, constants, errors, metrics from ._requests import Requests from ._status_tracker import StatusTracker from .v2 import channel_map, releases, validation_sets, whoami @@ -38,79 +38,65 @@ class DashboardAPI(Requests): at https://dashboard.snapcraft.io/docs/. """ - def __init__(self, auth_client: http_clients.AuthClient) -> None: + def __init__(self, auth_client: craft_store.BaseClient) -> None: + super().__init__() + self._auth_client = auth_client - self._root_url = os.environ.get( - "STORE_DASHBOARD_URL", constants.STORE_DASHBOARD_URL - ) def _request(self, method: str, urlpath: str, **kwargs) -> requests.Response: - url = urljoin(self._root_url, urlpath) + url = urljoin(self._auth_client._base_url, urlpath) response = self._auth_client.request(method, url, **kwargs) logger.debug("Call to %s returned: %s", url, response.text) return response - def get_macaroon( - self, - *, - acls: Iterable[str], - packages: Optional[Iterable[Dict[str, str]]] = None, - channels: Optional[Iterable[str]] = None, - expires: Optional[Iterable[str]] = None, - ): - data: Dict[str, Any] = {"permissions": acls} - if packages is not None: - data["packages"] = packages - if channels is not None: - data["channels"] = channels - if expires is not None: - data["expires"] = expires - - headers = {"Content-Type": "application/json", "Accept": "application/json"} - - if isinstance(self._auth_client, http_clients.CandidClient): - urlpath = "/api/v2/tokens" - else: - urlpath = "/dev/api/acl/" - - response = self.post(urlpath, json=data, headers=headers, auth_header=False) - - if response.ok: - return response.json()["macaroon"] - else: - raise errors.GeneralStoreError("Failed to get macaroon", response) - def verify_acl(self): - if not isinstance(self._auth_client, http_clients.UbuntuOneAuthClient): + if not isinstance(self._auth_client, craft_store.UbuntuOneStoreClient): raise NotImplementedError("Only supports UbuntuOneAuthClient.") - response = self.post( - "/dev/api/acl/verify/", - json={"auth_data": {"authorization": self._auth_client.auth}}, - headers={"Accept": "application/json"}, - auth_header=False, - ) - if response.ok: - return response.json() - else: - raise errors.StoreAccountInformationError(response) + try: + response = self.post( + "/dev/api/acl/verify/", + json={ + "auth_data": { + "authorization": self._auth_client._auth.get_credentials() + } + }, + headers={"Accept": "application/json"}, + ) + except craft_store.errors.StoreServerError as store_error: + raise errors.StoreAccountInformationError( + store_error.response + ) from store_error + + return response.json() def get_account_information(self) -> Dict[str, Any]: - response = self.get("/dev/api/account", headers={"Accept": "application/json"}) - if response.ok: - return response.json() - else: - raise errors.StoreAccountInformationError(response) + try: + response = self.get( + "/dev/api/account", headers={"Accept": "application/json"} + ) + except craft_store.errors.StoreServerError as store_error: + raise errors.StoreAccountInformationError( + store_error.response + ) from store_error + + return response.json() def register_key(self, account_key_request): data = {"account_key_request": account_key_request} - response = self.post( - "/dev/api/account/account-key", - data=json.dumps(data), - headers={"Content-Type": "application/json", "Accept": "application/json"}, - ) - if not response.ok: - raise errors.StoreKeyRegistrationError(response) + try: + self.post( + "/dev/api/account/account-key", + json=data, + headers={ + "Content-Type": "application/json", + "Accept": "application/json", + }, + ) + except craft_store.errors.StoreServerError as store_error: + raise errors.StoreKeyRegistrationError( + store_error.response + ) from store_error def register( self, snap_name: str, *, is_private: bool, series: str, store_id: Optional[str] @@ -118,23 +104,32 @@ def register( data = dict(snap_name=snap_name, is_private=is_private, series=series) if store_id is not None: data["store"] = store_id - response = self.post( - "/dev/api/register-name/", - data=json.dumps(data), - headers={"Content-Type": "application/json"}, - ) - if not response.ok: - raise errors.StoreRegistrationError(snap_name, response) + try: + self.post( + "/dev/api/register-name/", + json=data, + headers={"Content-Type": "application/json"}, + ) + except craft_store.errors.StoreServerError as store_error: + raise errors.StoreRegistrationError( + snap_name, store_error.response + ) from store_error - def snap_upload_precheck(self, snap_name): + def snap_upload_precheck(self, snap_name) -> None: data = {"name": snap_name, "dry_run": True} - response = self.post( - "/dev/api/snap-push/", - data=json.dumps(data), - headers={"Content-Type": "application/json", "Accept": "application/json"}, - ) - if not response.ok: - raise errors.StoreUploadError(snap_name, response) + try: + self.post( + "/dev/api/snap-push/", + json=data, + headers={ + "Content-Type": "application/json", + "Accept": "application/json", + }, + ) + except craft_store.errors.StoreServerError as store_error: + raise errors.StoreUploadError( + snap_name, store_error.response + ) from store_error def snap_upload_metadata( self, @@ -164,27 +159,37 @@ def snap_upload_metadata( data["built_at"] = built_at if channels is not None: data["channels"] = channels - response = self.post( - "/dev/api/snap-push/", - data=json.dumps(data), - headers={"Content-Type": "application/json", "Accept": "application/json"}, - ) - if not response.ok: - raise errors.StoreUploadError(data["name"], response) + try: + response = self.post( + "/dev/api/snap-push/", + json=data, + headers={ + "Content-Type": "application/json", + "Accept": "application/json", + }, + ) + except craft_store.errors.StoreServerError as store_error: + raise errors.StoreUploadError( + data["name"], store_error.response + ) from store_error return StatusTracker(response.json()["status_details_url"]) def upload_metadata(self, snap_id, snap_name, metadata, force): """Upload the metadata to SCA.""" metadata_handler = _metadata.StoreMetadataHandler( - request_method=self._request, snap_id=snap_id, snap_name=snap_name, + request_method=self._request, + snap_id=snap_id, + snap_name=snap_name, ) metadata_handler.upload(metadata, force) def upload_binary_metadata(self, snap_id, snap_name, metadata, force): """Upload the binary metadata to SCA.""" metadata_handler = _metadata.StoreMetadataHandler( - request_method=self._request, snap_id=snap_id, snap_name=snap_name, + request_method=self._request, + snap_id=snap_id, + snap_name=snap_name, ) metadata_handler.upload_binary(metadata, force) @@ -204,13 +209,19 @@ def snap_release( "percentage": progressive_percentage, "paused": False, } - response = self.post( - "/dev/api/snap-release/", - data=json.dumps(data), - headers={"Content-Type": "application/json", "Accept": "application/json"}, - ) - if not response.ok: - raise errors.StoreReleaseError(data["name"], response) + try: + response = self.post( + "/dev/api/snap-release/", + json=data, + headers={ + "Content-Type": "application/json", + "Accept": "application/json", + }, + ) + except craft_store.errors.StoreServerError as store_error: + raise errors.StoreReleaseError( + data["name"], store_error.response + ) from store_error response_json = response.json() @@ -231,14 +242,20 @@ def push_assertion(self, snap_id, assertion, endpoint, force): if force: url = url + "?ignore_revoked_uploads" - response = self.put( - url, - json=data, - headers={"Content-Type": "application/json", "Accept": "application/json"}, - ) + try: + response = self.put( + url, + json=data, + headers={ + "Content-Type": "application/json", + "Accept": "application/json", + }, + ) + except craft_store.errors.StoreServerError as craft_error: + raise errors.StoreValidationError( + snap_id, craft_error.response + ) from craft_error - if not response.ok: - raise errors.StoreValidationError(snap_id, response) try: response_json = response.json() except JSONDecodeError: @@ -253,13 +270,20 @@ def push_assertion(self, snap_id, assertion, endpoint, force): return response_json def get_assertion(self, snap_id, endpoint, params=None): - response = self.get( - f"/dev/api/snaps/{snap_id}/{endpoint}", - headers={"Content-Type": "application/json", "Accept": "application/json"}, - params=params, - ) - if not response.ok: - raise errors.StoreValidationError(snap_id, response) + try: + response = self.get( + f"/dev/api/snaps/{snap_id}/{endpoint}", + headers={ + "Content-Type": "application/json", + "Accept": "application/json", + }, + params=params, + ) + except craft_store.errors.StoreServerError as craft_error: + raise errors.StoreValidationError( + snap_id, craft_error.response + ) from craft_error + try: response_json = response.json() except JSONDecodeError: @@ -279,9 +303,10 @@ def push_snap_build(self, snap_id, snap_build): headers = { "Content-Type": "application/json", } - response = self.post(url, data=data, headers=headers) - if not response.ok: - raise errors.StoreSnapBuildError(response) + try: + self.post(url, data=data, headers=headers) + except craft_store.errors.StoreServerError as craft_error: + raise errors.StoreSnapBuildError(craft_error.response) from craft_error def snap_status(self, snap_id, series, arch): qs = {} @@ -292,12 +317,18 @@ def snap_status(self, snap_id, series, arch): url = "/dev/api/snaps/" + snap_id + "/state" if qs: url += "?" + urlencode(qs) - response = self.get( - url, - headers={"Content-Type": "application/json", "Accept": "application/json"}, - ) - if not response.ok: - raise errors.StoreSnapStatusError(response, snap_id, series, arch) + try: + response = self.get( + url, + headers={ + "Content-Type": "application/json", + "Accept": "application/json", + }, + ) + except craft_store.errors.StoreServerError as craft_error: + raise errors.StoreSnapStatusError( + craft_error.response, snap_id, series, arch + ) from craft_error response_json = response.json() @@ -308,42 +339,51 @@ def close_channels(self, snap_id, channel_names): data = {"channels": channel_names} headers = {"Content-Type": "application/json", "Accept": "application/json"} - response = self.post(url, data=json.dumps(data), headers=headers) - if not response.ok: - raise errors.StoreChannelClosingError(response) + try: + response = self.post(url, json=data, headers=headers) + except craft_store.errors.StoreServerError as craft_error: + raise errors.StoreChannelClosingError(craft_error.response) from craft_error try: results = response.json() - return results["closed_channels"], results["channel_map_tree"] except (JSONDecodeError, KeyError): logger.debug( "Invalid response from the server on channel closing:\n" - "{} {}\n{}".format( - response.status_code, response.reason, response.content - ) + f"{response.status_code} {response.reason}\n{response.content}" ) raise errors.StoreChannelClosingError(response) + return results["closed_channels"], results["channel_map_tree"] + def sign_developer_agreement(self, latest_tos_accepted=False): data = {"latest_tos_accepted": latest_tos_accepted} - response = self.post( - "/dev/api/agreement/", - json=data, - headers={"Content-Type": "application/json", "Accept": "application/json"}, - ) + try: + response = self.post( + "/dev/api/agreement/", + json=data, + headers={ + "Content-Type": "application/json", + "Accept": "application/json", + }, + ) + except craft_store.errors.StoreServerError as store_error: + raise errors.DeveloperAgreementSignError( + store_error.response + ) from store_error - if not response.ok: - raise errors.DeveloperAgreementSignError(response) return response.json() def get_snap_channel_map(self, *, snap_name: str) -> channel_map.ChannelMap: - response = self.get( - f"/api/v2/snaps/{snap_name}/channel-map", - headers={"Content-Type": "application/json", "Accept": "application/json"}, - ) - - if not response.ok: - raise errors.StoreSnapChannelMapError(snap_name=snap_name) + try: + response = self.get( + f"/api/v2/snaps/{snap_name}/channel-map", + headers={ + "Content-Type": "application/json", + "Accept": "application/json", + }, + ) + except craft_store.errors.StoreServerError as store_error: + raise errors.StoreSnapChannelMapError(snap_name=snap_name) from store_error return channel_map.ChannelMap.unmarshal(response.json()) @@ -354,11 +394,13 @@ def get_metrics( data = {"filters": [f.marshal() for f in filters]} headers = {"Content-Type": "application/json", "Accept": "application/json"} - response = self.post(url, data=json.dumps(data), headers=headers) - if not response.ok: + try: + response = self.post(url, json=data, headers=headers) + + except craft_store.errors.StoreServerError as store_error: raise errors.StoreMetricsError( - filters=filters, response=response, snap_name=snap_name - ) + filters=filters, response=store_error.response, snap_name=snap_name + ) from store_error try: results = response.json() @@ -369,57 +411,66 @@ def get_metrics( ) from error def get_snap_releases(self, *, snap_name: str) -> releases.Releases: - response = self.get( - f"/api/v2/snaps/{snap_name}/releases", - headers={"Content-Type": "application/json", "Accept": "application/json"}, - ) - - if not response.ok: - raise errors.StoreSnapChannelMapError(snap_name=snap_name) + try: + response = self.get( + f"/api/v2/snaps/{snap_name}/releases", + headers={ + "Content-Type": "application/json", + "Accept": "application/json", + }, + ) + except craft_store.errors.StoreServerError as store_error: + raise errors.StoreSnapChannelMapError(snap_name=snap_name) from store_error return releases.Releases.unmarshal(response.json()) def whoami(self) -> whoami.WhoAmI: - response = self.get( - "/api/v2/tokens/whoami", - headers={"Content-Type": "application/json", "Accept": "application/json"}, - ) - - if not response.ok: - raise errors.GeneralStoreError(message="whoami failed.", response=response) + try: + response = self.get( + "/api/v2/tokens/whoami", + headers={ + "Content-Type": "application/json", + "Accept": "application/json", + }, + ) + except craft_store.errors.StoreServerError as store_error: + raise errors.GeneralStoreError( + message="whoami failed.", response=store_error.response + ) from store_error return whoami.WhoAmI.unmarshal(response.json()) def post_validation_sets_build_assertion( self, validation_sets_data: Dict[str, Any] ) -> validation_sets.BuildAssertion: - url = "/api/v2/validation-sets/build-assertion" - response = self.post( - url, - headers={"Accept": "application/json", "Content-Type": "application/json"}, - json=validation_sets_data, - ) - - if not response.ok: - raise errors.StoreValidationSetsError(response) + try: + response = self.post( + "/api/v2/validation-sets/build-assertion", + headers={ + "Accept": "application/json", + "Content-Type": "application/json", + }, + json=validation_sets_data, + ) + except craft_store.errors.StoreServerError as store_error: + raise errors.StoreValidationSetsError(store_error.response) from store_error return validation_sets.BuildAssertion.unmarshal(response.json()) def post_validation_sets( self, signed_validation_sets: bytes ) -> validation_sets.ValidationSets: - url = "/api/v2/validation-sets" - response = self.post( - url, - headers={ - "Accept": "application/json", - "Content-Type": "application/x.ubuntu.assertion", - }, - data=signed_validation_sets, - ) - - if not response.ok: - raise errors.StoreValidationSetsError(response) + try: + response = self.post( + "/api/v2/validation-sets", + headers={ + "Accept": "application/json", + "Content-Type": "application/x.ubuntu.assertion", + }, + data=signed_validation_sets, + ) + except craft_store.errors.StoreServerError as store_error: + raise errors.StoreValidationSetsError(store_error.response) from store_error return validation_sets.ValidationSets.unmarshal(response.json()) @@ -432,10 +483,11 @@ def get_validation_sets( params = dict() if sequence is not None: params["sequence"] = sequence - - response = self.get(url, headers={"Accept": "application/json"}, params=params) - - if not response.ok: - raise errors.StoreValidationSetsError(response) + try: + response = self.get( + url, headers={"Accept": "application/json"}, params=params + ) + except craft_store.errors.StoreServerError as store_error: + raise errors.StoreValidationSetsError(store_error.response) from store_error return validation_sets.ValidationSets.unmarshal(response.json()) diff --git a/snapcraft_legacy/storeapi/_metadata.py b/snapcraft_legacy/storeapi/_metadata.py index 1eadf6880d..a15b8ff2ae 100644 --- a/snapcraft_legacy/storeapi/_metadata.py +++ b/snapcraft_legacy/storeapi/_metadata.py @@ -18,6 +18,8 @@ import json import os +import craft_store + from snapcraft_legacy.storeapi.errors import StoreMetadataError @@ -43,12 +45,12 @@ def upload(self, metadata, force): "Accept": "application/json", } method = "PUT" if force else "POST" - response = self._request( - method, url, data=json.dumps(metadata), headers=headers - ) - - if not response.ok: - raise StoreMetadataError(self.snap_name, response, metadata) + try: + self._request(method, url, json=metadata, headers=headers) + except craft_store.errors.StoreServerError as store_error: + raise StoreMetadataError( + self.snap_name, store_error.response, metadata + ) from store_error def _current_binary_metadata(self): """Get current icons and screenshots as set in the store.""" @@ -119,8 +121,11 @@ def upload_binary(self, metadata, force): "Accept": "application/json", } method = "PUT" if force else "POST" - response = self._request(method, url, data=data, files=files, headers=headers) - if not response.ok: + try: + self._request(method, url, data=data, files=files, headers=headers) + except craft_store.errors.StoreServerError as store_error: icon = metadata.get("icon") icon_name = os.path.basename(icon.name) if icon else None - raise StoreMetadataError(self.snap_name, response, {"icon": icon_name}) + raise StoreMetadataError( + self.snap_name, store_error.response, {"icon": icon_name} + ) from store_error diff --git a/snapcraft_legacy/storeapi/_snap_api.py b/snapcraft_legacy/storeapi/_snap_api.py index 868610d58c..e3e9ac64ef 100644 --- a/snapcraft_legacy/storeapi/_snap_api.py +++ b/snapcraft_legacy/storeapi/_snap_api.py @@ -19,11 +19,10 @@ from typing import Dict, Optional from urllib.parse import urljoin +import craft_store import requests -from snapcraft_legacy.storeapi import http_clients - -from . import constants, errors +from . import agent, constants, errors from ._requests import Requests from .info import SnapInfo @@ -38,9 +37,9 @@ class SnapAPI(Requests): at http://api.snapcraft.io/docs/. """ - def __init__(self, client: Optional[http_clients.Client] = None): + def __init__(self, client: Optional[craft_store.HTTPClient] = None): if client is None: - client = http_clients.Client() + client = craft_store.HTTPClient(user_agent=agent.get_user_agent()) self._client = client self._root_url = os.environ.get("STORE_API_URL", constants.STORE_API_URL) @@ -94,13 +93,17 @@ def get_info(self, snap_name: str, *, arch: str = None) -> SnapInfo: params["architecture"] = arch logger.debug("Getting information for {}".format(snap_name)) url = "/v2/snaps/info/{}".format(snap_name) - resp = self.get(url, headers=headers, params=params) - if resp.status_code == 404: - raise errors.SnapNotFoundError(snap_name=snap_name, arch=arch) - resp.raise_for_status() + try: + response = self.get(url, headers=headers, params=params) + except craft_store.errors.StoreServerError as store_error: + if store_error.response.status_code == 404: + raise errors.SnapNotFoundError( + snap_name=snap_name, arch=arch + ) from store_error + raise - return SnapInfo(resp.json()) + return SnapInfo(response.json()) def get_assertion( self, assertion_type: str, snap_id: str @@ -115,7 +118,11 @@ def get_assertion( headers = self._get_default_headers(api="v1") logger.debug("Getting snap-declaration for {}".format(snap_id)) url = f"/api/v1/snaps/assertions/{assertion_type}/{constants.DEFAULT_SERIES}/{snap_id}" - response = self.get(url, headers=headers) - if response.status_code != 200: - raise errors.SnapNotFoundError(snap_id=snap_id) + try: + response = self.get(url, headers=headers) + except craft_store.errors.StoreServerError as store_error: + if store_error.response.status_code == 404: + raise errors.SnapNotFoundError(snap_id=snap_id) from store_error + raise + return response.json() diff --git a/snapcraft_legacy/storeapi/_store_client.py b/snapcraft_legacy/storeapi/_store_client.py index 56bc7b770a..5042076f57 100644 --- a/snapcraft_legacy/storeapi/_store_client.py +++ b/snapcraft_legacy/storeapi/_store_client.py @@ -16,14 +16,16 @@ import logging import os +import platform from time import sleep -from typing import Any, Dict, Iterable, List, Optional, TextIO, Union +from typing import Any, Dict, Iterable, List, Optional, Sequence, Union +import craft_store import requests from snapcraft_legacy.internal.indicators import download_requests_stream -from . import _upload, constants, errors, http_clients, metrics +from . import _upload, agent, constants, errors, metrics from ._dashboard_api import DashboardAPI from ._snap_api import SnapAPI from ._up_down_client import UpDownClient @@ -33,16 +35,46 @@ logger = logging.getLogger(__name__) +def _get_hostname() -> str: + """Return the computer's network name or UNNKOWN if it cannot be determined.""" + hostname = platform.node() + if not hostname: + hostname = "UNKNOWN" + return hostname + + class StoreClient: """High-level client Snap resources.""" - def __init__(self) -> None: - self.client = http_clients.Client() + def __init__(self, ephemeral=False) -> None: + user_agent = agent.get_user_agent() + + self._root_url = os.getenv("STORE_DASHBOARD_URL", constants.STORE_DASHBOARD_URL) + storage_base_url = os.getenv("STORE_UPLOAD_URL", constants.STORE_UPLOAD_URL) + + self.client = craft_store.HTTPClient(user_agent=user_agent) if self.use_candid() is True: - self.auth_client: http_clients.AuthClient = http_clients.CandidClient() + self.auth_client = craft_store.StoreClient( + application_name="snapcraft", + base_url=self._root_url, + storage_base_url=storage_base_url, + endpoints=craft_store.endpoints.SNAP_STORE, + user_agent=user_agent, + environment_auth=constants.ENVIRONMENT_STORE_CREDENTIALS, + ephemeral=ephemeral, + ) else: - self.auth_client = http_clients.UbuntuOneAuthClient() + self.auth_client = craft_store.UbuntuOneStoreClient( + application_name="snapcraft", + base_url=self._root_url, + storage_base_url=storage_base_url, + auth_url=os.getenv("UBUNTU_ONE_SSO_URL", constants.UBUNTU_ONE_SSO_URL), + endpoints=craft_store.endpoints.U1_SNAP_STORE, + user_agent=user_agent, + environment_auth=constants.ENVIRONMENT_STORE_CREDENTIALS, + ephemeral=ephemeral, + ) self.snap = SnapAPI(self.client) self.dashboard = DashboardAPI(self.auth_client) @@ -55,16 +87,12 @@ def use_candid() -> bool: def login( self, *, - acls: Iterable[str] = None, - channels: Iterable[str] = None, - packages: Iterable[Dict[str, str]] = None, - expires: str = None, - config_fd: TextIO = None, + ttl: int, + acls: Optional[Sequence[str]] = None, + channels: Optional[Sequence[str]] = None, + packages: Optional[Sequence[str]] = None, **kwargs, - ) -> None: - if config_fd is not None: - return self.auth_client.login(config_fd=config_fd, **kwargs) - + ) -> str: if acls is None: acls = [ "package_access", @@ -76,16 +104,20 @@ def login( "package_update", ] - macaroon = self.dashboard.get_macaroon( - acls=acls, - packages=packages, + if channels is None: + channels = [] + + if packages is None: + packages = [] + + return self.auth_client.login( + permissions=acls, + description=f"snapcraft@{_get_hostname()}", + ttl=ttl, channels=channels, - expires=expires, + packages=[craft_store.endpoints.Package(p, "snap") for p in packages], + **kwargs, ) - self.auth_client.login(macaroon=macaroon, **kwargs) - - def export_login(self, *, config_fd: TextIO, encode=False) -> None: - self.auth_client.export_login(config_fd=config_fd, encode=encode) def logout(self): self.auth_client.logout() @@ -219,8 +251,9 @@ def get_validation_sets( def close_channels(self, snap_id, channel_names): return self.dashboard.close_channels(snap_id, channel_names) + @classmethod def download( - self, + cls, snap_name, *, risk: str, @@ -229,7 +262,7 @@ def download( arch: Optional[str] = None, except_hash: str = "", ): - snap_info = self.snap.get_info(snap_name) + snap_info = SnapAPI().get_info(snap_name) channel_mapping = snap_info.get_channel_mapping( risk=risk, track=track, arch=arch ) @@ -239,12 +272,13 @@ def download( try: channel_mapping.download.verify(download_path) except errors.StoreDownloadError: - self._download_snap(channel_mapping.download, download_path) + cls._download_snap(channel_mapping.download, download_path) channel_mapping.download.verify(download_path) return channel_mapping.download.sha3_384 - def _download_snap(self, download_details, download_path): + @classmethod + def _download_snap(cls, download_details, download_path): # we only resume when redirected to our CDN since we use internap's # special sauce. total_read = 0 @@ -265,7 +299,7 @@ def _download_snap(self, download_details, download_path): if resume_possible and os.path.exists(download_path): total_read = os.path.getsize(download_path) headers["Range"] = "bytes={}-".format(total_read) - request = self.client.request( + request = craft_store.HTTPClient(user_agent=agent.get_user_agent()).request( "GET", download_url, headers=headers, stream=True ) request.raise_for_status() diff --git a/snapcraft_legacy/storeapi/http_clients/agent.py b/snapcraft_legacy/storeapi/agent.py similarity index 100% rename from snapcraft_legacy/storeapi/http_clients/agent.py rename to snapcraft_legacy/storeapi/agent.py diff --git a/snapcraft_legacy/storeapi/constants.py b/snapcraft_legacy/storeapi/constants.py index 436c09175e..b03d4fc794 100644 --- a/snapcraft_legacy/storeapi/constants.py +++ b/snapcraft_legacy/storeapi/constants.py @@ -21,9 +21,11 @@ SCAN_STATUS_POLL_DELAY = 5 SCAN_STATUS_POLL_RETRIES = 5 -STORE_DASHBOARD_URL = "https://dashboard.snapcraft.io/" -STORE_API_URL = "https://api.snapcraft.io/" -STORE_UPLOAD_URL = "https://upload.apps.ubuntu.com/" +STORE_DASHBOARD_URL = "https://dashboard.snapcraft.io" +STORE_API_URL = "https://api.snapcraft.io" +STORE_UPLOAD_URL = "storage.snapcraftcontent.com" + +UBUNTU_ONE_SSO_URL = "https://login.ubuntu.com" # Messages and warnings. MISSING_AGREEMENT = "Developer has not signed agreement." @@ -47,6 +49,9 @@ "https://help.ubuntu.com/community/SSO/FAQs/2FA" ) +ENVIRONMENT_STORE_CREDENTIALS = "SNAPCRAFT_STORE_CREDENTIALS" +"""Environment variable where credentials can be picked up from.""" + ENVIRONMENT_STORE_AUTH = "SNAPCRAFT_STORE_AUTH" """Environment variable used to set an alterntive login method. diff --git a/snapcraft_legacy/storeapi/http_clients/__init__.py b/snapcraft_legacy/storeapi/http_clients/__init__.py deleted file mode 100644 index 0318e38608..0000000000 --- a/snapcraft_legacy/storeapi/http_clients/__init__.py +++ /dev/null @@ -1,25 +0,0 @@ -# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- -# -# Copyright 2021 Canonical Ltd -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License version 3 as -# published by the Free Software Foundation. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -from typing import Union - -from . import errors # noqa: F401 -from ._candid_client import CandidClient # noqa: F401 -from ._ubuntu_sso_client import UbuntuOneAuthClient # noqa: F401 -from ._http_client import Client # noqa: F401 - - -AuthClient = Union[CandidClient, UbuntuOneAuthClient] diff --git a/snapcraft_legacy/storeapi/http_clients/_candid_client.py b/snapcraft_legacy/storeapi/http_clients/_candid_client.py deleted file mode 100644 index 888c2c656f..0000000000 --- a/snapcraft_legacy/storeapi/http_clients/_candid_client.py +++ /dev/null @@ -1,160 +0,0 @@ -import base64 -import json -import os -import pathlib -from typing import Optional, TextIO -from urllib.parse import urlparse - -import requests -import macaroonbakery._utils as utils -from macaroonbakery import bakery, httpbakery -from xdg import BaseDirectory - -from snapcraft_legacy.storeapi import constants -from . import agent, errors, _config, _http_client - - -class WebBrowserWaitingInteractor(httpbakery.WebBrowserInteractor): - """WebBrowserInteractor implementation using .http_client.Client. - - Waiting for a token is implemented using _http_client.Client which mounts - a session with backoff retires. - - Better exception classes and messages are provided to handle errors. - """ - - # TODO: transfer implementation to macaroonbakery. - def _wait_for_token(self, ctx, wait_token_url): - request_client = _http_client.Client() - resp = request_client.request("GET", wait_token_url) - if resp.status_code != 200: - raise errors.TokenTimeoutError(url=wait_token_url) - json_resp = resp.json() - kind = json_resp.get("kind") - if kind is None: - raise errors.TokenKindError(url=wait_token_url) - token_val = json_resp.get("token") - if token_val is None: - token_val = json_resp.get("token64") - if token_val is None: - raise errors.TokenValueError(url=wait_token_url) - token_val = base64.b64decode(token_val) - return httpbakery._interactor.DischargeToken(kind=kind, value=token_val) - - -class CandidConfig(_config.Config): - """Hold configuration options in sections. - - There can be two sections for the sso related credentials: production and - staging. This is governed by the STORE_DASHBOARD_URL environment - variable. Other sections are ignored but preserved. - - """ - - def _get_section_name(self) -> str: - url = os.getenv("STORE_DASHBOARD_URL", constants.STORE_DASHBOARD_URL) - return urlparse(url).netloc - - def _get_config_path(self) -> pathlib.Path: - return pathlib.Path(BaseDirectory.save_config_path("snapcraft")) / "candid.cfg" - - -class CandidClient(_http_client.Client): - @classmethod - def has_credentials(cls) -> bool: - return not CandidConfig().is_section_empty() - - @property - def _macaroon(self) -> Optional[str]: - return self._conf.get("macaroon") - - @_macaroon.setter - def _macaroon(self, macaroon: str) -> None: - self._conf.set("macaroon", macaroon) - if self._conf_save: - self._conf.save() - - @property - def _auth(self) -> Optional[str]: - return self._conf.get("auth") - - @_auth.setter - def _auth(self, auth: str) -> None: - self._conf.set("auth", auth) - if self._conf_save: - self._conf.save() - - def __init__( - self, *, user_agent: str = agent.get_user_agent(), bakery_client=None - ) -> None: - super().__init__(user_agent=user_agent) - - if bakery_client is None: - self.bakery_client = httpbakery.Client( - interaction_methods=[WebBrowserWaitingInteractor()] - ) - else: - self.bakery_client = bakery_client - self._conf = CandidConfig() - self._conf_save = True - - def _login(self, macaroon: str) -> None: - bakery_macaroon = bakery.Macaroon.from_dict(json.loads(macaroon)) - discharges = bakery.discharge_all( - bakery_macaroon, self.bakery_client.acquire_discharge - ) - - # serialize macaroons the bakery-way - discharged_macaroons = ( - "[" + ",".join(map(utils.macaroon_to_json_string, discharges)) + "]" - ) - - self._auth = base64.urlsafe_b64encode( - utils.to_bytes(discharged_macaroons) - ).decode("ascii") - self._macaroon = macaroon - - def login( - self, - *, - macaroon: Optional[str] = None, - config_fd: Optional[TextIO] = None, - save: bool = True, - ) -> None: - self._conf_save = save - if macaroon is not None: - self._login(macaroon) - elif config_fd is not None: - self._conf.load(config_fd=config_fd) - if save: - self._conf.save() - else: - raise RuntimeError("Logic Error") - - def request( - self, method, url, params=None, headers=None, auth_header=True, **kwargs - ) -> requests.Response: - if headers and auth_header: - headers["Macaroons"] = self._auth - elif auth_header: - headers = {"Macaroons": self._auth} - - response = super().request( - method, url, params=params, headers=headers, **kwargs - ) - - if not response.ok and response.status_code == 401: - self.login(macaroon=self._macaroon) - - response = super().request( - method, url, params=params, headers=headers, **kwargs - ) - - return response - - def export_login(self, *, config_fd: TextIO, encode: bool): - self._conf.save(config_fd=config_fd, encode=encode) - - def logout(self) -> None: - self._conf.clear() - self._conf.save() diff --git a/snapcraft_legacy/storeapi/http_clients/_config.py b/snapcraft_legacy/storeapi/http_clients/_config.py deleted file mode 100644 index 45ce96570d..0000000000 --- a/snapcraft_legacy/storeapi/http_clients/_config.py +++ /dev/null @@ -1,119 +0,0 @@ -# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- -# -# Copyright 2021 Canonical Ltd -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License version 3 as -# published by the Free Software Foundation. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -import abc -import base64 -import io -import os -import pathlib -from typing import Optional, TextIO - -import configparser - -from . import errors - - -class Config(abc.ABC): - def __init__(self) -> None: - self.parser = configparser.ConfigParser() - self.load() - - @abc.abstractmethod - def _get_section_name(self) -> str: - """Return section name.""" - - @abc.abstractmethod - def _get_config_path(self) -> pathlib.Path: - """Return Path to configuration file.""" - - def get( - self, option_name: str, section_name: Optional[str] = None - ) -> Optional[str]: - """Return content of section_name/option_name or None if not found.""" - if section_name is None: - section_name = self._get_section_name() - try: - return self.parser.get(section_name, option_name) - except (configparser.NoSectionError, configparser.NoOptionError, KeyError): - return None - - def set( - self, option_name: str, value: str, section_name: Optional[str] = None - ) -> None: - """Set value to section_name/option_name.""" - if not section_name: - section_name = self._get_section_name() - if not self.parser.has_section(section_name): - self.parser.add_section(section_name) - self.parser.set(section_name, option_name, value) - - def is_section_empty(self, section_name: Optional[str] = None) -> bool: - """Check if section_name is empty.""" - if section_name is None: - section_name = self._get_section_name() - - if self.parser.has_section(section_name): - if self.parser.options(section_name): - return False - return True - - def _load_potentially_base64_config(self, config_content: str) -> None: - try: - self.parser.read_string(config_content) - except configparser.Error as parser_error: - # The config may be base64-encoded, try decoding it - try: - decoded_config_content = base64.b64decode(config_content).decode() - except base64.binascii.Error: # type: ignore - # It wasn't base64, so use the original error - raise errors.InvalidLoginConfig(parser_error) - - try: - self.parser.read_string(decoded_config_content) - except configparser.Error as parser_error: - raise errors.InvalidLoginConfig(parser_error) - - def load(self, *, config_fd: TextIO = None) -> None: - if config_fd is not None: - config_content = config_fd.read() - elif self._get_config_path().exists(): - with self._get_config_path().open() as config_file: - config_content = config_file.read() - else: - return - - self._load_potentially_base64_config(config_content) - - def save(self, *, config_fd: Optional[TextIO] = None, encode: bool = False) -> None: - with io.StringIO() as config_buffer: - self.parser.write(config_buffer) - config_content = config_buffer.getvalue() - if encode: - config_content = base64.b64encode(config_content.encode()).decode() - - if config_fd: - print(config_content, file=config_fd) - else: - with self._get_config_path().open("w") as config_file: - print(config_content, file=config_file) - config_file.flush() - os.fsync(config_file.fileno()) - - def clear(self, section_name: Optional[str] = None) -> None: - if section_name is None: - section_name = self._get_section_name() - - self.parser.remove_section(self._get_section_name()) diff --git a/snapcraft_legacy/storeapi/http_clients/_http_client.py b/snapcraft_legacy/storeapi/http_clients/_http_client.py deleted file mode 100644 index 0c9f10ff6f..0000000000 --- a/snapcraft_legacy/storeapi/http_clients/_http_client.py +++ /dev/null @@ -1,89 +0,0 @@ -# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- -# -# Copyright 2021 Canonical Ltd -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License version 3 as -# published by the Free Software Foundation. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -import os -import logging - -import requests -from requests.adapters import HTTPAdapter -from requests.exceptions import ConnectionError, RetryError -from requests.packages.urllib3.util.retry import Retry - -from . import agent, errors - - -# Set urllib3's logger to only emit errors, not warnings. Otherwise even -# retries are printed, and they're nasty. -logging.getLogger(requests.packages.urllib3.__package__).setLevel(logging.ERROR) -logger = logging.getLogger(__name__) - - -class Client: - """Generic Client to talk to the *Store.""" - - def __init__(self, *, user_agent: str = agent.get_user_agent()) -> None: - self.session = requests.Session() - self._user_agent = user_agent - - # Setup max retries for all store URLs and the CDN - retries = Retry( - total=int(os.environ.get("STORE_RETRIES", 5)), - backoff_factor=int(os.environ.get("STORE_BACKOFF", 2)), - status_forcelist=[104, 500, 502, 503, 504], - ) - self.session.mount("http://", HTTPAdapter(max_retries=retries)) - self.session.mount("https://", HTTPAdapter(max_retries=retries)) - - def request( - self, method, url, params=None, headers=None, **kwargs - ) -> requests.Response: - """Send a request to url relative to the root url. - - :param str method: Method used for the request. - :param str url: URL to request with method. - :param list params: Query parameters to be sent along with the request. - :param list headers: Headers to be sent along with the request. - - :return Response of the request. - """ - if headers: - headers["User-Agent"] = self._user_agent - else: - headers = {"User-Agent": self._user_agent} - - debug_headers = headers.copy() - if debug_headers.get("Authorization"): - debug_headers["Authorization"] = "" - if debug_headers.get("Macaroons"): - debug_headers["Macaroons"] = "" - logger.debug( - "Calling {} with params {} and headers {}".format( - url, params, debug_headers - ) - ) - try: - response = self.session.request( - method, url, headers=headers, params=params, **kwargs - ) - except (ConnectionError, RetryError) as e: - raise errors.StoreNetworkError(e) from e - - # Handle 5XX responses generically right here, so the callers don't - # need to worry about it. - if response.status_code >= 500: - raise errors.StoreServerError(response) - - return response diff --git a/snapcraft_legacy/storeapi/http_clients/_ubuntu_sso_client.py b/snapcraft_legacy/storeapi/http_clients/_ubuntu_sso_client.py deleted file mode 100644 index f66b05cb3f..0000000000 --- a/snapcraft_legacy/storeapi/http_clients/_ubuntu_sso_client.py +++ /dev/null @@ -1,233 +0,0 @@ -# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- -# -# Copyright 2021 Canonical Ltd -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License version 3 as -# published by the Free Software Foundation. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -import logging -import json -import os -import pathlib -from typing import Optional, TextIO -from urllib.parse import urljoin, urlparse - -import pymacaroons -import requests -from simplejson.scanner import JSONDecodeError -from xdg import BaseDirectory - -from . import agent, _config, errors, _http_client - - -UBUNTU_ONE_SSO_URL = "https://login.ubuntu.com/" - - -logger = logging.getLogger(__name__) - - -def _deserialize_macaroon(value): - try: - return pymacaroons.Macaroon.deserialize(value) - except: # noqa LP: #1733004 - raise errors.InvalidCredentialsError("Failed to deserialize macaroon") - - -def _macaroon_auth(conf): - """Format a macaroon and its associated discharge. - - :return: A string suitable to use in an Authorization header. - - """ - root_macaroon_raw = conf.get("macaroon") - if root_macaroon_raw is None: - raise errors.InvalidCredentialsError("Root macaroon not in the config file") - unbound_raw = conf.get("unbound_discharge") - if unbound_raw is None: - raise errors.InvalidCredentialsError("Unbound discharge not in the config file") - - root_macaroon = _deserialize_macaroon(root_macaroon_raw) - unbound = _deserialize_macaroon(unbound_raw) - bound = root_macaroon.prepare_for_request(unbound) - discharge_macaroon_raw = bound.serialize() - auth = "Macaroon root={}, discharge={}".format( - root_macaroon_raw, discharge_macaroon_raw - ) - - return auth - - -class UbuntuOneSSOConfig(_config.Config): - """Hold configuration options in sections. - - There can be two sections for the sso related credentials: production and - staging. This is governed by the UBUNTU_ONE_SSO_URL environment - variable. Other sections are ignored but preserved. - - """ - - def _get_section_name(self) -> str: - url = os.getenv("UBUNTU_ONE_SSO_URL", UBUNTU_ONE_SSO_URL) - return urlparse(url).netloc - - def _get_config_path(self) -> pathlib.Path: - return ( - pathlib.Path(BaseDirectory.save_config_path("snapcraft")) - / "snapcraft_legacy.cfg" - ) - - -class UbuntuOneAuthClient(_http_client.Client): - """Store Client using Ubuntu One SSO provided macaroons.""" - - @staticmethod - def _is_needs_refresh_response(response): - return ( - response.status_code == requests.codes.unauthorized - and response.headers.get("WWW-Authenticate") == "Macaroon needs_refresh=1" - ) - - def __init__(self, *, user_agent: str = agent.get_user_agent()) -> None: - super().__init__(user_agent=user_agent) - - self._conf = UbuntuOneSSOConfig() - self.auth_url = os.environ.get("UBUNTU_ONE_SSO_URL", UBUNTU_ONE_SSO_URL) - - try: - self.auth: Optional[str] = _macaroon_auth(self._conf) - except errors.InvalidCredentialsError: - self.auth = None - - def _extract_caveat_id(self, root_macaroon): - macaroon = pymacaroons.Macaroon.deserialize(root_macaroon) - # macaroons are all bytes, never strings - sso_host = urlparse(self.auth_url).netloc - for caveat in macaroon.caveats: - if caveat.location == sso_host: - return caveat.caveat_id - else: - raise errors.InvalidCredentialsError("Invalid root macaroon") - - def login( - self, - *, - email: Optional[str] = None, - password: Optional[str] = None, - macaroon: Optional[str] = None, - otp: Optional[str] = None, - config_fd: TextIO = None, - save: bool = True, - ) -> None: - if config_fd is not None: - self._conf.load(config_fd=config_fd) - # Verbose to keep static checks happy. - elif email is not None and password is not None and macaroon is not None: - # Ask the store for the needed capabilities to be associated with - # the macaroon. - caveat_id = self._extract_caveat_id(macaroon) - unbound_discharge = self._discharge_token(email, password, otp, caveat_id) - # Clear any old data before setting. - self._conf.clear() - # The macaroon has been discharged, save it in the config - self._conf.set("macaroon", macaroon) - self._conf.set("unbound_discharge", unbound_discharge) - self._conf.set("email", email) - else: - raise RuntimeError("Logic Error") - - # Set auth and headers. - self.auth = _macaroon_auth(self._conf) - - if save: - self._conf.save() - - def export_login(self, *, config_fd: TextIO, encode: bool = False) -> None: - self._conf.save(config_fd=config_fd, encode=encode) - - def logout(self) -> None: - self._conf.clear() - self._conf.save() - - def _discharge_token( - self, email: str, password: str, otp: Optional[str], caveat_id - ) -> str: - data = dict(email=email, password=password, caveat_id=caveat_id) - if otp: - data["otp"] = otp - - url = urljoin(self.auth_url, "/api/v2/tokens/discharge") - - response = self.request( - "POST", - url, - data=json.dumps(data), - headers={"Content-Type": "application/json", "Accept": "application/json"}, - ) - - if response.ok: - return response.json()["discharge_macaroon"] - - try: - response_json = response.json() - except JSONDecodeError: - response_json = dict() - - if response.status_code == requests.codes.unauthorized and any( - error.get("code") == "twofactor-required" - for error in response_json.get("error_list", []) - ): - raise errors.StoreTwoFactorAuthenticationRequired() - else: - raise errors.StoreAuthenticationError( - "Failed to get unbound discharge", response - ) - - def _refresh_token(self, unbound_discharge): - data = {"discharge_macaroon": unbound_discharge} - url = urljoin(self.auth_url, "/api/v2/tokens/refresh") - response = self.request( - "POST", - url, - json=data, - headers={"Content-Type": "application/json", "Accept": "application/json"}, - ) - if response.ok: - return response.json()["discharge_macaroon"] - else: - raise errors.StoreAuthenticationError( - "Failed to refresh unbound discharge", response - ) - - def request( - self, method, url, params=None, headers=None, auth_header=True, **kwargs - ) -> requests.Response: - if headers and auth_header: - headers["Authorization"] = self.auth - elif auth_header: - headers = {"Authorization": self.auth} - - response = super().request( - method, url, params=params, headers=headers, **kwargs - ) - - if self._is_needs_refresh_response(response): - unbound_discharge = self._refresh_token(self._conf.get("unbound_discharge")) - self._conf.set("unbound_discharge", unbound_discharge) - self._conf.save() - self.auth = _macaroon_auth(self._conf) - headers["Authorization"] = self.auth - - response = super().request( - method, url, params=params, headers=headers, **kwargs - ) - - return response diff --git a/snapcraft_legacy/storeapi/http_clients/errors.py b/snapcraft_legacy/storeapi/http_clients/errors.py deleted file mode 100644 index f927230ab0..0000000000 --- a/snapcraft_legacy/storeapi/http_clients/errors.py +++ /dev/null @@ -1,136 +0,0 @@ -# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- -# -# Copyright 2021 Canonical Ltd -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License version 3 as -# published by the Free Software Foundation. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -import contextlib -import logging -import urllib3 -from simplejson.scanner import JSONDecodeError - -from snapcraft_legacy.internal.errors import SnapcraftError - -logger = logging.getLogger(__name__) - - -_STORE_STATUS_URL = "https://status.snapcraft.io/" - - -# TODO: migrate to storeapi private exception to ready craft-store. -class HttpClientError(SnapcraftError): - """Base class http client errors. - - :cvar fmt: A format string that daughter classes override - """ - - def __init__(self, **kwargs): - with contextlib.suppress(KeyError, AttributeError): - logger.debug("Store error response: {}".format(kwargs["response"].__dict__)) - super().__init__(**kwargs) - - -class StoreServerError(HttpClientError): - - fmt = "{what}: {error_text} (code {error_code}).\n{action}" - - def __init__(self, response): - what = "The Snap Store encountered an error while processing your request" - error_code = response.status_code - error_text = response.reason - action = "The operational status of the Snap Store can be checked at {}".format( - _STORE_STATUS_URL - ) - self.response = response - - super().__init__( - response=response, - what=what, - error_text=error_text, - error_code=error_code, - action=action, - ) - - -class StoreNetworkError(HttpClientError): - - fmt = "There seems to be a network error: {message}" - - def __init__(self, exception): - message = str(exception) - with contextlib.suppress(IndexError): - underlying_exception = exception.args[0] - if isinstance(underlying_exception, urllib3.exceptions.MaxRetryError): - message = ( - "maximum retries exceeded trying to reach the store.\n" - "Check your network connection, and check the store " - "status at {}".format(_STORE_STATUS_URL) - ) - super().__init__(message=message) - - -class InvalidCredentialsError(HttpClientError): - - fmt = 'Invalid credentials: {message}. Have you run "snapcraft login"?' - - def __init__(self, message): - super().__init__(message=message) - - -class StoreAuthenticationError(HttpClientError): - - fmt = "Authentication error: {message}" - - def __init__(self, message, response=None): - # Unfortunately the store doesn't give us a consistent error response, - # so we'll check the ones of which we're aware. - with contextlib.suppress(AttributeError, JSONDecodeError): - response_json = response.json() - extra_error_message = "" - if "error_message" in response_json: - extra_error_message = response_json["error_message"] - elif "message" in response_json: - extra_error_message = response_json["message"] - - if extra_error_message: - message += ": {}".format(extra_error_message) - - super().__init__(response=response, message=message) - - -class StoreTwoFactorAuthenticationRequired(StoreAuthenticationError): - def __init__(self): - super().__init__("Two-factor authentication required.") - - -class InvalidLoginConfig(HttpClientError): - - fmt = "Invalid login config: {error}" - - def __init__(self, error): - super().__init__(error=error) - - -class TokenTimeoutError(SnapcraftError): - def __init__(self, *, url: str) -> None: - self.fmt = f"Timed out waiting for token response from {url!r}." - - -class TokenKindError(SnapcraftError): - def __init__(self, *, url: str) -> None: - self.fmt = f"Empty token kind returned from {url!r}." - - -class TokenValueError(SnapcraftError): - def __init__(self, *, url: str) -> None: - self.fmt = f"Empty token value returned from {url!r}." diff --git a/tests/conftest.py b/tests/conftest.py index 9cc950b657..702897bb88 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -16,8 +16,10 @@ import os +import keyring import pytest import xdg +from craft_store.auth import MemoryKeyring @pytest.fixture(autouse=True) @@ -48,3 +50,12 @@ def new_dir(tmp_path): yield tmp_path os.chdir(cwd) + + +@pytest.fixture +def memory_keyring(): + """In memory keyring backend for testing.""" + current_keyring = keyring.get_keyring() + keyring.set_keyring(MemoryKeyring()) + yield + keyring.set_keyring(current_keyring) diff --git a/tests/legacy/fixture_setup/_fixtures.py b/tests/legacy/fixture_setup/_fixtures.py index 8522644ca5..50ede1576e 100644 --- a/tests/legacy/fixture_setup/_fixtures.py +++ b/tests/legacy/fixture_setup/_fixtures.py @@ -245,7 +245,8 @@ def setUp(self): self.useFixture(self.fake_store_upload_server_fixture) self.useFixture( fixtures.EnvironmentVariable( - "STORE_UPLOAD_URL", self.fake_store_upload_server_fixture.url, + "STORE_UPLOAD_URL", + self.fake_store_upload_server_fixture.url, ) ) @@ -261,7 +262,8 @@ def setUp(self): self.useFixture(self.fake_store_search_server_fixture) self.useFixture( fixtures.EnvironmentVariable( - "STORE_API_URL", self.fake_store_search_server_fixture.url, + "STORE_API_URL", + self.fake_store_search_server_fixture.url, ) ) @@ -281,7 +283,7 @@ def _start_fake_server(self): server_thread = threading.Thread(target=self.server.serve_forever) server_thread.start() self.addCleanup(self._stop_fake_server, server_thread) - self.url = "http://localhost:{}/".format(self.server.server_port) + self.url = "http://localhost:{}".format(self.server.server_port) def _stop_fake_server(self, thread): self.server.shutdown() @@ -336,22 +338,24 @@ def setUp(self): super().setUp() self.useFixture( fixtures.EnvironmentVariable( - "STORE_DASHBOARD_URL", "https://dashboard.staging.snapcraft.io/", + "STORE_DASHBOARD_URL", + "https://dashboard.staging.snapcraft.io", ) ) self.useFixture( fixtures.EnvironmentVariable( - "STORE_UPLOAD_URL", "https://upload.apps.staging.ubuntu.com/", + "STORE_UPLOAD_URL", + "https://storage.staging.snapcraftcontent.com", ) ) self.useFixture( fixtures.EnvironmentVariable( - "UBUNTU_ONE_SSO_URL", "https://login.staging.ubuntu.com/" + "UBUNTU_ONE_SSO_URL", "https://login.staging.ubuntu.com" ) ) self.useFixture( fixtures.EnvironmentVariable( - "STORE_API_URL", "https://api.staging.snapcraft.io/" + "STORE_API_URL", "https://api.staging.snapcraft.io" ) ) diff --git a/tests/legacy/unit/commands/__init__.py b/tests/legacy/unit/commands/__init__.py index 4dee118127..b472287eab 100644 --- a/tests/legacy/unit/commands/__init__.py +++ b/tests/legacy/unit/commands/__init__.py @@ -20,7 +20,10 @@ from textwrap import dedent from unittest import mock +import craft_store import fixtures +import pytest +import requests from click.testing import CliRunner from snapcraft_legacy import storeapi @@ -55,7 +58,11 @@ def get_sample_key(name): def mock_check_output(command, *args, **kwargs): if isinstance(command[0], PosixPath): command[0] = str(command[0]) - if command[0].endswith("unsquashfs") or command[0].endswith("xdelta3"): + if ( + command[0].endswith("unsquashfs") + or command[0].endswith("xdelta3") + or command[0].endswith("file") + ): return original_check_output(command, *args, **kwargs) elif command[0].endswith("snap") and command[1:] == ["keys", "--json"]: return json.dumps(_sample_keys) @@ -81,10 +88,13 @@ def mock_check_output(command, *args, **kwargs): "new-key", ]: pass + elif command[0].endswith("snap") and command[1] == "sign-build": + return b"Mocked assertion" else: raise AssertionError("Unhandled command: {}".format(command)) +@pytest.mark.usefixtures("memory_keyring") class CommandBaseTestCase(unit.TestCase): def setUp(self): super().setUp() @@ -157,6 +167,8 @@ def setUp(self): self.useFixture(self.fake_store) self.client = storeapi.StoreClient() + self.client.login(email="dummy", password="test correct password", ttl=1) + class FakeStoreCommandsBaseTestCase(CommandBaseTestCase): def setUp(self): @@ -172,6 +184,11 @@ def setUp(self): self.fake_store_login = fixtures.MockPatchObject(storeapi.StoreClient, "login") self.useFixture(self.fake_store_login) + self.fake_store_logout = fixtures.MockPatchObject( + storeapi.StoreClient, "logout" + ) + self.useFixture(self.fake_store_logout) + self.fake_store_register = fixtures.MockPatchObject( storeapi._dashboard_api.DashboardAPI, "register" ) @@ -464,3 +481,33 @@ def setUp(self): return_value=True, ) self.useFixture(self.fake_package_installed) + + +class FakeResponse(requests.Response): + def __init__(self, content, status_code): + self._content = content + self.status_code = status_code + + @property + def content(self): + return self._content + + @property + def ok(self): + return self.status_code == 200 + + def json(self): + return json.loads(self._content) # type: ignore + + @property + def reason(self): + return self._content + + @property + def text(self): + return self.content + + +FAKE_UNAUTHORIZED_ERROR = craft_store.errors.StoreServerError( + FakeResponse(status_code=requests.codes.unauthorized, content="error") +) diff --git a/tests/legacy/unit/commands/test_edit_validation_sets.py b/tests/legacy/unit/commands/test_edit_validation_sets.py index 0129c8cde4..1692e8e264 100644 --- a/tests/legacy/unit/commands/test_edit_validation_sets.py +++ b/tests/legacy/unit/commands/test_edit_validation_sets.py @@ -99,6 +99,7 @@ def sign(assertion: Dict[str, Any], *, key_name: str) -> bytes: patched_snap_sign.stop() +@pytest.mark.usefixtures("memory_keyring") @pytest.mark.usefixtures("mock_subprocess_run") def test_edit_validation_sets_with_no_changes_to_existing_set( click_run, @@ -135,6 +136,7 @@ def fake_edit_validation_sets(): patched_edit_validation_sets.stop() +@pytest.mark.usefixtures("memory_keyring") @pytest.mark.parametrize("key_name", [None, "general", "main"]) def test_edit_validation_sets_with_changes_to_existing_set( click_run, @@ -201,7 +203,8 @@ def test_edit_validation_sets_with_changes_to_existing_set( ).encode() ) fake_snap_sign.assert_called_once_with( - validation_sets_payload.assertions[0].marshal(), key_name=key_name, + validation_sets_payload.assertions[0].marshal(), + key_name=key_name, ) assert result.exit_code == 0 assert result.output.strip() == "" diff --git a/tests/legacy/unit/commands/test_export_login.py b/tests/legacy/unit/commands/test_export_login.py index 2c4fc789e6..49a89acab7 100644 --- a/tests/legacy/unit/commands/test_export_login.py +++ b/tests/legacy/unit/commands/test_export_login.py @@ -18,11 +18,9 @@ from unittest import mock import fixtures -import pytest -from testtools.matchers import Contains, Equals, MatchesRegex, Not +from testtools.matchers import Contains, Equals, MatchesRegex from snapcraft_legacy import storeapi - from . import FakeStoreCommandsBaseTestCase @@ -70,9 +68,7 @@ def test_successful_export(self): acls=None, packages=None, channels=None, - expires=None, - save=False, - config_fd=None, + ttl=mock.ANY, ) def test_successful_export_stdout(self): @@ -105,9 +101,7 @@ def test_successful_export_stdout(self): acls=None, packages=None, channels=None, - expires=None, - save=False, - config_fd=None, + ttl=mock.ANY, ) def test_successful_export_expires(self): @@ -125,7 +119,7 @@ def test_successful_export_expires(self): ) result = self.run_command( - ["export-login", "--expires=2018-02-01T00:00:00", "exported"], + ["export-login", "--expires=2018-02-01T00:00:00Z", "exported"], input="user@example.com\nsecret\n", ) self.assertThat(result.exit_code, Equals(0)) @@ -150,80 +144,19 @@ def test_successful_export_expires(self): acls=None, packages=None, channels=None, - expires="2018-02-01T00:00:00", - save=False, - config_fd=None, + ttl=mock.ANY, ) - def test_successful_login_with_2fa(self): - self.fake_store_login.mock.side_effect = [ - storeapi.http_clients.errors.StoreTwoFactorAuthenticationRequired(), - None, - ] - + def test_bad_date_format(self): result = self.run_command( - ["export-login", "exported"], input="user@example.com\nsecret\n123456" - ) - - self.assertThat(result.exit_code, Equals(0)) - self.assertThat( - result.output, Not(Contains(storeapi.constants.TWO_FACTOR_WARNING)) - ) - self.assertThat(result.output, Contains("Login successfully exported")) - self.assertThat( - result.output, MatchesRegex(r".*snaps:.*?No restriction", re.DOTALL) - ) - self.assertThat( - result.output, MatchesRegex(r".*channels:.*?['edge']", re.DOTALL) - ) - self.assertThat( - result.output, MatchesRegex(r".*permissions:.*?No restriction", re.DOTALL) + ["export-login", "--expires=20180201", "exported"], + input="user@example.com\nsecret\n", ) + self.assertThat(result.exit_code, Equals(2)) self.assertThat( - result.output, MatchesRegex(r".*expires:.*?2018-01-01T00:00:00", re.DOTALL) - ) - - self.assertThat(self.fake_store_login.mock.call_count, Equals(2)) - self.fake_store_login.mock.assert_has_calls( - [ - mock.call( - email="user@example.com", - password="secret", - acls=None, - packages=None, - channels=None, - expires=None, - save=False, - config_fd=None, - ), - mock.call( - email="user@example.com", - password="secret", - otp="123456", - acls=None, - packages=None, - channels=None, - expires=None, - save=False, - config_fd=None, - ), - ] - ) - - def test_failed_login_with_invalid_credentials(self): - self.fake_store_login.mock.side_effect = storeapi.http_clients.errors.InvalidCredentialsError( - "error" - ) - - with pytest.raises( - storeapi.http_clients.errors.InvalidCredentialsError - ) as exc_info: - self.run_command( - ["export-login", "exported"], - input="bad-user@example.com\nbad-password\n", - ) - - assert ( - str(exc_info.value) - == 'Invalid credentials: error. Have you run "snapcraft login"?' + result.output, + Contains( + "Error: Invalid value: The expiry follow an ISO 8601 format " + "('%Y-%m-%d' or '%Y-%m-%dT%H:%M:%SZ')" + ), ) diff --git a/tests/legacy/unit/commands/test_gated.py b/tests/legacy/unit/commands/test_gated.py index ecd3c215a6..a7a355cfce 100644 --- a/tests/legacy/unit/commands/test_gated.py +++ b/tests/legacy/unit/commands/test_gated.py @@ -27,8 +27,6 @@ class GatedCommandTestCase(StoreCommandsBaseTestCase): def test_gated_unknown_snap(self): - self.client.login(email="dummy", password="test correct password") - raised = self.assertRaises( snapcraft_legacy.storeapi.errors.SnapNotFoundError, self.run_command, @@ -38,8 +36,6 @@ def test_gated_unknown_snap(self): self.assertThat(str(raised), Equals("Snap 'notfound' was not found.")) def test_gated_success(self): - self.client.login(email="dummy", password="test correct password") - result = self.run_command(["gated", "core"]) self.assertThat(result.exit_code, Equals(0)) @@ -53,8 +49,6 @@ def test_gated_success(self): self.assertThat(result.output, Contains(expected_output)) def test_gated_no_validations(self): - self.client.login(email="dummy", password="test correct password") - result = self.run_command(["gated", "test-snap-with-no-validations"]) self.assertThat(result.exit_code, Equals(0)) diff --git a/tests/legacy/unit/commands/test_list.py b/tests/legacy/unit/commands/test_list.py index faa9fb48eb..f7d089dc2f 100644 --- a/tests/legacy/unit/commands/test_list.py +++ b/tests/legacy/unit/commands/test_list.py @@ -18,9 +18,7 @@ from testtools.matchers import Contains, Equals -from snapcraft_legacy import storeapi - -from . import FakeStoreCommandsBaseTestCase +from . import FAKE_UNAUTHORIZED_ERROR, FakeStoreCommandsBaseTestCase class ListTest(FakeStoreCommandsBaseTestCase): @@ -30,7 +28,7 @@ class ListTest(FakeStoreCommandsBaseTestCase): def test_command_without_login_must_ask(self): # TODO: look into why this many calls are done inside snapcraft_legacy.storeapi self.fake_store_account_info.mock.side_effect = [ - storeapi.http_clients.errors.InvalidCredentialsError("error"), + FAKE_UNAUTHORIZED_ERROR, {"account_id": "abcd", "snaps": dict()}, {"account_id": "abcd", "snaps": dict()}, {"account_id": "abcd", "snaps": dict()}, diff --git a/tests/legacy/unit/commands/test_list_keys.py b/tests/legacy/unit/commands/test_list_keys.py index 301925579e..045e481bdd 100644 --- a/tests/legacy/unit/commands/test_list_keys.py +++ b/tests/legacy/unit/commands/test_list_keys.py @@ -16,12 +16,11 @@ from textwrap import dedent -from testtools.matchers import Contains, Equals import fixtures +from testtools.matchers import Contains, Equals -from snapcraft_legacy import storeapi -from . import FakeStoreCommandsBaseTestCase, get_sample_key +from . import FAKE_UNAUTHORIZED_ERROR, FakeStoreCommandsBaseTestCase, get_sample_key class ListKeysCommandTestCase(FakeStoreCommandsBaseTestCase): @@ -31,7 +30,7 @@ class ListKeysCommandTestCase(FakeStoreCommandsBaseTestCase): def test_command_without_login_must_ask(self): # TODO: look into why this many calls are done inside snapcraft_legacy.storeapi self.fake_store_account_info.mock.side_effect = [ - storeapi.http_clients.errors.InvalidCredentialsError("error"), + FAKE_UNAUTHORIZED_ERROR, {"account_id": "abcd", "account_keys": list()}, {"account_id": "abcd", "account_keys": list()}, {"account_id": "abcd", "account_keys": list()}, diff --git a/tests/legacy/unit/commands/test_list_revisions.py b/tests/legacy/unit/commands/test_list_revisions.py index 44f7c759fe..82ad631269 100644 --- a/tests/legacy/unit/commands/test_list_revisions.py +++ b/tests/legacy/unit/commands/test_list_revisions.py @@ -20,9 +20,7 @@ import fixtures from testtools.matchers import Contains, Equals -from snapcraft_legacy import storeapi - -from . import FakeStoreCommandsBaseTestCase +from . import FAKE_UNAUTHORIZED_ERROR, FakeStoreCommandsBaseTestCase class RevisionsCommandTestCase(FakeStoreCommandsBaseTestCase): @@ -37,7 +35,7 @@ def test_revisions_without_snap_raises_exception(self): def test_revisions_without_login_must_ask(self): self.fake_store_get_releases.mock.side_effect = [ - storeapi.http_clients.errors.InvalidCredentialsError("error"), + FAKE_UNAUTHORIZED_ERROR, self.releases, ] diff --git a/tests/legacy/unit/commands/test_list_tracks.py b/tests/legacy/unit/commands/test_list_tracks.py index ad4b013109..6db06bc8ce 100644 --- a/tests/legacy/unit/commands/test_list_tracks.py +++ b/tests/legacy/unit/commands/test_list_tracks.py @@ -18,9 +18,7 @@ from testtools.matchers import Contains, Equals -from snapcraft_legacy import storeapi - -from . import FakeStoreCommandsBaseTestCase +from . import FAKE_UNAUTHORIZED_ERROR, FakeStoreCommandsBaseTestCase class ListTracksCommandTestCase(FakeStoreCommandsBaseTestCase): @@ -32,7 +30,7 @@ def test_list_tracks_without_snap_raises_exception(self): def test_list_tracks_without_login_must_ask(self): self.fake_store_get_snap_channel_map.mock.side_effect = [ - storeapi.http_clients.errors.InvalidCredentialsError("error"), + FAKE_UNAUTHORIZED_ERROR, self.channel_map, ] diff --git a/tests/legacy/unit/commands/test_list_validation_sets.py b/tests/legacy/unit/commands/test_list_validation_sets.py index 5184daaa62..55537faf8b 100644 --- a/tests/legacy/unit/commands/test_list_validation_sets.py +++ b/tests/legacy/unit/commands/test_list_validation_sets.py @@ -58,6 +58,7 @@ def fake_dashboard_get_validation_sets(): ] +@pytest.mark.usefixtures("memory_keyring") @pytest.mark.parametrize("combo,", combinations) def test_no_sets(click_run, fake_dashboard_get_validation_sets, combo): cmd = ["list-validation-sets"] @@ -75,6 +76,7 @@ def test_no_sets(click_run, fake_dashboard_get_validation_sets, combo): ) +@pytest.mark.usefixtures("memory_keyring") def test_list_validation_sets(click_run, fake_dashboard_get_validation_sets): fake_dashboard_get_validation_sets.return_value = validation_sets.ValidationSets.unmarshal( { @@ -141,5 +143,6 @@ def test_list_validation_sets(click_run, fake_dashboard_get_validation_sets): """ ) fake_dashboard_get_validation_sets.assert_called_once_with( - name=None, sequence=None, + name=None, + sequence=None, ) diff --git a/tests/legacy/unit/commands/test_login.py b/tests/legacy/unit/commands/test_login.py index 40cf5ce48e..c5017d9b61 100644 --- a/tests/legacy/unit/commands/test_login.py +++ b/tests/legacy/unit/commands/test_login.py @@ -14,18 +14,18 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -import pathlib -import re +import json from unittest import mock -import fixtures +import craft_store import pytest +import requests from simplejson.scanner import JSONDecodeError -from testtools.matchers import Contains, Equals, MatchesRegex, Not +from testtools.matchers import Contains, Equals, Not from snapcraft_legacy import storeapi -from . import FakeStoreCommandsBaseTestCase +from . import FakeResponse, FakeStoreCommandsBaseTestCase class LoginCommandTestCase(FakeStoreCommandsBaseTestCase): @@ -44,14 +44,23 @@ def test_login(self): acls=None, packages=None, channels=None, - expires=None, - save=True, - config_fd=None, + ttl=mock.ANY, ) def test_login_with_2fa(self): self.fake_store_login.mock.side_effect = [ - storeapi.http_clients.errors.StoreTwoFactorAuthenticationRequired(), + craft_store.errors.StoreServerError( + FakeResponse( + status_code=requests.codes.unauthorized, + content=json.dumps( + { + "error_list": [ + {"message": "2fa", "code": "twofactor-required"} + ] + } + ), + ) + ), None, ] @@ -73,9 +82,7 @@ def test_login_with_2fa(self): acls=None, packages=None, channels=None, - expires=None, - save=True, - config_fd=None, + ttl=mock.ANY, ), mock.call( email="user@example.com", @@ -84,94 +91,18 @@ def test_login_with_2fa(self): acls=None, packages=None, channels=None, - expires=None, - save=True, - config_fd=None, + ttl=mock.ANY, ), ] ) - def test_successful_login_with(self): - self.useFixture( - fixtures.MockPatchObject( - storeapi.StoreClient, - "acl", - return_value={ - "snap_ids": None, - "channels": None, - "permissions": None, - "expires": "2018-01-01T00:00:00", - }, - ) - ) - self.fake_store_login.mock.side_effect = None - - pathlib.Path("exported-login").touch() - - result = self.run_command(["login", "--with", "exported-login"]) - - self.assertThat(result.exit_code, Equals(0)) - self.assertThat(result.output, Contains("Login successful")) - self.assertThat( - result.output, MatchesRegex(r".*snaps:.*?No restriction", re.DOTALL) - ) - self.assertThat( - result.output, MatchesRegex(r".*channels:.*?No restriction", re.DOTALL) - ) - self.assertThat( - result.output, MatchesRegex(r".*permissions:.*?No restriction", re.DOTALL) - ) - self.assertThat( - result.output, MatchesRegex(r".*expires:.*?2018-01-01T00:00:00", re.DOTALL) - ) - - self.fake_store_login.mock.assert_called_once_with( - email="", - password="", - acls=None, - packages=None, - channels=None, - expires=None, - save=True, - config_fd=mock.ANY, - ) - - def test_login_failed_with_invalid_credentials(self): - self.fake_store_login.mock.side_effect = storeapi.http_clients.errors.InvalidCredentialsError( - "error" - ) - - with pytest.raises( - storeapi.http_clients.errors.InvalidCredentialsError - ) as exc_info: - self.run_command(["login"], input="user@example.com\nbadsecret\n") - - assert ( - str(exc_info.value) - == 'Invalid credentials: error. Have you run "snapcraft login"?' - ) - - def test_login_failed_with_store_authentication_error(self): - self.fake_store_login.mock.side_effect = storeapi.http_clients.errors.StoreAuthenticationError( - "error" - ) - - raised = self.assertRaises( - storeapi.http_clients.errors.StoreAuthenticationError, - self.run_command, - ["login"], - input="user@example.com\nbad-secret\n", - ) - - self.assertThat(raised.message, Equals("error")) - def test_failed_login_with_store_account_info_error(self): response = mock.Mock() response.json.side_effect = JSONDecodeError("mock-fail", "doc", 1) response.status_code = 500 response.reason = "Internal Server Error" - self.fake_store_login.mock.side_effect = storeapi.errors.StoreAccountInformationError( - response + self.fake_store_login.mock.side_effect = ( + storeapi.errors.StoreAccountInformationError(response) ) with pytest.raises(storeapi.errors.StoreAccountInformationError) as exc_info: @@ -195,8 +126,8 @@ def test_failed_login_with_dev_namespace_error(self): ] } response.json.return_value = content - self.fake_store_account_info.mock.side_effect = storeapi.errors.StoreAccountInformationError( - response + self.fake_store_account_info.mock.side_effect = ( + storeapi.errors.StoreAccountInformationError(response) ) with pytest.raises(storeapi.errors.NeedTermsSignedError) as exc_info: @@ -221,8 +152,8 @@ def test_failed_login_with_unexpected_account_error(self): ] } response.json.return_value = content - self.fake_store_account_info.mock.side_effect = storeapi.errors.StoreAccountInformationError( - response + self.fake_store_account_info.mock.side_effect = ( + storeapi.errors.StoreAccountInformationError(response) ) with pytest.raises(storeapi.errors.StoreAccountInformationError) as exc_info: @@ -246,8 +177,8 @@ def test_failed_login_with_dev_agreement_error_with_choice_no(self): ] } response.json.return_value = content - self.fake_store_account_info.mock.side_effect = storeapi.errors.StoreAccountInformationError( - response + self.fake_store_account_info.mock.side_effect = ( + storeapi.errors.StoreAccountInformationError(response) ) with pytest.raises(storeapi.errors.NeedTermsSignedError) as exc_info: @@ -271,8 +202,8 @@ def test_failed_login_with_dev_agreement_error_with_choice_yes(self): ] } response.json.return_value = content - self.fake_store_account_info.mock.side_effect = storeapi.errors.StoreAccountInformationError( - response + self.fake_store_account_info.mock.side_effect = ( + storeapi.errors.StoreAccountInformationError(response) ) with pytest.raises(storeapi.errors.NeedTermsSignedError) as exc_info: diff --git a/tests/legacy/unit/commands/test_metrics.py b/tests/legacy/unit/commands/test_metrics.py index e941daa4c8..146d692794 100644 --- a/tests/legacy/unit/commands/test_metrics.py +++ b/tests/legacy/unit/commands/test_metrics.py @@ -18,9 +18,7 @@ import pytest -from snapcraft_legacy import storeapi - -from . import FakeStoreCommandsBaseTestCase +from . import FAKE_UNAUTHORIZED_ERROR, FakeStoreCommandsBaseTestCase class MetricsCommandTestCase(FakeStoreCommandsBaseTestCase): @@ -49,7 +47,7 @@ def test_metrics_without_format_raises_exception(self): @pytest.mark.skip("needs more work") def test_status_without_login_must_ask(self): self.fake_store_account_info.mock.side_effect = [ - storeapi.http_clients.errors.InvalidCredentialsError("error"), + FAKE_UNAUTHORIZED_ERROR, self.fake_store_account_info_data, ] diff --git a/tests/legacy/unit/commands/test_register.py b/tests/legacy/unit/commands/test_register.py index 720540f5a2..a30495126e 100644 --- a/tests/legacy/unit/commands/test_register.py +++ b/tests/legacy/unit/commands/test_register.py @@ -21,7 +21,7 @@ from snapcraft_legacy import storeapi -from . import FakeStoreCommandsBaseTestCase +from . import FAKE_UNAUTHORIZED_ERROR, FakeStoreCommandsBaseTestCase class RegisterTestCase(FakeStoreCommandsBaseTestCase): @@ -33,7 +33,7 @@ def test_register_without_name_must_error(self): def test_register_without_login_must_ask(self): self.fake_store_register.mock.side_effect = [ - storeapi.http_clients.errors.InvalidCredentialsError("error"), + FAKE_UNAUTHORIZED_ERROR, None, ] @@ -104,8 +104,8 @@ def test_register_private_name_successfully(self): def test_registration_failed(self): response = mock.Mock() response.json.side_effect = JSONDecodeError("mock-fail", "doc", 1) - self.fake_store_register.mock.side_effect = storeapi.errors.StoreRegistrationError( - "test-snap", response + self.fake_store_register.mock.side_effect = ( + storeapi.errors.StoreRegistrationError("test-snap", response) ) raised = self.assertRaises( @@ -120,8 +120,8 @@ def test_registration_failed(self): def test_registration_cancelled(self): response = mock.Mock() response.json.side_effect = JSONDecodeError("mock-fail", "doc", 1) - self.fake_store_register.mock.side_effect = storeapi.errors.StoreRegistrationError( - "test-snap", response + self.fake_store_register.mock.side_effect = ( + storeapi.errors.StoreRegistrationError("test-snap", response) ) result = self.run_command(["register", "test-snap"], input="n\n") diff --git a/tests/legacy/unit/commands/test_register_key.py b/tests/legacy/unit/commands/test_register_key.py index 5cd8a4d685..4a4fe28404 100644 --- a/tests/legacy/unit/commands/test_register_key.py +++ b/tests/legacy/unit/commands/test_register_key.py @@ -48,9 +48,8 @@ def test_register_key(self): acls=["modify_account_key"], packages=None, channels=None, - expires=None, - save=False, - config_fd=None, + # one day + ttl=86400, ) self.fake_store_register_key.mock.call_once_with( dedent( @@ -83,29 +82,13 @@ def test_register_key_no_keys_with_name(self): str(raised), Contains("You have no usable key named 'nonexistent'") ) - def test_register_key_login_failed(self): - self.fake_store_login.mock.side_effect = storeapi.http_clients.errors.InvalidCredentialsError( - "error" - ) - - raised = self.assertRaises( - storeapi.http_clients.errors.InvalidCredentialsError, - self.run_command, - ["register-key", "default"], - input="user@example.com\nsecret\n", - ) - - assert ( - str(raised) == 'Invalid credentials: error. Have you run "snapcraft login"?' - ) - def test_register_key_account_info_failed(self): response = mock.Mock() response.json.side_effect = JSONDecodeError("mock-fail", "doc", 1) response.status_code = 500 response.reason = "Internal Server Error" - self.fake_store_account_info.mock.side_effect = storeapi.errors.StoreAccountInformationError( - response + self.fake_store_account_info.mock.side_effect = ( + storeapi.errors.StoreAccountInformationError(response) ) # Fake the login check @@ -132,8 +115,8 @@ def test_register_key_failed(self): response.json.side_effect = JSONDecodeError("mock-fail", "doc", 1) response.status_code = 500 response.reason = "Internal Server Error" - self.fake_store_register_key.mock.side_effect = storeapi.errors.StoreKeyRegistrationError( - response + self.fake_store_register_key.mock.side_effect = ( + storeapi.errors.StoreKeyRegistrationError(response) ) raised = self.assertRaises( diff --git a/tests/legacy/unit/commands/test_release.py b/tests/legacy/unit/commands/test_release.py index 8bed58cc05..ba501da1a5 100644 --- a/tests/legacy/unit/commands/test_release.py +++ b/tests/legacy/unit/commands/test_release.py @@ -18,7 +18,6 @@ from testtools.matchers import Contains, Equals -from snapcraft_legacy import storeapi from snapcraft_legacy.storeapi.v2.channel_map import ( MappedChannel, Progressive, @@ -26,7 +25,7 @@ SnapChannel, ) -from . import FakeStoreCommandsBaseTestCase +from . import FAKE_UNAUTHORIZED_ERROR, FakeStoreCommandsBaseTestCase class ReleaseCommandTestCase(FakeStoreCommandsBaseTestCase): @@ -269,7 +268,7 @@ def test_progressive_release_with_null_current_percentage(self): def test_release_without_login_must_ask(self): self.fake_store_release.mock.side_effect = [ - storeapi.http_clients.errors.InvalidCredentialsError("error"), + FAKE_UNAUTHORIZED_ERROR, {"opened_channels": ["beta"]}, ] diff --git a/tests/legacy/unit/commands/test_set_default_track.py b/tests/legacy/unit/commands/test_set_default_track.py index 2424121b62..31008d4be0 100644 --- a/tests/legacy/unit/commands/test_set_default_track.py +++ b/tests/legacy/unit/commands/test_set_default_track.py @@ -20,7 +20,7 @@ import snapcraft_legacy from snapcraft_legacy import storeapi -from . import FakeStoreCommandsBaseTestCase +from . import FAKE_UNAUTHORIZED_ERROR, FakeStoreCommandsBaseTestCase class SetDefaultTrackCommandTestCase(FakeStoreCommandsBaseTestCase): @@ -40,7 +40,7 @@ def test_set_default_track_without_snap_raises_exception(self): def test_set_default_track_without_login_must_ask(self): self.fake_metadata.mock.side_effect = [ - storeapi.http_clients.errors.InvalidCredentialsError("error"), + FAKE_UNAUTHORIZED_ERROR, None, ] diff --git a/tests/legacy/unit/commands/test_sign_build.py b/tests/legacy/unit/commands/test_sign_build.py index b6c751c2f2..507250c168 100644 --- a/tests/legacy/unit/commands/test_sign_build.py +++ b/tests/legacy/unit/commands/test_sign_build.py @@ -15,7 +15,6 @@ # along with this program. If not, see . import os import shutil -import subprocess from unittest import mock import fixtures @@ -24,7 +23,7 @@ import tests.legacy from snapcraft_legacy import internal, storeapi -from . import CommandBaseTestCase +from . import FakeStoreCommandsBaseTestCase, get_sample_key, mock_check_output class SnapTest(fixtures.TempDir): @@ -47,7 +46,7 @@ def _setUp(self): shutil.copyfile(test_snap_path, self.snap_path) -class SignBuildTestCase(CommandBaseTestCase): +class SignBuildTestCase(FakeStoreCommandsBaseTestCase): def setUp(self): super().setUp() self.snap_test = SnapTest("test-snap.snap") @@ -77,18 +76,18 @@ def test_sign_build_invalid_snap(self): self.assertThat(str(raised), Contains("Cannot read data from snap")) - @mock.patch.object(storeapi._dashboard_api.DashboardAPI, "get_account_information") @mock.patch("snapcraft_legacy._store._get_data_from_snap_file") def test_sign_build_missing_account_info( - self, mock_get_snap_data, mock_get_account_info, + self, + mock_get_snap_data, ): - mock_get_account_info.return_value = {"account_id": "abcd", "snaps": {}} mock_get_snap_data.return_value = {"name": "test-snap", "grade": "stable"} raised = self.assertRaises( storeapi.errors.StoreBuildAssertionPermissionError, self.run_command, ["sign-build", self.snap_test.snap_path], + input="1\n", ) self.assertThat( @@ -100,16 +99,12 @@ def test_sign_build_missing_account_info( ), ) - @mock.patch.object(storeapi._dashboard_api.DashboardAPI, "get_account_information") @mock.patch("snapcraft_legacy._store._get_data_from_snap_file") def test_sign_build_no_usable_keys( - self, mock_get_snap_data, mock_get_account_info, + self, + mock_get_snap_data, ): - mock_get_account_info.return_value = { - "account_id": "abcd", - "snaps": {"16": {"test-snap": {"snap-id": "snap-id"}}}, - } - mock_get_snap_data.return_value = {"name": "test-snap", "grade": "stable"} + mock_get_snap_data.return_value = {"name": "snap-test", "grade": "stable"} self.useFixture( fixtures.MockPatch("subprocess.check_output", return_value="[]".encode()) @@ -135,7 +130,9 @@ def test_sign_build_no_usable_keys( @mock.patch.object(storeapi._dashboard_api.DashboardAPI, "get_account_information") @mock.patch("snapcraft_legacy._store._get_data_from_snap_file") def test_sign_build_no_usable_named_key( - self, mock_get_snap_data, mock_get_account_info, + self, + mock_get_snap_data, + mock_get_account_info, ): mock_get_account_info.return_value = { "account_id": "abcd", @@ -166,7 +163,9 @@ def test_sign_build_no_usable_named_key( @mock.patch.object(storeapi._dashboard_api.DashboardAPI, "get_account_information") @mock.patch("snapcraft_legacy._store._get_data_from_snap_file") def test_sign_build_unregistered_key( - self, mock_get_snap_data, mock_get_account_info, + self, + mock_get_snap_data, + mock_get_account_info, ): mock_get_account_info.return_value = { "account_id": "abcd", @@ -200,48 +199,12 @@ def test_sign_build_unregistered_key( snap_build_path = self.snap_test.snap_path + "-build" self.assertThat(snap_build_path, Not(FileExists())) - @mock.patch.object(storeapi._dashboard_api.DashboardAPI, "get_account_information") - @mock.patch("snapcraft_legacy._store._get_data_from_snap_file") - def test_sign_build_snapd_failure( - self, mock_get_snap_data, mock_get_account_info, - ): - mock_get_account_info.return_value = { - "account_id": "abcd", - "account_keys": [{"public-key-sha3-384": "a_hash"}], - "snaps": {"16": {"test-snap": {"snap-id": "snap-id"}}}, - } - mock_get_snap_data.return_value = {"name": "test-snap", "grade": "stable"} - self.useFixture( - fixtures.MockPatch( - "subprocess.check_output", - side_effect=[ - '[{"name": "default", "sha3-384": "a_hash"}]'.encode(), - subprocess.CalledProcessError(1, ["a", "b"]), - ], - ) - ) - - raised = self.assertRaises( - storeapi.errors.SignBuildAssertionError, - self.run_command, - ["sign-build", self.snap_test.snap_path], - ) - - self.assertThat( - str(raised), - Contains( - "Failed to sign build assertion for {!r}".format( - self.snap_test.snap_path - ) - ), - ) - snap_build_path = self.snap_test.snap_path + "-build" - self.assertThat(snap_build_path, Not(FileExists())) - @mock.patch.object(storeapi._dashboard_api.DashboardAPI, "get_account_information") @mock.patch("snapcraft_legacy._store._get_data_from_snap_file") def test_sign_build_locally_successfully( - self, mock_get_snap_data, mock_get_account_info, + self, + mock_get_snap_data, + mock_get_account_info, ): mock_get_account_info.return_value = { "account_id": "abcd", @@ -250,11 +213,13 @@ def test_sign_build_locally_successfully( mock_get_snap_data.return_value = {"name": "test-snap", "grade": "stable"} fake_check_output = fixtures.MockPatch( "subprocess.check_output", - side_effect=['[{"name": "default"}]'.encode(), b"Mocked assertion"], + side_effect=mock_check_output, ) self.useFixture(fake_check_output) - result = self.run_command(["sign-build", self.snap_test.snap_path, "--local"]) + result = self.run_command( + ["sign-build", self.snap_test.snap_path, "--local"], input="1\n" + ) self.assertThat(result.exit_code, Equals(0)) snap_build_path = self.snap_test.snap_path + "-build" @@ -279,7 +244,9 @@ def test_sign_build_locally_successfully( @mock.patch.object(storeapi._dashboard_api.DashboardAPI, "get_account_information") @mock.patch("snapcraft_legacy._store._get_data_from_snap_file") def test_sign_build_missing_grade( - self, mock_get_snap_data, mock_get_account_info, + self, + mock_get_snap_data, + mock_get_account_info, ): mock_get_account_info.return_value = { "account_id": "abcd", @@ -288,12 +255,13 @@ def test_sign_build_missing_grade( } mock_get_snap_data.return_value = {"name": "test-snap"} fake_check_output = fixtures.MockPatch( - "subprocess.check_output", - side_effect=['[{"name": "default"}]'.encode(), b"Mocked assertion"], + "subprocess.check_output", side_effect=mock_check_output ) self.useFixture(fake_check_output) - result = self.run_command(["sign-build", self.snap_test.snap_path, "--local"]) + result = self.run_command( + ["sign-build", self.snap_test.snap_path, "--local"], input="1\n" + ) self.assertThat(result.exit_code, Equals(0)) snap_build_path = self.snap_test.snap_path + "-build" @@ -319,24 +287,28 @@ def test_sign_build_missing_grade( @mock.patch.object(storeapi._dashboard_api.DashboardAPI, "get_account_information") @mock.patch("snapcraft_legacy._store._get_data_from_snap_file") def test_sign_build_upload_successfully( - self, mock_get_snap_data, mock_get_account_info, mock_push_snap_build, + self, + mock_get_snap_data, + mock_get_account_info, + mock_push_snap_build, ): mock_get_account_info.return_value = { "account_id": "abcd", - "account_keys": [{"public-key-sha3-384": "a_hash"}], + "account_keys": [ + { + "public-key-sha3-384": "vdEeQvRxmZ26npJCFaGnl-VfGz0lU2jZZkWp_s7E-RxVCNtH2_mtjcxq2NkDKkIp" + } + ], "snaps": {"16": {"test-snap": {"snap-id": "snap-id"}}}, } mock_get_snap_data.return_value = {"name": "test-snap", "grade": "stable"} fake_check_output = fixtures.MockPatch( "subprocess.check_output", - side_effect=[ - '[{"name": "default", "sha3-384": "a_hash"}]'.encode(), - b"Mocked assertion", - ], + side_effect=mock_check_output, ) self.useFixture(fake_check_output) - result = self.run_command(["sign-build", self.snap_test.snap_path]) + result = self.run_command(["sign-build", self.snap_test.snap_path], input="1\n") self.assertThat(result.exit_code, Equals(0)) snap_build_path = self.snap_test.snap_path + "-build" @@ -369,7 +341,10 @@ def test_sign_build_upload_successfully( @mock.patch.object(storeapi._dashboard_api.DashboardAPI, "get_account_information") @mock.patch("snapcraft_legacy._store._get_data_from_snap_file") def test_sign_build_upload_existing( - self, mock_get_snap_data, mock_get_account_info, mock_push_snap_build, + self, + mock_get_snap_data, + mock_get_account_info, + mock_push_snap_build, ): mock_get_account_info.return_value = { "account_id": "abcd", diff --git a/tests/legacy/unit/commands/test_status.py b/tests/legacy/unit/commands/test_status.py index 49267d9584..cd1f52dba1 100644 --- a/tests/legacy/unit/commands/test_status.py +++ b/tests/legacy/unit/commands/test_status.py @@ -18,7 +18,6 @@ from testtools.matchers import Contains, Equals -from snapcraft_legacy import storeapi from snapcraft_legacy.storeapi.v2.channel_map import ( MappedChannel, Progressive, @@ -26,7 +25,7 @@ SnapChannel, ) -from . import FakeStoreCommandsBaseTestCase +from . import FAKE_UNAUTHORIZED_ERROR, FakeStoreCommandsBaseTestCase class StatusCommandTestCase(FakeStoreCommandsBaseTestCase): @@ -38,7 +37,7 @@ def test_status_without_snap_raises_exception(self): def test_status_without_login_must_ask(self): self.fake_store_get_snap_channel_map.mock.side_effect = [ - storeapi.http_clients.errors.InvalidCredentialsError("error"), + FAKE_UNAUTHORIZED_ERROR, self.channel_map, ] diff --git a/tests/legacy/unit/commands/test_upload.py b/tests/legacy/unit/commands/test_upload.py index be971680dc..ed718de1c6 100644 --- a/tests/legacy/unit/commands/test_upload.py +++ b/tests/legacy/unit/commands/test_upload.py @@ -14,11 +14,14 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +import json import logging import os from unittest import mock +import craft_store import fixtures +import requests from testtools.matchers import Contains, Equals, FileExists, Not from xdg import BaseDirectory @@ -31,7 +34,7 @@ StoreUploadError, ) -from . import FakeStoreCommandsBaseTestCase +from . import FAKE_UNAUTHORIZED_ERROR, FakeResponse, FakeStoreCommandsBaseTestCase class UploadCommandBaseTestCase(FakeStoreCommandsBaseTestCase): @@ -159,7 +162,7 @@ def test_upload_with_started_at(self): def test_upload_without_login_must_ask(self): self.fake_store_upload_precheck.mock.side_effect = [ - storeapi.http_clients.errors.InvalidCredentialsError("error"), + FAKE_UNAUTHORIZED_ERROR, None, ] @@ -459,22 +462,22 @@ def test_upload_with_disabled_delta_falls_back(self): self.assertThat(result.exit_code, Equals(0)) - class _FakeResponse: - status_code = 501 - reason = "disabled" - - def json(self): - return { - "error_list": [ + self.fake_store_upload.mock.side_effect = [ + craft_store.errors.StoreServerError( + FakeResponse( + status_code=requests.codes.not_implemented, + content=json.dumps( { - "code": "feature-disabled", - "message": "The delta upload support is currently disabled.", + "error_list": [ + { + "code": "feature-disabled", + "message": "The delta upload support is currently disabled.", + } + ] } - ] - } - - self.fake_store_upload.mock.side_effect = [ - storeapi.http_clients.errors.StoreServerError(_FakeResponse()), + ), + ) + ), self.mock_tracker, ] diff --git a/tests/legacy/unit/commands/test_upload_metadata.py b/tests/legacy/unit/commands/test_upload_metadata.py index e2a34c55b8..aabc4f09c5 100644 --- a/tests/legacy/unit/commands/test_upload_metadata.py +++ b/tests/legacy/unit/commands/test_upload_metadata.py @@ -25,7 +25,7 @@ from snapcraft_legacy import storeapi from snapcraft_legacy.storeapi.errors import StoreUploadError -from . import CommandBaseTestCase +from . import FAKE_UNAUTHORIZED_ERROR, CommandBaseTestCase class UploadMetadataCommandTestCase(CommandBaseTestCase): @@ -140,6 +140,9 @@ def test_upload_metadata_without_login_must_ask(self): self.fake_store_login = fixtures.MockPatchObject(storeapi.StoreClient, "login") self.useFixture(self.fake_store_login) + self.fake_store_login = fixtures.MockPatchObject(storeapi.StoreClient, "logout") + self.useFixture(self.fake_store_login) + self.fake_store_account_info = fixtures.MockPatchObject( storeapi._dashboard_api.DashboardAPI, "get_account_information", @@ -162,7 +165,7 @@ def test_upload_metadata_without_login_must_ask(self): self.useFixture(self.fake_store_account_info) self.fake_metadata.mock.side_effect = [ - storeapi.http_clients.errors.InvalidCredentialsError("error"), + FAKE_UNAUTHORIZED_ERROR, None, ] diff --git a/tests/legacy/unit/commands/test_validate.py b/tests/legacy/unit/commands/test_validate.py index a9e04cc736..a968cad1ed 100644 --- a/tests/legacy/unit/commands/test_validate.py +++ b/tests/legacy/unit/commands/test_validate.py @@ -35,8 +35,6 @@ def setUp(self): self.popen_mock.return_value = rv_mock self.addCleanup(patcher.stop) - self.client.login(email="dummy", password="test correct password") - def test_validate_success(self): result = self.run_command(["validate", "core", "core=3", "test-snap=4"]) diff --git a/tests/legacy/unit/commands/test_whoami.py b/tests/legacy/unit/commands/test_whoami.py index 3b5feeda9a..3b8275cd54 100644 --- a/tests/legacy/unit/commands/test_whoami.py +++ b/tests/legacy/unit/commands/test_whoami.py @@ -38,6 +38,7 @@ def fake_dashboard_whoami(monkeypatch): ) +@pytest.mark.usefixtures("memory_keyring") @pytest.mark.usefixtures("fake_dashboard_whoami") def test_whoami(click_run): result = click_run(["whoami"]) diff --git a/tests/legacy/unit/store/http_client/__init__.py b/tests/legacy/unit/store/http_client/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/legacy/unit/store/http_client/test_candid_client.py b/tests/legacy/unit/store/http_client/test_candid_client.py deleted file mode 100644 index 88b5665e01..0000000000 --- a/tests/legacy/unit/store/http_client/test_candid_client.py +++ /dev/null @@ -1,342 +0,0 @@ -# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- -# -# Copyright (C) 2021 Canonical Ltd -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License version 3 as -# published by the Free Software Foundation. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -import io -import json -from textwrap import dedent -from unittest.mock import Mock, call, patch - -import pytest -from macaroonbakery import bakery, httpbakery -from pymacaroons.macaroon import Macaroon - -from snapcraft_legacy.storeapi.http_clients._candid_client import ( - CandidClient, - CandidConfig, - WebBrowserWaitingInteractor, - errors, - _http_client, -) - - -def test_config_section_name(): - assert CandidConfig()._get_section_name() == "dashboard.snapcraft.io" - - -def test_config_section_name_with_env(monkeypatch): - monkeypatch.setenv("STORE_DASHBOARD_URL", "http://dashboard.other.com") - - assert CandidConfig()._get_section_name() == "dashboard.other.com" - - -def test_config_path(xdg_dirs): - assert ( - CandidConfig()._get_config_path() == xdg_dirs / ".config/snapcraft/candid.cfg" - ) - - -def test_candid_client_has_no_credentials(xdg_dirs): - assert CandidClient().has_credentials() is False - - -def test_candid_client_has_credentials(xdg_dirs): - # Baseline check. - assert CandidClient().has_credentials() is False - - # Setup. - client = CandidClient() - client._macaroon = "macaroon" - client._auth = "auth" - - assert client.has_credentials() is True - assert CandidClient().has_credentials() is True - - -@pytest.fixture -def candid_client(xdg_dirs, monkeypatch): - """Return a CandidClient with an alterate requests method.""" - bakery_client = Mock(spec=httpbakery.Client) - - def mock_discharge(*args, **kwargs): - return [ - Macaroon( - location="api.snapcraft.io", - signature="d9533461d7835e4851c7e3b639144406cf768597dea6e133232fbd2385a5c050", - ) - ] - - monkeypatch.setattr(bakery, "discharge_all", mock_discharge) - - return CandidClient(bakery_client=bakery_client) - - -@pytest.fixture -def snapcraft_macaroon(): - return json.dumps( - { - "s64": "a0Vi7CwhHWjS4bxzKPhCZQIEJDvlbh9FyhOtWx0tNFQ", - "c": [ - {"i": "time-before 2022-03-18T19:54:57.151721Z"}, - { - "v64": "pDqaL9KDrPfCQCLDUdPc8yO2bTQheWGsM1tpxRaS_4BT3r6zpdnT5TelXz8vpjb4iUhTnc60-x5DPKJOpRuwAi4qMdNa67Vo", - "l": "https://api.jujucharms.com/identity/", - "i64": "AoZh2j7mbDQgh3oK3qMqoXKKFAnJvmOKwmDCNYHIxHqQnFLJZJUBpqoiJtqra-tyXPPMUTmfuXMgOWP7xKwTD26FBgtJBdh1mE1wt3kf0Ur_TnOzbAWQCHKxqK9jAp1jYv-LlLLAlQAmoqvz9fBf2--dIxHiLIRTThmAESAnlLZHOJ7praDmIScsLQC475a85avA", - }, - { - "i": 'extra {"package_id": null, "channel": null, "acl": ["package_access", "package_manage", "package_push", "package_register", "package_release", "package_update"], "store_ids": null}' - }, - ], - "l": "api.snapcraft.io", - "i64": "AwoQ2Ft5YBjnovqdr8VNV3TSlhIBMBoOCgVsb2dpbhIFbG9naW4", - } - ) - - -def test_login_discharge_macaroon(candid_client, snapcraft_macaroon): - candid_client.request - candid_client.login(macaroon=snapcraft_macaroon) - - assert candid_client.has_credentials() is True - assert candid_client._macaroon == snapcraft_macaroon - assert candid_client._auth == ( - "W3siaWRlbnRpZmllciI6ICIiLCAic2lnbmF0dXJlIjogImQ5NTMzNDYxZDc4MzVlNDg1MWM" - "3ZTNiNjM5MTQ0NDA2Y2Y3Njg1OTdkZWE2ZTEzMzIzMmZiZDIzODVhNWMwNTAiLCAibG9jYX" - "Rpb24iOiAiYXBpLnNuYXBjcmFmdC5pbyJ9XQ==" - ) - - -def test_login_discharge_macaroon_no_save(candid_client, snapcraft_macaroon): - candid_client.login(macaroon=snapcraft_macaroon, save=False) - - assert candid_client.has_credentials() is False - assert candid_client._macaroon == snapcraft_macaroon - assert candid_client._auth == ( - "W3siaWRlbnRpZmllciI6ICIiLCAic2lnbmF0dXJlIjogImQ5NTMzNDYxZDc4MzVlNDg1MWM" - "3ZTNiNjM5MTQ0NDA2Y2Y3Njg1OTdkZWE2ZTEzMzIzMmZiZDIzODVhNWMwNTAiLCAibG9jYX" - "Rpb24iOiAiYXBpLnNuYXBjcmFmdC5pbyJ9XQ==" - ) - - -def test_login_with_config_fd(candid_client, snapcraft_macaroon): - with io.StringIO() as config_fd: - print("[dashboard.snapcraft.io]", file=config_fd) - print(f"macaroon = {snapcraft_macaroon}", file=config_fd) - print("auth = 1234567890noshare", file=config_fd) - config_fd.seek(0) - - candid_client.login(config_fd=config_fd) - - assert candid_client.has_credentials() is True - assert candid_client._macaroon == snapcraft_macaroon - assert candid_client._auth == "1234567890noshare" - - -def test_login_with_config_fd_no_save(candid_client, snapcraft_macaroon): - with io.StringIO() as config_fd: - print("[dashboard.snapcraft.io]", file=config_fd) - print(f"macaroon = {snapcraft_macaroon}", file=config_fd) - print("auth = 1234567890noshare", file=config_fd) - config_fd.seek(0) - - candid_client.login(config_fd=config_fd, save=False) - - assert candid_client.has_credentials() is False - assert candid_client._macaroon == snapcraft_macaroon - assert candid_client._auth == "1234567890noshare" - - -@pytest.fixture -def authed_client(candid_client, snapcraft_macaroon): - candid_client.login(macaroon=snapcraft_macaroon) - assert candid_client.has_credentials() is True - - return candid_client - - -def test_logout(authed_client): - authed_client.logout() - - assert authed_client.has_credentials() is False - - -def test_export_login(authed_client): - with io.StringIO() as config_fd: - authed_client.export_login(config_fd=config_fd, encode=False) - - config_fd.seek(0) - - assert config_fd.getvalue().strip() == dedent( - f"""\ - [dashboard.snapcraft.io] - auth = {authed_client._auth} - macaroon = {authed_client._macaroon}""" - ) - - -def test_export_login_base64_encoded(authed_client): - with io.StringIO() as config_fd: - authed_client.export_login(config_fd=config_fd, encode=True) - - config_fd.seek(0) - - assert config_fd.getvalue().strip() == ( - "W2Rhc2hib2FyZC5zbmFwY3JhZnQuaW9dCmF1dGggPSBXM3NpYVdSbGJuUnBabWxsY2lJNklDSWlMQ0FpY" - "zJsbmJtRjBkWEpsSWpvZ0ltUTVOVE16TkRZeFpEYzRNelZsTkRnMU1XTTNaVE5pTmpNNU1UUTBOREEyWT" - "JZM05qZzFPVGRrWldFMlpURXpNekl6TW1aaVpESXpPRFZoTldNd05UQWlMQ0FpYkc5allYUnBiMjRpT2l" - "BaVlYQnBMbk51WVhCamNtRm1kQzVwYnlKOVhRPT0KbWFjYXJvb24gPSB7InM2NCI6ICJhMFZpN0N3aEhX" - "alM0Ynh6S1BoQ1pRSUVKRHZsYmg5RnloT3RXeDB0TkZRIiwgImMiOiBbeyJpIjogInRpbWUtYmVmb3JlI" - "DIwMjItMDMtMThUMTk6NTQ6NTcuMTUxNzIxWiJ9LCB7InY2NCI6ICJwRHFhTDlLRHJQZkNRQ0xEVWRQYz" - "h5TzJiVFFoZVdHc00xdHB4UmFTXzRCVDNyNnpwZG5UNVRlbFh6OHZwamI0aVVoVG5jNjAteDVEUEtKT3B" - "SdXdBaTRxTWROYTY3Vm8iLCAibCI6ICJodHRwczovL2FwaS5qdWp1Y2hhcm1zLmNvbS9pZGVudGl0eS8i" - "LCAiaTY0IjogIkFvWmgyajdtYkRRZ2gzb0szcU1xb1hLS0ZBbkp2bU9Ld21EQ05ZSEl4SHFRbkZMSlpKV" - "UJwcW9pSnRxcmEtdHlYUFBNVVRtZnVYTWdPV1A3eEt3VEQyNkZCZ3RKQmRoMW1FMXd0M2tmMFVyX1RuT3" - "piQVdRQ0hLeHFLOWpBcDFqWXYtTGxMTEFsUUFtb3F2ejlmQmYyLS1kSXhIaUxJUlRUaG1BRVNBbmxMWkh" - "PSjdwcmFEbUlTY3NMUUM0NzVhODVhdkEifSwgeyJpIjogImV4dHJhIHtcInBhY2thZ2VfaWRcIjogbnVs" - "bCwgXCJjaGFubmVsXCI6IG51bGwsIFwiYWNsXCI6IFtcInBhY2thZ2VfYWNjZXNzXCIsIFwicGFja2FnZ" - "V9tYW5hZ2VcIiwgXCJwYWNrYWdlX3B1c2hcIiwgXCJwYWNrYWdlX3JlZ2lzdGVyXCIsIFwicGFja2FnZV" - "9yZWxlYXNlXCIsIFwicGFja2FnZV91cGRhdGVcIl0sIFwic3RvcmVfaWRzXCI6IG51bGx9In1dLCAibCI" - "6ICJhcGkuc25hcGNyYWZ0LmlvIiwgImk2NCI6ICJBd29RMkZ0NVlCam5vdnFkcjhWTlYzVFNsaElCTUJv" - "T0NnVnNiMmRwYmhJRmJHOW5hVzQifQoK" - ) - - -@pytest.fixture -def request_mock(): - patched = patch.object( - _http_client.Client, "request", spec=_http_client.Client.request - ) - try: - yield patched.start() - finally: - patched.stop() - - -@pytest.fixture -def token_response_mock(): - class Response: - MOCK_JSON = { - "kind": "kind", - "token": "TOKEN", - "token64": b"VE9LRU42NA==", - } - - status_code = 200 - - def json(self): - return self.MOCK_JSON - - return Response() - - -def test_wait_for_token_success(request_mock, token_response_mock): - request_mock.return_value = token_response_mock - - wbi = WebBrowserWaitingInteractor() - discharged_token = wbi._wait_for_token(ctx=None, wait_token_url="https://localhost") - - assert discharged_token.kind == "kind" - assert discharged_token.value == "TOKEN" - - -def test_wait_for_token64_success(request_mock, token_response_mock): - token_response_mock.MOCK_JSON.pop("token") - request_mock.return_value = token_response_mock - - wbi = WebBrowserWaitingInteractor() - discharged_token = wbi._wait_for_token(ctx=None, wait_token_url="https://localhost") - - assert discharged_token.kind == "kind" - assert discharged_token.value == b"TOKEN64" - - -def test_wait_for_token_requests_status_not_200(request_mock, token_response_mock): - token_response_mock.status_code = 504 - request_mock.return_value = token_response_mock - - wbi = WebBrowserWaitingInteractor() - with pytest.raises(errors.TokenTimeoutError): - wbi._wait_for_token(ctx=None, wait_token_url="https://localhost") - - -def test_wait_for_token_requests_no_kind(request_mock, token_response_mock): - token_response_mock.MOCK_JSON.pop("kind") - request_mock.return_value = token_response_mock - - wbi = WebBrowserWaitingInteractor() - with pytest.raises(errors.TokenKindError): - wbi._wait_for_token(ctx=None, wait_token_url="https://localhost") - - -def test_wait_for_token_requests_no_token(request_mock, token_response_mock): - token_response_mock.MOCK_JSON.pop("token") - token_response_mock.MOCK_JSON.pop("token64") - request_mock.return_value = token_response_mock - - wbi = WebBrowserWaitingInteractor() - with pytest.raises(errors.TokenValueError): - wbi._wait_for_token(ctx=None, wait_token_url="https://localhost") - - -@pytest.mark.parametrize("method", ["GET", "PUT", "POST"]) -@pytest.mark.parametrize("params", [None, {}, {"foo": "bar"}]) -def test_request(authed_client, request_mock, method, params): - authed_client.request(method, "https://dashboard.snapcraft.io/foo", params=params) - - assert request_mock.mock_calls == [ - call( - method, - "https://dashboard.snapcraft.io/foo", - params=params, - headers={"Macaroons": authed_client._auth}, - ), - call().ok.__bool__(), - ] - - -def test_request_with_headers(authed_client, request_mock): - authed_client.request( - "GET", "https://dashboard.snapcraft.io/foo", headers={"foo": "bar"} - ) - - assert request_mock.mock_calls == [ - call( - "GET", - "https://dashboard.snapcraft.io/foo", - params=None, - headers={"foo": "bar", "Macaroons": authed_client._auth}, - ), - call().ok.__bool__(), - ] - - -@pytest.mark.parametrize("method", ["GET", "PUT", "POST"]) -@pytest.mark.parametrize("params", [None, {}, {"foo": "bar"}]) -@pytest.mark.parametrize("headers", [None, {}, {"foo": "bar"}]) -def test_request_no_auth(authed_client, request_mock, method, params, headers): - authed_client.request( - method, - "https://dashboard.snapcraft.io/foo", - params=params, - headers=headers, - auth_header=False, - ) - - assert request_mock.mock_calls == [ - call( - method, "https://dashboard.snapcraft.io/foo", params=params, headers=headers - ), - call().ok.__bool__(), - ] diff --git a/tests/legacy/unit/store/http_client/test_config.py b/tests/legacy/unit/store/http_client/test_config.py deleted file mode 100644 index 253adc2d3a..0000000000 --- a/tests/legacy/unit/store/http_client/test_config.py +++ /dev/null @@ -1,112 +0,0 @@ -# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- -# -# Copyright 2021 Canonical Ltd -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License version 3 as -# published by the Free Software Foundation. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -import pathlib - -import pytest - -from snapcraft_legacy.storeapi.http_clients import errors -from snapcraft_legacy.storeapi.http_clients._config import Config - - -class ConfigImpl(Config): - def _get_section_name(self) -> str: - return "test-section" - - def _get_config_path(self) -> pathlib.Path: - return pathlib.Path("config.cfg") - - -@pytest.fixture -def conf(tmp_work_path): - conf = ConfigImpl() - yield conf - - -def test_non_existing_file_succeeds(conf): - assert conf.parser.sections() == [] - assert conf.is_section_empty() is True - - -def test_existing_file(conf): - conf.set("foo", "bar") - conf.save() - - conf.load() - - assert conf.get("foo") == "bar" - assert conf.is_section_empty() is False - - -def test_irrelevant_sections_are_ignored(conf): - with conf._get_config_path().open("w") as config_file: - print("[example.com]", file=config_file) - print("foo=bar", file=config_file) - - conf.load() - - assert conf.get("foo") is None - - -def test_clear_preserver_other_sections(conf): - with conf._get_config_path().open("w") as config_file: - print("[keep_me]", file=config_file) - print("foo=bar", file=config_file) - - conf.load() - conf.set("bar", "baz") - - assert conf.get("bar") == "baz" - - conf.clear() - conf.save() - conf.load() - - assert conf.get("bar") is None - assert conf.get("foo", "keep_me") == "bar" - assert conf.is_section_empty() is True - - -def test_save_encoded(conf): - conf.set("bar", "baz") - conf.save(encode=True) - conf.load() - - assert conf.get("bar") == "baz" - with conf._get_config_path().open() as config_file: - assert config_file.read() == "W3Rlc3Qtc2VjdGlvbl0KYmFyID0gYmF6Cgo=\n" - - -def test_save_encoded_other_config_file(conf): - conf.set("bar", "baz") - test_config_file = pathlib.Path("test-config") - with test_config_file.open("w") as config_fd: - conf.save(config_fd=config_fd, encode=True) - config_fd.flush() - - with test_config_file.open() as config_file: - assert config_file.read() == "W3Rlc3Qtc2VjdGlvbl0KYmFyID0gYmF6Cgo=\n" - - -def test_load_invalid_config(conf): - test_config_file = pathlib.Path("test-config") - with test_config_file.open("w") as config_fd: - print("invalid config", file=config_fd) - config_fd.flush() - - with test_config_file.open() as config_fd: - with pytest.raises(errors.InvalidLoginConfig): - conf.load(config_fd=config_fd) diff --git a/tests/legacy/unit/store/http_client/test_errors.py b/tests/legacy/unit/store/http_client/test_errors.py deleted file mode 100644 index a249418307..0000000000 --- a/tests/legacy/unit/store/http_client/test_errors.py +++ /dev/null @@ -1,138 +0,0 @@ -# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- -# -# Copyright 2021 Canonical Ltd -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License version 3 as -# published by the Free Software Foundation. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -import requests -import urllib3 -from unittest import mock - -from snapcraft_legacy.storeapi.http_clients import errors - - -def _fake_error_response(status_code, reason): - response = mock.Mock() - response.status_code = status_code - response.reason = reason - return response - - -class TestSnapcraftException: - scenarios = ( - ( - "InvalidCredentialsError", - { - "exception_class": errors.InvalidCredentialsError, - "kwargs": {"message": "macaroon expired"}, - "expected_message": ( - "Invalid credentials: macaroon expired. " - 'Have you run "snapcraft login"?' - ), - }, - ), - ( - "StoreAuthenticationError", - { - "exception_class": errors.StoreAuthenticationError, - "kwargs": {"message": "invalid password"}, - "expected_message": ("Authentication error: invalid password"), - }, - ), - ( - "StoreNetworkError generic error", - { - "exception_class": errors.StoreNetworkError, - "kwargs": { - "exception": requests.exceptions.ConnectionError("bad error") - }, - "expected_message": "There seems to be a network error: bad error", - }, - ), - ( - "StoreNetworkError max retry error", - { - "exception_class": errors.StoreNetworkError, - "kwargs": { - "exception": requests.exceptions.ConnectionError( - urllib3.exceptions.MaxRetryError( - pool="test-pool", url="test-url" - ) - ) - }, - "expected_message": ( - "There seems to be a network error: maximum retries exceeded " - "trying to reach the store.\n" - "Check your network connection, and check the store status at " - "https://status.snapcraft.io/" - ), - }, - ), - ( - "StoreServerError 500", - { - "exception_class": errors.StoreServerError, - "kwargs": { - "response": _fake_error_response(500, "internal server error") - }, - "expected_message": ( - "The Snap Store encountered an error while processing your " - "request: internal server error (code 500).\nThe operational " - "status of the Snap Store can be checked at " - "https://status.snapcraft.io/" - ), - }, - ), - ( - "StoreServerError 501", - { - "exception_class": errors.StoreServerError, - "kwargs": {"response": _fake_error_response(501, "not implemented")}, - "expected_message": ( - "The Snap Store encountered an error while processing your " - "request: not implemented (code 501).\nThe operational " - "status of the Snap Store can be checked at " - "https://status.snapcraft.io/" - ), - }, - ), - ( - "TokenTimeoutError", - { - "exception_class": errors.TokenTimeoutError, - "kwargs": {"url": "https://foo"}, - "expected_message": ( - "Timed out waiting for token response from 'https://foo'." - ), - }, - ), - ( - "TokenKindError", - { - "exception_class": errors.TokenKindError, - "kwargs": {"url": "https://foo"}, - "expected_message": ("Empty token kind returned from 'https://foo'."), - }, - ), - ( - "TokenValueError", - { - "exception_class": errors.TokenValueError, - "kwargs": {"url": "https://foo"}, - "expected_message": ("Empty token value returned from 'https://foo'."), - }, - ), - ) - - def test_error_formatting(self, exception_class, expected_message, kwargs): - assert str(exception_class(**kwargs)) == expected_message diff --git a/tests/legacy/unit/store/http_client/test_ubuntu_one_auth_client.py b/tests/legacy/unit/store/http_client/test_ubuntu_one_auth_client.py deleted file mode 100644 index ac8efb0dab..0000000000 --- a/tests/legacy/unit/store/http_client/test_ubuntu_one_auth_client.py +++ /dev/null @@ -1,48 +0,0 @@ -# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- -# -# Copyright 2021 Canonical Ltd -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License version 3 as -# published by the Free Software Foundation. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -import pathlib - -import pymacaroons -import pytest - -from snapcraft_legacy.storeapi import http_clients - - -def test_invalid_macaroon_root_raises_exception(tmp_work_path): - with pathlib.Path("conf").open("w") as config_fd: - print("[login.ubuntu.com]", file=config_fd) - print("macaroon=inval'id", file=config_fd) - config_fd.flush() - - client = http_clients.UbuntuOneAuthClient() - with pathlib.Path("conf").open() as config_fd: - with pytest.raises(http_clients.errors.InvalidCredentialsError): - client.login(config_fd=config_fd) - - -def test_invalid_discharge_raises_exception(): - with pathlib.Path("conf").open("w") as config_fd: - print("[login.ubuntu.com]", file=config_fd) - print("macaroon={}".format(pymacaroons.Macaroon().serialize()), file=config_fd) - print("unbound_discharge=inval'id", file=config_fd) - config_fd.flush() - - client = http_clients.UbuntuOneAuthClient() - - with pathlib.Path("conf").open() as config_fd: - with pytest.raises(http_clients.errors.InvalidCredentialsError): - client.login(config_fd=config_fd) diff --git a/tests/legacy/unit/store/http_client/test_agent.py b/tests/legacy/unit/store/test_agent.py similarity index 98% rename from tests/legacy/unit/store/http_client/test_agent.py rename to tests/legacy/unit/store/test_agent.py index a2382e696d..0150c68bc9 100644 --- a/tests/legacy/unit/store/http_client/test_agent.py +++ b/tests/legacy/unit/store/test_agent.py @@ -21,7 +21,7 @@ from snapcraft_legacy import ProjectOptions from snapcraft_legacy import __version__ as snapcraft_version -from snapcraft_legacy.storeapi.http_clients import agent +from snapcraft_legacy.storeapi import agent from tests.legacy import unit from tests.legacy.fixture_setup.os_release import FakeOsRelease diff --git a/tests/legacy/unit/store/test_store_client.py b/tests/legacy/unit/store/test_store_client.py index 3322c5442b..c891dcea29 100644 --- a/tests/legacy/unit/store/test_store_client.py +++ b/tests/legacy/unit/store/test_store_client.py @@ -17,10 +17,10 @@ import json import logging import os -import pathlib import tempfile from textwrap import dedent from unittest import mock +import craft_store import fixtures import pytest @@ -36,141 +36,19 @@ import tests.legacy from snapcraft_legacy import storeapi -from snapcraft_legacy.storeapi import errors, http_clients, metrics +from snapcraft_legacy.storeapi import errors, metrics from snapcraft_legacy.storeapi.v2 import channel_map, releases, validation_sets, whoami from tests.legacy import fixture_setup, unit +@pytest.mark.usefixtures("memory_keyring") class StoreTestCase(unit.TestCase): def setUp(self): super().setUp() self.fake_store = self.useFixture(fixture_setup.FakeStore()) self.client = storeapi.StoreClient() - - -class LoginTestCase(StoreTestCase): - def test_login_successful(self): - self.client.login(email="dummy email", password="test correct password") - self.assertIsNotNone(self.client.auth_client.auth) - - def test_login_successful_with_one_time_password(self): - self.client.login( - email="dummy email", - password="test correct password", - otp="test correct one-time password", - ) - self.assertIsNotNone(self.client.auth_client.auth) - - def test_login_successful_with_package_attenuation(self): - self.client.login( - email="dummy email", - password="test correct password", - packages=[{"name": "foo", "series": "16"}], - ) - self.assertIsNotNone(self.client.auth_client.auth) - - def test_login_successful_with_channel_attenuation(self): - self.client.login( - email="dummy email", password="test correct password", channels=["edge"] - ) - self.assertIsNotNone(self.client.auth_client.auth) - - def test_login_successful_fully_attenuated(self): - self.client.login( - email="dummy email", - password="test correct password", - packages=[{"name": "foo", "series": "16"}], - channels=["edge"], - save=False, - ) - # Client configuration is filled, but it's not saved on disk. - self.assertThat( - self.client.auth_client._conf._get_config_path(), Not(FileExists()) - ) - self.assertIsNotNone(self.client.auth_client.auth) - - def test_login_successful_with_expiration(self): - self.client.login( - email="dummy email", - password="test correct password", - packages=[{"name": "foo", "series": "16"}], - channels=["edge"], - expires="2017-12-22", - ) - self.assertIsNotNone(self.client.auth_client.auth) - - def test_login_with_exported_login(self): - with pathlib.Path("test-exported-login").open("w") as config_fd: - print( - "[{}]".format(self.client.auth_client._conf._get_section_name()), - file=config_fd, - ) - print( - "macaroon=MDAwZWxvY2F0aW9uIAowMDEwaWRlbnRpZmllciAKMDAxNGNpZCB0ZXN0IGNhdmVhdAowMDE5dmlkIHRlc3QgdmVyaWZpYWNpb24KMDAxN2NsIGxvY2FsaG9zdDozNTM1MQowMDBmc2lnbmF0dXJlIAo", - file=config_fd, - ) - print( - "unbound_discharge=MDAwZWxvY2F0aW9uIAowMDEwaWRlbnRpZmllciAKMDAwZnNpZ25hdHVyZSAK", - file=config_fd, - ) - config_fd.flush() - - with pathlib.Path("test-exported-login").open() as config_fd: - self.client.login(config_fd=config_fd) - - self.assertThat( - self.client.auth_client._conf.get("macaroon"), - Equals( - "MDAwZWxvY2F0aW9uIAowMDEwaWRlbnRpZmllciAKMDAxNGNpZCB0ZXN0IGNhdmVhdAowMDE5dmlkIHRlc3QgdmVyaWZpYWNpb24KMDAxN2NsIGxvY2FsaG9zdDozNTM1MQowMDBmc2lnbmF0dXJlIAo" - ), - ) - self.assertThat( - self.client.auth_client._conf.get("unbound_discharge"), - Equals("MDAwZWxvY2F0aW9uIAowMDEwaWRlbnRpZmllciAKMDAwZnNpZ25hdHVyZSAK"), - ) - self.assertThat( - self.client.auth_client.auth, - Equals( - "Macaroon root=MDAwZWxvY2F0aW9uIAowMDEwaWRlbnRpZmllciAKMDAxNGNpZCB0ZXN0IGNhdmVhdAowMDE5dmlkIHRlc3QgdmVyaWZpYWNpb24KMDAxN2NsIGxvY2FsaG9zdDozNTM1MQowMDBmc2lnbmF0dXJlIAo, discharge=MDAwZWxvY2F0aW9uIAowMDEwaWRlbnRpZmllciAKMDAyZnNpZ25hdHVyZSDmRizXTOkAmfmy5hGCm7F0H4LBea16YbJYVhDkAJZ-Ago" - ), - ) - - def test_failed_login_with_wrong_password(self): - self.assertRaises( - http_clients.errors.StoreAuthenticationError, - self.client.login, - email="dummy email", - password="wrong password", - ) - - def test_failed_login_requires_one_time_password(self): - self.assertRaises( - http_clients.errors.StoreTwoFactorAuthenticationRequired, - self.client.login, - email="dummy email", - password="test requires 2fa", - ) - - def test_failed_login_with_wrong_one_time_password(self): - self.assertRaises( - http_clients.errors.StoreAuthenticationError, - self.client.login, - email="dummy email", - password="test correct password", - otp="wrong one-time password", - ) - - def test_failed_login_with_unregistered_snap(self): - raised = self.assertRaises( - errors.GeneralStoreError, - self.client.login, - email="dummy email", - password="test correct password", - packages=[{"name": "unregistered-snap-name", "series": "16"}], - ) - - self.assertThat(str(raised), Contains("not found")) + self.client.login(email="dummy", password="test correct password", ttl=2) class DownloadTestCase(StoreTestCase): @@ -179,7 +57,6 @@ class DownloadTestCase(StoreTestCase): EXPECTED_SHA3_384 = "" def test_download_nonexistent_snap_raises_exception(self): - self.client.login(email="dummy", password="test correct password") raised = self.assertRaises( errors.SnapNotFoundError, @@ -197,7 +74,6 @@ def test_download_nonexistent_snap_raises_exception(self): def test_download_snap(self): fake_logger = fixtures.FakeLogger(level=logging.INFO) self.useFixture(fake_logger) - self.client.login(email="dummy", password="test correct password") download_path = os.path.join(self.path, "test-snap.snap") self.client.download("test-snap", risk="stable", download_path=download_path) self.assertThat(download_path, FileExists()) @@ -205,7 +81,6 @@ def test_download_snap(self): def test_download_snap_missing_risk(self): fake_logger = fixtures.FakeLogger(level=logging.INFO) self.useFixture(fake_logger) - self.client.login(email="dummy", password="test correct password") raised = self.assertRaises( errors.SnapNotFoundError, @@ -220,7 +95,6 @@ def test_download_snap_missing_risk(self): self.expectThat(raised._arch, Is(None)) def test_download_from_brand_store_requires_store(self): - self.client.login(email="dummy", password="test correct password") raised = self.assertRaises( errors.SnapNotFoundError, self.client.download, @@ -243,7 +117,6 @@ def test_download_from_branded_store(self): self.useFixture( fixtures.EnvironmentVariable("SNAPCRAFT_UBUNTU_STORE", "Test-Branded") ) - self.client.login(email="dummy", password="test correct password") download_path = os.path.join(self.path, "brand.snap") self.client.download( @@ -252,7 +125,6 @@ def test_download_from_branded_store(self): self.assertThat(download_path, FileExists()) def test_download_already_downloaded_snap(self): - self.client.login(email="dummy", password="test correct password") download_path = os.path.join(self.path, "test-snap.snap") # download first time. self.client.download("test-snap", risk="stable", download_path=download_path) @@ -266,7 +138,6 @@ def test_download_already_downloaded_snap(self): def test_download_on_sha_mismatch(self): fake_logger = fixtures.FakeLogger(level=logging.INFO) self.useFixture(fake_logger) - self.client.login(email="dummy", password="test correct password") download_path = os.path.join(self.path, "test-snap.snap") # Write a wrong file in the download path. open(download_path, "w").close() @@ -277,7 +148,6 @@ def test_download_on_sha_mismatch(self): self.assertThat(second_stat, Not(Equals(first_stat))) def test_download_with_hash_mismatch_raises_exception(self): - self.client.login(email="dummy", password="test correct password") download_path = os.path.join(self.path, "test-snap.snap") self.assertRaises( errors.SHAMismatchError, @@ -289,26 +159,7 @@ def test_download_with_hash_mismatch_raises_exception(self): class PushSnapBuildTestCase(StoreTestCase): - def test_push_snap_build_refreshes_macaroon(self): - self.client.login(email="dummy", password="test correct password") - self.fake_store.needs_refresh = True - self.client.push_snap_build("snap-id", "dummy") - self.assertFalse(self.fake_store.needs_refresh) - - def test_push_snap_build_not_implemented(self): - # If the "enable_snap_build" feature switch is off in the store, we - # will get a descriptive error message. - self.client.login(email="dummy", password="test correct password") - raised = self.assertRaises( - http_clients.errors.StoreServerError, - self.client.push_snap_build, - "snap-id", - "test-not-implemented", - ) - self.assertThat(raised.error_code, Equals(501)) - def test_push_snap_build_invalid_data(self): - self.client.login(email="dummy", password="test correct password") raised = self.assertRaises( errors.StoreSnapBuildError, self.client.push_snap_build, @@ -320,107 +171,13 @@ def test_push_snap_build_invalid_data(self): Equals("Could not assert build: The snap-build assertion is not " "valid."), ) - def test_push_snap_build_unexpected_data(self): - # The endpoint in SCA would never return plain/text, however anything - # might happen in the internet, so we are a little defensive. - self.client.login(email="dummy", password="test correct password") - raised = self.assertRaises( - http_clients.errors.StoreServerError, - self.client.push_snap_build, - "snap-id", - "test-unexpected-data", - ) - self.assertThat(raised.error_code, Equals(500)) - def test_push_snap_build_successfully(self): - self.client.login(email="dummy", password="test correct password") # No exception will be raised if this is successful. self.client.push_snap_build("snap-id", "dummy") class GetAccountInformationTestCase(StoreTestCase): def test_get_account_information_successfully(self): - self.client.login(email="dummy", password="test correct password") - self.assertThat( - self.client.get_account_information(), - Equals( - { - "account_id": "abcd", - "account_keys": [], - "snaps": { - "16": { - "basic": { - "snap-id": "snap-id", - "status": "Approved", - "private": False, - "price": None, - "since": "2016-12-12T01:01:01Z", - }, - "core": { - "snap-id": "good", - "status": "Approved", - "private": False, - "price": None, - "since": "2016-12-12T01:01:01Z", - }, - "core-no-dev": { - "snap-id": "no-dev", - "status": "Approved", - "private": False, - "price": None, - "since": "2016-12-12T01:01:01Z", - }, - "badrequest": { - "snap-id": "badrequest", - "status": "Approved", - "private": False, - "price": None, - "since": "2016-12-12T01:01:01Z", - }, - "no-revoked": { - "snap-id": "no-revoked", - "status": "Approved", - "private": False, - "price": None, - "since": "2016-12-12T01:01:01Z", - }, - "revoked": { - "snap-id": "revoked", - "status": "Approved", - "private": False, - "price": None, - "since": "2016-12-12T01:01:01Z", - }, - "test-snap-with-dev": { - "price": None, - "private": False, - "since": "2016-12-12T01:01:01Z", - "snap-id": "test-snap-id-with-dev", - "status": "Approved", - }, - "test-snap-with-no-validations": { - "price": None, - "private": False, - "since": "2016-12-12T01:01:01Z", - "snap-id": "test-snap-id-with-no-validations", - "status": "Approved", - }, - "no-id": { - "snap-id": None, - "status": "Approved", - "private": False, - "price": None, - "since": "2016-12-12T01:01:01Z", - }, - } - }, - } - ), - ) - - def test_get_account_information_refreshes_macaroon(self): - self.client.login(email="dummy", password="test correct password") - self.fake_store.needs_refresh = True self.assertThat( self.client.get_account_information(), Equals( @@ -497,12 +254,10 @@ def test_get_account_information_refreshes_macaroon(self): } ), ) - self.assertFalse(self.fake_store.needs_refresh) class RegisterKeyTestCase(StoreTestCase): def test_register_key_successfully(self): - self.client.login(email="dummy", password="test correct password") # No exception will be raised if this is successful. self.client.register_key( dedent( @@ -513,32 +268,7 @@ def test_register_key_successfully(self): ) ) - def test_register_key_refreshes_macaroon(self): - self.client.login(email="dummy", password="test correct password") - self.fake_store.needs_refresh = True - self.client.register_key( - dedent( - """\ - name: default - public-key-sha3-384: abcd - """ - ) - ) - self.assertFalse(self.fake_store.needs_refresh) - - def test_not_implemented(self): - # If the enable_account_key feature switch is off in the store, we - # will get a 501 Not Implemented response. - self.client.login(email="dummy", password="test correct password") - raised = self.assertRaises( - http_clients.errors.StoreServerError, - self.client.register_key, - "test-not-implemented", - ) - self.assertThat(raised.error_code, Equals(501)) - def test_invalid_data(self): - self.client.login(email="dummy", password="test correct password") raised = self.assertRaises( errors.StoreKeyRegistrationError, self.client.register_key, @@ -555,28 +285,18 @@ def test_invalid_data(self): class RegisterTestCase(StoreTestCase): def test_register_name_successfully(self): - self.client.login(email="dummy", password="test correct password") # No exception will be raised if this is successful self.client.register("test-good-snap-name") def test_register_name_successfully_to_store_id(self): - self.client.login(email="dummy", password="test correct password") # No exception will be raised if this is successful self.client.register("test-good-snap-name", store_id="my-brand") def test_register_private_name_successfully(self): - self.client.login(email="dummy", password="test correct password") # No exception will be raised if this is successful self.client.register("test-good-snap-name", is_private=True) - def test_register_refreshes_macaroon(self): - self.client.login(email="dummy", password="test correct password") - self.fake_store.needs_refresh = True - self.client.register("test-good-snap-name") - self.assertFalse(self.fake_store.needs_refresh) - def test_already_registered(self): - self.client.login(email="dummy", password="test correct password") raised = self.assertRaises( errors.StoreRegistrationError, self.client.register, @@ -595,7 +315,6 @@ def test_already_registered(self): ) def test_register_a_reserved_name(self): - self.client.login(email="dummy", password="test correct password") raised = self.assertRaises( errors.StoreRegistrationError, self.client.register, @@ -614,7 +333,6 @@ def test_register_a_reserved_name(self): ) def test_register_already_owned_name(self): - self.client.login(email="dummy", password="test correct password") raised = self.assertRaises( errors.StoreRegistrationError, self.client.register, @@ -626,7 +344,6 @@ def test_register_already_owned_name(self): ) def test_registering_too_fast_in_a_row(self): - self.client.login(email="dummy", password="test correct password") raised = self.assertRaises( errors.StoreRegistrationError, self.client.register, "test-snapcraft-fast" ) @@ -638,7 +355,6 @@ def test_registering_too_fast_in_a_row(self): ) def test_registering_name_too_long(self): - self.client.login(email="dummy", password="test correct password") name = "name-too-l{}ng".format("0" * 40) raised = self.assertRaises( errors.StoreRegistrationError, self.client.register, name @@ -650,7 +366,6 @@ def test_registering_name_too_long(self): self.assertThat(str(raised), Equals(expected)) def test_registering_name_invalid(self): - self.client.login(email="dummy", password="test correct password") name = "test_invalid" raised = self.assertRaises( errors.StoreRegistrationError, self.client.register, name @@ -663,7 +378,6 @@ def test_registering_name_invalid(self): self.assertThat(str(raised), Equals(expected)) def test_unhandled_registration_error_path(self): - self.client.login(email="dummy", password="test correct password") raised = self.assertRaises( errors.StoreRegistrationError, self.client.register, @@ -677,7 +391,6 @@ def test_unhandled_registration_error_path(self): class ValidationSetsTestCase(StoreTestCase): def setUp(self): super().setUp() - self.client.login(email="dummy", password="test correct password") self.validation_sets_build = { "name": "acme-cert-2020-10", @@ -842,7 +555,6 @@ def setUp(self): self.useFixture(self.fake_logger) def test_get_success(self): - self.client.login(email="dummy", password="test correct password") expected = [ { "approved-snap-id": "snap-id-1", @@ -888,8 +600,6 @@ def test_get_success(self): self.assertThat(result, Equals(expected)) def test_get_bad_response(self): - self.client.login(email="dummy", password="test correct password") - err = self.assertRaises( errors.StoreValidationError, self.client.get_assertion, "bad", "validations" ) @@ -898,21 +608,7 @@ def test_get_bad_response(self): self.assertThat(str(err), Equals(expected)) self.assertIn("Invalid response from the server", self.fake_logger.output) - def test_get_error_response(self): - self.client.login(email="dummy", password="test correct password") - - err = self.assertRaises( - http_clients.errors.StoreNetworkError, - self.client.get_assertion, - "err", - "validations", - ) - - expected = "maximum retries exceeded" - self.assertThat(str(err), Contains(expected)) - def test_push_success(self): - self.client.login(email="dummy", password="test correct password") assertion = json.dumps({"foo": "bar"}).encode("utf-8") result = self.client.push_assertion("good", assertion, "validations") @@ -921,7 +617,6 @@ def test_push_success(self): self.assertThat(result, Equals(expected)) def test_push_bad_response(self): - self.client.login(email="dummy", password="test correct password") assertion = json.dumps({"foo": "bar"}).encode("utf-8") err = self.assertRaises( @@ -936,19 +631,6 @@ def test_push_bad_response(self): self.assertThat(str(err), Equals(expected)) self.assertIn("Invalid response from the server", self.fake_logger.output) - def test_push_error_response(self): - self.client.login(email="dummy", password="test correct password") - assertion = json.dumps({"foo": "bar"}).encode("utf-8") - - err = self.assertRaises( - http_clients.errors.StoreServerError, - self.client.push_assertion, - "err", - assertion, - "validations", - ) - self.assertThat(err.error_code, Equals(501)) - class UploadTestCase(StoreTestCase): def setUp(self): @@ -967,7 +649,6 @@ def setUp(self): self.addCleanup(patcher.stop) def test_upload_snap(self): - self.client.login(email="dummy", password="test correct password") self.client.register("test-snap") tracker = self.client.upload("test-snap", self.snap_path) self.assertTrue(isinstance(tracker, storeapi._status_tracker.StatusTracker)) @@ -984,42 +665,7 @@ def test_upload_snap(self): # This should not raise tracker.raise_for_code() - def test_upload_refreshes_macaroon(self): - self.client.login(email="dummy", password="test correct password") - self.client.register("test-snap") - self.fake_store.needs_refresh = True - tracker = self.client.upload("test-snap", self.snap_path) - result = tracker.track() - expected_result = { - "code": "ready_to_release", - "revision": "1", - "url": "/dev/click-apps/5349/rev/1", - "can_release": True, - "processed": True, - } - self.assertThat(result, Equals(expected_result)) - - # This should not raise - tracker.raise_for_code() - - self.assertFalse(self.fake_store.needs_refresh) - - def test_upload_snap_fails_due_to_upload_fail(self): - # Tells the fake updown server to return a 5xx response - self.useFixture(fixtures.EnvironmentVariable("UPDOWN_BROKEN", "1")) - - self.client.login(email="dummy", password="test correct password") - - raised = self.assertRaises( - http_clients.errors.StoreServerError, - self.client.upload, - "test-snap", - self.snap_path, - ) - self.assertThat(raised.error_code, Equals(500)) - def test_upload_snap_requires_review(self): - self.client.login(email="dummy", password="test correct password") self.client.register("test-review-snap") tracker = self.client.upload("test-review-snap", self.snap_path) self.assertTrue(isinstance(tracker, storeapi._status_tracker.StatusTracker)) @@ -1036,7 +682,6 @@ def test_upload_snap_requires_review(self): self.assertRaises(errors.StoreReviewError, tracker.raise_for_code) def test_upload_duplicate_snap(self): - self.client.login(email="dummy", password="test correct password") self.client.register("test-duplicate-snap") tracker = self.client.upload("test-duplicate-snap", self.snap_path) self.assertTrue(isinstance(tracker, storeapi._status_tracker.StatusTracker)) @@ -1062,7 +707,6 @@ def test_upload_duplicate_snap(self): ) def test_braces_in_error_messages_are_literals(self): - self.client.login(email="dummy", password="test correct password") self.client.register("test-scan-error-with-braces") tracker = self.client.upload("test-scan-error-with-braces", self.snap_path) self.assertTrue(isinstance(tracker, storeapi._status_tracker.StatusTracker)) @@ -1088,7 +732,6 @@ def test_braces_in_error_messages_are_literals(self): ) def test_upload_unregistered_snap(self): - self.client.login(email="dummy", password="test correct password") raised = self.assertRaises( errors.StoreUploadError, self.client.upload, @@ -1101,7 +744,6 @@ def test_upload_unregistered_snap(self): ) def test_upload_forbidden_snap(self): - self.client.login(email="dummy", password="test correct password") raised = self.assertRaises( errors.StoreUploadError, self.client.upload, @@ -1120,7 +762,6 @@ def test_upload_forbidden_snap(self): class ReleaseTest(StoreTestCase): def test_release_snap(self): - self.client.login(email="dummy", password="test correct password") channel_map = self.client.release("test-snap", "19", ["beta"]) expected_channel_map = { "opened_channels": ["beta"], @@ -1134,7 +775,6 @@ def test_release_snap(self): self.assertThat(channel_map, Equals(expected_channel_map)) def test_progressive_release_snap(self): - self.client.login(email="dummy", password="test correct password") channel_map = self.client.release( "test-snap", "19", ["beta"], progressive_percentage=10 ) @@ -1151,61 +791,14 @@ def test_progressive_release_snap(self): # done. self.assertThat(channel_map, Equals(expected_channel_map)) - def test_release_refreshes_macaroon(self): - self.client.login(email="dummy", password="test correct password") - self.fake_store.needs_refresh = True - channel_map = self.client.release("test-snap", "19", ["beta"]) - expected_channel_map = { - "opened_channels": ["beta"], - "channel_map": [ - {"channel": "stable", "info": "none"}, - {"channel": "candidate", "info": "none"}, - {"revision": 19, "channel": "beta", "version": "0", "info": "specific"}, - {"channel": "edge", "info": "tracking"}, - ], - } - self.assertThat(channel_map, Equals(expected_channel_map)) - self.assertFalse(self.fake_store.needs_refresh) - def test_release_snap_to_invalid_channel(self): - self.client.login(email="dummy", password="test correct password") raised = self.assertRaises( errors.StoreReleaseError, self.client.release, "test-snap", "19", ["alpha"] ) self.assertThat(str(raised), Equals("Not a valid channel: alpha")) - def test_release_snap_to_bad_channel(self): - self.client.login(email="dummy", password="test correct password") - self.assertRaises( - http_clients.errors.StoreServerError, - self.client.release, - "test-snap", - "19", - ["bad-channel"], - ) - - def test_release_unregistered_snap(self): - self.client.login(email="dummy", password="test correct password") - raised = self.assertRaises( - errors.StoreReleaseError, - self.client.release, - "test-snap-unregistered", - "19", - ["alpha"], - ) - - self.assertThat( - str(raised), - Equals( - "Sorry, try `snapcraft register test-snap-unregistered` " - "before trying to release or choose an existing " - "revision." - ), - ) - def test_release_with_invalid_revision(self): - self.client.login(email="dummy", password="test correct password") raised = self.assertRaises( errors.StoreReleaseError, self.client.release, @@ -1220,7 +813,6 @@ def test_release_with_invalid_revision(self): ) def test_release_to_curly_braced_channel(self): - self.client.login(email="dummy", password="test correct password") raised = self.assertRaises( errors.StoreReleaseError, self.client.release, @@ -1245,14 +837,7 @@ def setUp(self): self.fake_logger = fixtures.FakeLogger(level=logging.DEBUG) self.useFixture(self.fake_logger) - def test_close_refreshes_macaroon(self): - self.client.login(email="dummy", password="test correct password") - self.fake_store.needs_refresh = True - self.client.close_channels("snap-id", ["dummy"]) - self.assertFalse(self.fake_store.needs_refresh) - def test_close_invalid_data(self): - self.client.login(email="dummy", password="test correct password") raised = self.assertRaises( errors.StoreChannelClosingError, self.client.close_channels, @@ -1266,22 +851,9 @@ def test_close_invalid_data(self): ), ) - def test_close_unexpected_data(self): - # The endpoint in SCA would never return plain/text, however anything - # might happen in the internet, so we are a little defensive. - self.client.login(email="dummy", password="test correct password") - raised = self.assertRaises( - http_clients.errors.StoreServerError, - self.client.close_channels, - "snap-id", - ["unexpected"], - ) - self.assertThat(raised.error_code, Equals(500)) - def test_close_broken_store_plain(self): # If the contract is broken by the Store, users will be have additional # debug information available. - self.client.login(email="dummy", password="test correct password") raised = self.assertRaises( errors.StoreChannelClosingError, self.client.close_channels, @@ -1304,34 +876,9 @@ def test_close_broken_store_plain(self): self.assertThat(actual_lines, Equals(expected_lines)) - def test_close_broken_store_json(self): - self.client.login(email="dummy", password="test correct password") - raised = self.assertRaises( - errors.StoreChannelClosingError, - self.client.close_channels, - "snap-id", - ["broken-json"], - ) - self.assertThat(str(raised), Equals("Could not close channel: 200 OK")) - - expected_lines = [ - "Invalid response from the server on channel closing:", - "200 OK", - 'b\'{"closed_channels": ["broken-json"]}\'', - ] - - actual_lines = [] - for line in self.fake_logger.output.splitlines(): - line = line.strip() - if line in expected_lines: - actual_lines.append(line) - - self.assertThat(actual_lines, Equals(expected_lines)) - def test_close_successfully(self): # Successfully closing a channels returns 'closed_channels' # and 'channel_map_tree' from the Store. - self.client.login(email="dummy", password="test correct password") closed_channels, channel_map_tree = self.client.close_channels( "snap-id", ["beta"] ) @@ -1442,11 +989,9 @@ def setUp(self): } def test_get_snap_status_successfully(self): - self.client.login(email="dummy", password="test correct password") self.assertThat(self.client.get_snap_status("basic"), Equals(self.expected)) def test_get_snap_status_filter_by_arch(self): - self.client.login(email="dummy", password="test correct password") exp_arch = self.expected["channel_map_tree"]["latest"]["16"]["amd64"] self.assertThat( self.client.get_snap_status("basic", arch="amd64"), @@ -1454,7 +999,6 @@ def test_get_snap_status_filter_by_arch(self): ) def test_get_snap_status_filter_by_unknown_arch(self): - self.client.login(email="dummy", password="test correct password") raised = self.assertRaises( storeapi.errors.SnapNotFoundError, @@ -1468,45 +1012,14 @@ def test_get_snap_status_filter_by_unknown_arch(self): self.expectThat(raised._arch, Is("some-arch")) def test_get_snap_status_no_id(self): - self.client.login(email="dummy", password="test correct password") e = self.assertRaises( storeapi.errors.NoSnapIdError, self.client.get_snap_status, "no-id" ) self.assertThat(e.snap_name, Equals("no-id")) - def test_get_snap_status_refreshes_macaroon(self): - self.client.login(email="dummy", password="test correct password") - self.fake_store.needs_refresh = True - self.assertThat(self.client.get_snap_status("basic"), Equals(self.expected)) - self.assertFalse(self.fake_store.needs_refresh) - - @mock.patch.object(storeapi.StoreClient, "get_account_information") - @mock.patch.object(storeapi._dashboard_api.DashboardAPI, "get") - def test_get_snap_status_server_error(self, mock_sca_get, mock_account_info): - mock_account_info.return_value = { - "snaps": {"16": {"basic": {"snap-id": "my_snap_id"}}} - } - - mock_sca_get.return_value = mock.Mock( - ok=False, status_code=500, reason="Server error", json=lambda: {} - ) - - self.client.login(email="dummy", password="test correct password") - e = self.assertRaises( - storeapi.errors.StoreSnapStatusError, self.client.get_snap_status, "basic" - ) - self.assertThat( - str(e), - Equals( - "Error fetching status of snap id 'my_snap_id' for 'any arch' " - "in '16' series: 500 Server error." - ), - ) - class SnapChannelMapTest(StoreTestCase): def test_get_snap_channel_map(self): - self.client.login(email="dummy", password="test correct password") self.assertThat( self.client.get_snap_channel_map(snap_name="basic"), IsInstance(channel_map.ChannelMap), @@ -1515,7 +1028,6 @@ def test_get_snap_channel_map(self): class SnapReleasesTest(StoreTestCase): def test_get_snap_releases(self): - self.client.login(email="dummy", password="test correct password") self.assertThat( self.client.get_snap_releases(snap_name="basic"), IsInstance(releases.Releases), @@ -1530,29 +1042,11 @@ def test_get_metrics(self): start="2021-01-01", end="2021-01-01", ) - self.client.login(email="dummy", password="test correct password") self.assertThat( self.client.get_metrics(snap_name="basic", filters=[mf]), IsInstance(metrics.MetricsResults), ) - def test_get_metrics_general_error(self): - mf = metrics.MetricsFilter( - snap_id="err", - metric_name="test-name", - start="2021-01-01", - end="2021-01-01", - ) - self.client.login(email="dummy", password="test correct password") - - raised = self.assertRaises( - http_clients.errors.StoreServerError, - self.client.get_metrics, - snap_name="error", - filters=[mf], - ) - self.assertThat(raised.error_code, Equals(503)) - def test_get_metrics_invalid_date_error(self): mf = metrics.MetricsFilter( snap_id="err-invalid-date-interval", @@ -1560,7 +1054,6 @@ def test_get_metrics_invalid_date_error(self): start="2021-01-01", end="2021-01-01", ) - self.client.login(email="dummy", password="test correct password") with pytest.raises(errors.StoreMetricsError) as exc_info: self.client.get_metrics(snap_name="error", filters=[mf]) @@ -1590,7 +1083,6 @@ def test_get_metrics_unmarshal_error(self): start="2021-01-01", end="2021-01-01", ) - self.client.login(email="dummy", password="test correct password") with pytest.raises(errors.StoreMetricsUnmarshalError) as exc_info: self.client.get_metrics(snap_name="error", filters=[mf]) @@ -1613,15 +1105,14 @@ def test_get_metrics_unmarshal_error(self): class WhoAmITest(StoreTestCase): def test_whoami(self): - self.client.login(email="dummy", password="test correct password") self.assertThat( - self.client.whoami(), IsInstance(whoami.WhoAmI), + self.client.whoami(), + IsInstance(whoami.WhoAmI), ) class SignDeveloperAgreementTestCase(StoreTestCase): def test_sign_dev_agreement_success(self): - self.client.login(email="dummy", password="test correct password") response = { "content": { "latest_tos_accepted": True, @@ -1636,7 +1127,6 @@ def test_sign_dev_agreement_success(self): ) def test_sign_dev_agreement_exception(self): - self.client.login(email="dummy", password="test correct password") raised = self.assertRaises( errors.DeveloperAgreementSignError, self.client.sign_developer_agreement, @@ -1648,16 +1138,6 @@ def test_sign_dev_agreement_exception(self): str(raised), ) - def test_sign_dev_agreement_exception_store_down(self): - self.useFixture(fixtures.EnvironmentVariable("STORE_DOWN", "1")) - self.client.login(email="dummy", password="test correct password") - raised = self.assertRaises( - http_clients.errors.StoreServerError, - self.client.sign_developer_agreement, - latest_tos_accepted=True, - ) - self.assertThat(raised.error_code, Equals(500)) - class UploadMetadataTestCase(StoreTestCase): def setUp(self): @@ -1670,7 +1150,6 @@ def _setup_snap(self): These are all the previous steps needed to upload metadata. """ - self.client.login(email="dummy", password="test correct password") self.client.register("basic") path = os.path.join( os.path.dirname(tests.legacy.__file__), "data", "test-snap.snap" @@ -1678,13 +1157,6 @@ def _setup_snap(self): tracker = self.client.upload("basic", path) tracker.track() - def test_refreshes_macaroon(self): - self._setup_snap() - self.fake_store.needs_refresh = True - metadata = {"field_ok": "foo"} - self.client.upload_metadata("basic", metadata, False) - self.assertFalse(self.fake_store.needs_refresh) - def test_invalid_data(self): self._setup_snap() metadata = {"invalid": "foo"} @@ -1782,7 +1254,6 @@ def _setup_snap(self): These are all the previous steps needed to upload binary metadata. """ - self.client.login(email="dummy", password="test correct password") self.client.register("basic") path = os.path.join( os.path.dirname(tests.legacy.__file__), "data", "test-snap.snap" @@ -1790,14 +1261,6 @@ def _setup_snap(self): tracker = self.client.upload("basic", path) tracker.track() - def test_refreshes_macaroon(self): - self._setup_snap() - self.fake_store.needs_refresh = True - with tempfile.NamedTemporaryFile(suffix="ok") as f: - metadata = {"icon": f} - self.client.upload_binary_metadata("basic", metadata, False) - self.assertFalse(self.fake_store.needs_refresh) - def test_invalid_data(self): self._setup_snap() with tempfile.NamedTemporaryFile(suffix="invalid") as f: @@ -1879,7 +1342,6 @@ def _setup_snap(self): These are all the previous steps needed to upload binary metadata. """ - self.client.login(email="dummy", password="test correct password") self.client.register("basic") path = os.path.join( os.path.dirname(tests.legacy.__file__), "data", "test-snap.snap" diff --git a/tests/spread/general/store/task.yaml b/tests/spread/general/store/task.yaml index 873fc9fd1d..81da481af9 100644 --- a/tests/spread/general/store/task.yaml +++ b/tests/spread/general/store/task.yaml @@ -4,14 +4,19 @@ manual: true environment: SNAP: dump-hello - SNAP_STORE_MACAROON/ubuntu_one: "$(HOST: echo ${SNAP_STORE_MACAROON})" - SNAP_STORE_MACAROON/candid: "$(HOST: echo ${SNAP_STORE_CANDID_MACAROON})" - STORE_DASHBOARD_URL: https://dashboard.staging.snapcraft.io/ - STORE_API_URL: https://api.staging.snapcraft.io/ - STORE_UPLOAD_URL: https://upload.apps.staging.ubuntu.com/ - UBUNTU_ONE_SSO_URL: https://login.staging.ubuntu.com/ + SNAPCRAFT_STORE_CREDENTIALS/ubuntu_one: "$(HOST: echo ${SNAPCRAFT_STORE_CREDENTIALS_STAGING})" + SNAPCRAFT_STORE_CREDENTIALS/candid: "$(HOST: echo ${SNAPCRAFT_STORE_CREDENTIALS_STAGING_CANDID})" + STORE_DASHBOARD_URL: https://dashboard.staging.snapcraft.io + STORE_API_URL: https://api.staging.snapcraft.io + STORE_UPLOAD_URL: https://storage.staging.snapcraftcontent.com + UBUNTU_ONE_SSO_URL: https://login.staging.ubuntu.com prepare: | + if [[ -z "$SNAPCRAFT_STORE_CREDENTIALS" ]]; then + echo "No credentials set in env SNAPCRAFT_STORE_CREDENTIALS" + exit 1 + fi + # Install the review tools to make sure we do not break anything # assumed in there. # TODO: requires running inside $HOME. @@ -47,12 +52,8 @@ execute: | snap_file=$(ls ./*.snap) snap_name=$(grep "name: " snap/snapcraft.yaml | sed -e "s/name: \(.*$\)/\1/") - # Login - set +x - echo "${SNAP_STORE_MACAROON}" > login - set -x + # Login mechanism export SNAPCRAFT_STORE_AUTH="${SPREAD_VARIANT}" - snapcraft login --with login # Who Am I? snapcraft whoami @@ -84,6 +85,3 @@ execute: | # Show metrics for a snap that we have registered in the past (empty metrics as no users!). snapcraft metrics fun --format json --name installed_base_by_operating_system snapcraft metrics fun --format table --name installed_base_by_operating_system - - # Logout - snapcraft logout From f552b0b7cd6f4ab06fbd2ee0e98ac41091767e2a Mon Sep 17 00:00:00 2001 From: Claudio Matsuoka Date: Wed, 20 Apr 2022 11:30:04 -0300 Subject: [PATCH 098/167] projects: define constant for mandatory adoptable fields Signed-off-by: Claudio Matsuoka --- snapcraft/parts/lifecycle.py | 11 +++++------ snapcraft/projects.py | 5 ++++- tests/unit/test_projects.py | 22 ++++++++++++++++++---- 3 files changed, 27 insertions(+), 11 deletions(-) diff --git a/snapcraft/parts/lifecycle.py b/snapcraft/parts/lifecycle.py index ac6aae599f..b670e92040 100644 --- a/snapcraft/parts/lifecycle.py +++ b/snapcraft/parts/lifecycle.py @@ -28,7 +28,7 @@ from snapcraft import errors, extensions, pack, providers, utils from snapcraft.meta import snap_yaml from snapcraft.parts import PartsLifecycle -from snapcraft.projects import GrammarAwareProject, Project +from snapcraft.projects import MANDATORY_ADOPTABLE_FIELDS, GrammarAwareProject, Project from snapcraft.providers import capture_logs_from_instance from . import grammar, yaml_utils @@ -143,9 +143,7 @@ def _run_command( ) if parsed_args.use_lxd and providers.get_platform_default_provider() == "lxd": - emit.message( - "LXD is used by default on this platform.", intermediate=True - ) + emit.message("LXD is used by default on this platform.", intermediate=True) if not managed_mode and not parsed_args.destructive_mode: if command_name == "clean" and not part_names: @@ -219,11 +217,12 @@ def _update_project_metadata(project: Project, project_vars: Dict[str, str]) -> raise errors.SnapcraftError(f"error setting variable: {err}") # Fields that must not end empty - for field in ("version", "grade", "summary", "description"): + for field in MANDATORY_ADOPTABLE_FIELDS: if not getattr(project, field): raise errors.SnapcraftError( f"Field {field!r} was not adopted from metadata" - ) + ) + def _raise_formatted_validation_error(err: pydantic.ValidationError): error_list = err.errors() diff --git a/snapcraft/projects.py b/snapcraft/projects.py index ab04130aaa..90ae71a11f 100644 --- a/snapcraft/projects.py +++ b/snapcraft/projects.py @@ -225,6 +225,9 @@ class ContentPlug(ProjectModel): default_provider: Optional[str] +MANDATORY_ADOPTABLE_FIELDS = ("version", "summary", "description", "grade") + + class Project(ProjectModel): """Snapcraft project definition. @@ -287,7 +290,7 @@ def _validate_plugs(cls, plugs): @pydantic.root_validator(pre=True) @classmethod def _validate_adoptable_fields(cls, values): - for field in ("version", "summary", "description", "grade"): + for field in MANDATORY_ADOPTABLE_FIELDS: if field not in values and "adopt-info" not in values: raise ValueError(f"Snap {field} is required if not using adopt-info") return values diff --git a/tests/unit/test_projects.py b/tests/unit/test_projects.py index 3d125e2658..ce2d5d3dc6 100644 --- a/tests/unit/test_projects.py +++ b/tests/unit/test_projects.py @@ -20,7 +20,13 @@ import pytest from snapcraft import errors -from snapcraft.projects import ContentPlug, GrammarAwareProject, Hook, Project +from snapcraft.projects import ( + MANDATORY_ADOPTABLE_FIELDS, + ContentPlug, + GrammarAwareProject, + Hook, + Project, +) # pylint: disable=too-many-lines @@ -162,7 +168,15 @@ def test_mandatory_base(self, snap_type, requires_base, project_yaml_data): project = Project.unmarshal(data) assert project.base is None - @pytest.mark.parametrize("field", ["version", "summary", "description", "grade"]) + def test_mandatory_adoptable_fields_definition(self): + assert MANDATORY_ADOPTABLE_FIELDS == ( + "version", + "summary", + "description", + "grade", + ) + + @pytest.mark.parametrize("field", MANDATORY_ADOPTABLE_FIELDS) def test_adoptable_fields(self, field, project_yaml_data): data = project_yaml_data() data.pop(field) @@ -170,7 +184,7 @@ def test_adoptable_fields(self, field, project_yaml_data): with pytest.raises(errors.ProjectValidationError, match=error): Project.unmarshal(data) - @pytest.mark.parametrize("field", ["version", "summary", "description", "grade"]) + @pytest.mark.parametrize("field", MANDATORY_ADOPTABLE_FIELDS) def test_adoptable_field_not_required(self, field, project_yaml_data): data = project_yaml_data() data.pop(field) @@ -178,7 +192,7 @@ def test_adoptable_field_not_required(self, field, project_yaml_data): project = Project.unmarshal(data) assert getattr(project, field) is None - @pytest.mark.parametrize("field", ["version", "summary", "description", "grade"]) + @pytest.mark.parametrize("field", MANDATORY_ADOPTABLE_FIELDS) def test_adoptable_field_assignment(self, field, project_yaml_data): data = project_yaml_data() project = Project.unmarshal(data) From 3a50e3860edc8fa8f3ea42fd10bd617e42f26f67 Mon Sep 17 00:00:00 2001 From: Claudio Matsuoka Date: Wed, 20 Apr 2022 11:30:40 -0300 Subject: [PATCH 099/167] tests: fix metadata test working directory Signed-off-by: Claudio Matsuoka --- tests/unit/parts/test_lifecycle.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/parts/test_lifecycle.py b/tests/unit/parts/test_lifecycle.py index 9aa3761e86..684dc2ebcb 100644 --- a/tests/unit/parts/test_lifecycle.py +++ b/tests/unit/parts/test_lifecycle.py @@ -380,7 +380,7 @@ def test_lifecycle_pack_metadata_error(cmd, snapcraft_yaml, new_dir, mocker): @pytest.mark.parametrize("field", ["version", "summary", "description", "grade"]) -def test_lifecycle_metadata_empty(field, snapcraft_yaml): +def test_lifecycle_metadata_empty(field, snapcraft_yaml, new_dir): """Adoptable fields shouldn't be empty after adoption.""" yaml_data = snapcraft_yaml(base="core22") yaml_data.pop(field) From f3900430aee7c9465754941a7948cfeb0078cfa8 Mon Sep 17 00:00:00 2001 From: Claudio Matsuoka Date: Wed, 20 Apr 2022 11:32:57 -0300 Subject: [PATCH 100/167] projects: explain why project data is mutable Signed-off-by: Claudio Matsuoka --- snapcraft/projects.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/snapcraft/projects.py b/snapcraft/projects.py index 90ae71a11f..655b59fb2e 100644 --- a/snapcraft/projects.py +++ b/snapcraft/projects.py @@ -36,7 +36,7 @@ class Config: # pylint: disable=too-few-public-methods validate_assignment = True extra = "allow" # FIXME: change to 'forbid' after model complete - allow_mutation = True + allow_mutation = True # project is updated with adopted metadata allow_population_by_field_name = True alias_generator = lambda s: s.replace("_", "-") # noqa: E731 From 8fa380fdf3f2f2783132e17ab4421737efdadbe8 Mon Sep 17 00:00:00 2001 From: Claudio Matsuoka Date: Wed, 20 Apr 2022 18:06:46 -0300 Subject: [PATCH 101/167] meta: add appstream metadata extractor Extract application metadata from appstream files. Co-authored-by: Kyle Fazzari Co-authored-by: Leo Arias Co-authored-by: Sergio Schvezov Signed-off-by: Claudio Matsuoka --- Makefile | 2 +- pyproject.toml | 1 + snapcraft/errors.py | 7 + snapcraft/meta/__init__.py | 3 + snapcraft/meta/appstream.py | 276 ++++++++++ snapcraft/meta/extracted_metadata.py | 49 ++ snapcraft/meta/metadata.py | 33 ++ tests/unit/meta/test_appstream.py | 765 +++++++++++++++++++++++++++ tests/unit/meta/test_metadata.py | 28 + 9 files changed, 1163 insertions(+), 1 deletion(-) create mode 100644 snapcraft/meta/appstream.py create mode 100644 snapcraft/meta/extracted_metadata.py create mode 100644 snapcraft/meta/metadata.py create mode 100644 tests/unit/meta/test_appstream.py create mode 100644 tests/unit/meta/test_metadata.py diff --git a/Makefile b/Makefile index 2c8ea3c202..3a87b0d822 100644 --- a/Makefile +++ b/Makefile @@ -35,7 +35,7 @@ test-pydocstyle: .PHONY: test-pylint test-pylint: pylint snapcraft - pylint tests/*.py tests/unit --disable=invalid-name,missing-module-docstring,missing-function-docstring,no-self-use,duplicate-code,protected-access,unspecified-encoding,too-many-public-methods + pylint tests/*.py tests/unit --disable=invalid-name,missing-module-docstring,missing-function-docstring,no-self-use,duplicate-code,protected-access,unspecified-encoding,too-many-public-methods,too-many-arguments .PHONY: test-pyright test-pyright: diff --git a/pyproject.toml b/pyproject.toml index 57d20cb985..807102191f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,6 +41,7 @@ good-names = "id" [tool.pylint.MASTER] extension-pkg-allow-list = [ + "lxml.etree", "pydantic", "pytest", ] diff --git a/snapcraft/errors.py b/snapcraft/errors.py index 014ef1061d..ff6747ef5c 100644 --- a/snapcraft/errors.py +++ b/snapcraft/errors.py @@ -42,5 +42,12 @@ class ExtensionError(SnapcraftError): """Error during parts processing.""" +class MetadataExtractionError(SnapcraftError): + """Attempt to extract metadata from file was unsuccessful.""" + + def __init__(self, filename: str) -> None: + super().__init__(f"Error extracting metadata from {filename!r}") + + class LegacyFallback(Exception): """Fall back to legacy snapcraft implementation.""" diff --git a/snapcraft/meta/__init__.py b/snapcraft/meta/__init__.py index ad61d920b5..70c7932f24 100644 --- a/snapcraft/meta/__init__.py +++ b/snapcraft/meta/__init__.py @@ -15,3 +15,6 @@ # along with this program. If not, see . """Snap metadata definitions and helpers.""" + +from .extracted_metadata import ExtractedMetadata # noqa: F401 +from .metadata import extract_metadata # noqa: F401 diff --git a/snapcraft/meta/appstream.py b/snapcraft/meta/appstream.py new file mode 100644 index 0000000000..0fbc8b046a --- /dev/null +++ b/snapcraft/meta/appstream.py @@ -0,0 +1,276 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright 2017-2022 Canonical Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +"""Appstream metadata extractor.""" + +import contextlib +import operator +import os +from io import StringIO +from typing import List, Optional + +import lxml.etree +from xdg.DesktopEntry import DesktopEntry + +from snapcraft import errors + +from .extracted_metadata import ExtractedMetadata + +_XSLT = """\ + + + + + + + + + + + + + + + + + + + + + + +- + + + + + + + + + + + + + + + +_ + +_ + + + + + + + +""" + + +def extract(relpath: str, *, workdir: str) -> Optional[ExtractedMetadata]: + """Extract appstream metadata. + + :param file_relpath: Relative path to the file containing metadata. + :param workdir: The part working directory where the metadata file is located. + + :return: The extracted metadata, if any. + """ + if not relpath.endswith(".metainfo.xml") and not relpath.endswith(".appdata.xml"): + return None + + dom = _get_transformed_dom(os.path.join(workdir, relpath)) + + common_id = _get_value_from_xml_element(dom, "id") + summary = _get_value_from_xml_element(dom, "summary") + description = _get_value_from_xml_element(dom, "description") + title = _get_value_from_xml_element(dom, "name") + version = _get_latest_release_from_nodes(dom.findall("releases/release")) + + desktop_file_paths = [] + desktop_file_ids = _get_desktop_file_ids_from_nodes(dom.findall("launchable")) + # if there are no launchables, use the appstream id to take into + # account the legacy appstream definitions + if common_id and not desktop_file_ids: + if common_id.endswith(".desktop"): + desktop_file_ids.append(common_id) + else: + desktop_file_ids.append(common_id + ".desktop") + + for desktop_file_id in desktop_file_ids: + desktop_file_path = _desktop_file_id_to_path(desktop_file_id, workdir=workdir) + if desktop_file_path: + desktop_file_paths.append(desktop_file_path) + + icon = _extract_icon(dom, workdir, desktop_file_paths) + + return ExtractedMetadata( + common_id=common_id, + title=title, + summary=summary, + description=description, + version=version, + icon=icon, + desktop_file_paths=desktop_file_paths, + ) + + +def _get_transformed_dom(path: str): + dom = _get_dom(path) + transform = _get_xslt() + return transform(dom) + + +def _get_dom(path: str) -> lxml.etree.ElementTree: + try: + return lxml.etree.parse(path) + except lxml.etree.ParseError as err: + raise errors.MetadataExtractionError(path) from err + + +def _get_xslt(): + xslt = lxml.etree.parse(StringIO(_XSLT)) + return lxml.etree.XSLT(xslt) + + +def _get_value_from_xml_element(tree, key) -> Optional[str]: + node = tree.find(key) + if node is not None and node.text: + # Lines that should be empty end up with empty space after the + # transformation. One example of this is seen for paragraphs (i.e.;

) + # than hold list in then (i.e.;

    or
      ) so we split all lines + # here and strip any unwanted space. + # TODO: Improve the XSLT to remove the need for this. + return "\n".join([n.strip() for n in node.text.splitlines()]).strip() + return None + + +def _get_latest_release_from_nodes(nodes) -> Optional[str]: + for node in nodes: + if "version" in node.attrib: + return node.attrib["version"] + return None + + +def _get_desktop_file_ids_from_nodes(nodes) -> List[str]: + desktop_file_ids = [] # type: List[str] + for node in nodes: + if "type" in node.attrib and node.attrib["type"] == "desktop-id": + desktop_file_ids.append(node.text.strip()) + return desktop_file_ids + + +def _desktop_file_id_to_path(desktop_file_id: str, *, workdir: str) -> Optional[str]: + # For details about desktop file ids and their corresponding paths, see + # https://standards.freedesktop.org/desktop-entry-spec/desktop-entry-spec-latest.html#desktop-file-id + for xdg_data_dir in ("usr/local/share", "usr/share"): + desktop_file_path = os.path.join( + xdg_data_dir, "applications", desktop_file_id.replace("-", "/") + ) + # Check if it exists in workdir, but do not add it to the resulting path + # as it later needs to exist in the prime directory to effectively be + # used. + if os.path.exists(os.path.join(workdir, desktop_file_path)): + return desktop_file_path + return None + + +def _extract_icon(dom, workdir: str, desktop_file_paths: List[str]) -> Optional[str]: + icon_node = dom.find("icon") + if icon_node is not None and "type" in icon_node.attrib: + icon_node_type = icon_node.attrib["type"] + else: + icon_node_type = None + + icon = icon_node.text.strip() if icon_node is not None else None + + if icon_node_type == "remote": + return icon + + if icon_node_type == "stock": + return _get_icon_from_theme(workdir, "hicolor", icon) + + # If an icon path is specified and the icon file exists, we'll use that, otherwise + # we'll fall back to what's listed in the desktop file. + if icon is None: + return _get_icon_from_desktop_file(workdir, desktop_file_paths) + + if os.path.exists(os.path.join(workdir, icon.lstrip("/"))): + return icon + + return _get_icon_from_desktop_file(workdir, desktop_file_paths) + + +def _get_icon_from_desktop_file( + workdir: str, desktop_file_paths: List[str] +) -> Optional[str]: + # Icons in the desktop file can be either a full path to the icon file, or a name + # to be searched in the standard locations. If the path is specified, use that, + # otherwise look for the icon in the hicolor theme (also covers icon type="stock"). + # See https://standards.freedesktop.org/icon-theme-spec/icon-theme-spec-latest.html + # for further information. + for path in desktop_file_paths: + entry = DesktopEntry() + entry.parse(os.path.join(workdir, path)) + icon = entry.getIcon() + icon_path = ( + icon + if os.path.isabs(icon) + else _get_icon_from_theme(workdir, "hicolor", icon) + ) + return icon_path + + return None + + +def _get_icon_from_theme(workdir: str, theme: str, icon: str) -> Optional[str]: + # Icon themes can carry icons in different pre-rendered sizes or scalable. Scalable + # implementation is optional, so we'll try the largest pixmap and then scalable if + # no other sizes are available. + theme_dir = os.path.join("usr", "share", "icons", theme) + if not os.path.exists(os.path.join(workdir, theme_dir)): + return None + + # TODO: use index.theme + entries = os.listdir(os.path.join(workdir, theme_dir)) + # size is NxN + x_entries = (e.split("x") for e in entries if "x" in e) + sized_entries = (e[0] for e in x_entries if e[0] == e[1]) + sizes = {} + for icon_size_entry in sized_entries: + with contextlib.suppress(ValueError): + isize = int(icon_size_entry) + sizes[isize] = f"{isize}x{isize}" + + icon_size = None + suffixes = [] + if sizes: + size = max(sizes.items(), key=operator.itemgetter(1))[0] + icon_size = sizes[size] + suffixes = [".png", ".xpm"] + elif "scalable" in entries: + icon_size = "scalable" + suffixes = [".svg", ".svgz"] + + icon_path = None + if icon_size: + for suffix in suffixes: + icon_path = os.path.join(theme_dir, icon_size, "apps", icon + suffix) + if os.path.exists(os.path.join(workdir, icon_path)): + break + + return icon_path diff --git a/snapcraft/meta/extracted_metadata.py b/snapcraft/meta/extracted_metadata.py new file mode 100644 index 0000000000..18823cfd45 --- /dev/null +++ b/snapcraft/meta/extracted_metadata.py @@ -0,0 +1,49 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright 2017-2022 Canonical Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +"""External metadata definition.""" + +from dataclasses import dataclass, field +from typing import List, Optional + + +@dataclass +class ExtractedMetadata: + """Collection of metadata extracted from a part.""" + + common_id: Optional[str] = None + """The common identifier across multiple packaging formats.""" + + title: Optional[str] = None + """The extracted package title.""" + + summary: Optional[str] = None + """The extracted package summary.""" + + description: Optional[str] = None + """The extracted package description.""" + + version: Optional[str] = None + """The extracted package version.""" + + grade: Optional[str] = None + """The extracted package version.""" + + icon: Optional[str] = None + """The extracted application icon.""" + + desktop_file_paths: List[str] = field(default_factory=list) + """The extracted application desktop file paths.""" diff --git a/snapcraft/meta/metadata.py b/snapcraft/meta/metadata.py new file mode 100644 index 0000000000..edd775088a --- /dev/null +++ b/snapcraft/meta/metadata.py @@ -0,0 +1,33 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright 2022 Canonical Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +"""External metadata helpers.""" + +from typing import Optional + +from . import appstream +from .extracted_metadata import ExtractedMetadata + + +def extract_metadata(file_relpath: str, *, workdir: str) -> Optional[ExtractedMetadata]: + """Retrieve external metadata from part files. + + :param file_relpath: Relative path to the file containing metadata. + :param workdir: The part working directory where the metadata file is located. + + :return: The extracted metadata, if any. + """ + return appstream.extract(file_relpath, workdir=workdir) diff --git a/tests/unit/meta/test_appstream.py b/tests/unit/meta/test_appstream.py new file mode 100644 index 0000000000..c25333cc9f --- /dev/null +++ b/tests/unit/meta/test_appstream.py @@ -0,0 +1,765 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright 2017-2022 Canonical Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import os +import textwrap +from pathlib import Path +from typing import Optional + +import pytest + +from snapcraft.meta import ExtractedMetadata, appstream + + +def _create_desktop_file(desktop_file_path, icon: Optional[str] = None) -> None: + dir_name = os.path.dirname(desktop_file_path) + if not os.path.exists(dir_name): + os.makedirs(dir_name) + with open(desktop_file_path, "w") as f: + print("[Desktop Entry]", file=f) + if icon: + print(f"Icon={icon}", file=f) + + +@pytest.mark.usefixtures("new_dir") +class TestAppstreamData: + """Extract metadata and check each extracted field.""" + + @pytest.mark.parametrize("file_extension", ["metainfo.xml", "appdata.xml"]) + @pytest.mark.parametrize( + "key,attributes,param_name,value,expect", + [ + ("summary", {}, "summary", "test-summary", "test-summary"), + ("description", {}, "description", "test-description", "test-description"), + ("icon", {"type": "local"}, "icon", "/test/path", None), + ("icon", {"type": "local"}, "icon", "/icon.png", "/icon.png"), + ("id", {}, "common_id", "test-id", "test-id"), + ("name", {}, "title", "test-title", "test-title"), + ], + ) + def test_entries(self, file_extension, key, attributes, param_name, value, expect): + file_name = f"foo.{file_extension}" + attrs = " ".join(f'{attr}="{attributes[attr]}"' for attr in attributes) + Path(file_name).write_text( + textwrap.dedent( + f"""\ + + + <{key} {attrs}>{value} + """ + ) + ) + + Path("icon.png").touch() + kwargs = {param_name: expect} + expected = ExtractedMetadata(**kwargs) + + assert appstream.extract(file_name, workdir=".") == expected + + +# See LP #1814898 for a description of possible fallbacks +@pytest.mark.usefixtures("new_dir") +class TestAppstreamIcons: + """Check extraction of icon-related metadata.""" + + def _create_appstream_file( + self, icon: Optional[str] = None, icon_type: str = "local" + ): + with open("foo.appdata.xml", "w") as f: + if icon: + f.write( + textwrap.dedent( + f"""\ + + + my.app.desktop + {icon} + """ + ) + ) + else: + f.write( + textwrap.dedent( + """\ + + + my.app.desktop + """ + ) + ) + + def _create_index_theme(self, theme: str): + # TODO: populate index.theme + dir_name = os.path.join("usr", "share", "icons", theme) + if not os.path.exists(dir_name): + os.makedirs(dir_name) + Path(dir_name, "index.theme").touch() + + def _create_icon_file(self, theme: str, size: str, filename: str) -> None: + dir_name = os.path.join("usr", "share", "icons", theme, size, "apps") + if not os.path.exists(dir_name): + os.makedirs(dir_name) + Path(dir_name, filename).touch() + + def _expect_icon(self, icon): + expected = ExtractedMetadata(icon=icon) + actual = appstream.extract("foo.appdata.xml", workdir=".") + assert actual is not None + assert actual.icon == expected.icon + + def test_appstream_NxN_size_not_int_is_skipped(self): + self._create_appstream_file(icon="icon", icon_type="stock") + dir_name = os.path.join("usr", "share", "icons", "hicolor", "NxN") + os.makedirs(dir_name) + self._expect_icon(None) + + def test_appstream_index_theme_is_not_confused_for_size(self): + self._create_appstream_file(icon="icon", icon_type="stock") + self._create_index_theme("hicolor") + self._expect_icon(None) + + def test_appstream_stock_icon_exists_png(self): + self._create_appstream_file(icon="icon", icon_type="stock") + self._create_icon_file("hicolor", "48x48", "icon.png") + self._create_icon_file("hicolor", "64x64", "icon.png") + self._expect_icon("usr/share/icons/hicolor/64x64/apps/icon.png") + + def test_appstream_stock_icon_not_exist(self): + self._create_appstream_file(icon="missing", icon_type="stock") + self._expect_icon(None) + + def test_appstream_no_icon_no_fallback(self): + self._create_appstream_file() + self._expect_icon(None) + + def test_appstream_local_icon_exists(self): + self._create_appstream_file(icon="/icon.png") + Path("icon.png").touch() + self._expect_icon("/icon.png") + + def test_appstream_local_icon_not_exist_no_fallback(self): + self._create_appstream_file(icon="/missing.png") + self._expect_icon(None) + + def test_appstream_local_icon_not_absolute_no_fallback(self): + self._create_appstream_file(icon="foo") + self._expect_icon(None) + + def test_appstream_no_icon_desktop_fallback_no_icon(self): + self._create_appstream_file() + _create_desktop_file("usr/share/applications/my.app.desktop") + self._expect_icon(None) + + def test_appstream_no_icon_desktop_fallback_icon_not_exist(self): + self._create_appstream_file() + _create_desktop_file( + "usr/share/applications/my.app.desktop", icon="/missing.png" + ) + self._expect_icon("/missing.png") + + def test_appstream_no_icon_desktop_fallback_icon_exists(self): + self._create_appstream_file() + _create_desktop_file("usr/share/applications/my.app.desktop", icon="/icon.png") + Path("icon.png").touch() + self._expect_icon("/icon.png") + + def test_appstream_no_icon_theme_fallback_png(self): + self._create_appstream_file() + _create_desktop_file("usr/share/applications/my.app.desktop", icon="icon") + self._create_icon_file("hicolor", "scalable", "icon.svg") + self._create_icon_file("hicolor", "48x48", "icon.png") + self._create_icon_file("hicolor", "64x64", "icon.png") + self._expect_icon("usr/share/icons/hicolor/64x64/apps/icon.png") + + def test_appstream_no_icon_theme_fallback_xpm(self): + self._create_appstream_file() + _create_desktop_file("usr/share/applications/my.app.desktop", icon="icon") + self._create_icon_file("hicolor", "scalable", "icon.svg") + self._create_icon_file("hicolor", "48x48", "icon.png") + self._create_icon_file("hicolor", "64x64", "icon.xpm") + self._expect_icon("usr/share/icons/hicolor/64x64/apps/icon.xpm") + + def test_appstream_no_icon_theme_fallback_svg(self): + self._create_appstream_file() + _create_desktop_file("usr/share/applications/my.app.desktop", icon="icon") + self._create_icon_file("hicolor", "scalable", "icon.svg") + self._expect_icon("usr/share/icons/hicolor/scalable/apps/icon.svg") + + def test_appstream_no_icon_theme_fallback_svgz(self): + self._create_appstream_file() + _create_desktop_file("usr/share/applications/my.app.desktop", icon="icon") + self._create_icon_file("hicolor", "scalable", "icon.svgz") + self._expect_icon("usr/share/icons/hicolor/scalable/apps/icon.svgz") + + +@pytest.mark.usefixtures("new_dir") +class TestAppstreamContent: + """Check variations of the Appstream file content.""" + + def test_appstream_with_ul(self): + file_name = "snapcraft_legacy.appdata.xml" + content = textwrap.dedent( + """\ + + + io.snapcraft.snapcraft + CC0-1.0 + GPL-3.0 + snapcraft + snapcraft + Create snaps + Crea snaps + +

      Command Line Utility to create snaps.

      +

      Aplicativo de línea de comandos para crear snaps.

      +

      Features:

      +

      Funciones:

      +
        +
      • Build snaps.
      • +
      • Construye snaps.
      • +
      • Publish snaps to the store.
      • +
      • Publica snaps en la tienda.
      • +
      +
      + + snapcraft + +
      + """ + ) + + Path(file_name).write_text(content) + + metadata = appstream.extract(file_name, workdir=".") + + assert metadata is not None + assert metadata.summary == "Create snaps" + assert metadata.description == textwrap.dedent( + """\ + Command Line Utility to create snaps. + + Features: + + - Build snaps. + - Publish snaps to the store.""" + ) + + def test_appstream_with_ol(self): + file_name = "snapcraft_legacy.appdata.xml" + content = textwrap.dedent( + """\ + + + io.snapcraft.snapcraft + CC0-1.0 + GPL-3.0 + snapcraft + snapcraft + Create snaps + Crea snaps + +

      Command Line Utility to create snaps.

      +

      Aplicativo de línea de comandos para crear snaps.

      +

      Features:

      +

      Funciones:

      +
        +
      1. Build snaps.
      2. +
      3. Construye snaps.
      4. +
      5. Publish snaps to the store.
      6. +
      7. Publica snaps en la tienda.
      8. +
      +
      + + snapcraft + +
      + """ + ) + + Path(file_name).write_text(content) + + metadata = appstream.extract(file_name, workdir=".") + + assert metadata is not None + assert metadata.summary == "Create snaps" + assert metadata.description == textwrap.dedent( + """\ + Command Line Utility to create snaps. + + Features: + + 1. Build snaps. + 2. Publish snaps to the store.""" + ) + + def test_appstream_with_ul_in_p(self): + file_name = "snapcraft_legacy.appdata.xml" + # pylint: disable=line-too-long + content = textwrap.dedent( + """\ + + + com.github.maoschanz.drawing + CC0-1.0 + GPL-3.0-or-later + + Drawing + Çizim + Drawing + A drawing application for the GNOME desktop + Uma aplicacao de desenho para o ambiente GNOME + Een tekenprogramma voor de GNOME-werkomgeving + +

      "Drawing" is a basic image editor, supporting PNG, JPEG and BMP file types.

      +

      "Drawing" e um simples editor de imagens, que suporta arquivos PNG,JPEG e BMP

      +

      "Tekenen" is een eenvoudige afbeeldingsbewerker, met ondersteuning voor PNG, JPEG en BMP.

      +

      "Dessin" est un éditeur d'images basique, qui supporte les fichiers de type PNG, JPEG ou BMP.

      +

      It allows you to draw or edit pictures with tools such as: +

        +
      • Pencil (with various options)
      • +
      • Lápis (Com varias opções)
      • +
      • Potlood (verschillende soorten)
      • +
      • Crayon (avec diverses options)
      • +
      • Selection (cut/copy/paste/drag/…)
      • +
      • Seçim (kes/kopyala/yapıştır/sürükle /…)
      • +
      • Выделение (вырезать/копировать/вставить/перетащить/…)
      • +
      • Seleção (cortar/copiar/colar/arrastar/…)
      • +
      • Selectie (knippen/kopiëren/plakken/verslepen/...)
      • +
      • Selezione (taglia/copia/incolla/trascina/…)
      • +
      • בחירה (חתיכה/העתקה/הדבקה/גרירה/...)
      • +
      • Sélection (copier/coller/déplacer/…)
      • +
      • Selección (cortar/copiar/pegar/arrastrar/…)
      • +
      • Auswahl (Ausschneiden/Kopieren/Einfügen/Ziehen/...)
      • +
      • Line, Arc (with various options)
      • +
      • Linha, Arco (com varias opcoes)
      • +
      • Lijn, Boog (verschillende soorten)
      • +
      • Trait, Arc (avec diverses options)
      • +
      • Shapes (rectangle, circle, polygon, …)
      • +
      • Formas (retângulo, circulo, polígono, …)
      • +
      • Vormen (vierkant, cirkel, veelhoek, ...)
      • +
      • Formes (rectangle, cercle, polygone, …)
      • +
      • Text insertion
      • +
      • Inserção de texto
      • +
      • Tekst invoeren
      • +
      • Insertion de texte
      • +
      • Resizing, cropping, rotating
      • +
      • Redimencionar, cortar, rotacionar
      • +
      • Afmetingen wijzigen, bijsnijden, draaien
      • +
      • Redimensionnement, rognage, rotation
      • +
      +

      +
      +
      + """ + ) + # pylint: enable=line-too-long + + Path(file_name).write_text(content) + + metadata = appstream.extract(file_name, workdir=".") + + assert metadata is not None + assert metadata.summary == "A drawing application for the GNOME desktop" + assert metadata.description == textwrap.dedent( + """\ + "Drawing" is a basic image editor, supporting PNG, JPEG and BMP file types. + + It allows you to draw or edit pictures with tools such as: + + - Pencil (with various options) + - Selection (cut/copy/paste/drag/…) + - Line, Arc (with various options) + - Shapes (rectangle, circle, polygon, …) + - Text insertion + - Resizing, cropping, rotating""" + ) + + def test_appstream_multilang_title(self): + file_name = "foliate.appdata.xml" + content = textwrap.dedent( + """\ + + + Foliate + Foliate_id + Foliate_pt + Foliate_ru + Foliate_nl + Foliate_fr + Foliate_cs + + """ + ) + + Path(file_name).write_text(content) + + metadata = appstream.extract(file_name, workdir=".") + + assert metadata is not None + assert metadata.title == "Foliate" + + def test_appstream_release(self): + file_name = "foliate.appdata.xml" + # pylint: disable=line-too-long + content = textwrap.dedent( + """\ + + + + + +
        +
      • Fixed Flatpak version not being able to open .mobi, .azw, and .azw3 files
      • +
      • Improved Wiktionary lookup, now with links and example sentences
      • +
      • Improved popover footnote extraction and formatting
      • +
      • Added option to export annotations to BibTeX
      • +
      +
      +
      + + +
        +
      • Fixed table of contents navigation not working with some books
      • +
      • Fixed not being able to zoom images with Kindle books
      • +
      • Fixed not being able to open books with .epub3 filename extension
      • +
      • Fixed temporary directory not being cleaned after closing
      • +
      +
      +
      + + +
        +
      • Fixed F9 shortcut not working
      • +
      • Updated translations
      • +
      +
      +
      +
      +
      + """ + ) + # pylint: enable=line-too-long + + Path(file_name).write_text(content) + + metadata = appstream.extract(file_name, workdir=".") + + assert metadata is not None + assert metadata.version == "1.5.3" + + def test_appstream_em(self): + file_name = "foliate.appdata.xml" + content = textwrap.dedent( + """\ + + + com.github.maoschanz.drawing + CC0-1.0 + GPL-3.0-or-later + + Drawing + +

      Command Line Utility to create snaps quickly.

      +

      Aplicativo de línea de comandos para crear snaps.

      +

      Ordered Features:

      +

      Funciones:

      +
        +
      1. Build snaps.
      2. +
      3. Construye snaps.
      4. +
      5. Publish snaps to the store.
      6. +
      7. Publica snaps en la tienda.
      8. +
      +

      Unordered Features:

      +
        +
      • Build snaps.
      • +
      • Construye snaps.
      • +
      • Publish snaps to the store.
      • +
      • Publica snaps en la tienda.
      • +
      +
      +
      + """ + ) + + Path(file_name).write_text(content) + + metadata = appstream.extract(file_name, workdir=".") + + assert metadata is not None + assert metadata.description == textwrap.dedent( + """\ + Command Line Utility to _create snaps_ quickly. + + Ordered Features: + + 1. _Build snaps_. + 2. Publish snaps to the store. + + Unordered Features: + + - _Build snaps_. + - Publish snaps to the store.""" + ) + + def test_appstream_code_tags_not_swallowed(self): + file_name = "foliate.appdata.xml" + content = textwrap.dedent( + """\ + + + com.github.maoschanz.drawing + CC0-1.0 + GPL-3.0-or-later + + Drawing + +

      Command Line Utility to create snaps quickly.

      +

      Aplicativo de línea de comandos para crear snaps.

      +

      Ordered Features:

      +

      Funciones:

      +
        +
      1. Build snaps.
      2. +
      3. Construye snaps.
      4. +
      5. Publish snaps to the store.
      6. +
      7. Publica snaps en la tienda.
      8. +
      +

      Unordered Features:

      +
        +
      • Build snaps.
      • +
      • Construye snaps.
      • +
      • Publish snaps to the store.
      • +
      • Publica snaps en la tienda.
      • +
      +
      +
      + """ + ) + + Path(file_name).write_text(content) + + metadata = appstream.extract(file_name, workdir=".") + + assert metadata is not None + assert metadata.description == textwrap.dedent( + """\ + Command Line Utility to create snaps quickly. + + Ordered Features: + + 1. Build snaps. + 2. Publish snaps to the store. + + Unordered Features: + + - Build snaps. + - Publish snaps to the store.""" + ) + + def test_appstream_with_comments(self): + file_name = "foo.appdata.xml" + content = textwrap.dedent( + """\ + + + com.github.maoschanz.drawing + CC0-1.0 + GPL-3.0-or-later + + + Drawing + + Draw stuff + + +

      Command Line Utility to create snaps quickly.

      +

      Aplicativo de línea de comandos para crear snaps.

      +

      Ordered Features:

      +

      Funciones:

      +
        +
      1. Build snaps.
      2. +
      3. Construye snaps.
      4. +
      5. Publish snaps to the store.
      6. +
      7. Publica snaps en la tienda.
      8. +
      +

      Unordered Features:

      +
        +
      • Build snaps.
      • +
      • Construye snaps.
      • +
      • Publish snaps to the store.
      • +
      • Publica snaps en la tienda.
      • +
      +
      +
      + """ + ) + + Path(file_name).write_text(content) + + metadata = appstream.extract(file_name, workdir=".") + + assert metadata is not None + assert metadata.description == textwrap.dedent( + """\ + Command Line Utility to create snaps quickly. + + Ordered Features: + + 1. Build snaps. + 2. Publish snaps to the store. + + Unordered Features: + + - Build snaps. + - Publish snaps to the store.""" + ) + + +@pytest.mark.usefixtures("new_dir") +class TestAppstreamUnhandledFile: + """Unhandled files should return None.""" + + def test_unhandled_file_test_case(self): + assert appstream.extract("unhandled-file", workdir=".") is None + + +@pytest.mark.usefixtures("new_dir") +class TestAppstreamLaunchable: + """Desktop file path must be extracted correctly.""" + + @pytest.mark.parametrize( + "desktop_file_path", + [ + "usr/share/applications/com.example.test/app.desktop", + "usr/local/share/applications/com.example.test/app.desktop", + ], + ) + def test(self, desktop_file_path): + Path("foo.metainfo.xml").write_text( + textwrap.dedent( + """\ + + + + com.example.test-app.desktop + + """ + ) + ) + + _create_desktop_file(desktop_file_path) + + extracted = appstream.extract("foo.metainfo.xml", workdir=".") + + assert extracted is not None + assert extracted.desktop_file_paths == [desktop_file_path] + + +@pytest.mark.usefixtures("new_dir") +class TestAppstreamLegacyDesktop: + """Legacy desktop file path must be extracted correctly.""" + + @pytest.mark.parametrize( + "desktop_file_path", + [ + "usr/share/applications/com.example.test/app.desktop", + "usr/local/share/applications/com.example.test/app.desktop", + ], + ) + def test_launchable(self, desktop_file_path): + Path("foo.metainfo.xml").write_text( + textwrap.dedent( + """\ + + + com.example.test-app.desktop + """ + ) + ) + + _create_desktop_file(desktop_file_path) + + extracted = appstream.extract("foo.metainfo.xml", workdir=".") + + assert extracted is not None + assert extracted.desktop_file_paths == [desktop_file_path] + + @pytest.mark.parametrize( + "desktop_file_path", + [ + "usr/share/applications/com.example.test/app.desktop", + "usr/local/share/applications/com.example.test/app.desktop", + ], + ) + def test_appstream_no_desktop_suffix(self, desktop_file_path): + Path("foo.metainfo.xml").write_text( + textwrap.dedent( + """\ + + + com.example.test-app + """ + ) + ) + + _create_desktop_file(desktop_file_path) + + extracted = appstream.extract("foo.metainfo.xml", workdir=".") + + assert extracted is not None + assert extracted.desktop_file_paths == [desktop_file_path] + + +@pytest.mark.usefixtures("new_dir") +class TestAppstreamMultipleLaunchable: + """Multiple desktop file paths must be extracted correctly.""" + + def test_appstream_with_multiple_launchables(self): + Path("foo.metainfo.xml").write_text( + textwrap.dedent( + """\ + + + + com.example.test-app1.desktop + + + dummy + + + com.example.test-app2.desktop + + + unexisting + + """ + ) + ) + + _create_desktop_file( + "usr/local/share/applications/com.example.test/app1.desktop" + ) + _create_desktop_file( + "usr/local/share/applications/com.example.test/app2.desktop" + ) + + expected = [ + "usr/local/share/applications/com.example.test/app1.desktop", + "usr/local/share/applications/com.example.test/app2.desktop", + ] + extracted = appstream.extract("foo.metainfo.xml", workdir=".") + + assert extracted is not None + assert extracted.desktop_file_paths == expected diff --git a/tests/unit/meta/test_metadata.py b/tests/unit/meta/test_metadata.py new file mode 100644 index 0000000000..debce4c821 --- /dev/null +++ b/tests/unit/meta/test_metadata.py @@ -0,0 +1,28 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright 2022 Canonical Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from unittest.mock import call + +from snapcraft import meta + + +def test_extract_metadata(mocker): + mock_appstream_extract = mocker.patch("snapcraft.meta.appstream.extract") + meta.extract_metadata("some/file", workdir="workdir") + + assert mock_appstream_extract.mock_calls == [ + call("some/file", workdir="workdir"), + ] From 7c6e8383c896d1807c15816a1d782782356f6875 Mon Sep 17 00:00:00 2001 From: Claudio Matsuoka Date: Thu, 21 Apr 2022 22:32:35 -0300 Subject: [PATCH 102/167] meta: add appstream extraction error test Signed-off-by: Claudio Matsuoka --- tests/unit/meta/test_appstream.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/tests/unit/meta/test_appstream.py b/tests/unit/meta/test_appstream.py index c25333cc9f..688a80ec20 100644 --- a/tests/unit/meta/test_appstream.py +++ b/tests/unit/meta/test_appstream.py @@ -21,6 +21,7 @@ import pytest +from snapcraft import errors from snapcraft.meta import ExtractedMetadata, appstream @@ -626,6 +627,35 @@ def test_appstream_with_comments(self): - Publish snaps to the store.""" ) + def test_appstream_parse_error(self): + file_name = "snapcraft_legacy.appdata.xml" + content = textwrap.dedent( + """\ + + + io.snapcraft.snapcraft + CC0-1.0 + GPL-3.0 + snapcraft + Create snaps + +

      Command Line Utility to create snaps.

      +
      + + snapcraft +
      + """ + ) + + Path(file_name).write_text(content) + + with pytest.raises(errors.MetadataExtractionError) as raised: + appstream.extract(file_name, workdir=".") + + assert str(raised.value) == ( + "Error extracting metadata from './snapcraft_legacy.appdata.xml'" + ) + @pytest.mark.usefixtures("new_dir") class TestAppstreamUnhandledFile: From 525aae9ccf9a9373aab4d289425b6bce447a1102 Mon Sep 17 00:00:00 2001 From: Claudio Matsuoka Date: Fri, 22 Apr 2022 09:38:20 -0300 Subject: [PATCH 103/167] providers: use final 22.04 lxd image Update lxd image from daily to final 22.04. Signed-off-by: Claudio Matsuoka --- snapcraft/providers/_lxd.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/snapcraft/providers/_lxd.py b/snapcraft/providers/_lxd.py index b571a25d4c..24d78329ab 100644 --- a/snapcraft/providers/_lxd.py +++ b/snapcraft/providers/_lxd.py @@ -31,8 +31,7 @@ logger = logging.getLogger(__name__) -# FIXME: use final 22.04 image -_BASE_IMAGE = {"core22": "ubuntu-daily:22.04"} +_BASE_IMAGE = {"core22": "ubuntu:22.04"} class LXDProvider(Provider): From 86101c86cbec14839240ce6885ef642ea60cc85e Mon Sep 17 00:00:00 2001 From: Sergio Schvezov Date: Fri, 22 Apr 2022 19:54:17 -0300 Subject: [PATCH 104/167] utils: platform detection logic Imported from Charmcraft with tests updated to use mocker. Signed-off-by: Sergio Schvezov --- snapcraft/utils.py | 68 +++++++++++++++++++++-- tests/unit/test_utils.py | 113 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 176 insertions(+), 5 deletions(-) diff --git a/snapcraft/utils.py b/snapcraft/utils.py index 2428dfd36a..c3d83ff65a 100644 --- a/snapcraft/utils.py +++ b/snapcraft/utils.py @@ -16,16 +16,74 @@ """Utilities for snapcraft.""" -import logging import os import pathlib +import platform import sys -from collections import namedtuple +from dataclasses import dataclass from typing import Optional -logger = logging.getLogger(__name__) - -OSPlatform = namedtuple("OSPlatform", "system release machine") +from craft_cli import emit + + +@dataclass +class OSPlatform: + """Platform definition for a given host.""" + + system: str + release: str + machine: str + + def __str__(self) -> str: + """Return the string representation of an OSPlatform.""" + return f"{self.system}/{self.release} ({self.machine})" + + +# translations from what the platform module informs to the term deb and +# snaps actually use +ARCH_TRANSLATIONS = { + "aarch64": "arm64", + "armv7l": "armhf", + "i686": "i386", + "ppc": "powerpc", + "ppc64le": "ppc64el", + "x86_64": "amd64", + "AMD64": "amd64", # Windows support +} + + +def get_os_platform(filepath=pathlib.Path("/etc/os-release")): + """Determine a system/release combo for an OS using /etc/os-release if available.""" + system = platform.system() + release = platform.release() + machine = platform.machine() + + if system == "Linux": + try: + with filepath.open("rt", encoding="utf-8") as release_file: + lines = release_file.readlines() + except FileNotFoundError: + emit.trace("Unable to locate 'os-release' file, using default values") + else: + os_release = {} + for line in lines: + line = line.strip() + if not line or line.startswith("#") or "=" not in line: + continue + key, value = line.rstrip().split("=", 1) + if value[0] == value[-1] and value[0] in ('"', "'"): + value = value[1:-1] + os_release[key] = value + system = os_release.get("ID", system) + release = os_release.get("VERSION_ID", release) + + return OSPlatform(system=system, release=release, machine=machine) + + +def get_host_architecture(): + """Get host architecture in deb format suitable for base definition.""" + os_platform = get_os_platform() + return ARCH_TRANSLATIONS.get(os_platform.machine, os_platform.machine) def strtobool(value: str) -> bool: diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py index f9d6801d87..5143962413 100644 --- a/tests/unit/test_utils.py +++ b/tests/unit/test_utils.py @@ -14,6 +14,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +from textwrap import dedent import pytest @@ -81,3 +82,115 @@ def test_strtobool_false(value: str): def test_strtobool_value_error(value: str): with pytest.raises(ValueError): utils.strtobool(value) + + +##################### +# Get Host Platform # +##################### + + +def test_get_os_platform_linux(tmp_path, mocker): + """Utilize an /etc/os-release file to determine platform.""" + # explicitly add commented and empty lines, for parser robustness + filepath = tmp_path / "os-release" + filepath.write_text( + dedent( + """ + # the following is an empty line + + NAME="Ubuntu" + VERSION="20.04.1 LTS (Focal Fossa)" + ID=ubuntu + ID_LIKE=debian + PRETTY_NAME="Ubuntu 20.04.1 LTS" + VERSION_ID="20.04" + HOME_URL="https://www.ubuntu.com/" + SUPPORT_URL="https://help.ubuntu.com/" + BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/" + + # more in the middle; the following even would be "out of standard", but + # we should not crash, just ignore it + SOMETHING-WEIRD + + PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy" + VERSION_CODENAME=focal + UBUNTU_CODENAME=focal + """ + ) + ) + mocker.patch("platform.machine", return_value="x86_64") + mocker.patch("platform.system", return_value="Linux") + + os_platform = utils.get_os_platform(filepath) + + assert os_platform.system == "ubuntu" + assert os_platform.release == "20.04" + assert os_platform.machine == "x86_64" + + +@pytest.mark.parametrize( + "name", + [ + ('"foo bar"', "foo bar"), # what's normally found + ("foo bar", "foo bar"), # no quotes + ('"foo " bar"', 'foo " bar'), # quotes in the middle + ('foo bar"', 'foo bar"'), # unbalanced quotes (no really enclosing) + ('"foo bar', '"foo bar'), # unbalanced quotes (no really enclosing) + ("'foo bar'", "foo bar"), # enclosing with single quote + ("'foo ' bar'", "foo ' bar"), # single quote in the middle + ("foo bar'", "foo bar'"), # unbalanced single quotes (no really enclosing) + ("'foo bar", "'foo bar"), # unbalanced single quotes (no really enclosing) + ("'foo bar\"", "'foo bar\""), # unbalanced mixed quotes + ("\"foo bar'", "\"foo bar'"), # unbalanced mixed quotes + ], +) +def test_get_os_platform_alternative_formats(tmp_path, mocker, name): + """Support different ways of building the string.""" + source, result = name + filepath = tmp_path / "os-release" + filepath.write_text( + dedent( + f""" + ID={source} + VERSION_ID="20.04" + """ + ) + ) + # need to patch this to "Linux" so actually uses /etc/os-release... + mocker.patch("platform.system", return_value="Linux") + + os_platform = utils.get_os_platform(filepath) + + assert os_platform.system == result + + +def test_get_os_platform_windows(mocker): + """Get platform from a patched Windows machine.""" + mocker.patch("platform.system", return_value="Windows") + mocker.patch("platform.release", return_value="10") + mocker.patch("platform.machine", return_value="AMD64") + + os_platform = utils.get_os_platform() + + assert os_platform.system == "Windows" + assert os_platform.release == "10" + assert os_platform.machine == "AMD64" + + +@pytest.mark.parametrize( + "platform_arch,deb_arch", + [ + ("AMD64", "amd64"), + ("aarch64", "arm64"), + ("armv7l", "armhf"), + ("ppc", "powerpc"), + ("ppc64le", "ppc64el"), + ("x86_64", "amd64"), + ("unknown-arch", "unknown-arch"), + ], +) +def test_get_host_architecture(platform_arch, mocker, deb_arch): + """Test all platform mappings in addition to unknown.""" + mocker.patch("platform.machine", return_value=platform_arch) + + assert utils.get_host_architecture() == deb_arch From 97ac84907cad1f2c0ef755d72cfb67c6ab931f71 Mon Sep 17 00:00:00 2001 From: Sergio Schvezov Date: Fri, 22 Apr 2022 20:06:03 -0300 Subject: [PATCH 105/167] utils: port humanize_list from Snapcraft legacy No functional change, tests update to pytest Signed-off-by: Sergio Schvezov --- snapcraft/utils.py | 27 ++++++++++++++++++++++++++- tests/unit/test_utils.py | 24 ++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/snapcraft/utils.py b/snapcraft/utils.py index c3d83ff65a..2d2149cf52 100644 --- a/snapcraft/utils.py +++ b/snapcraft/utils.py @@ -21,7 +21,7 @@ import platform import sys from dataclasses import dataclass -from typing import Optional +from typing import Iterable, Optional from craft_cli import emit @@ -159,3 +159,28 @@ def confirm_with_user(prompt, default=False) -> bool: return False return default + + +def humanize_list( + items: Iterable[str], conjunction: str, item_format: str = "{!r}" +) -> str: + """Format a list into a human-readable string. + + :param items: list to humanize. + :param conjunction: the conjunction used to join the final element to + the rest of the list (e.g. 'and'). + :param item_format: format string to use per item. + """ + if not items: + return "" + + quoted_items = [item_format.format(item) for item in sorted(items)] + if len(quoted_items) == 1: + return quoted_items[0] + + humanized = ", ".join(quoted_items[:-1]) + + if len(quoted_items) > 2: + humanized += "," + + return f"{humanized} {conjunction} {quoted_items[-1]}" diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py index 5143962413..bd360fbbc2 100644 --- a/tests/unit/test_utils.py +++ b/tests/unit/test_utils.py @@ -194,3 +194,27 @@ def test_get_host_architecture(platform_arch, mocker, deb_arch): mocker.patch("platform.machine", return_value=platform_arch) assert utils.get_host_architecture() == deb_arch + + +################# +# Humanize List # +################# + + +@pytest.mark.parametrize( + "items,conjunction,expected", + ( + ([], "and", ""), + (["foo"], "and", "'foo'"), + (["foo", "bar"], "and", "'bar' and 'foo'"), + (["foo", "bar", "baz"], "and", "'bar', 'baz', and 'foo'"), + (["foo", "bar", "baz", "qux"], "and", "'bar', 'baz', 'foo', and 'qux'"), + ([], "or", ""), + (["foo"], "or", "'foo'"), + (["foo", "bar"], "or", "'bar' or 'foo'"), + (["foo", "bar", "baz"], "or", "'bar', 'baz', or 'foo'"), + (["foo", "bar", "baz", "qux"], "or", "'bar', 'baz', 'foo', or 'qux'"), + ), +) +def test_humanize_list(items, conjunction, expected): + assert utils.humanize_list(items, conjunction) == expected From ec6f1a05efd6c319b09e9b97a5e1860c01a26c35 Mon Sep 17 00:00:00 2001 From: Sergio Schvezov Date: Fri, 22 Apr 2022 20:09:52 -0300 Subject: [PATCH 106/167] commands: improve fallback and use new help system Move sys.exit logic to __main__ and remove the legacy help by default. ArgumentParsingError is handled separately and checked for command availability before falling back to legacy_run. Signed-off-by: Sergio Schvezov --- snapcraft/__main__.py | 4 +- snapcraft/cli.py | 62 ++++++++++++++++++-------- snapcraft/commands/lifecycle.py | 20 +++++++++ tests/unit/cli/test_default_command.py | 16 +++++++ tests/unit/cli/test_help.py | 48 -------------------- tests/unit/cli/test_lifecycle.py | 41 ++++++++++++++++- 6 files changed, 122 insertions(+), 69 deletions(-) delete mode 100644 tests/unit/cli/test_help.py diff --git a/snapcraft/__main__.py b/snapcraft/__main__.py index cfc0de29be..8675237d10 100644 --- a/snapcraft/__main__.py +++ b/snapcraft/__main__.py @@ -16,6 +16,8 @@ """Main entry point.""" +import sys + from snapcraft import cli -cli.run() +sys.exit(cli.run()) diff --git a/snapcraft/cli.py b/snapcraft/cli.py index 74cc611104..9590462dc4 100644 --- a/snapcraft/cli.py +++ b/snapcraft/cli.py @@ -16,6 +16,7 @@ """Command-line application entry point.""" +import contextlib import logging import os import sys @@ -59,34 +60,33 @@ ] -def run(): - """Run the CLI.""" +def get_dispatcher() -> craft_cli.Dispatcher: + """Return an instance of Dispatcher. + + Run all the checks and setup required to ensure the Dispatcher can run. + """ # Run the legacy implementation if inside a legacy managed environment. if os.getenv("SNAPCRAFT_BUILD_ENVIRONMENT") == "managed-host": legacy.legacy_run() - # Let legacy snapcraft handle --help until we have all command stubs registered - # in craft-cli. - if "-h" in sys.argv or "--help" in sys.argv: - legacy.legacy_run() - # set lib loggers to debug level so that all messages are sent to Emitter for lib_name in ("craft_parts", "craft_providers"): logger = logging.getLogger(lib_name) logger.setLevel(logging.DEBUG) - emit_args = { - "mode": EmitterMode.NORMAL, - "appname": "snapcraft", - "greeting": f"Starting Snapcraft {__version__}", - } - if utils.is_managed_mode(): - emit_args["log_filepath"] = utils.get_managed_environment_log_path() - - emit.init(**emit_args) + log_filepath = utils.get_managed_environment_log_path() + else: + log_filepath = None + + emit.init( + mode=EmitterMode.NORMAL, + appname="snapcraft", + greeting=f"Starting Snapcraft {__version__}", + log_filepath=log_filepath, + ) - dispatcher = craft_cli.Dispatcher( + return craft_cli.Dispatcher( "snapcraft", COMMAND_GROUPS, summary="Package, distribute, and update snaps for Linux and IoT", @@ -94,6 +94,10 @@ def run(): default_command=commands.PackCommand, ) + +def run(): + """Run the CLI.""" + dispatcher = get_dispatcher() try: global_args = dispatcher.pre_parse_args(sys.argv[1:]) if global_args.get("version"): @@ -102,10 +106,30 @@ def run(): dispatcher.load_command(None) dispatcher.run() emit.ended_ok() - except (ProvideHelpException, errors.LegacyFallback, ArgumentParsingError) as err: + retcode = 0 + except ArgumentParsingError as err: + # TODO https://github.com/canonical/craft-cli/issues/78 + with contextlib.suppress(KeyError, IndexError): + if ( + err.__context__ is not None + and err.__context__.args[0] not in dispatcher.commands + ): + emit.trace(f"run legacy implementation: {err!s}") + emit.ended_ok() + legacy.legacy_run() + print(err, file=sys.stderr) # to stderr, as argparse normally does + emit.ended_ok() + retcode = 1 + except ProvideHelpException as err: + print(err, file=sys.stderr) # to stderr, as argparse normally does + emit.ended_ok() + retcode = 0 + except errors.LegacyFallback as err: emit.trace(f"run legacy implementation: {err!s}") emit.ended_ok() legacy.legacy_run() except errors.SnapcraftError as err: emit.error(err) - sys.exit(1) + retcode = 1 + + return retcode diff --git a/snapcraft/commands/lifecycle.py b/snapcraft/commands/lifecycle.py index f8a124afb9..d5a61c891a 100644 --- a/snapcraft/commands/lifecycle.py +++ b/snapcraft/commands/lifecycle.py @@ -43,6 +43,26 @@ def fill_parser(self, parser: "argparse.ArgumentParser") -> None: action="store_true", help="Use LXD to build", ) + # --enable-experimental-extensions is only available in legacy + parser.add_argument( + "--enable-experimental-extensions", + action="store_true", + help=argparse.SUPPRESS, + ) + # --enable-developer-debug is only available in legacy + parser.add_argument( + "--enable-developer-debug", + action="store_true", + help=argparse.SUPPRESS, + ) + # --enable-experimental-target-arch is only available in legacy + parser.add_argument( + "--enable-experimental-target-arch", + action="store_true", + help=argparse.SUPPRESS, + ) + # --target-arch is only available in legacy + parser.add_argument("--target-arch", help=argparse.SUPPRESS) # --provider is only available in legacy parser.add_argument("--provider", help=argparse.SUPPRESS) diff --git a/tests/unit/cli/test_default_command.py b/tests/unit/cli/test_default_command.py index 848d2d301b..30eab5ac93 100644 --- a/tests/unit/cli/test_default_command.py +++ b/tests/unit/cli/test_default_command.py @@ -34,6 +34,10 @@ def test_default_command(mocker): output=None, destructive_mode=False, use_lxd=False, + enable_experimental_extensions=False, + enable_developer_debug=False, + enable_experimental_target_arch=False, + target_arch=None, provider=None, ) ) @@ -51,6 +55,10 @@ def test_default_command_destructive_mode(mocker): output=None, destructive_mode=True, use_lxd=False, + enable_experimental_extensions=False, + enable_developer_debug=False, + enable_experimental_target_arch=False, + target_arch=None, provider=None, ) ) @@ -68,6 +76,10 @@ def test_default_command_use_lxd(mocker): output=None, destructive_mode=False, use_lxd=True, + enable_experimental_extensions=False, + enable_developer_debug=False, + enable_experimental_target_arch=False, + target_arch=None, provider=None, ) ) @@ -86,6 +98,10 @@ def test_default_command_output(mocker, option): output="name", destructive_mode=False, use_lxd=False, + enable_experimental_extensions=False, + enable_developer_debug=False, + enable_experimental_target_arch=False, + target_arch=None, provider=None, ) ) diff --git a/tests/unit/cli/test_help.py b/tests/unit/cli/test_help.py deleted file mode 100644 index 36d63f2af5..0000000000 --- a/tests/unit/cli/test_help.py +++ /dev/null @@ -1,48 +0,0 @@ -# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- -# -# Copyright 2022 Canonical Ltd. -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License version 3 as -# published by the Free Software Foundation. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -import sys -from unittest.mock import call - -import pytest - -from snapcraft import cli - - -def test_help_command(mocker): - mocker.patch.object(sys, "argv", ["cmd", "help"]) - mock_dispatcher_run = mocker.patch("craft_cli.dispatcher.Dispatcher.run") - mock_legacy_run = mocker.patch("snapcraft_legacy.cli.legacy.legacy_run") - - cli.run() - - assert mock_dispatcher_run.mock_calls == [] - assert mock_legacy_run.mock_calls == [call()] - - -@pytest.mark.parametrize("arg", ["-h", "--help"]) -def test_help_option(mocker, arg): - mocker.patch.object(sys, "argv", ["cmd", arg]) - mock_dispatcher_run = mocker.patch("craft_cli.dispatcher.Dispatcher.run") - mock_legacy_run = mocker.patch( - "snapcraft_legacy.cli.legacy.legacy_run", side_effect=SystemExit - ) - - with pytest.raises(SystemExit): - cli.run() - - assert mock_dispatcher_run.mock_calls == [] - assert mock_legacy_run.mock_calls == [call()] diff --git a/tests/unit/cli/test_lifecycle.py b/tests/unit/cli/test_lifecycle.py index 5ad5e05bd6..5bcf5a45fc 100644 --- a/tests/unit/cli/test_lifecycle.py +++ b/tests/unit/cli/test_lifecycle.py @@ -39,7 +39,14 @@ def test_lifecycle_command(cmd, run_method, mocker): assert mock_lifecycle_cmd.mock_calls == [ call( argparse.Namespace( - parts=[], destructive_mode=False, use_lxd=False, provider=None + parts=[], + destructive_mode=False, + use_lxd=False, + enable_experimental_extensions=False, + enable_developer_debug=False, + enable_experimental_target_arch=False, + target_arch=None, + provider=None, ) ) ] @@ -73,6 +80,10 @@ def test_lifecycle_command_arguments(cmd, run_method, mocker): parts=["part1", "part2"], destructive_mode=False, use_lxd=False, + enable_experimental_extensions=False, + enable_developer_debug=False, + enable_experimental_target_arch=False, + target_arch=None, provider=None, ) ) @@ -108,6 +119,10 @@ def test_lifecycle_command_arguments_destructive_mode(cmd, run_method, mocker): parts=["part1", "part2"], destructive_mode=True, use_lxd=False, + enable_experimental_extensions=False, + enable_developer_debug=False, + enable_experimental_target_arch=False, + target_arch=None, provider=None, ) ) @@ -143,6 +158,10 @@ def test_lifecycle_command_arguments_use_lxd(cmd, run_method, mocker): parts=["part1", "part2"], destructive_mode=False, use_lxd=True, + enable_experimental_extensions=False, + enable_developer_debug=False, + enable_experimental_target_arch=False, + target_arch=None, provider=None, ) ) @@ -164,6 +183,10 @@ def test_lifecycle_command_pack(mocker): output=None, destructive_mode=False, use_lxd=False, + enable_experimental_extensions=False, + enable_developer_debug=False, + enable_experimental_target_arch=False, + target_arch=None, provider=None, ) ) @@ -185,6 +208,10 @@ def test_lifecycle_command_pack_destructive_mode(mocker): output=None, destructive_mode=True, use_lxd=False, + enable_experimental_extensions=False, + enable_developer_debug=False, + enable_experimental_target_arch=False, + target_arch=None, provider=None, ) ) @@ -206,6 +233,10 @@ def test_lifecycle_command_pack_use_lxd(mocker): output=None, destructive_mode=False, use_lxd=True, + enable_experimental_extensions=False, + enable_developer_debug=False, + enable_experimental_target_arch=False, + target_arch=None, provider=None, ) ) @@ -224,6 +255,10 @@ def test_lifecycle_command_pack_output(mocker, option): output="name", destructive_mode=False, use_lxd=False, + enable_experimental_extensions=False, + enable_developer_debug=False, + enable_experimental_target_arch=False, + target_arch=None, provider=None, ) ) @@ -241,6 +276,10 @@ def test_lifecycle_command_pack_directory(mocker): output=None, destructive_mode=False, use_lxd=False, + enable_experimental_extensions=False, + enable_developer_debug=False, + enable_experimental_target_arch=False, + target_arch=None, provider=None, ) ) From b84c6543c6c573297c38b2d73835b41c7e6cec3c Mon Sep 17 00:00:00 2001 From: Sergio Schvezov Date: Fri, 22 Apr 2022 20:12:12 -0300 Subject: [PATCH 107/167] commands: add account related commands - Introduce login, logout, export-login and whoami - Improve whoami to show attenuations if present - Remove support for showing account acls after export login (whoami can now display that information) Signed-off-by: Sergio Schvezov --- requirements-devel.txt | 2 + snapcraft/cli.py | 13 + snapcraft/commands/__init__.py | 10 + snapcraft/commands/account.py | 277 +++++++++++++++++ .../commands/store/__init__.py | 24 +- snapcraft/commands/store/client.py | 184 ++++++++++++ snapcraft/commands/store/constants.py | 38 +++ snapcraft/utils.py | 28 +- snapcraft_legacy/cli/store.py | 213 ------------- .../legacy/unit/commands/test_export_login.py | 162 ---------- tests/legacy/unit/commands/test_login.py | 215 -------------- tests/legacy/unit/commands/test_whoami.py | 52 ---- tests/unit/commands/conftest.py | 26 ++ tests/unit/commands/store/__init__.py | 0 tests/unit/commands/store/test_client.py | 281 ++++++++++++++++++ tests/unit/commands/store/utils.py | 46 +++ tests/unit/commands/test_account.py | 245 +++++++++++++++ 17 files changed, 1156 insertions(+), 660 deletions(-) create mode 100644 snapcraft/commands/account.py rename tests/legacy/unit/commands/test_logout.py => snapcraft/commands/store/__init__.py (52%) create mode 100644 snapcraft/commands/store/client.py create mode 100644 snapcraft/commands/store/constants.py delete mode 100644 tests/legacy/unit/commands/test_export_login.py delete mode 100644 tests/legacy/unit/commands/test_login.py delete mode 100644 tests/legacy/unit/commands/test_whoami.py create mode 100644 tests/unit/commands/conftest.py create mode 100644 tests/unit/commands/store/__init__.py create mode 100644 tests/unit/commands/store/test_client.py create mode 100644 tests/unit/commands/store/utils.py create mode 100644 tests/unit/commands/test_account.py diff --git a/requirements-devel.txt b/requirements-devel.txt index 1276f35810..c8ffea17cb 100644 --- a/requirements-devel.txt +++ b/requirements-devel.txt @@ -103,6 +103,8 @@ types-Deprecated==1.2.6 types-PyYAML==6.0.6 types-setuptools==57.4.14 types-tabulate==0.8.7 +types-requests==2.27.20 +types-urllib3==1.26.13 typing-utils==0.1.0 typing_extensions==4.2.0 urllib3==1.26.9 diff --git a/snapcraft/cli.py b/snapcraft/cli.py index 9590462dc4..5aad19a637 100644 --- a/snapcraft/cli.py +++ b/snapcraft/cli.py @@ -22,6 +22,7 @@ import sys import craft_cli +import craft_store from craft_cli import ArgumentParsingError, EmitterMode, ProvideHelpException, emit from snapcraft import __version__, errors, utils @@ -42,6 +43,15 @@ commands.SnapCommand, # hidden (legacy compatibility) ], ), + craft_cli.CommandGroup( + "Store Account", + [ + commands.StoreLoginCommand, + commands.StoreExportLoginCommand, + commands.StoreLogoutCommand, + commands.StoreWhoAmICommand, + ], + ), craft_cli.CommandGroup( "Extensions", [ @@ -128,6 +138,9 @@ def run(): emit.trace(f"run legacy implementation: {err!s}") emit.ended_ok() legacy.legacy_run() + except craft_store.errors.CraftStoreError as err: + emit.error(craft_cli.errors.CraftError(f"craft-store error: {err}")) + retcode = 1 except errors.SnapcraftError as err: emit.error(err) retcode = 1 diff --git a/snapcraft/commands/__init__.py b/snapcraft/commands/__init__.py index 9d5c4e8fef..146d854f06 100644 --- a/snapcraft/commands/__init__.py +++ b/snapcraft/commands/__init__.py @@ -16,6 +16,12 @@ """Snapcraft commands.""" +from .account import ( + StoreExportLoginCommand, + StoreLoginCommand, + StoreLogoutCommand, + StoreWhoAmICommand, +) from .extensions import ( ExpandExtensionsCommand, ExtensionsCommand, @@ -41,6 +47,10 @@ "PullCommand", "SnapCommand", "StageCommand", + "StoreLoginCommand", + "StoreExportLoginCommand", + "StoreLogoutCommand", + "StoreWhoAmICommand", "ExtensionsCommand", "ListExtensionsCommand", "VersionCommand", diff --git a/snapcraft/commands/account.py b/snapcraft/commands/account.py new file mode 100644 index 0000000000..1a9418f23d --- /dev/null +++ b/snapcraft/commands/account.py @@ -0,0 +1,277 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright 2022 Canonical Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +"""Snapcraft Store Account management commands.""" + +import contextlib +import functools +import os +import stat +import textwrap +from datetime import datetime +from typing import TYPE_CHECKING, Dict, Union + +from craft_cli import BaseCommand, emit +from craft_cli.errors import ArgumentParsingError +from overrides import overrides + +from snapcraft import utils + +from . import store + +if TYPE_CHECKING: + import argparse + + +_VALID_DATE_FORMATS = [ + "%Y-%m-%d", + "%Y-%m-%dT%H:%M:%SZ", +] + + +class StoreLoginCommand(BaseCommand): + """Command to login to the Snap Store.""" + + name = "login" + help_msg = "Login to the Snap Store" + overview = textwrap.dedent( + f""" + Login to the Snap Store with your Ubuntu One SSO credentials. + If you do not have any, you can create them on https://login.ubuntu.com + + To use the alternative authentication mechanism (Candid), set the + environment variable {store.constants.ENVIRONMENT_STORE_AUTH!r} to 'candid'. + + The login command requires a working keyring on the system it is used on. + As an alternative to login in one can export + {store.constants.ENVIRONMENT_STORE_CREDENTIALS!r} with the exported credentials. + """ + ) + + @overrides + def fill_parser(self, parser: "argparse.ArgumentParser") -> None: + """Add arguments specific to the export-login command.""" + parser.add_argument( + "--with", + metavar="", + dest="login_with", + type=str, + nargs=1, + default=None, + help="File to use for imported credentials", + ) + parser.add_argument( + "--experimental-login", + action="store_true", + default=False, + help=( + "Deprecated option to enable candid login. " + f"Set {store.constants.ENVIRONMENT_STORE_AUTH}=candid instead" + ), + ) + + @overrides + def run(self, parsed_args): + if parsed_args.experimental_login: + raise ArgumentParsingError( + "--experimental-login no longer supported. " + f"Set {store.constants.ENVIRONMENT_STORE_AUTH}=candid instead", + ) + + if parsed_args.login_with: + raise ArgumentParsingError( + "--with is no longer supported, export the auth to the environment " + f"variable {store.constants.ENVIRONMENT_STORE_CREDENTIALS!r} instead", + ) + + store.StoreClientCLI().login() + emit.message("Login successful") + + +class StoreExportLoginCommand(BaseCommand): + """Command to export login to use with the Snap Store.""" + + name = "export-login" + help_msg = "Login to the Snap Store exporting the credentials" + overview = textwrap.dedent( + f""" + Login to the Snap Store with your Ubuntu One SSO credentials. + If you do not have any, you can create them on https://login.ubuntu.com + + To use the alternative authentication mechanism (Candid), set the + environment variable {store.constants.ENVIRONMENT_STORE_AUTH!r} to 'candid'. + + This command exports credentials to use on systems where login is not + possible or desired. + """ + ) + + @overrides + def fill_parser(self, parser: "argparse.ArgumentParser") -> None: + """Add arguments specific to the export-login command.""" + parser.add_argument( + "login_file", + metavar="", + type=str, + help="Where to write the exported credentials, - for stdout", + ) + parser.add_argument( + "--snaps", + metavar="", + type=str, + nargs="?", + default=None, + help="Comma-separated list of snaps to limit access", + ) + parser.add_argument( + "--channels", + metavar="", + type=str, + nargs="?", + default=None, + help="Comma-separated list of channels to limit access", + ) + parser.add_argument( + "--acls", + metavar="", + type=str, + nargs="?", + default=None, + help="Comma-separated list of ACLs to limit access", + ) + parser.add_argument( + "--expires", + metavar="", + type=str, + nargs="?", + default=None, + help="Date/time (in ISO 8601) when this exported login expires", + ) + parser.add_argument( + "--experimental-login", + action="store_true", + default=False, + help=( + "Deprecated option to enable candid login. " + f"Set {store.constants.ENVIRONMENT_STORE_AUTH}=candid instead" + ), + ) + + @overrides + def run(self, parsed_args): + if parsed_args.experimental_login: + raise ArgumentParsingError( + "--experimental-login no longer supported. " + f"Set {store.constants.ENVIRONMENT_STORE_AUTH}=candid instead", + ) + + kwargs: Dict[str, Union[str, int]] = {} + if parsed_args.snaps: + kwargs["packages"] = parsed_args.snaps.split(",") + if parsed_args.channels: + kwargs["channels"] = parsed_args.channels.split(",") + if parsed_args.acls: + kwargs["acls"] = parsed_args.acls.split(",") + if parsed_args.expires is not None: + for date_format in _VALID_DATE_FORMATS: + with contextlib.suppress(ValueError): + expiry_date = datetime.strptime(parsed_args.expires, date_format) + break + else: + valid_formats = utils.humanize_list(_VALID_DATE_FORMATS, "or") + raise ArgumentParsingError( + f"The expiry follow an ISO 8601 format ({valid_formats})" + ) + + kwargs["ttl"] = int((expiry_date - datetime.now()).total_seconds()) + + credentials = store.StoreClientCLI(ephemeral=True).login(**kwargs) + + # Support a login_file of '-', which indicates a desire to print to stdout + if parsed_args.login_file.strip() == "-": + message = f"Exported login credentials:\n{credentials}" + else: + # This is sensitive-- it should only be accessible by the owner + private_open = functools.partial(os.open, mode=0o600) + + with open( + parsed_args.login_file, "w", opener=private_open, encoding="utf-8" + ) as login_fd: + print(credentials, file=login_fd, end="") + + # Now that the file has been written, we can just make it + # owner-readable + os.chmod(parsed_args.login_file, stat.S_IRUSR) + + message = f"Exported login credentials to {parsed_args.login_file!r}" + + emit.message(message) + + +class StoreWhoAmICommand(BaseCommand): + """Command to show login information from Snap Store.""" + + name = "whoami" + help_msg = "Get information about the current login" + overview = textwrap.dedent( + """ + Return useful information about the current login. + """ + ) + + @overrides + def run(self, parsed_args): + whoami = store.StoreClientCLI().store_client.whoami() + + if whoami.get("permissions"): + permissions = ", ".join(whoami["permissions"]) + else: + permissions = "no restrictions" + + if whoami.get("channels"): + channels = ", ".join(whoami["channels"]) + else: + channels = "no restrictions" + + account = whoami["account"] + message = textwrap.dedent( + f"""\ + email: {account["email"]} + username: {account["username"]} + id: {account["id"]} + permissions: {permissions} + channels: {channels} + expires: {whoami["expires"]}Z""" + ) + + emit.message(message) + + +class StoreLogoutCommand(BaseCommand): + """Command to logout from the Snap Store.""" + + name = "logout" + help_msg = "Clear Snap Store credentials." + overview = textwrap.dedent( + """ + Remove stored credentials Snap Store credentials from the system. + """ + ) + + @overrides + def run(self, parsed_args): + store.StoreClientCLI().store_client.logout() + emit.message("Credentials cleared") diff --git a/tests/legacy/unit/commands/test_logout.py b/snapcraft/commands/store/__init__.py similarity index 52% rename from tests/legacy/unit/commands/test_logout.py rename to snapcraft/commands/store/__init__.py index e2856ac12f..477f902726 100644 --- a/tests/legacy/unit/commands/test_logout.py +++ b/snapcraft/commands/store/__init__.py @@ -1,6 +1,6 @@ # -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- # -# Copyright (C) 2016-2017 Canonical Ltd +# Copyright 2022 Canonical Ltd. # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License version 3 as @@ -13,22 +13,14 @@ # # You should have received a copy of the GNU General Public License # along with this program. If not, see . -import re -from unittest import mock -from testtools.matchers import Equals, MatchesRegex +"""Snapcraft CLI interface for the Snap Store.""" -from snapcraft_legacy.storeapi import StoreClient -from . import CommandBaseTestCase +from . import constants +from .client import StoreClientCLI - -class LogoutCommandTestCase(CommandBaseTestCase): - @mock.patch.object(StoreClient, "logout") - def test_logout_clears_config(self, mock_logout): - result = self.run_command(["logout"]) - - self.assertThat(result.exit_code, Equals(0)) - self.assertThat( - result.output, MatchesRegex(".*Credentials cleared.\n", flags=re.DOTALL) - ) +__all__ = [ + "StoreClientCLI", + "constants", +] diff --git a/snapcraft/commands/store/client.py b/snapcraft/commands/store/client.py new file mode 100644 index 0000000000..2cd9730524 --- /dev/null +++ b/snapcraft/commands/store/client.py @@ -0,0 +1,184 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright 2022 Canonical Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +"""Snapcraft Store Client with CLI hooks.""" + +import os +import platform +from datetime import timedelta +from typing import Any, Dict, Optional, Sequence, Tuple + +import craft_store +from craft_cli import emit + +from snapcraft import __version__, utils + +from . import constants + +_TESTING_ENV_PREFIXES = ["TRAVIS", "AUTOPKGTEST_TMP"] + + +def build_user_agent( + version=__version__, os_platform: utils.OSPlatform = utils.get_os_platform() +): + """Build Snapcraft's user agent.""" + if any( + key.startswith(prefix) for prefix in _TESTING_ENV_PREFIXES for key in os.environ + ): + testing = " (testing) " + else: + testing = " " + return f"snapcraft/{version}{testing}{os_platform!s}" + + +def use_candid() -> bool: + """Return True if using candid as the auth backend.""" + return os.getenv(constants.ENVIRONMENT_STORE_AUTH) == "candid" + + +def get_store_url() -> str: + """Return the Snap Store url considering the environment.""" + return os.getenv("STORE_DASHBOARD_URL", constants.STORE_URL) + + +def get_store_upload_url() -> str: + """Return the Snap Store Upload url considering the environment.""" + return os.getenv("STORE_UPLOAD_URL", constants.STORE_UPLOAD_URL) + + +def get_store_login_url() -> str: + """Return the Ubuntu Login url considering the environment. + + This is only useful when using Ubuntu One SSO. + """ + return os.getenv("UBUNTU_ONE_SSO_URL", constants.UBUNTU_ONE_SSO_URL) + + +def _prompt_login() -> Tuple[str, str]: + emit.message( + "Enter your Ubuntu One e-mail address and password.", intermediate=True + ) + emit.message( + "If you do not have an Ubuntu One account, you can create one " + "at https://snapcraft.io/account", + intermediate=True, + ) + email = utils.prompt("Email: ") + password = utils.prompt("Password: ", hide=True) + + return (email, password) + + +def _get_hostname(hostname: Optional[str] = platform.node()) -> str: + """Return the computer's network name or UNNKOWN if it cannot be determined.""" + if not hostname: + hostname = "UNKNOWN" + return hostname + + +def get_client(ephemeral: bool) -> craft_store.BaseClient: + """Store Client factory.""" + store_url = get_store_url() + store_upload_url = get_store_upload_url() + user_agent = build_user_agent() + + if use_candid() is True: + client: craft_store.BaseClient = craft_store.StoreClient( + base_url=store_url, + storage_base_url=store_upload_url, + application_name="snapcraft", + user_agent=user_agent, + endpoints=craft_store.endpoints.SNAP_STORE, + environment_auth=constants.ENVIRONMENT_STORE_CREDENTIALS, + ephemeral=ephemeral, + ) + else: + client = craft_store.UbuntuOneStoreClient( + base_url=store_url, + storage_base_url=store_upload_url, + auth_url=get_store_login_url(), + application_name="snapcraft", + user_agent=user_agent, + endpoints=craft_store.endpoints.U1_SNAP_STORE, + environment_auth=constants.ENVIRONMENT_STORE_CREDENTIALS, + ephemeral=ephemeral, + ) + + return client + + +class StoreClientCLI: + """A BaseClient implementation considering command line prompts.""" + + def __init__(self, ephemeral=False): + self.store_client = get_client(ephemeral=ephemeral) + + def login( + self, + *, + ttl: int = int(timedelta(days=365).total_seconds()), + acls: Optional[Sequence[str]] = None, + packages: Optional[Sequence[str]] = None, + channels: Optional[Sequence[str]] = None, + ) -> str: + """Login to the Snap Store and prompt if required.""" + kwargs: Dict[str, Any] = {} + if use_candid() is False: + kwargs["email"], kwargs["password"] = _prompt_login() + + if packages is None: + packages = [] + _packages = [ + craft_store.endpoints.Package(package_name=p, package_type="snap") + for p in packages + ] + if acls is None: + acls = [ + "package_access", + "package_manage", + "package_metrics", + "package_push", + "package_register", + "package_release", + "package_update", + ] + + description = f"snapcraft@{_get_hostname()}" + + try: + credentials = self.store_client.login( + ttl=ttl, + permissions=acls, + channels=channels, + packages=_packages, + description=description, + **kwargs, + ) + except craft_store.errors.StoreServerError as store_error: + if "twofactor-required" not in store_error.error_list: + raise + kwargs["otp"] = utils.prompt("Second-factor auth: ") + + credentials = self.store_client.login( + ttl=ttl, + permissions=acls, + channels=channels, + packages=_packages, + description=description, + **kwargs, + ) + + return credentials diff --git a/snapcraft/commands/store/constants.py b/snapcraft/commands/store/constants.py new file mode 100644 index 0000000000..907a0b6993 --- /dev/null +++ b/snapcraft/commands/store/constants.py @@ -0,0 +1,38 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright 2022 Canonical Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +"""Snap Store constants.""" + +from typing import Final + +ENVIRONMENT_STORE_CREDENTIALS: Final[str] = "SNAPCRAFT_STORE_CREDENTIALS" +"""Environment variable where credentials can be picked up from.""" + +ENVIRONMENT_STORE_AUTH: Final[str] = "SNAPCRAFT_STORE_AUTH" +"""Environment variable used to set an alterntive login method. + +The only setting that changes the behavior is `candid`, every +other value uses Ubuntu SSO. +""" + +STORE_URL: Final[str] = "https://dashboard.snapcraft.io" +"""Default store backend URL.""" + +STORE_UPLOAD_URL: Final[str] = "https://storage.snapcraftcontent.com" +"""Default store upload URL.""" + +UBUNTU_ONE_SSO_URL = "https://login.ubuntu.com" +"""Default Ubuntu One Login URL.""" diff --git a/snapcraft/utils.py b/snapcraft/utils.py index 2d2149cf52..bb8a4bf148 100644 --- a/snapcraft/utils.py +++ b/snapcraft/utils.py @@ -21,10 +21,13 @@ import platform import sys from dataclasses import dataclass +from getpass import getpass from typing import Iterable, Optional from craft_cli import emit +from snapcraft import errors + @dataclass class OSPlatform: @@ -132,7 +135,7 @@ def get_managed_environment_snap_channel() -> Optional[str]: return os.getenv("SNAPCRAFT_INSTALL_SNAP_CHANNEL") -def confirm_with_user(prompt, default=False) -> bool: +def confirm_with_user(prompt_text, default=False) -> bool: """Query user for yes/no answer. If stdin is not a tty, the default value is returned. @@ -151,7 +154,7 @@ def confirm_with_user(prompt, default=False) -> bool: choices = " [Y/n]: " if default else " [y/N]: " - reply = str(input(prompt + choices)).lower().strip() + reply = str(input(prompt_text + choices)).lower().strip() if reply and reply[0] == "y": return True @@ -161,6 +164,27 @@ def confirm_with_user(prompt, default=False) -> bool: return default +def prompt(prompt_text: str, *, hide: bool = False) -> str: + """Prompt and return the entered string. + + :param prompt_text: string used for the prompt. + :param hide: hide user input if True. + """ + if is_managed_mode(): + raise RuntimeError("prompting not yet supported in managed-mode") + + if not sys.stdin.isatty(): + raise errors.SnapcraftError("prompting not possible with no tty") + + if hide: + method = getpass + else: + method = input # type: ignore + + with emit.pause(): + return str(method(prompt_text)) + + def humanize_list( items: Iterable[str], conjunction: str, item_format: str = "{!r}" ) -> str: diff --git a/snapcraft_legacy/cli/store.py b/snapcraft_legacy/cli/store.py index b5d512e7d8..7c75ae9b3f 100644 --- a/snapcraft_legacy/cli/store.py +++ b/snapcraft_legacy/cli/store.py @@ -660,219 +660,6 @@ def list_registered(): snapcraft_legacy.list_registered() -@storecli.command("export-login") -@click.argument( - "login_file", metavar="FILE", type=click.Path(dir_okay=False, writable=True) -) -@click.option( - "--snaps", metavar="", help="Comma-separated list of snaps to limit access" -) -@click.option( - "--channels", - metavar="", - help="Comma-separated list of channels to limit access", -) -@click.option( - "--acls", metavar="", help="Comma-separated list of ACLs to limit access" -) -@click.option( - "--expires", - metavar="", - help="Date/time (in ISO 8601) when this exported login expires", -) -@click.option( - "--experimental-login", - is_flag=True, - help="*EXPERIMENTAL* Enables login through candid.", - envvar="SNAPCRAFT_EXPERIMENTAL_LOGIN", -) -def export_login( - login_file: str, - snaps: str, - channels: str, - acls: str, - expires: str, - experimental_login: bool, -): - """Save login configuration for a store account in FILE. - - This file can then be used to log in to the given account with the - specified permissions. One can also request the login to be exported to - stdout instead of a file: - - snapcraft export-login - - - For example, to limit access to the edge channel of any snap the account - can access: - - snapcraft export-login --channels=edge exported - - Or to limit access to only the edge channel of a single snap: - - snapcraft export-login --snaps=my-snap --channels=edge exported - - To limit access to a single snap, but only until 2019: - - snapcraft export-login --expires="2019-01-01T00:00:00" exported - """ - if experimental_login: - raise click.BadOptionUsage( - "--experimental-login", - "--experimental-login no longer supported. " - f"Set {storeapi.constants.ENVIRONMENT_STORE_AUTH}=candid instead", - ) - - snap_list = None - channel_list = None - acl_list = None - - if snaps: - snap_list = [] - for package in snaps.split(","): - snap_list.append( - {"name": package, "series": storeapi.constants.DEFAULT_SERIES} - ) - - if channels: - channel_list = channels.split(",") - - if acls: - acl_list = acls.split(",") - - if expires is not None: - for date_format in _VALID_DATE_FORMATS: - with contextlib.suppress(ValueError): - expiry_date = datetime.strptime(expires, date_format) - break - else: - valid_formats = formatting_utils.humanize_list(_VALID_DATE_FORMATS, "or") - raise click.BadParameter( - message=f"The expiry follow an ISO 8601 format ({valid_formats})" - ) - - ttl = int((expiry_date - datetime.now()).total_seconds()) - else: - # Default to 1y - ttl = int((datetime.now() + timedelta(days=365)).timestamp()) - - store_client = storeapi.StoreClient(ephemeral=True) - credentials = snapcraft_legacy.login( - store_client=store_client, - packages=snap_list, - channels=channel_list, - acls=acl_list, - ttl=ttl, - ) - - # Support a login_file of '-', which indicates a desire to print to stdout - if login_file.strip() == "-": - echo.info("\nExported login starts on next line:") - - echo.info(credentials) - - preamble = "Login successfully exported and printed above" - credentials_action = f"{storeapi.constants.ENVIRONMENT_STORE_CREDENTIALS}='' snapcraft " - else: - # This is sensitive-- it should only be accessible by the owner - private_open = functools.partial(os.open, mode=0o600) - - with open(login_file, "w", opener=private_open) as login_fd: - print(credentials, file=login_fd) - - # Now that the file has been written, we can just make it - # owner-readable - os.chmod(login_file, stat.S_IRUSR) - - preamble = f"Login successfully exported to {login_file!r}" - credentials_action = f"{storeapi.constants.ENVIRONMENT_STORE_CREDENTIALS}=$(cat {login_file}) snapcraft " - - print() - echo.info( - dedent( - f"""\ - {preamble}. Any store action that now requires authentication can be used by running - - {credentials_action} - - """ - ) - ) - try: - human_acls = _human_readable_acls(store_client) - echo.info( - "to log in to this account with no password and have these " - f"capabilities:\n{human_acls}" - ) - except NotImplementedError: - pass - - echo.warning( - "This exported login is not encrypted. Do not commit it to version control!" - ) - - -@storecli.command() -@click.option( - "--with", - "login_file", - metavar="", - type=click.File("r"), - help="Path to file created with 'snapcraft export-login'", -) -@click.option( - "--experimental-login", - is_flag=True, - help="*EXPERIMENTAL* Enables login through candid.", - envvar="SNAPCRAFT_EXPERIMENTAL_LOGIN", -) -def login(login_file, experimental_login: bool): - """Login with your Ubuntu One e-mail address and password. - - If you do not have an Ubuntu One account, you can create one at - https://snapcraft.io/account - """ - if login_file: - raise click.BadOptionUsage( - "--with", - "--with is no longer supported, export the auth to the environment " - f"variable {storeapi.constants.ENVIRONMENT_STORE_CREDENTIALS!r} instead", - ) - - if experimental_login: - raise click.BadOptionUsage( - "--experimental-login", - "--experimental-login no longer supported. " - f"Set {storeapi.constants.ENVIRONMENT_STORE_AUTH}=candid instead", - ) - - store_client = storeapi.StoreClient() - snapcraft_legacy.login(store_client=store_client) - - echo.info("Login successful.") - - -@storecli.command() -def logout(): - """Clear session credentials.""" - store = storeapi.StoreClient() - store.logout() - echo.info("Credentials cleared.") - - -@storecli.command() -def whoami(): - """Returns your login information relevant to the store.""" - account = StoreClientCLI().whoami().account - - click.echo( - dedent( - f"""\ - email: {account.email} - developer-id: {account.account_id}""" - ) - ) - - @storecli.command() @click.argument("snap-name", metavar="") @click.argument("track_name", metavar="") diff --git a/tests/legacy/unit/commands/test_export_login.py b/tests/legacy/unit/commands/test_export_login.py deleted file mode 100644 index 49a89acab7..0000000000 --- a/tests/legacy/unit/commands/test_export_login.py +++ /dev/null @@ -1,162 +0,0 @@ -# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- -# -# Copyright (C) 2016-2019 Canonical Ltd -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License version 3 as -# published by the Free Software Foundation. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -import re -from unittest import mock - -import fixtures -from testtools.matchers import Contains, Equals, MatchesRegex - -from snapcraft_legacy import storeapi -from . import FakeStoreCommandsBaseTestCase - - -class ExportLoginCommandTestCase(FakeStoreCommandsBaseTestCase): - def setUp(self): - super().setUp() - - self.useFixture( - fixtures.MockPatchObject( - storeapi.StoreClient, - "acl", - return_value={ - "snap_ids": None, - "channels": None, - "permissions": None, - "expires": "2018-01-01T00:00:00", - }, - ) - ) - - def test_successful_export(self): - result = self.run_command( - ["export-login", "exported"], input="user@example.com\nsecret\n" - ) - - self.assertThat(result.exit_code, Equals(0)) - self.assertThat(result.output, Contains(storeapi.constants.TWO_FACTOR_WARNING)) - self.assertThat(result.output, Contains("Login successfully exported")) - self.assertThat( - result.output, MatchesRegex(r".*snaps:.*?No restriction", re.DOTALL) - ) - self.assertThat( - result.output, MatchesRegex(r".*channels:.*?No restriction", re.DOTALL) - ) - self.assertThat( - result.output, MatchesRegex(r".*permissions:.*?No restriction", re.DOTALL) - ) - self.assertThat( - result.output, MatchesRegex(r".*expires:.*?2018-01-01T00:00:00", re.DOTALL) - ) - - self.fake_store_login.mock.assert_called_once_with( - email="user@example.com", - password="secret", - acls=None, - packages=None, - channels=None, - ttl=mock.ANY, - ) - - def test_successful_export_stdout(self): - result = self.run_command( - ["export-login", "-"], input="user@example.com\nsecret\n" - ) - - self.assertThat(result.exit_code, Equals(0)) - self.assertThat(result.output, Contains(storeapi.constants.TWO_FACTOR_WARNING)) - self.assertThat(result.output, Contains("Exported login starts on next line")) - self.assertThat( - result.output, Contains("Login successfully exported and printed above") - ) - self.assertThat( - result.output, MatchesRegex(r".*snaps:.*?No restriction", re.DOTALL) - ) - self.assertThat( - result.output, MatchesRegex(r".*channels:.*?No restriction", re.DOTALL) - ) - self.assertThat( - result.output, MatchesRegex(r".*permissions:.*?No restriction", re.DOTALL) - ) - self.assertThat( - result.output, MatchesRegex(r".*expires:.*?2018-01-01T00:00:00", re.DOTALL) - ) - - self.fake_store_login.mock.assert_called_once_with( - email="user@example.com", - password="secret", - acls=None, - packages=None, - channels=None, - ttl=mock.ANY, - ) - - def test_successful_export_expires(self): - self.useFixture( - fixtures.MockPatchObject( - storeapi.StoreClient, - "acl", - return_value={ - "snap_ids": None, - "channels": None, - "permissions": None, - "expires": "2018-02-01T00:00:00", - }, - ) - ) - - result = self.run_command( - ["export-login", "--expires=2018-02-01T00:00:00Z", "exported"], - input="user@example.com\nsecret\n", - ) - self.assertThat(result.exit_code, Equals(0)) - self.assertThat(result.output, Contains(storeapi.constants.TWO_FACTOR_WARNING)) - self.assertThat(result.output, Contains("Login successfully exported")) - self.assertThat( - result.output, MatchesRegex(r".*snaps:.*?No restriction", re.DOTALL) - ) - self.assertThat( - result.output, MatchesRegex(r".*channels:.*?No restriction", re.DOTALL) - ) - self.assertThat( - result.output, MatchesRegex(r".*permissions:.*?No restriction", re.DOTALL) - ) - self.assertThat( - result.output, MatchesRegex(r".*expires:.*?2018-02-01T00:00:00", re.DOTALL) - ) - - self.fake_store_login.mock.assert_called_once_with( - email="user@example.com", - password=mock.ANY, - acls=None, - packages=None, - channels=None, - ttl=mock.ANY, - ) - - def test_bad_date_format(self): - result = self.run_command( - ["export-login", "--expires=20180201", "exported"], - input="user@example.com\nsecret\n", - ) - self.assertThat(result.exit_code, Equals(2)) - self.assertThat( - result.output, - Contains( - "Error: Invalid value: The expiry follow an ISO 8601 format " - "('%Y-%m-%d' or '%Y-%m-%dT%H:%M:%SZ')" - ), - ) diff --git a/tests/legacy/unit/commands/test_login.py b/tests/legacy/unit/commands/test_login.py deleted file mode 100644 index c5017d9b61..0000000000 --- a/tests/legacy/unit/commands/test_login.py +++ /dev/null @@ -1,215 +0,0 @@ -# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- -# -# Copyright (C) 2016-2019 Canonical Ltd -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License version 3 as -# published by the Free Software Foundation. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -import json -from unittest import mock - -import craft_store -import pytest -import requests -from simplejson.scanner import JSONDecodeError -from testtools.matchers import Contains, Equals, Not - -from snapcraft_legacy import storeapi - -from . import FakeResponse, FakeStoreCommandsBaseTestCase - - -class LoginCommandTestCase(FakeStoreCommandsBaseTestCase): - def test_login(self): - # No 2fa - self.fake_store_login.mock.side_effect = None - - result = self.run_command(["login"], input="user@example.com\nsecret\n") - - self.assertThat(result.exit_code, Equals(0)) - self.assertThat(result.output, Contains(storeapi.constants.TWO_FACTOR_WARNING)) - self.assertThat(result.output, Contains("Login successful.")) - self.fake_store_login.mock.assert_called_once_with( - email="user@example.com", - password=mock.ANY, - acls=None, - packages=None, - channels=None, - ttl=mock.ANY, - ) - - def test_login_with_2fa(self): - self.fake_store_login.mock.side_effect = [ - craft_store.errors.StoreServerError( - FakeResponse( - status_code=requests.codes.unauthorized, - content=json.dumps( - { - "error_list": [ - {"message": "2fa", "code": "twofactor-required"} - ] - } - ), - ) - ), - None, - ] - - # no exception raised. - result = self.run_command(["login"], input="user@example.com\nsecret\n123456\n") - - self.assertThat(result.exit_code, Equals(0)) - self.assertThat( - result.output, Not(Contains(storeapi.constants.TWO_FACTOR_WARNING)) - ) - self.assertThat(result.output, Contains("Login successful.")) - - self.assertThat(self.fake_store_login.mock.call_count, Equals(2)) - self.fake_store_login.mock.assert_has_calls( - [ - mock.call( - email="user@example.com", - password="secret", - acls=None, - packages=None, - channels=None, - ttl=mock.ANY, - ), - mock.call( - email="user@example.com", - password="secret", - otp="123456", - acls=None, - packages=None, - channels=None, - ttl=mock.ANY, - ), - ] - ) - - def test_failed_login_with_store_account_info_error(self): - response = mock.Mock() - response.json.side_effect = JSONDecodeError("mock-fail", "doc", 1) - response.status_code = 500 - response.reason = "Internal Server Error" - self.fake_store_login.mock.side_effect = ( - storeapi.errors.StoreAccountInformationError(response) - ) - - with pytest.raises(storeapi.errors.StoreAccountInformationError) as exc_info: - self.run_command(["login"], input="user@example.com\nsecret\n\n") - - assert ( - str(exc_info.value) - == "Error fetching account information from store: 500 Internal Server Error" - ) - - def test_failed_login_with_dev_namespace_error(self): - response = mock.Mock() - response.status_code = 403 - response.reason = storeapi.constants.MISSING_NAMESPACE - content = { - "error_list": [ - { - "message": storeapi.constants.MISSING_NAMESPACE, - "extra": {"url": "http://fake-url.com", "api": "fake-api"}, - } - ] - } - response.json.return_value = content - self.fake_store_account_info.mock.side_effect = ( - storeapi.errors.StoreAccountInformationError(response) - ) - - with pytest.raises(storeapi.errors.NeedTermsSignedError) as exc_info: - self.run_command(["login"], input="user@example.com\nsecret\n") - - assert ( - str(exc_info.value) - == "Developer Terms of Service agreement must be signed before continuing: You need to set a username. It will appear in the developer field alongside the other details for your snap. Please visit http://fake-url.com and login again." - ) - - def test_failed_login_with_unexpected_account_error(self): - # Test to simulate get_account_info raising unexpected errors. - response = mock.Mock() - response.status_code = 500 - response.reason = "Internal Server Error" - content = { - "error_list": [ - { - "message": "Just another error", - "extra": {"url": "http://fake-url.com", "api": "fake-api"}, - } - ] - } - response.json.return_value = content - self.fake_store_account_info.mock.side_effect = ( - storeapi.errors.StoreAccountInformationError(response) - ) - - with pytest.raises(storeapi.errors.StoreAccountInformationError) as exc_info: - self.run_command(["login"], input="user@example.com\nsecret\n\n") - - assert ( - str(exc_info.value) - == "Error fetching account information from store: Just another error" - ) - - def test_failed_login_with_dev_agreement_error_with_choice_no(self): - response = mock.Mock() - response.status_code = 403 - response.reason = storeapi.constants.MISSING_AGREEMENT - content = { - "error_list": [ - { - "message": storeapi.constants.MISSING_AGREEMENT, - "extra": {"url": "http://fake-url.com", "api": "fake-api"}, - } - ] - } - response.json.return_value = content - self.fake_store_account_info.mock.side_effect = ( - storeapi.errors.StoreAccountInformationError(response) - ) - - with pytest.raises(storeapi.errors.NeedTermsSignedError) as exc_info: - self.run_command(["login"], input="user@example.com\nsecret\nn\n") - - assert ( - str(exc_info.value) - == "Developer Terms of Service agreement must be signed before continuing: You must agree to the developer terms and conditions to upload snaps." - ) - - def test_failed_login_with_dev_agreement_error_with_choice_yes(self): - response = mock.Mock() - response.status_code = 403 - response.reason = storeapi.constants.MISSING_AGREEMENT - content = { - "error_list": [ - { - "message": storeapi.constants.MISSING_AGREEMENT, - "extra": {"url": "http://fake-url.com", "api": "fake-api"}, - } - ] - } - response.json.return_value = content - self.fake_store_account_info.mock.side_effect = ( - storeapi.errors.StoreAccountInformationError(response) - ) - - with pytest.raises(storeapi.errors.NeedTermsSignedError) as exc_info: - self.run_command(["login"], input="user@example.com\nsecret\ny\n") - - assert ( - str(exc_info.value) - == "Developer Terms of Service agreement must be signed before continuing: Unexpected error encountered during signing the developer terms and conditions. Please visit http://fake-url.com and agree to the terms and conditions before continuing." - ) diff --git a/tests/legacy/unit/commands/test_whoami.py b/tests/legacy/unit/commands/test_whoami.py deleted file mode 100644 index 3b8275cd54..0000000000 --- a/tests/legacy/unit/commands/test_whoami.py +++ /dev/null @@ -1,52 +0,0 @@ -# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- -# -# Copyright (C) 2017-2021 Canonical Ltd -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License version 3 as -# published by the Free Software Foundation. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -from textwrap import dedent - -import pytest - -from snapcraft_legacy.storeapi.v2 import whoami -from snapcraft_legacy.storeapi import StoreClient - - -@pytest.fixture -def fake_dashboard_whoami(monkeypatch): - monkeypatch.setattr( - StoreClient, - "whoami", - lambda x: whoami.WhoAmI( - account=whoami.Account( - email="foo@bar.baz", - account_id="1234567890", - name="Foo from Baz", - username="foo", - ) - ), - ) - - -@pytest.mark.usefixtures("memory_keyring") -@pytest.mark.usefixtures("fake_dashboard_whoami") -def test_whoami(click_run): - result = click_run(["whoami"]) - - assert result.exit_code == 0 - assert result.output == dedent( - """\ - email: foo@bar.baz - developer-id: 1234567890 - """ - ) diff --git a/tests/unit/commands/conftest.py b/tests/unit/commands/conftest.py new file mode 100644 index 0000000000..31358dbfa1 --- /dev/null +++ b/tests/unit/commands/conftest.py @@ -0,0 +1,26 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright 2022 Canonical Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + + +import pytest + + +@pytest.fixture +def fake_client(mocker): + """Forces get_client to return a fake craft_store.BaseClient""" + client = mocker.patch("craft_store.BaseClient", autospec=True) + mocker.patch("snapcraft.commands.store.client.get_client", return_value=client) + return client diff --git a/tests/unit/commands/store/__init__.py b/tests/unit/commands/store/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/unit/commands/store/test_client.py b/tests/unit/commands/store/test_client.py new file mode 100644 index 0000000000..fa3060d43f --- /dev/null +++ b/tests/unit/commands/store/test_client.py @@ -0,0 +1,281 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright 2022 Canonical Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import json +from unittest.mock import call + +import craft_store +import pytest +import requests +from craft_store import endpoints + +from snapcraft.commands.store import client +from snapcraft.utils import OSPlatform + +from .utils import FakeResponse + +#################### +# User Agent Tests # +#################### + + +def test_useragent_linux(): + """Construct a user-agent as a patched Linux machine""" + os_platform = OSPlatform( + system="Arch Linux", release="5.10.10-arch1-1", machine="x86_64" + ) + + assert client.build_user_agent(version="7.1.0", os_platform=os_platform) == ( + "snapcraft/7.1.0 Arch Linux/5.10.10-arch1-1 (x86_64)" + ) + + +@pytest.mark.parametrize("testing_env", ("TRAVIS_TESTING", "AUTOPKGTEST_TMP")) +def test_useragent_linux_with_testing(monkeypatch, testing_env): + """Construct a user-agent as a patched Linux machine""" + monkeypatch.setenv(testing_env, "1") + os_platform = OSPlatform( + system="Arch Linux", release="5.10.10-arch1-1", machine="x86_64" + ) + + assert client.build_user_agent(version="7.1.0", os_platform=os_platform) == ( + "snapcraft/7.1.0 (testing) Arch Linux/5.10.10-arch1-1 (x86_64)" + ) + + +@pytest.mark.parametrize("testing_env", ("TRAVIS_TESTING", "AUTOPKGTEST_TMP")) +def test_useragent_windows_with_testing(monkeypatch, testing_env): + """Construct a user-agent as a patched Windows machine""" + monkeypatch.setenv(testing_env, "1") + os_platform = OSPlatform(system="Windows", release="10", machine="AMD64") + + assert client.build_user_agent(version="7.1.0", os_platform=os_platform) == ( + "snapcraft/7.1.0 (testing) Windows/10 (AMD64)" + ) + + +##################### +# Store Environment # +##################### + + +@pytest.mark.parametrize("env, expected", (("candid", True), ("not-candid", False))) +def test_use_candid(monkeypatch, env, expected): + monkeypatch.setenv("SNAPCRAFT_STORE_AUTH", env) + + assert client.use_candid() is expected + + +def test_get_store_url(): + assert client.get_store_url() == "https://dashboard.snapcraft.io" + + +def test_get_store_url_from_env(monkeypatch): + monkeypatch.setenv("STORE_DASHBOARD_URL", "https://fake-store.io") + + assert client.get_store_url() == "https://fake-store.io" + + +def test_get_store_upload_url(): + assert client.get_store_upload_url() == "https://storage.snapcraftcontent.com" + + +def test_get_store_url_upload_from_env(monkeypatch): + monkeypatch.setenv("STORE_UPLOAD_URL", "https://fake-store-upload.io") + + assert client.get_store_upload_url() == "https://fake-store-upload.io" + + +def test_get_store_login_url(): + assert client.get_store_login_url() == "https://login.ubuntu.com" + + +def test_get_store_login_from_env(monkeypatch): + monkeypatch.setenv("UBUNTU_ONE_SSO_URL", "https://fake-login.io") + + assert client.get_store_login_url() == "https://fake-login.io" + + +#################### +# Host Environment # +#################### + + +def test_get_hostname_none_is_unkown(): + assert client._get_hostname(hostname=None) == "UNKNOWN" + + +def test_get_hostname(): + assert client._get_hostname(hostname="acme") == "acme" + + +####################### +# StoreClient factory # +####################### + + +@pytest.mark.parametrize("ephemeral", (True, False)) +def test_get_store_client(monkeypatch, ephemeral): + monkeypatch.setenv("SNAPCRAFT_STORE_AUTH", "candid") + + store_client = client.get_client(ephemeral) + + assert isinstance(store_client, craft_store.StoreClient) + + +@pytest.mark.parametrize("ephemeral", (True, False)) +def test_get_ubuntu_client(ephemeral): + store_client = client.get_client(ephemeral) + + assert isinstance(store_client, craft_store.UbuntuOneStoreClient) + + +################## +# StoreClientCLI # +################## + + +@pytest.fixture +def fake_user_password(mocker): + """Return a canned user name and password""" + mocker.patch.object( + client, + "_prompt_login", + return_value=("fake-username@acme.com", "fake-password"), + ) + + +@pytest.fixture +def fake_otp(mocker): + """Return a canned user name and password""" + mocker.patch.object( + client.utils, + "prompt", + return_value="123456", + ) + + +@pytest.fixture +def fake_hostname(mocker): + mocker.patch.object(client, "_get_hostname", return_value="fake-host") + + +@pytest.mark.usefixtures("fake_user_password", "fake_hostname") +def test_login(fake_client): + client.StoreClientCLI().login() + + assert fake_client.login.mock_calls == [ + call( + ttl=31536000, + permissions=[ + "package_access", + "package_manage", + "package_metrics", + "package_push", + "package_register", + "package_release", + "package_update", + ], + channels=None, + packages=[], + description="snapcraft@fake-host", + email="fake-username@acme.com", + password="fake-password", + ) + ] + + +@pytest.mark.usefixtures("fake_user_password", "fake_otp", "fake_hostname") +def test_login_otp(fake_client): + fake_client.login.side_effect = [ + craft_store.errors.StoreServerError( + FakeResponse( + status_code=requests.codes.unauthorized, # pylint: disable=no-member + content=json.dumps( + {"error_list": [{"message": "2fa", "code": "twofactor-required"}]} + ), + ) + ), + None, + ] + + client.StoreClientCLI().login() + + assert fake_client.login.mock_calls == [ + call( + ttl=31536000, + permissions=[ + "package_access", + "package_manage", + "package_metrics", + "package_push", + "package_register", + "package_release", + "package_update", + ], + channels=None, + packages=[], + description="snapcraft@fake-host", + email="fake-username@acme.com", + password="fake-password", + ), + call( + ttl=31536000, + permissions=[ + "package_access", + "package_manage", + "package_metrics", + "package_push", + "package_register", + "package_release", + "package_update", + ], + channels=None, + packages=[], + description="snapcraft@fake-host", + email="fake-username@acme.com", + password="fake-password", + otp="123456", + ), + ] + + +@pytest.mark.usefixtures("fake_user_password", "fake_hostname") +def test_with_params(fake_client): + client.StoreClientCLI().login( + ttl=20, + acls=["package_access", "package_push"], + packages=["fake-snap", "fake-other-snap"], + channels=["stable/fake", "edge/fake"], + ) + + assert fake_client.login.mock_calls == [ + call( + ttl=20, + permissions=[ + "package_access", + "package_push", + ], + channels=["stable/fake", "edge/fake"], + packages=[ + endpoints.Package(package_name="fake-snap", package_type="snap"), + endpoints.Package(package_name="fake-other-snap", package_type="snap"), + ], + description="snapcraft@fake-host", + email="fake-username@acme.com", + password="fake-password", + ) + ] diff --git a/tests/unit/commands/store/utils.py b/tests/unit/commands/store/utils.py new file mode 100644 index 0000000000..a3e5a6d9e5 --- /dev/null +++ b/tests/unit/commands/store/utils.py @@ -0,0 +1,46 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright 2022 Canonical Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import json + +import requests + + +class FakeResponse(requests.Response): + """A fake requests.Response.""" + + def __init__(self, content, status_code): # pylint: disable=super-init-not-called + self._content = content + self.status_code = status_code + + @property + def content(self): + return self._content + + @property + def ok(self): + return self.status_code == 200 + + def json(self, **kwargs): + return json.loads(self._content) # type: ignore + + @property + def reason(self): + return self._content + + @property + def text(self): + return self.content diff --git a/tests/unit/commands/test_account.py b/tests/unit/commands/test_account.py new file mode 100644 index 0000000000..a933458304 --- /dev/null +++ b/tests/unit/commands/test_account.py @@ -0,0 +1,245 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright 2022 Canonical Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import argparse +from textwrap import dedent +from unittest.mock import ANY, call + +import craft_cli +import pytest + +from snapcraft import commands + +############ +# Fixtures # +############ + + +@pytest.fixture +def fake_store_login(mocker): + fake_client = mocker.patch( + "snapcraft.commands.store.StoreClientCLI.login", + autospec=True, + return_value="secret", + ) + return fake_client + + +################# +# Login Command # +################# + + +@pytest.mark.usefixtures("memory_keyring") +def test_login(emitter, fake_store_login): + cmd = commands.StoreLoginCommand(None) + + cmd.run(argparse.Namespace(login_with=None, experimental_login=False)) + + assert fake_store_login.mock_calls == [ + call( + ANY, + ) + ] + emitter.assert_recorded(["Login successful"]) + + +def test_login_with_file_fails(): + cmd = commands.StoreLoginCommand(None) + + with pytest.raises(craft_cli.errors.ArgumentParsingError) as raised: + cmd.run(argparse.Namespace(login_with="fake-file", experimental_login=False)) + + assert str(raised.value) == ( + "--with is no longer supported, export the auth to the environment " + "variable 'SNAPCRAFT_STORE_CREDENTIALS' instead" + ) + + +def test_login_with_experimental_fails(): + cmd = commands.StoreLoginCommand(None) + + with pytest.raises(craft_cli.errors.ArgumentParsingError) as raised: + cmd.run(argparse.Namespace(login_with=None, experimental_login=True)) + + assert str(raised.value) == ( + "--experimental-login no longer supported. Set SNAPCRAFT_STORE_AUTH=candid instead" + ) + + +######################## +# Export Login Command # +######################## + + +def test_export_login(emitter, fake_store_login): + cmd = commands.StoreExportLoginCommand(None) + + cmd.run( + argparse.Namespace( + login_file="-", + snaps=None, + channels=None, + acls=None, + expires=None, + experimental_login=False, + ) + ) + + assert fake_store_login.mock_calls == [ + call( + ANY, + ) + ] + emitter.assert_recorded(["Exported login credentials:\nsecret"]) + + +def test_export_login_file(new_dir, emitter, fake_store_login): + cmd = commands.StoreExportLoginCommand(None) + + cmd.run( + argparse.Namespace( + login_file="target_file", + snaps=None, + channels=None, + acls=None, + expires=None, + experimental_login=False, + ) + ) + + assert fake_store_login.mock_calls == [ + call( + ANY, + ) + ] + emitter.assert_recorded(["Exported login credentials to 'target_file'"]) + login_file = new_dir / "target_file" + assert login_file.exists() + assert login_file.read_text() == "secret" + + +def test_export_login_with_params(emitter, fake_store_login): + cmd = commands.StoreExportLoginCommand(None) + + cmd.run( + argparse.Namespace( + login_file="-", + snaps="fake-snap,fake-other-snap", + channels="stable,edge", + acls="package_manage,package_push", + expires="2030-12-12", + experimental_login=False, + ) + ) + + assert fake_store_login.mock_calls == [ + call( + ANY, + packages=["fake-snap", "fake-other-snap"], + channels=["stable", "edge"], + acls=["package_manage", "package_push"], + ttl=ANY, + ) + ] + emitter.assert_recorded(["Exported login credentials:\nsecret"]) + + +def test_export_login_with_experimental_fails(): + cmd = commands.StoreExportLoginCommand(None) + + with pytest.raises(craft_cli.errors.ArgumentParsingError) as raised: + cmd.run( + argparse.Namespace( + login_file="-", + snaps=None, + channels=None, + acls=None, + expires=None, + experimental_login=True, + ) + ) + + assert str(raised.value) == ( + "--experimental-login no longer supported. Set SNAPCRAFT_STORE_AUTH=candid instead" + ) + + +################## +# WhoAmI Command # +################## + + +def test_who(emitter, fake_client): + fake_client.whoami.return_value = { + "account": {"email": "user@acme.org", "id": "id", "username": "user"}, + "expires": "2023-04-22T21:48:57.000", + } + + cmd = commands.StoreWhoAmICommand(None) + + cmd.run(argparse.Namespace()) + + assert fake_client.whoami.mock_calls == [call()] + expected_message = dedent( + """\ + email: user@acme.org + username: user + id: id + permissions: no restrictions + channels: no restrictions + expires: 2023-04-22T21:48:57.000Z""" + ) + emitter.assert_recorded([expected_message]) + + +def test_who_with_attenuations(emitter, fake_client): + fake_client.whoami.return_value = { + "account": {"email": "user@acme.org", "id": "id", "username": "user"}, + "permissions": ["package_manage", "package_access"], + "channels": ["edge", "beta"], + "expires": "2023-04-22T21:48:57.000", + } + + cmd = commands.StoreWhoAmICommand(None) + + cmd.run(argparse.Namespace()) + + assert fake_client.whoami.mock_calls == [call()] + expected_message = dedent( + """\ + email: user@acme.org + username: user + id: id + permissions: package_manage, package_access + channels: edge, beta + expires: 2023-04-22T21:48:57.000Z""" + ) + emitter.assert_recorded([expected_message]) + + +################## +# Logout Command # +################## + + +def test_logout(emitter, fake_client): + cmd = commands.StoreLogoutCommand(None) + + cmd.run(argparse.Namespace()) + + assert fake_client.logout.mock_calls == [call()] + emitter.assert_recorded(["Credentials cleared"]) From deedc44e1c6632ab5f8aded3a656a243f89cecf0 Mon Sep 17 00:00:00 2001 From: Claudio Matsuoka Date: Fri, 15 Apr 2022 16:58:08 -0300 Subject: [PATCH 108/167] meta: adopt metadata info Update project fields with entries from external metadata. Signed-off-by: Claudio Matsuoka --- Makefile | 2 +- snapcraft/errors.py | 4 +- snapcraft/meta/appstream.py | 4 +- snapcraft/parts/lifecycle.py | 82 ++-- snapcraft/parts/parts.py | 33 +- snapcraft/parts/update_metadata.py | 167 +++++++ snapcraft/projects.py | 8 +- snapcraft/repo/projects.py | 3 +- tests/unit/meta/test_appstream.py | 12 +- tests/unit/parts/test_lifecycle.py | 39 +- tests/unit/parts/test_parts.py | 6 + tests/unit/parts/test_update_metadata.py | 560 +++++++++++++++++++++++ tests/unit/test_projects.py | 29 +- 13 files changed, 865 insertions(+), 84 deletions(-) create mode 100644 snapcraft/parts/update_metadata.py create mode 100644 tests/unit/parts/test_update_metadata.py diff --git a/Makefile b/Makefile index 3a87b0d822..902ecfd184 100644 --- a/Makefile +++ b/Makefile @@ -14,7 +14,7 @@ test-black: .PHONY: test-codespell test-codespell: - codespell --quiet-level 4 --ignore-words-list crate,keyserver --skip '*.tar,*.xz,*.zip,*.bz2,*.7z,*.gz,*.deb,*.rpm,*.snap,*.gpg,*.pyc,*.png,*.ico,*.jar,*.so,changelog,.git,.hg,.mypy_cache,.tox,.venv,_build,buck-out,__pycache__,build,dist,.vscode,parts,stage,prime,test_appstream.py,./snapcraft.spec,./.direnv,./.pytest_cache' + codespell --quiet-level 4 --ignore-words-list crate,keyserver,comandos --skip '*.tar,*.xz,*.zip,*.bz2,*.7z,*.gz,*.deb,*.rpm,*.snap,*.gpg,*.pyc,*.png,*.ico,*.jar,*.so,changelog,.git,.hg,.mypy_cache,.tox,.venv,_build,buck-out,__pycache__,build,dist,.vscode,parts,stage,prime,test_appstream.py,./snapcraft.spec,./.direnv,./.pytest_cache' .PHONY: test-flake8 test-flake8: diff --git a/snapcraft/errors.py b/snapcraft/errors.py index ff6747ef5c..c549251b6d 100644 --- a/snapcraft/errors.py +++ b/snapcraft/errors.py @@ -45,8 +45,8 @@ class ExtensionError(SnapcraftError): class MetadataExtractionError(SnapcraftError): """Attempt to extract metadata from file was unsuccessful.""" - def __init__(self, filename: str) -> None: - super().__init__(f"Error extracting metadata from {filename!r}") + def __init__(self, filename: str, message: str) -> None: + super().__init__(f"Error extracting metadata from {filename!r}: {message}") class LegacyFallback(Exception): diff --git a/snapcraft/meta/appstream.py b/snapcraft/meta/appstream.py index 0fbc8b046a..40862d9ebe 100644 --- a/snapcraft/meta/appstream.py +++ b/snapcraft/meta/appstream.py @@ -138,8 +138,10 @@ def _get_transformed_dom(path: str): def _get_dom(path: str) -> lxml.etree.ElementTree: try: return lxml.etree.parse(path) + except OSError as err: + raise errors.SnapcraftError(str(err)) from err except lxml.etree.ParseError as err: - raise errors.MetadataExtractionError(path) from err + raise errors.MetadataExtractionError(path, str(err)) from err def _get_xslt(): diff --git a/snapcraft/parts/lifecycle.py b/snapcraft/parts/lifecycle.py index 18caeb8fb2..482d0853c6 100644 --- a/snapcraft/parts/lifecycle.py +++ b/snapcraft/parts/lifecycle.py @@ -19,19 +19,19 @@ import subprocess from dataclasses import dataclass from pathlib import Path -from typing import TYPE_CHECKING, Any, Dict, cast +from typing import TYPE_CHECKING, Any, Dict, List, cast -import pydantic from craft_cli import EmitterMode, emit from craft_parts import infos from snapcraft import errors, extensions, pack, providers, utils from snapcraft.meta import snap_yaml from snapcraft.parts import PartsLifecycle -from snapcraft.projects import MANDATORY_ADOPTABLE_FIELDS, GrammarAwareProject, Project +from snapcraft.projects import GrammarAwareProject, Project from snapcraft.providers import capture_logs_from_instance from . import grammar, yaml_utils +from .update_metadata import update_project_metadata if TYPE_CHECKING: import argparse @@ -101,6 +101,23 @@ def process_yaml(project_file: Path) -> Dict[str, Any]: return yaml_data +def _extract_parse_info(yaml_data: Dict[str, Any]) -> Dict[str, List[str]]: + """Remove parse-info data from parts. + + :param yaml_data: The project YAML data. + + :return: The extracted parse info for each part. + """ + parse_info: Dict[str, List[str]] = {} + + if "parts" in yaml_data: + for name, data in yaml_data["parts"].items(): + if "parse-info" in data: + parse_info[name] = data.pop("parse-info") + + return parse_info + + def run(command_name: str, parsed_args: "argparse.Namespace") -> None: """Run the parts lifecycle. @@ -112,6 +129,7 @@ def run(command_name: str, parsed_args: "argparse.Namespace") -> None: snap_project = get_snap_project() yaml_data = process_yaml(snap_project.project_file) + parse_info = _extract_parse_info(yaml_data) # argument --provider is only supported by legacy snapcraft if parsed_args.provider: @@ -122,6 +140,7 @@ def run(command_name: str, parsed_args: "argparse.Namespace") -> None: _run_command( command_name, project=project, + parse_info=parse_info, assets_dir=snap_project.assets_dir, parsed_args=parsed_args, ) @@ -131,6 +150,7 @@ def _run_command( command_name: str, *, project: Project, + parse_info: Dict[str, List[str]], assets_dir: Path, parsed_args: "argparse.Namespace", ) -> None: @@ -167,6 +187,7 @@ def _run_command( part_names=part_names, adopt_info=project.adopt_info, project_name=project.name, + parse_info=parse_info, project_vars={ "version": project.version or "", "grade": project.grade or "", @@ -178,10 +199,19 @@ def _run_command( lifecycle.run(step_name) - # Generate snap.yaml + # Extract metadata and generate snap.yaml project_vars = lifecycle.project_vars if step_name == "prime" and not part_names: - _update_project_metadata(project, project_vars) + metadata_list = lifecycle.extract_metadata() + update_project_metadata( + project, + project_vars=project_vars, + metadata_list=metadata_list, + assets_dir=assets_dir, + prime_dir=lifecycle.prime_dir, + ) + + # TODO: copy meta/gui assets emit.progress("Generating snap metadata...") snap_yaml.write( @@ -199,48 +229,6 @@ def _run_command( ) -def _update_project_metadata(project: Project, project_vars: Dict[str, str]) -> None: - """Set project fields using corresponding adopted entries. - - :param project: The project to update. - :param project_vars: The variables updated during lifecycle execution. - - :raises SnapcraftError: If project update failed. - """ - # Update project variables - try: - if project_vars["version"]: - project.version = project_vars["version"] - if project_vars["grade"]: - project.grade = project_vars["grade"] # type: ignore - except pydantic.ValidationError as err: - _raise_formatted_validation_error(err) - raise errors.SnapcraftError(f"error setting variable: {err}") - - # Fields that must not end empty - for field in MANDATORY_ADOPTABLE_FIELDS: - if not getattr(project, field): - raise errors.SnapcraftError( - f"Field {field!r} was not adopted from metadata" - ) - - -def _raise_formatted_validation_error(err: pydantic.ValidationError): - error_list = err.errors() - if len(error_list) != 1: - return - - error = error_list[0] - loc = error.get("loc") - msg = error.get("msg") - - if not (loc and msg) or not isinstance(loc, tuple): - return - - varname = ".".join((x for x in loc if isinstance(x, str))) - raise errors.SnapcraftError(f"error setting {varname}: {msg}") - - def _clean_provider(project: Project, parsed_args: "argparse.Namespace") -> None: """Clean the provider environment. diff --git a/snapcraft/parts/parts.py b/snapcraft/parts/parts.py index c3fd576f15..2dffd08d83 100644 --- a/snapcraft/parts/parts.py +++ b/snapcraft/parts/parts.py @@ -21,10 +21,11 @@ import craft_parts from craft_cli import emit -from craft_parts import ActionType, Step +from craft_parts import ActionType, Part, Step from xdg import BaseDirectory # type: ignore from snapcraft import errors, repo +from snapcraft.meta import ExtractedMetadata, extract_metadata _LIFECYCLE_STEPS = { "pull": Step.PULL, @@ -55,12 +56,15 @@ def __init__( package_repositories: List[Dict[str, Any]], part_names: Optional[List[str]], adopt_info: Optional[str], + parse_info: Dict[str, List[str]], project_name: str, project_vars: Dict[str, str], ): self._assets_dir = assets_dir self._package_repositories = package_repositories self._part_names = part_names + self._adopt_info = adopt_info + self._parse_info = parse_info emit.progress("Initializing parts lifecycle") @@ -166,6 +170,33 @@ def clean(self, *, part_names: Optional[List[str]] = None) -> None: emit.message(message, intermediate=True) self._lcm.clean(part_names=part_names) + def extract_metadata(self) -> List[ExtractedMetadata]: + """Obtain metadata information.""" + if self._adopt_info is None or self._adopt_info not in self._parse_info: + return [] + + part = Part(self._adopt_info, {}) + locations = ( + part.part_src_dir, + part.part_build_dir, + part.part_install_dir, + ) + metadata_list: List[ExtractedMetadata] = [] + + for metadata_file in self._parse_info[self._adopt_info]: + emit.trace(f"extract metadata: parse info from {metadata_file}") + + for location in locations: + if pathlib.Path(location, metadata_file).is_file(): + metadata = extract_metadata(metadata_file, workdir=str(location)) + if metadata: + metadata_list.append(metadata) + break + + emit.message(f"No metadata extracted from {metadata_file}", intermediate=True) + + return metadata_list + def _action_message(action: craft_parts.Action) -> str: msg = { diff --git a/snapcraft/parts/update_metadata.py b/snapcraft/parts/update_metadata.py new file mode 100644 index 0000000000..4a0660ca07 --- /dev/null +++ b/snapcraft/parts/update_metadata.py @@ -0,0 +1,167 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright 2022 Canonical Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +"""External metadata helpers.""" + +from pathlib import Path +from typing import Dict, Final, List + +import pydantic +from craft_cli import emit + +from snapcraft import errors +from snapcraft.meta import ExtractedMetadata +from snapcraft.projects import MANDATORY_ADOPTABLE_FIELDS, Project + +_VALID_ICON_EXTENSIONS: Final[List[str]] = ["png", "svg"] + + +def update_project_metadata( + project: Project, + *, + project_vars: Dict[str, str], + metadata_list: List[ExtractedMetadata], + assets_dir: Path, + prime_dir: Path, +) -> None: + """Set project fields using corresponding adopted entries. + + Fields are validated on assignment by pydantic. + + :param project: The project to update. + :param project_vars: The variables updated during lifecycle execution. + :param metadata_list: List containing parsed information from metadata files. + + :raises SnapcraftError: If project update failed. + """ + _update_project_variables(project, project_vars) + + for metadata in metadata_list: + # Data specified in the project yaml has precedence over extracted data + if metadata.title and not project.title: + project.title = metadata.title + + if metadata.summary and not project.summary: + project.summary = metadata.summary + + if metadata.description and not project.description: + project.description = metadata.description + + if metadata.version and not project.version: + project.version = metadata.version + + if metadata.grade and not project.grade: + project.grade = metadata.grade # type: ignore + + if not project.icon: + _update_project_icon( + project, metadata=metadata, assets_dir=assets_dir, prime_dir=prime_dir + ) + + _update_project_app_desktop_file( + project, metadata=metadata, assets_dir=assets_dir, prime_dir=prime_dir + ) + + # Fields that must not end empty + for field in MANDATORY_ADOPTABLE_FIELDS: + if not getattr(project, field): + raise errors.SnapcraftError( + f"Field {field!r} was not adopted from metadata" + ) + + +def _update_project_variables(project: Project, project_vars: Dict[str, str]): + """Update project fields with values set during lifecycle processing.""" + try: + if project_vars["version"]: + project.version = project_vars["version"] + if project_vars["grade"]: + project.grade = project_vars["grade"] # type: ignore + except pydantic.ValidationError as err: + _raise_formatted_validation_error(err) + raise errors.SnapcraftError(f"error setting variable: {err}") + + +def _update_project_icon( + project: Project, *, metadata: ExtractedMetadata, assets_dir: Path, prime_dir: Path +) -> None: + """Look for icons files and update project. + + Existing icon in snap/gui/icon.{png,svg} has precedence over extracted data + """ + icon_files = (f"{assets_dir}/gui/icon.{ext}" for ext in _VALID_ICON_EXTENSIONS) + + for icon_file in icon_files: + if Path(icon_file).is_file(): + break + else: + if metadata.icon and Path(prime_dir, metadata.icon).is_file(): + project.icon = metadata.icon + + +def _update_project_app_desktop_file( + project: Project, *, metadata: ExtractedMetadata, assets_dir: Path, prime_dir: Path +) -> None: + """Look for desktop files and update project. + + Existing desktop file snap/gui/.desktop has precedence over extracted data + """ + if metadata.common_id and project.apps: + app_name = None + for name, data in project.apps.items(): + if data.common_id == metadata.common_id: + app_name = name + break + + if not app_name: + emit.trace(f"no app declares id {metadata.common_id!r}") + return + + if project.apps[app_name].desktop: + emit.trace("app {app_name!r} already declares a desktop file") + return + + emit.trace( + f"look for desktop file with id {metadata.common_id!r} in app {app_name!r}" + ) + + desktop_file = f"{assets_dir}/gui/{app_name}.desktop" + if Path(desktop_file).is_file(): + emit.trace(f"use already existing desktop file {desktop_file!r}") + return + + if metadata.desktop_file_paths: + for filename in metadata.desktop_file_paths: + if Path(prime_dir, filename).is_file(): + project.apps[app_name].desktop = filename + emit.trace(f"use desktop file {filename!r}") + break + + +def _raise_formatted_validation_error(err: pydantic.ValidationError): + error_list = err.errors() + if len(error_list) != 1: + return + + error = error_list[0] + loc = error.get("loc") + msg = error.get("msg") + + if not (loc and msg) or not isinstance(loc, tuple): + return + + varname = ".".join((x for x in loc if isinstance(x, str))) + raise errors.SnapcraftError(f"error setting {varname}: {msg}") diff --git a/snapcraft/projects.py b/snapcraft/projects.py index 655b59fb2e..30fa0192a4 100644 --- a/snapcraft/projects.py +++ b/snapcraft/projects.py @@ -399,7 +399,7 @@ def unmarshal(cls, data: Dict[str, Any]) -> "Project": :raise TypeError: If data is not a dictionary. """ if not isinstance(data, dict): - raise TypeError("part data is not a dictionary") + raise TypeError("Project data is not a dictionary") try: project = Project(**data) @@ -426,6 +426,7 @@ class _GrammarAwarePart(_GrammarAwareModel): stage_packages: Optional[GrammarStrList] build_snaps: Optional[GrammarStrList] stage_snaps: Optional[GrammarStrList] + parse_info: Optional[List[str]] class GrammarAwareProject(_GrammarAwareModel): @@ -436,7 +437,10 @@ class GrammarAwareProject(_GrammarAwareModel): @classmethod def validate_grammar(cls, data: Dict[str, Any]) -> None: """Ensure grammar-enabled entries are syntactically valid.""" - cls(**data) + try: + cls(**data) + except pydantic.ValidationError as err: + raise ProjectValidationError(_format_pydantic_errors(err.errors())) from err def _format_pydantic_errors(errors, *, file_name: str = "snapcraft.yaml"): diff --git a/snapcraft/repo/projects.py b/snapcraft/repo/projects.py index e1fd4b216b..8ee59b9d65 100644 --- a/snapcraft/repo/projects.py +++ b/snapcraft/repo/projects.py @@ -16,8 +16,7 @@ """Project model definitions and helpers.""" -from typing import Literal # type: ignore -from typing import TYPE_CHECKING, Any, Dict, List, Optional +from typing import TYPE_CHECKING, Any, Dict, List, Literal, Optional import pydantic from pydantic import constr diff --git a/tests/unit/meta/test_appstream.py b/tests/unit/meta/test_appstream.py index 688a80ec20..2acc27df9d 100644 --- a/tests/unit/meta/test_appstream.py +++ b/tests/unit/meta/test_appstream.py @@ -653,9 +653,19 @@ def test_appstream_parse_error(self): appstream.extract(file_name, workdir=".") assert str(raised.value) == ( - "Error extracting metadata from './snapcraft_legacy.appdata.xml'" + "Error extracting metadata from './snapcraft_legacy.appdata.xml': " + "Opening and ending tag mismatch: provides line 11 and component, " + "line 13, column 13 (snapcraft_legacy.appdata.xml, line 13)" ) + def test_appstream_parse_os_error(self): + file_name = "snapcraft_legacy.appdata.xml" + assert not Path(file_name).is_file() + + error = "Error reading file './snapcraft_legacy.appdata.xml': failed to load" + with pytest.raises(errors.SnapcraftError, match=error): + appstream.extract(file_name, workdir=".") + @pytest.mark.usefixtures("new_dir") class TestAppstreamUnhandledFile: diff --git a/tests/unit/parts/test_lifecycle.py b/tests/unit/parts/test_lifecycle.py index 684dc2ebcb..040fd2c0e6 100644 --- a/tests/unit/parts/test_lifecycle.py +++ b/tests/unit/parts/test_lifecycle.py @@ -24,6 +24,7 @@ from snapcraft import errors from snapcraft.parts import lifecycle as parts_lifecycle +from snapcraft.parts.update_metadata import update_project_metadata from snapcraft.projects import Project _SNAPCRAFT_YAML_FILENAMES = [ @@ -137,6 +138,7 @@ def test_snapcraft_yaml_load(new_dir, snapcraft_yaml, filename, mocker): call( "pull", project=project, + parse_info={}, assets_dir=assets_dir, parsed_args=argparse.Namespace( parts=["part1"], destructive_mode=True, use_lxd=False, provider=None @@ -145,7 +147,9 @@ def test_snapcraft_yaml_load(new_dir, snapcraft_yaml, filename, mocker): ] -@pytest.mark.parametrize("cmd", ["pull", "build", "stage", "prime", "pack", "snap", "clean"]) +@pytest.mark.parametrize( + "cmd", ["pull", "build", "stage", "prime", "pack", "snap", "clean"] +) def test_lifecycle_run_provider(cmd, snapcraft_yaml, new_dir, mocker): """Option --provider is not supported in core22.""" snapcraft_yaml(base="core22") @@ -205,6 +209,7 @@ def test_lifecycle_run_command_step( parts_lifecycle._run_command( cmd, project=project, + parse_info={}, assets_dir=Path(), parsed_args=argparse.Namespace(destructive_mode=True, use_lxd=False, parts=[]), ) @@ -223,6 +228,7 @@ def test_lifecycle_run_command_pack(cmd, snapcraft_yaml, project_vars, new_dir, parts_lifecycle._run_command( cmd, project=project, + parse_info={}, assets_dir=Path(), parsed_args=argparse.Namespace( directory=None, @@ -240,7 +246,9 @@ def test_lifecycle_run_command_pack(cmd, snapcraft_yaml, project_vars, new_dir, @pytest.mark.parametrize("cmd", ["pack", "snap"]) -def test_lifecycle_pack_destructive_mode(cmd, snapcraft_yaml, project_vars, new_dir, mocker): +def test_lifecycle_pack_destructive_mode( + cmd, snapcraft_yaml, project_vars, new_dir, mocker +): project = Project.unmarshal(snapcraft_yaml(base="core22")) run_in_provider_mock = mocker.patch("snapcraft.parts.lifecycle._run_in_provider") run_mock = mocker.patch("snapcraft.parts.PartsLifecycle.run") @@ -255,6 +263,7 @@ def test_lifecycle_pack_destructive_mode(cmd, snapcraft_yaml, project_vars, new_ parts_lifecycle._run_command( cmd, project=project, + parse_info={}, assets_dir=Path(), parsed_args=argparse.Namespace( directory=None, @@ -288,6 +297,7 @@ def test_lifecycle_pack_managed(cmd, snapcraft_yaml, project_vars, new_dir, mock parts_lifecycle._run_command( cmd, project=project, + parse_info={}, assets_dir=Path(), parsed_args=argparse.Namespace( directory=None, @@ -315,6 +325,7 @@ def test_lifecycle_pack_not_managed(cmd, snapcraft_yaml, new_dir, mocker): parts_lifecycle._run_command( cmd, project=project, + parse_info={}, assets_dir=Path(), parsed_args=argparse.Namespace( directory=None, @@ -363,6 +374,7 @@ def test_lifecycle_pack_metadata_error(cmd, snapcraft_yaml, new_dir, mocker): cmd, project=project, assets_dir=Path(), + parse_info={}, parsed_args=argparse.Namespace( directory=None, output=None, @@ -388,8 +400,12 @@ def test_lifecycle_metadata_empty(field, snapcraft_yaml, new_dir): project = Project.unmarshal(yaml_data) with pytest.raises(errors.SnapcraftError) as raised: - parts_lifecycle._update_project_metadata( - project, project_vars={"version": "", "grade": ""} + update_project_metadata( + project, + project_vars={"version": "", "grade": ""}, + metadata_list=[], + assets_dir=new_dir, + prime_dir=new_dir, ) assert str(raised.value) == f"Field {field!r} was not adopted from metadata" @@ -406,6 +422,7 @@ def test_lifecycle_run_command_clean(snapcraft_yaml, project_vars, new_dir, mock parts_lifecycle._run_command( "clean", project=project, + parse_info={}, assets_dir=Path(), parsed_args=argparse.Namespace( directory=None, @@ -429,6 +446,7 @@ def test_lifecycle_clean_destructive_mode( parts_lifecycle._run_command( "clean", project=project, + parse_info={}, assets_dir=Path(), parsed_args=argparse.Namespace( directory=None, @@ -450,6 +468,7 @@ def test_lifecycle_clean_part_names(snapcraft_yaml, project_vars, new_dir, mocke parts_lifecycle._run_command( "clean", project=project, + parse_info={}, assets_dir=Path(), parsed_args=argparse.Namespace( directory=None, @@ -485,6 +504,7 @@ def test_lifecycle_clean_part_names_destructive_mode( parts_lifecycle._run_command( "clean", project=project, + parse_info={}, assets_dir=Path(), parsed_args=argparse.Namespace( directory=None, @@ -511,6 +531,7 @@ def test_lifecycle_clean_managed(snapcraft_yaml, project_vars, new_dir, mocker): parts_lifecycle._run_command( "clean", project=project, + parse_info={}, assets_dir=Path(), parsed_args=argparse.Namespace( directory=None, @@ -523,3 +544,13 @@ def test_lifecycle_clean_managed(snapcraft_yaml, project_vars, new_dir, mocker): assert run_in_provider_mock.mock_calls == [] assert clean_mock.mock_calls == [call(part_names=["part1"])] + + +def test_extract_parse_info(): + yaml_data = { + "name": "foo", + "parts": {"p1": {"plugin": "nil", "parse-info": "foo/metadata.xml"}, "p2": {}}, + } + parse_info = parts_lifecycle._extract_parse_info(yaml_data) + assert yaml_data == {"name": "foo", "parts": {"p1": {"plugin": "nil"}, "p2": {}}} + assert parse_info == {"p1": "foo/metadata.xml"} diff --git a/tests/unit/parts/test_parts.py b/tests/unit/parts/test_parts.py index 182189b862..3391166122 100644 --- a/tests/unit/parts/test_parts.py +++ b/tests/unit/parts/test_parts.py @@ -39,6 +39,7 @@ def test_parts_lifecycle_run(parts_data, step_name, new_dir, emitter): package_repositories=[], adopt_info=None, project_name="test-project", + parse_info={}, project_vars={"version": "1", "grade": "stable"}, ) lifecycle.run(step_name) @@ -55,6 +56,7 @@ def test_parts_lifecycle_run_bad_step(parts_data, new_dir): part_names=[], package_repositories=[], adopt_info=None, + parse_info={}, project_name="test-project", project_vars={"version": "1", "grade": "stable"}, ) @@ -72,6 +74,7 @@ def test_parts_lifecycle_run_internal_error(parts_data, new_dir, mocker): package_repositories=[], adopt_info=None, project_name="test-project", + parse_info={}, project_vars={"version": "1", "grade": "stable"}, ) mocker.patch("craft_parts.LifecycleManager.plan", side_effect=RuntimeError("crash")) @@ -89,6 +92,7 @@ def test_parts_lifecycle_run_parts_error(new_dir): package_repositories=[], adopt_info=None, project_name="test-project", + parse_info={}, project_vars={"version": "1", "grade": "stable"}, ) with pytest.raises(errors.PartsLifecycleError) as raised: @@ -107,6 +111,7 @@ def test_parts_lifecycle_clean(parts_data, new_dir, emitter): package_repositories=[], adopt_info=None, project_name="test-project", + parse_info={}, project_vars={"version": "1", "grade": "stable"}, ) lifecycle.clean(part_names=None) @@ -122,6 +127,7 @@ def test_parts_lifecycle_clean_parts(parts_data, new_dir, emitter): package_repositories=[], adopt_info=None, project_name="test-project", + parse_info={}, project_vars={"version": "1", "grade": "stable"}, ) lifecycle.clean(part_names=["p1"]) diff --git a/tests/unit/parts/test_update_metadata.py b/tests/unit/parts/test_update_metadata.py new file mode 100644 index 0000000000..51977bcd38 --- /dev/null +++ b/tests/unit/parts/test_update_metadata.py @@ -0,0 +1,560 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright 2022 Canonical Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import textwrap +from pathlib import Path +from typing import Any, Dict + +import pytest + +from snapcraft.meta import ExtractedMetadata +from snapcraft.parts.update_metadata import update_project_metadata +from snapcraft.projects import App, Project + + +@pytest.fixture +def appstream_file(new_dir): + content = textwrap.dedent( + """ + + + io.snapcraft.snapcraft + CC0-1.0 + GPL-3.0 + snapcraft + snapcraft + Create snaps + Crea snaps + +

      Command Line Utility to create snaps.

      +

      Aplicativo de línea de comandos para crear snaps.

      +

      Features:

      +

      Funciones:

      +
        +
      1. Build snaps.
      2. +
      3. Construye snaps.
      4. +
      5. Publish snaps to the store.
      6. +
      7. Publica snaps en la tienda.
      8. +
      +
      + + snapcraft + +
      + """ + ) + yaml_path = Path("appstream.appdata.xml") + yaml_path.parent.mkdir(parents=True, exist_ok=True) + yaml_path.write_text(content) + + +@pytest.fixture +def project_yaml_data(): + def yaml_data(extra_args: Dict[str, Any]): + return { + "name": "name", + "summary": "summary", + "description": "description", + "base": "core22", + "grade": "stable", + "confinement": "strict", + "parts": {}, + **extra_args, + } + + yield yaml_data + + +def _project_app(data: Dict[str, Any]) -> App: + return App(**data) + + +def test_update_project_metadata(project_yaml_data, appstream_file, new_dir): + project = Project.unmarshal(project_yaml_data({"adopt-info": "part"})) + metadata = ExtractedMetadata( + common_id="common.id", + title="title", + summary="summary", + description="description", + version="1.2.3", + icon="assets/icon.png", + desktop_file_paths=["assets/file.desktop"], + ) + assets_dir = Path("assets") + prime_dir = Path("prime") + + # set up project apps + project.apps = { + "app1": _project_app({"command": "bin/app1"}), + "app2": _project_app({"command": "bin/app2", "common_id": "other.id"}), + "app3": _project_app({"command": "bin/app3", "common_id": "common.id"}), + } + + prime_dir.mkdir() + (prime_dir / "assets").mkdir() + (prime_dir / "assets/icon.png").touch() + (prime_dir / "assets/file.desktop").touch() + + prj_vars = {"version": "0.1", "grade": "stable"} + update_project_metadata( + project, + project_vars=prj_vars, + metadata_list=[metadata], + assets_dir=assets_dir, + prime_dir=prime_dir, + ) + + assert project.title == "title" + assert project.summary == "summary" # already set in project + assert project.description == "description" # already set in project + assert project.version == "0.1" # already set in project + assert project.icon == "assets/icon.png" + assert project.apps["app3"].desktop == "assets/file.desktop" + + +@pytest.mark.parametrize( + "project_entries,expected", + [ + ( + { + "version": "1.2.3", + "summary": "project summary", + "description": "project description", + "title": "project title", + "grade": "stable", + }, + { + "version": "1.2.3", + "summary": "project summary", + "description": "project description", + "title": "project title", + "grade": "stable", + }, + ), + ( + {}, + { + "version": "4.5.6", + "summary": "metadata summary", + "description": "metadata description", + "title": "metadata title", + "grade": "devel", + }, + ), + ], +) +def test_update_project_metadata_fields( + appstream_file, project_entries, expected, new_dir +): + yaml_data = { + "name": "my-project", + "base": "core22", + "confinement": "strict", + "adopt-info": "part", + "parts": {}, + **project_entries, + } + project = Project(**yaml_data) + metadata = ExtractedMetadata( + version="4.5.6", + summary="metadata summary", + description="metadata description", + title="metadata title", + grade="devel", + ) + prj_vars = {"version": "", "grade": ""} + + update_project_metadata( + project, + project_vars=prj_vars, + metadata_list=[metadata], + assets_dir=new_dir, + prime_dir=new_dir, + ) + + assert project.version == expected["version"] + assert project.summary == expected["summary"] + assert project.description == expected["description"] + assert project.title == expected["title"] + assert project.grade == expected["grade"] + + +@pytest.mark.parametrize( + "project_entries,expected", + [ + ( + { + "version": "1.2.3", + "summary": "project summary", + "description": "project description", + "title": "project title", + "grade": "stable", + }, + { + "version": "1.2.3", + "summary": "project summary", + "description": "project description", + "title": "project title", + "grade": "stable", + }, + ), + ( + {}, + { + "version": "4.5.6", + "summary": "metadata summary", + "description": "metadata description", + "title": "metadata title", + "grade": "devel", + }, + ), + ], +) +def test_update_project_metadata_multiple( + appstream_file, project_entries, expected, new_dir +): + yaml_data = { + "name": "my-project", + "base": "core22", + "confinement": "strict", + "adopt-info": "part", + "parts": {}, + **project_entries, + } + project = Project(**yaml_data) + metadata1 = ExtractedMetadata(version="4.5.6") + metadata2 = ExtractedMetadata( + summary="metadata summary", description="metadata description" + ) + metadata3 = ExtractedMetadata( + version="7.8.9", title="metadata title", grade="devel" + ) + metadata4 = ExtractedMetadata( + summary="extra summary", description="extra description" + ) + prj_vars = {"version": "", "grade": ""} + + update_project_metadata( + project, + project_vars=prj_vars, + metadata_list=[metadata1, metadata2, metadata3, metadata4], + assets_dir=new_dir, + prime_dir=new_dir, + ) + + assert project.version == expected["version"] + assert project.summary == expected["summary"] + assert project.description == expected["description"] + assert project.title == expected["title"] + assert project.grade == expected["grade"] + + +@pytest.mark.parametrize( + "project_entries,icon_exists,asset_exists,expected_icon", + [ + ({"icon": "icon.png"}, True, True, "icon.png"), # use project icon if defined + ( # use project icon if defined even if already in assets + {"icon": "icon.png"}, + True, + False, + "icon.png", + ), + ( # use metadata icon if not defined in project + {}, + True, + False, + "metadata_icon.png", + ), + ({}, False, False, None), # only use metadata icon if file exists + ({}, True, True, None), # don't use metadata if asset icon already exists + ], +) +def test_update_project_metadata_icon( + project_yaml_data, + project_entries, + icon_exists, + asset_exists, + expected_icon, + new_dir, +): + yaml_data = project_yaml_data( + {"version": "1.0", "adopt-info": "part", "parts": {}, **project_entries} + ) + project = Project(**yaml_data) + metadata = ExtractedMetadata(icon="metadata_icon.png") + + # create icon file + if icon_exists: + Path("metadata_icon.png").touch() + + # create icon file in assets dir + if asset_exists: + Path("assets/gui").mkdir(parents=True) + Path("assets/gui/icon.svg").touch() + + prj_vars = {"version": "", "grade": "stable"} + + update_project_metadata( + project, + project_vars=prj_vars, + metadata_list=[metadata], + assets_dir=new_dir / "assets", + prime_dir=new_dir, + ) + + assert project.icon == expected_icon + + +@pytest.mark.parametrize( + "project_entries,desktop_exists,asset_exists,expected_desktop", + [ + ( # use project desktop file if defined + { + "apps": { + "foo": { + "command": "foo", + "common-id": "test.id", + "desktop": "project/foo.desktop", + }, + }, + }, + True, + False, + "project/foo.desktop", + ), + ( # use project desktop if no common-id defined + { + "apps": { + "foo": { + "command": "foo", + "desktop": "project/foo.desktop", + }, + }, + }, + True, + False, + "project/foo.desktop", + ), + ( # don't read from metadata if common-id not defined + { + "apps": { + "foo": { + "command": "foo", + }, + }, + }, + True, + False, + None, + ), + ( # use metadata if no project definition and metadata icon exists + { + "apps": { + "foo": { + "command": "foo", + "common-id": "test.id", + }, + }, + }, + True, + False, + "metadata/foo.desktop", + ), + ( # only use metadata desktop file if it exists + { + "apps": { + "foo": { + "command": "foo", + "common-id": "test.id", + }, + }, + }, + False, + False, + None, + ), + ( # existing file has precedence over metadata + { + "apps": { + "foo": { + "command": "foo", + "common-id": "test.id", + }, + }, + }, + True, + True, + None, + ), + ], +) +def test_update_project_metadata_desktop( + project_yaml_data, + project_entries, + desktop_exists, + asset_exists, + expected_desktop, + new_dir, +): + yaml_data = project_yaml_data( + {"version": "1.0", "adopt-info": "part", "parts": {}, **project_entries} + ) + project = Project(**yaml_data) + metadata = ExtractedMetadata( + common_id="test.id", desktop_file_paths=["metadata/foo.desktop"] + ) + + # create desktop file + if desktop_exists: + Path("metadata").mkdir() + Path("metadata/foo.desktop").touch() + + # create desktop file in assets dir + if asset_exists: + Path("assets/gui").mkdir(parents=True) + Path("assets/gui/foo.desktop").touch() + + prj_vars = {"version": "", "grade": "stable"} + + update_project_metadata( + project, + project_vars=prj_vars, + metadata_list=[metadata], + assets_dir=new_dir / "assets", + prime_dir=new_dir, + ) + + assert project.apps is not None + assert project.apps["foo"].desktop == expected_desktop + + +def test_update_project_metadata_desktop_multiple(project_yaml_data, new_dir): + yaml_data = project_yaml_data( + { + "version": "1.0", + "adopt-info": "part", + "parts": {}, + "apps": { + "foo": { + "command": "foo", + "common-id": "test.id", + }, + }, + } + ) + project = Project(**yaml_data) + metadata = ExtractedMetadata( + common_id="test.id", + desktop_file_paths=["metadata/foo.desktop", "metadata/bar.desktop"], + ) + + # create desktop files + Path("metadata").mkdir() + Path("metadata/foo.desktop").touch() + Path("metadata/bar.desktop").touch() + + prj_vars = {"version": "", "grade": "stable"} + + update_project_metadata( + project, + project_vars=prj_vars, + metadata_list=[metadata], + assets_dir=new_dir / "assets", + prime_dir=new_dir, + ) + + assert project.apps is not None + assert project.apps["foo"].desktop == "metadata/foo.desktop" + + +def test_update_project_metadata_multiple_apps(project_yaml_data, new_dir): + yaml_data = project_yaml_data( + { + "version": "1.0", + "adopt-info": "part", + "parts": {}, + "apps": { + "foo": { + "command": "foo", + "common-id": "foo.id", + }, + "bar": { + "command": "bar", + "common-id": "bar.id", + }, + }, + } + ) + project = Project(**yaml_data) + metadata1 = ExtractedMetadata( + common_id="foo.id", + desktop_file_paths=["metadata/foo.desktop"], + ) + metadata2 = ExtractedMetadata( + common_id="bar.id", + desktop_file_paths=["metadata/bar.desktop"], + ) + + # create desktop files + Path("metadata").mkdir() + Path("metadata/foo.desktop").touch() + Path("metadata/bar.desktop").touch() + + prj_vars = {"version": "", "grade": "stable"} + + update_project_metadata( + project, + project_vars=prj_vars, + metadata_list=[metadata1, metadata2], + assets_dir=new_dir / "assets", + prime_dir=new_dir, + ) + + assert project.apps is not None + assert project.apps["foo"].desktop == "metadata/foo.desktop" + assert project.apps["bar"].desktop == "metadata/bar.desktop" + + +def test_update_project_metadata_desktop_no_apps(project_yaml_data, new_dir): + yaml_data = project_yaml_data( + { + "version": "1.0", + "adopt-info": "part", + "parts": {}, + } + ) + project = Project(**yaml_data) + metadata = ExtractedMetadata( + common_id="test.id", + desktop_file_paths=["metadata/foo.desktop", "metadata/bar.desktop"], + ) + + # create desktop file + Path("metadata").mkdir() + Path("metadata/foo.desktop").touch() + Path("metadata/bar.desktop").touch() + + prj_vars = {"version": "", "grade": "stable"} + + update_project_metadata( + project, + project_vars=prj_vars, + metadata_list=[metadata], + assets_dir=new_dir / "assets", + prime_dir=new_dir, + ) + + assert project.apps is None diff --git a/tests/unit/test_projects.py b/tests/unit/test_projects.py index ce2d5d3dc6..0d74e5afd2 100644 --- a/tests/unit/test_projects.py +++ b/tests/unit/test_projects.py @@ -1071,17 +1071,10 @@ def test_grammar_try(self, project_yaml_data): } ) - with pytest.raises(pydantic.ValidationError) as raised: + error = r".*- 'try' was removed from grammar, use 'on ' instead" + with pytest.raises(errors.ProjectValidationError, match=error): GrammarAwareProject.validate_grammar(data) - err = raised.value.errors() - assert len(err) == 1 - assert err[0]["loc"] == ("parts", "p1", "source") - assert err[0]["type"] == "value_error" - assert ( - err[0]["msg"] == "'try' was removed from grammar, use 'on ' instead" - ) - def test_grammar_type_error(self, project_yaml_data): data = project_yaml_data( parts={ @@ -1094,15 +1087,10 @@ def test_grammar_type_error(self, project_yaml_data): } ) - with pytest.raises(pydantic.ValidationError) as raised: + error = r".*- value must be a string: \[25\]" + with pytest.raises(errors.ProjectValidationError, match=error): GrammarAwareProject.validate_grammar(data) - err = raised.value.errors() - assert len(err) == 1 - assert err[0]["loc"] == ("parts", "p1", "source") - assert err[0]["type"] == "type_error" - assert err[0]["msg"] == "value must be a string: [25]" - def test_grammar_syntax_error(self, project_yaml_data): data = project_yaml_data( parts={ @@ -1115,11 +1103,6 @@ def test_grammar_syntax_error(self, project_yaml_data): } ) - with pytest.raises(pydantic.ValidationError) as raised: + error = r".*- syntax error in 'on' selector" + with pytest.raises(errors.ProjectValidationError, match=error): GrammarAwareProject.validate_grammar(data) - - err = raised.value.errors() - assert len(err) == 1 - assert err[0]["loc"] == ("parts", "p1", "source") - assert err[0]["type"] == "value_error" - assert err[0]["msg"] == "syntax error in 'on' selector" From e2a57b321563e4dcde3b3231d84d87f3da38e7ce Mon Sep 17 00:00:00 2001 From: Claudio Matsuoka Date: Fri, 22 Apr 2022 21:48:16 -0300 Subject: [PATCH 109/167] meta: add appstream adoption spread test Signed-off-by: Claudio Matsuoka --- .../expected_appstream-desktop.desktop | 6 +++ .../appstream-desktop/expected_snap.yaml | 29 +++++++++++ .../snaps/appstream-desktop/appstream-desktop | 3 ++ .../io.snapcraft.appstream.desktop | 5 ++ .../io.snapcraft.appstream.metainfo.xml | 48 +++++++++++++++++++ .../appstream-desktop/snap/snapcraft.yaml | 24 ++++++++++ .../snaps/appstream-desktop/snapcraft.svg | 14 ++++++ .../core22/appstream-desktop/task.yaml | 36 ++++++++++++++ 8 files changed, 165 insertions(+) create mode 100644 tests/spread/general/core22/appstream-desktop/expected_appstream-desktop.desktop create mode 100644 tests/spread/general/core22/appstream-desktop/expected_snap.yaml create mode 100755 tests/spread/general/core22/appstream-desktop/snaps/appstream-desktop/appstream-desktop create mode 100644 tests/spread/general/core22/appstream-desktop/snaps/appstream-desktop/io.snapcraft.appstream.desktop create mode 100644 tests/spread/general/core22/appstream-desktop/snaps/appstream-desktop/io.snapcraft.appstream.metainfo.xml create mode 100644 tests/spread/general/core22/appstream-desktop/snaps/appstream-desktop/snap/snapcraft.yaml create mode 100644 tests/spread/general/core22/appstream-desktop/snaps/appstream-desktop/snapcraft.svg create mode 100644 tests/spread/general/core22/appstream-desktop/task.yaml diff --git a/tests/spread/general/core22/appstream-desktop/expected_appstream-desktop.desktop b/tests/spread/general/core22/appstream-desktop/expected_appstream-desktop.desktop new file mode 100644 index 0000000000..c11175ebd0 --- /dev/null +++ b/tests/spread/general/core22/appstream-desktop/expected_appstream-desktop.desktop @@ -0,0 +1,6 @@ +[Desktop Entry] +Name=appstream-desktop +Exec=appstream-desktop +Type=Application +Icon=${SNAP}/meta/gui/icon.svg + diff --git a/tests/spread/general/core22/appstream-desktop/expected_snap.yaml b/tests/spread/general/core22/appstream-desktop/expected_snap.yaml new file mode 100644 index 0000000000..ac871c5d85 --- /dev/null +++ b/tests/spread/general/core22/appstream-desktop/expected_snap.yaml @@ -0,0 +1,29 @@ +name: appstream-desktop +title: Appstream Desktop +version: 1.0.0 +summary: Appstream Desktop test +description: |- + Some description. + + + Some list: + + - First item + - Second item + + + Test me please. +type: app +architectures: +- amd64 +base: core22 +assumes: +- command-chain +apps: + appstream-desktop: + command: usr/bin/appstream-desktop + common-id: io.snapcraft.appstream + command-chain: + - snap/command-chain/snapcraft-runner +confinement: strict +grade: devel diff --git a/tests/spread/general/core22/appstream-desktop/snaps/appstream-desktop/appstream-desktop b/tests/spread/general/core22/appstream-desktop/snaps/appstream-desktop/appstream-desktop new file mode 100755 index 0000000000..10501a451d --- /dev/null +++ b/tests/spread/general/core22/appstream-desktop/snaps/appstream-desktop/appstream-desktop @@ -0,0 +1,3 @@ +#!/bin/sh + +echo "appstream desktop" diff --git a/tests/spread/general/core22/appstream-desktop/snaps/appstream-desktop/io.snapcraft.appstream.desktop b/tests/spread/general/core22/appstream-desktop/snaps/appstream-desktop/io.snapcraft.appstream.desktop new file mode 100644 index 0000000000..85db82eab9 --- /dev/null +++ b/tests/spread/general/core22/appstream-desktop/snaps/appstream-desktop/io.snapcraft.appstream.desktop @@ -0,0 +1,5 @@ +[Desktop Entry] +Name=appstream-desktop +Exec=appstream +Type=Application +Icon=/usr/share/icons/hicolor/scalable/apps/snapcraft.svg diff --git a/tests/spread/general/core22/appstream-desktop/snaps/appstream-desktop/io.snapcraft.appstream.metainfo.xml b/tests/spread/general/core22/appstream-desktop/snaps/appstream-desktop/io.snapcraft.appstream.metainfo.xml new file mode 100644 index 0000000000..11affa5ea9 --- /dev/null +++ b/tests/spread/general/core22/appstream-desktop/snaps/appstream-desktop/io.snapcraft.appstream.metainfo.xml @@ -0,0 +1,48 @@ + + + + io.snapcraft.appstream + FSFAP + GPL-2.0+ + Appstream Desktop + Appstream Desktop test + + +

      + Some description. +

      +

      Some list:

      +
        +
      • First item
      • +
      • Second item
      • +
      +

      + Test me please. +

      +
      + + /usr/share/icons/hicolor/scalable/apps/snapcraft.svg + io.snapcraft.appstream.desktop + + + + snapcraft + https://admin.insights.ubuntu.com/wp-content/uploads/3124/snapcraft_db_brandmark@4x.png + + + + https://snapcraft.io + snapcraft + + + appstream + + + + + +

      Initial release.

      +
      +
      +
      +
      diff --git a/tests/spread/general/core22/appstream-desktop/snaps/appstream-desktop/snap/snapcraft.yaml b/tests/spread/general/core22/appstream-desktop/snaps/appstream-desktop/snap/snapcraft.yaml new file mode 100644 index 0000000000..18ddffd6c3 --- /dev/null +++ b/tests/spread/general/core22/appstream-desktop/snaps/appstream-desktop/snap/snapcraft.yaml @@ -0,0 +1,24 @@ +name: appstream-desktop +base: core22 + +grade: devel +confinement: strict +adopt-info: appstream-desktop + +apps: + appstream-desktop: + command: usr/bin/appstream-desktop + common-id: io.snapcraft.appstream + desktop: usr/share/applications/io.snapcraft.appstream.desktop + +parts: + appstream-desktop: + plugin: nil + source: . + build-packages: [gcc, libc6-dev] + parse-info: [usr/share/metainfo/io.snapcraft.appstream.metainfo.xml] + override-build: | + install -D -m 0644 io.snapcraft.appstream.desktop $CRAFT_PART_INSTALL/usr/share/applications/io.snapcraft.appstream.desktop + install -D -m 0644 io.snapcraft.appstream.metainfo.xml $CRAFT_PART_INSTALL/usr/share/metainfo/io.snapcraft.appstream.metainfo.xml + install -D -m 0644 snapcraft.svg $CRAFT_PART_INSTALL/usr/share/icons/hicolor/scalable/apps/snapcraft.svg + install -D -m 0755 appstream-desktop $CRAFT_PART_INSTALL/usr/bin/appstream-desktop diff --git a/tests/spread/general/core22/appstream-desktop/snaps/appstream-desktop/snapcraft.svg b/tests/spread/general/core22/appstream-desktop/snaps/appstream-desktop/snapcraft.svg new file mode 100644 index 0000000000..38cdee4083 --- /dev/null +++ b/tests/spread/general/core22/appstream-desktop/snaps/appstream-desktop/snapcraft.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/tests/spread/general/core22/appstream-desktop/task.yaml b/tests/spread/general/core22/appstream-desktop/task.yaml new file mode 100644 index 0000000000..78723ecd7a --- /dev/null +++ b/tests/spread/general/core22/appstream-desktop/task.yaml @@ -0,0 +1,36 @@ +summary: Build a snap that tests appstream settings + +# This test snap uses core18, and is limited to amd64 arch due to +# architectures specified in expected_snap.yaml. +systems: + - ubuntu-20.04-64 + - ubuntu-20.04-amd64 + - ubuntu-20.04 + +environment: + SNAP_DIR: snaps/appstream-desktop + +restore: | + cd "$SNAP_DIR" + snapcraft clean + rm -f ./*.snap + +execute: | + cd "$SNAP_DIR" + snapcraft prime --destructive-mode + + expected_snap_yaml="../../expected_snap.yaml" + + if ! diff -U10 prime/meta/snap.yaml "$expected_snap_yaml"; then + echo "The formatting for snap.yaml is incorrect" + exit 1 + fi + + # Enable after installing assets + # + # expected_desktop="$PWD/../../appstream-desktop/expected_appstream-desktop.desktop" + # + # if ! diff -U10 prime/meta/gui/appstream-desktop.desktop "$expected_desktop"; then + # echo "The formatting for appstream-desktop.desktop is incorrect" + # exit 1 + # fi From 8e2f28e4806a7a79ef99cc3afcffbe476a5f61b1 Mon Sep 17 00:00:00 2001 From: Claudio Matsuoka Date: Sat, 23 Apr 2022 17:09:06 -0300 Subject: [PATCH 110/167] parts: set up snap assets Copy icon and desktop files to the snap payload and edit entries in the desktop file to point to the correct executables and icon files. Co-authored-by: Leo Arias Co-authored-by: Chris Patterson Co-authored-by: Sergio Schvezov Signed-off-by: Claudio Matsuoka --- setup.py | 1 + snapcraft/errors.py | 7 + snapcraft/meta/snap_yaml.py | 18 -- snapcraft/parts/desktop_file.py | 128 +++++++++ snapcraft/parts/lifecycle.py | 9 +- snapcraft/parts/parts.py | 2 +- snapcraft/parts/setup_assets.py | 215 +++++++++++++++ snapcraft/parts/update_metadata.py | 4 +- .../appstream-desktop/snap/snapcraft.yaml | 1 - .../core22/appstream-desktop/task.yaml | 14 +- tests/unit/parts/test_desktop_file.py | 236 +++++++++++++++++ tests/unit/parts/test_setup_assets.py | 249 ++++++++++++++++++ 12 files changed, 853 insertions(+), 31 deletions(-) create mode 100644 snapcraft/parts/desktop_file.py create mode 100644 snapcraft/parts/setup_assets.py create mode 100644 tests/unit/parts/test_desktop_file.py create mode 100644 tests/unit/parts/test_setup_assets.py diff --git a/setup.py b/setup.py index 8981b28e05..1edfb07d7e 100755 --- a/setup.py +++ b/setup.py @@ -84,6 +84,7 @@ def recursive_data_files(directory, install_directory): "pytest-mock", "pytest-subprocess", "types-PyYAML", + "types-requests", "types-setuptools", "types-tabulate", ] diff --git a/snapcraft/errors.py b/snapcraft/errors.py index c549251b6d..0aff817069 100644 --- a/snapcraft/errors.py +++ b/snapcraft/errors.py @@ -49,5 +49,12 @@ def __init__(self, filename: str, message: str) -> None: super().__init__(f"Error extracting metadata from {filename!r}: {message}") +class DesktopFileError(SnapcraftError): + """Failed to create application desktop file.""" + + def __init__(self, filename: str, message: str) -> None: + super().__init__(f"Failed to generate desktop file {filename!r}: {message}") + + class LegacyFallback(Exception): """Fall back to legacy snapcraft implementation.""" diff --git a/snapcraft/meta/snap_yaml.py b/snapcraft/meta/snap_yaml.py index 555c590786..8ecaba03e9 100644 --- a/snapcraft/meta/snap_yaml.py +++ b/snapcraft/meta/snap_yaml.py @@ -16,7 +16,6 @@ """Create snap.yaml metadata file.""" -import textwrap from pathlib import Path from typing import Any, Dict, List, Optional, Union, cast @@ -118,8 +117,6 @@ def write(project: Project, prime_dir: Path, *, arch: str): meta_dir = prime_dir / "meta" meta_dir.mkdir(parents=True, exist_ok=True) - _write_snapcraft_runner(prime_dir) - snap_apps: Dict[str, SnapApp] = {} if project.apps: for name, app in project.apps.items(): @@ -191,21 +188,6 @@ def write(project: Project, prime_dir: Path, *, arch: str): snap_yaml.write_text(yaml_data) -def _write_snapcraft_runner(prime_dir: Path): - content = textwrap.dedent( - """#!/bin/sh - export PATH="$SNAP/usr/sbin:$SNAP/usr/bin:$SNAP/sbin:$SNAP/bin:$PATH" - export LD_LIBRARY_PATH=$SNAP_LIBRARY_PATH:$LD_LIBRARY_PATH - exec "$@" - """ - ) - - runner_path = prime_dir / "snap/command-chain/snapcraft-runner" - runner_path.parent.mkdir(parents=True, exist_ok=True) - runner_path.write_text(content) - runner_path.chmod(0o755) - - def _repr_str(dumper, data): """Multi-line string representer for the YAML dumper.""" if "\n" in data: diff --git a/snapcraft/parts/desktop_file.py b/snapcraft/parts/desktop_file.py new file mode 100644 index 0000000000..7db5bd085a --- /dev/null +++ b/snapcraft/parts/desktop_file.py @@ -0,0 +1,128 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright 2016-2022 Canonical Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +"""Desktop file parser.""" + +import configparser +import os +import shlex +from pathlib import Path +from typing import Optional + +from craft_cli import emit + +from snapcraft import errors + + +class DesktopFile: + """Parse and process a .desktop file. + + :param snap_name: The snap package name. + :param app_name: The name of the app using the desktop file. + :param filename: The desktop file name. + :param prime_dir: The prime directory path. + + :raises DesktopFileError: If the desktop file does not exist. + """ + + def __init__( + self, *, snap_name: str, app_name: str, filename: str, prime_dir: Path + ) -> None: + self._snap_name = snap_name + self._app_name = app_name + self._filename = filename + self._prime_dir = prime_dir + + file_path = prime_dir / filename + if not file_path.is_file(): + raise errors.DesktopFileError( + filename, f"file does not exist (defined in app {app_name!r})" + ) + + self._parser = configparser.ConfigParser(interpolation=None) + # mypy type checking ignored, see https://github.com/python/mypy/issues/506 + self._parser.optionxform = str # type: ignore + self._parser.read(file_path, encoding="utf-8") + + def _parse_and_reformat_section_exec(self, section): + exec_value = self._parser[section]["Exec"] + exec_split = shlex.split(exec_value, posix=False) + + # Ensure command is invoked correctly. + if self._app_name == self._snap_name: + exec_split[0] = self._app_name + else: + exec_split[0] = f"{self._snap_name}.{self._app_name}" + + self._parser[section]["Exec"] = " ".join(exec_split) + + def _parse_and_reformat_section(self, *, section, icon_path: Optional[str] = None): + if "Exec" not in self._parser[section]: + raise errors.DesktopFileError(self._filename, "missing 'Exec' key") + + self._parse_and_reformat_section_exec(section) + + if "Icon" in self._parser[section]: + icon = self._parser[section]["Icon"] + + if icon_path is not None: + icon = icon_path + + # Strip any leading slash. + icon = icon[1:] if icon.startswith("/") else icon + + # Strip any leading ${SNAP}. + icon = icon[8:] if icon.startswith("${SNAP}") else icon + + # With everything stripped, check to see if the icon is there. + # if it is, add "${SNAP}" back and set the icon + if (self._prime_dir / icon).is_file(): + self._parser[section]["Icon"] = os.path.join("${SNAP}", icon) + else: + emit.message( + f"Icon {icon!r} specified in desktop file {self._filename!r} " + f"not found in prime directory." + ) + + def _parse_and_reformat(self, *, icon_path: Optional[str] = None) -> None: + if "Desktop Entry" not in self._parser.sections(): + raise errors.DesktopFileError( + self._filename, "missing 'Desktop Entry' section" + ) + + for section in self._parser.sections(): + self._parse_and_reformat_section(section=section, icon_path=icon_path) + + def write(self, *, gui_dir: Path, icon_path: Optional[str] = None) -> None: + """Write the desktop file. + + :param gui_dir: The desktop file destination directory. + :param icon_path: The icon corresponding to this desktop file. + """ + self._parse_and_reformat(icon_path=icon_path) + + gui_dir.mkdir(parents=True, exist_ok=True) + + # Rename the desktop file to match the app name. This will help + # unity8 associate them (https://launchpad.net/bugs/1659330). + target = gui_dir / f"{self._app_name}.desktop" + + if target.exists(): + # Unlikely. A desktop file in meta/gui/ already existed for + # this app. Let's pretend it wasn't there and overwrite it. + target.unlink() + with target.open("w", encoding="utf-8") as target_file: + self._parser.write(target_file, space_around_delimiters=False) diff --git a/snapcraft/parts/lifecycle.py b/snapcraft/parts/lifecycle.py index 482d0853c6..16fccd9e31 100644 --- a/snapcraft/parts/lifecycle.py +++ b/snapcraft/parts/lifecycle.py @@ -31,6 +31,7 @@ from snapcraft.providers import capture_logs_from_instance from . import grammar, yaml_utils +from .setup_assets import setup_assets from .update_metadata import update_project_metadata if TYPE_CHECKING: @@ -202,6 +203,7 @@ def _run_command( # Extract metadata and generate snap.yaml project_vars = lifecycle.project_vars if step_name == "prime" and not part_names: + emit.progress("Extracting and updating metadata...") metadata_list = lifecycle.extract_metadata() update_project_metadata( project, @@ -211,7 +213,12 @@ def _run_command( prime_dir=lifecycle.prime_dir, ) - # TODO: copy meta/gui assets + emit.progress("Copying snap assets...") + setup_assets( + project, + assets_dir=assets_dir, + prime_dir=lifecycle.prime_dir, + ) emit.progress("Generating snap metadata...") snap_yaml.write( diff --git a/snapcraft/parts/parts.py b/snapcraft/parts/parts.py index 2dffd08d83..ba779b2b68 100644 --- a/snapcraft/parts/parts.py +++ b/snapcraft/parts/parts.py @@ -187,7 +187,7 @@ def extract_metadata(self) -> List[ExtractedMetadata]: emit.trace(f"extract metadata: parse info from {metadata_file}") for location in locations: - if pathlib.Path(location, metadata_file).is_file(): + if pathlib.Path(location, metadata_file.lstrip("/")).is_file(): metadata = extract_metadata(metadata_file, workdir=str(location)) if metadata: metadata_list.append(metadata) diff --git a/snapcraft/parts/setup_assets.py b/snapcraft/parts/setup_assets.py new file mode 100644 index 0000000000..15d22c5fbb --- /dev/null +++ b/snapcraft/parts/setup_assets.py @@ -0,0 +1,215 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright 2022 Canonical Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +"""Copy assets to their final locations.""" + +import itertools +import os +import shutil +import stat +import textwrap +import urllib.parse +from pathlib import Path +from typing import List, Optional + +import requests +from craft_cli import emit + +from snapcraft import errors +from snapcraft.projects import Project + +from .desktop_file import DesktopFile + + +def setup_assets(project: Project, *, assets_dir: Path, prime_dir: Path) -> None: + """Copy gui assets to the appropriate location in the snap filesystem. + + :param project: The snap project file. + :param assets_dir: The directory containing snap project assets. + :param prime_dir: The directory containing the content to be snapped. + """ + meta_dir = prime_dir / "meta" + gui_dir = meta_dir / "gui" + gui_dir.mkdir(parents=True, exist_ok=True) + + _write_snap_directory(assets_dir=assets_dir, prime_dir=prime_dir, meta_dir=meta_dir) + _write_snapcraft_runner(prime_dir=prime_dir) + # TODO: write snapcraft + + if not project.apps: + return + + icon_path = _finalize_icon( + project.icon, assets_dir=assets_dir, gui_dir=gui_dir, prime_dir=prime_dir + ) + relative_icon_path: Optional[str] = None + + if icon_path is not None: + if prime_dir in icon_path.parents: + icon_path = icon_path.relative_to(prime_dir) + relative_icon_path = str(icon_path) + + for app_name, app in project.apps.items(): + if not app.desktop: + continue + + desktop_file = DesktopFile( + snap_name=project.name, + app_name=app_name, + filename=app.desktop, + prime_dir=prime_dir, + ) + desktop_file.write(gui_dir=gui_dir, icon_path=relative_icon_path) + + _validate_command_chain( + app.command_chain, app_name=app_name, prime_dir=prime_dir + ) + + # TODO: copy gadget and kernel assets + + +def _finalize_icon( + icon: Optional[str], *, assets_dir: Path, gui_dir: Path, prime_dir: Path +) -> Optional[Path]: + """Ensure sure icon is properly configured and installed. + + Fetch from a remote URL, if required, and place in the meta/gui + directory. + """ + # Nothing to do if no icon is configured, search for existing icon. + if icon is None: + return _find_icon_file(assets_dir) + + # Extracted appstream icon paths will either: + # (1) point to a file relative to prime + # (2) point to a remote http(s) url + # + # The 'icon' specified in the snapcraft.yaml has the same + # constraint as (2) and would have already been validated + # as existing by the schema. So we can treat it the same + # at this point, regardless of the source of the icon. + parsed_url = urllib.parse.urlparse(icon) + parsed_path = Path(parsed_url.path) + icon_ext = parsed_path.suffix[1:] + target_icon_path = Path(gui_dir, f"icon.{icon_ext}") + + target_icon_path.parent.mkdir(parents=True, exist_ok=True) + if parsed_url.scheme in ["http", "https"]: + # Remote - fetch URL and write to target. + emit.progress(f"Fetching icon from {icon!r}") + icon_data = requests.get(icon).content + target_icon_path.write_bytes(icon_data) + elif parsed_url.scheme == "": + source_path = Path( + prime_dir, + parsed_path.relative_to("/") if parsed_path.is_absolute() else parsed_path, + ) + if source_path.exists(): + # Local with path relative to prime. + shutil.copy(source_path, target_icon_path) + elif parsed_path.exists(): + # Local with path relative to project. + shutil.copy(parsed_path, target_icon_path) + else: + # No icon found, fall back to searching for existing icon. + return _find_icon_file(assets_dir) + else: + raise RuntimeError(f"Unexpected icon path: {parsed_url!r}") + + return target_icon_path + + +def _find_icon_file(assets_dir: Path) -> Optional[Path]: + for icon_path in (assets_dir / "gui/icon.png", assets_dir / "gui/icon.svg"): + if icon_path.is_file(): + return icon_path + return None + + +def _validate_command_chain( + command_chain: List[str], *, app_name: str, prime_dir: Path +) -> None: + """Verify if each item in the command chain is executble.""" + for item in command_chain: + executable_path = prime_dir / item + + # command-chain entries must always be relative to the root of + # the snap, i.e. PATH is not used. + if not _is_executable(executable_path): + raise errors.SnapcraftError( + f"Failed to generate snap metadata: The command-chain item {item!r} " + f"defined in the app {app_name!r} does not exist or is not executable.", + resolution=f"Ensure that {item!r} is relative to the prime directory.", + ) + + +def _is_executable(path: Path) -> bool: + """Verify if the given path corresponds to an executable file.""" + if not path.is_file(): + return False + + mode = path.stat().st_mode + return bool(mode & stat.S_IXUSR or mode & stat.S_IXGRP or mode & stat.S_IXOTH) + + +def _write_snapcraft_runner(*, prime_dir: Path): + content = textwrap.dedent( + """#!/bin/sh + export PATH="$SNAP/usr/sbin:$SNAP/usr/bin:$SNAP/sbin:$SNAP/bin:$PATH" + export LD_LIBRARY_PATH=$SNAP_LIBRARY_PATH:$LD_LIBRARY_PATH + exec "$@" + """ + ) + + runner_path = prime_dir / "snap/command-chain/snapcraft-runner" + runner_path.parent.mkdir(parents=True, exist_ok=True) + runner_path.write_text(content) + runner_path.chmod(0o755) + + +def _write_snap_directory(*, assets_dir: Path, prime_dir: Path, meta_dir: Path) -> None: + """Record manifest and copy assets found under the assets directory. + + These assets have priority over any code generated assets and include: + - hooks + - gui + """ + prime_snap_dir = prime_dir / "snap" + + snap_dir_iter = itertools.product([prime_snap_dir], ["hooks", "gui"]) + meta_dir_iter = itertools.product([meta_dir], ["hooks", "gui"]) + + for origin in itertools.chain(snap_dir_iter, meta_dir_iter): + src_dir = assets_dir / origin[1] + dst_dir = origin[0] / origin[1] + + if src_dir.is_dir(): + dst_dir.mkdir(parents=True, exist_ok=True) + for asset in os.listdir(src_dir): + source = src_dir / asset + destination = dst_dir / asset + + destination.unlink(missing_ok=True) + + shutil.copy(source, destination, follow_symlinks=True) + + # Ensure that the hook is executable in meta/hooks, this is a moot + # point considering the prior link_or_copy call, but is technically + # correct and allows for this operation to take place only once. + if origin[0] == meta_dir and origin[1] == "hooks": + destination.chmod(0o755) + + # TODO: record manifest and source snapcraft.yaml diff --git a/snapcraft/parts/update_metadata.py b/snapcraft/parts/update_metadata.py index 4a0660ca07..ef781ee8d9 100644 --- a/snapcraft/parts/update_metadata.py +++ b/snapcraft/parts/update_metadata.py @@ -108,7 +108,7 @@ def _update_project_icon( if Path(icon_file).is_file(): break else: - if metadata.icon and Path(prime_dir, metadata.icon).is_file(): + if metadata.icon and Path(prime_dir, metadata.icon.lstrip("/")).is_file(): project.icon = metadata.icon @@ -145,7 +145,7 @@ def _update_project_app_desktop_file( if metadata.desktop_file_paths: for filename in metadata.desktop_file_paths: - if Path(prime_dir, filename).is_file(): + if Path(prime_dir, filename.lstrip("/")).is_file(): project.apps[app_name].desktop = filename emit.trace(f"use desktop file {filename!r}") break diff --git a/tests/spread/general/core22/appstream-desktop/snaps/appstream-desktop/snap/snapcraft.yaml b/tests/spread/general/core22/appstream-desktop/snaps/appstream-desktop/snap/snapcraft.yaml index 18ddffd6c3..d5329c80d4 100644 --- a/tests/spread/general/core22/appstream-desktop/snaps/appstream-desktop/snap/snapcraft.yaml +++ b/tests/spread/general/core22/appstream-desktop/snaps/appstream-desktop/snap/snapcraft.yaml @@ -15,7 +15,6 @@ parts: appstream-desktop: plugin: nil source: . - build-packages: [gcc, libc6-dev] parse-info: [usr/share/metainfo/io.snapcraft.appstream.metainfo.xml] override-build: | install -D -m 0644 io.snapcraft.appstream.desktop $CRAFT_PART_INSTALL/usr/share/applications/io.snapcraft.appstream.desktop diff --git a/tests/spread/general/core22/appstream-desktop/task.yaml b/tests/spread/general/core22/appstream-desktop/task.yaml index 78723ecd7a..4067e5f8a0 100644 --- a/tests/spread/general/core22/appstream-desktop/task.yaml +++ b/tests/spread/general/core22/appstream-desktop/task.yaml @@ -26,11 +26,9 @@ execute: | exit 1 fi - # Enable after installing assets - # - # expected_desktop="$PWD/../../appstream-desktop/expected_appstream-desktop.desktop" - # - # if ! diff -U10 prime/meta/gui/appstream-desktop.desktop "$expected_desktop"; then - # echo "The formatting for appstream-desktop.desktop is incorrect" - # exit 1 - # fi + expected_desktop="../../expected_appstream-desktop.desktop" + + if ! diff -U10 prime/meta/gui/appstream-desktop.desktop "$expected_desktop"; then + echo "The formatting for appstream-desktop.desktop is incorrect" + exit 1 + fi diff --git a/tests/unit/parts/test_desktop_file.py b/tests/unit/parts/test_desktop_file.py new file mode 100644 index 0000000000..4932370d5c --- /dev/null +++ b/tests/unit/parts/test_desktop_file.py @@ -0,0 +1,236 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright 2019-2022 Canonical Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from pathlib import Path +from textwrap import dedent + +import pytest + +from snapcraft import errors +from snapcraft.parts.desktop_file import DesktopFile + + +class TestDesktopExec: + """Exec entry rewriting.""" + + @pytest.mark.parametrize( + "app_name,app_args,expected_exec", + [ + # snap name == app name + ("foo", "", "foo"), + ("foo", "--arg", "foo --arg"), + ("foo", "--arg %U", "foo --arg %U"), + ("foo", "%U", "foo %U"), + + # snap name != app name + ("bar", "", "foo.bar"), + ("bar", "--arg", "foo.bar --arg"), + ] + ) + def test_generate_desktop_file( + self, new_dir, app_name, app_args, expected_exec + ): + snap_name = "foo" + + desktop_file_path = new_dir / "app.desktop" + with desktop_file_path.open("w") as desktop_file: + print("[Desktop Entry]", file=desktop_file) + print( + f"Exec={' '.join(['in-snap-exe', app_args])}", file=desktop_file + ) + + d = DesktopFile( + snap_name=snap_name, + app_name=app_name, + filename=desktop_file_path, + prime_dir=new_dir.as_posix(), + ) + d.write(gui_dir=Path()) + + expected_desktop_file = new_dir / f"{app_name}.desktop" + assert expected_desktop_file.exists() + with expected_desktop_file.open() as desktop_file: + assert desktop_file.read() == dedent( + f"""\ + [Desktop Entry] + Exec={expected_exec} + + """ + ) + + +class TestDesktopIcon: + """Icon entry rewriting.""" + + @pytest.mark.parametrize( + "icon,icon_path,expected_icon", + [ + # icon_path preferred + ("other.png", "foo.png", "${SNAP}/foo.png"), + + # icon_path with / preferred + ("/foo.png", "foo.png", "${SNAP}/foo.png"), + + # icon path with ${SNAP} + ("${SNAP}/foo.png", None, "${SNAP}/foo.png"), + + # icon name + ("foo", None, "foo"), + ] + ) + def test_generate_desktop_file(self, new_dir, icon, icon_path, expected_icon): + snap_name = app_name = "foo" + + desktop_file_path = new_dir / "app.desktop" + with desktop_file_path.open("w") as desktop_file: + print("[Desktop Entry]", file=desktop_file) + print("Exec=in-snap-exe", file=desktop_file) + print(f"Icon={icon}", file=desktop_file) + + if icon_path is not None: + (new_dir / icon_path).touch() + + d = DesktopFile( + snap_name=snap_name, + app_name=app_name, + filename=desktop_file_path, + prime_dir=new_dir, + ) + d.write(gui_dir=Path()) + + if icon_path is not None: + d.write(icon_path=icon_path, gui_dir=Path()) + else: + d.write(gui_dir=Path()) + + expected_desktop_file = new_dir / f"{app_name}.desktop" + assert expected_desktop_file.exists() + with expected_desktop_file.open() as desktop_file: + assert ( + desktop_file.read() + == dedent( + """\ + [Desktop Entry] + Exec=foo + Icon={} + + """ + ).format(expected_icon) + ) + + @pytest.mark.parametrize( + "icon,icon_path,expected_icon", + [ + # icon_path preferred + ("other.png", "foo.png", "${SNAP}/foo.png"), + + # icon_path with / preferred + ("/foo.png", "foo.png", "${SNAP}/foo.png"), + + # icon path with ${SNAP} + ("${SNAP}/foo.png", None, "${SNAP}/foo.png"), + + # icon name + ("foo", None, "foo"), + ] + ) + def test_generate_desktop_file_multisection( + self, new_dir, icon, icon_path, expected_icon + ): + snap_name = app_name = "foo" + + desktop_file_path = new_dir / "app.desktop" + with desktop_file_path.open("w") as desktop_file: + print("[Desktop Entry]", file=desktop_file) + print("Exec=in-snap-exe", file=desktop_file) + print(f"Icon={icon}", file=desktop_file) + print("[Desktop Entry Two]", file=desktop_file) + print("Exec=in-snap-exe2", file=desktop_file) + print(f"Icon={icon}", file=desktop_file) + + if icon_path is not None: + (new_dir / icon_path).touch() + + d = DesktopFile( + snap_name=snap_name, + app_name=app_name, + filename=desktop_file_path, + prime_dir=new_dir, + ) + + if icon_path is not None: + d.write(icon_path=icon_path, gui_dir=Path()) + else: + d.write(gui_dir=Path()) + + expected_desktop_file = new_dir / f"{app_name}.desktop" + assert expected_desktop_file.exists() + with expected_desktop_file.open() as desktop_file: + assert desktop_file.read() == dedent( + f"""\ + [Desktop Entry] + Exec=foo + Icon={expected_icon} + + [Desktop Entry Two] + Exec=foo + Icon={expected_icon} + + """ + ) + + +def test_not_found(new_dir): + with pytest.raises(errors.DesktopFileError): + DesktopFile( + snap_name="foo", + app_name="foo", + filename="desktop-file-not-found", + prime_dir=new_dir, + ) + + +def test_no_desktop_section(new_dir): + with open("foo.desktop", "w") as desktop_file: + print("[Random Entry]", file=desktop_file) + print("Exec=foo", file=desktop_file) + print("Icon=foo", file=desktop_file) + + d = DesktopFile( + snap_name="foo", + app_name="foo", + filename="foo.desktop", + prime_dir=new_dir, + ) + + with pytest.raises(errors.DesktopFileError): + d.write(gui_dir=new_dir) + + +def test_missing_exec_entry(new_dir): + with open("foo.desktop", "w") as desktop_file: + print("[Desktop Entry]", file=desktop_file) + print("Icon=foo", file=desktop_file) + + d = DesktopFile( + snap_name="foo", + app_name="foo", + filename="foo.desktop", + prime_dir=new_dir, + ) + + with pytest.raises(errors.DesktopFileError): + d.write(gui_dir=new_dir) diff --git a/tests/unit/parts/test_setup_assets.py b/tests/unit/parts/test_setup_assets.py new file mode 100644 index 0000000000..5fe5ef3eb5 --- /dev/null +++ b/tests/unit/parts/test_setup_assets.py @@ -0,0 +1,249 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright 2017-2022 Canonical Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import os +import textwrap +from pathlib import Path +from typing import Any, Dict + +import pytest + +from snapcraft import errors +from snapcraft.parts.setup_assets import _validate_command_chain, setup_assets +from snapcraft.projects import Project + + +@pytest.fixture +def desktop_file(): + def _write_file(filename: str): + Path(filename).write_text( + textwrap.dedent( + """\ + [Desktop Entry] + Name=appstream-desktop + Exec=appstream + Type=Application + Icon=/usr/share/icons/my-icon.svg""" + ) + ) + + yield _write_file + + +@pytest.fixture +def yaml_data(): + def _yaml_data(extra_data: Dict[str, Any]) -> Dict[str, Any]: + return { + "name": "test-project", + "base": "core22", + "confinement": "strict", + "parts": {}, + **extra_data, + } + + yield _yaml_data + + +class TestSetupAssets: + """Check copied assets and desktop entries.""" + + @pytest.fixture(autouse=True) + def setup_method_fixture(self, new_dir): + # create prime tree + Path("prime").mkdir() + Path("prime/test.sh").touch() + Path("prime/test.sh").chmod(0o755) + + # create assets dir + Path("snap").mkdir() + + def test_setup_assets_happy(self, desktop_file, yaml_data, new_dir): + desktop_file("prime/test.desktop") + Path("prime/usr/share/icons").mkdir(parents=True) + Path("prime/usr/share/icons/my-icon.svg").touch() + + # define project + project = Project.unmarshal( + yaml_data( + { + "adopt-info": "part", + "apps": { + "app1": { + "command": "test.sh", + "common-id": "my-test", + "desktop": "test.desktop", + }, + }, + }, + ) + ) + + setup_assets(project, assets_dir=Path("snap"), prime_dir=Path("prime")) + + # desktop file should be in meta/gui and named after app + desktop_path = Path("prime/meta/gui/app1.desktop") + assert desktop_path.is_file() + + # desktop file content should make icon relative to ${SNAP} + content = desktop_path.read_text() + assert content == textwrap.dedent( + """\ + [Desktop Entry] + Name=appstream-desktop + Exec=test-project.app1 + Type=Application + Icon=${SNAP}/usr/share/icons/my-icon.svg + + """ + ) + + def test_setup_assets_icon_in_assets_dir(self, desktop_file, yaml_data, new_dir): + desktop_file("prime/test.desktop") + Path("snap/gui").mkdir(parents=True) + Path("snap/gui/icon.svg").touch() + + # define project + project = Project.unmarshal( + yaml_data( + { + "adopt-info": "part", + "apps": { + "app1": { + "command": "test.sh", + "common-id": "my-test", + "desktop": "test.desktop", + }, + }, + }, + ) + ) + + setup_assets(project, assets_dir=Path("snap"), prime_dir=Path("prime")) + + # desktop file should be in meta/gui and named after app + desktop_path = Path("prime/meta/gui/app1.desktop") + assert desktop_path.is_file() + + # desktop file content should make icon relative to ${SNAP} + content = desktop_path.read_text() + assert content == textwrap.dedent( + """\ + [Desktop Entry] + Name=appstream-desktop + Exec=test-project.app1 + Type=Application + Icon=${SNAP}/snap/gui/icon.svg + + """ + ) + + # icon file exists + Path("prime/snap/gui/icon.svg").is_file() + + def test_setup_assets_no_apps(self, desktop_file, yaml_data, new_dir): + desktop_file("prime/test.desktop") + Path("prime/usr/share/icons").mkdir(parents=True) + Path("prime/usr/share/icons/icon.svg").touch() + Path("snap/gui").mkdir() + + # define project + project = Project.unmarshal(yaml_data({"adopt-info": "part"})) + + # setting up assets does not crash + setup_assets(project, assets_dir=Path("snap"), prime_dir=Path("prime")) + + assert os.listdir("prime/meta/gui") == [] + + def test_setup_assets_remote_icon(self, desktop_file, yaml_data, new_dir): + # create primed tree (no icon) + desktop_file("prime/test.desktop") + + # define project + # pylint: disable=line-too-long + project = Project.unmarshal( + yaml_data( + { + "adopt-info": "part", + "icon": "https://dashboard.snapcraft.io/site_media/appmedia/2018/04/Snapcraft-logo-bird.png", + "apps": { + "app1": { + "command": "test.sh", + "common-id": "my-test", + "desktop": "test.desktop", + }, + }, + }, + ) + ) + # pylint: enable=line-too-long + + setup_assets(project, assets_dir=Path("snap"), prime_dir=Path("prime")) + + # desktop file should be in meta/gui and named after app + desktop_path = Path("prime/meta/gui/app1.desktop") + assert desktop_path.is_file() + + # desktop file content should make icon relative to ${SNAP} + content = desktop_path.read_text() + assert content == textwrap.dedent( + """\ + [Desktop Entry] + Name=appstream-desktop + Exec=test-project.app1 + Type=Application + Icon=${SNAP}/meta/gui/icon.png + + """ + ) + + # icon was downloaded + icon_path = Path("prime/meta/gui/icon.png") + assert icon_path.is_file() + assert icon_path.stat().st_size > 0 + + +class TestCommandChain: + """Command chain items are valid.""" + + def test_command_chain_path_not_found(self, new_dir): + + with pytest.raises(errors.SnapcraftError) as raised: + _validate_command_chain( + ["file-not-found"], app_name="foo", prime_dir=new_dir + ) + + assert str(raised.value) == ( + "Failed to generate snap metadata: The command-chain item 'file-not-found' " + "defined in the app 'foo' does not exist or is not executable." + ) + + def test_command_chain_path_not_executable(self, new_dir): + Path("file-executable").touch() + Path("file-executable").chmod(0o755) + + Path("file-not-executable").touch() + + with pytest.raises(errors.SnapcraftError) as raised: + _validate_command_chain( + ["file-executable", "file-not-executable"], + app_name="foo", + prime_dir=new_dir, + ) + + assert str(raised.value) == ( + "Failed to generate snap metadata: The command-chain item 'file-not-executable' " + "defined in the app 'foo' does not exist or is not executable." + ) From 1f5dde4e68d14dfc74911e5cbd7f1ba48717ff52 Mon Sep 17 00:00:00 2001 From: Sergio Schvezov Date: Mon, 25 Apr 2022 18:50:01 -0300 Subject: [PATCH 111/167] providers: support SNAPCRAFT_BUILD_ENVIRONMENT Signed-off-by: Sergio Schvezov --- snapcraft/parts/lifecycle.py | 8 ++++++-- snapcraft/providers/_get_provider.py | 11 +++++++++-- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/snapcraft/parts/lifecycle.py b/snapcraft/parts/lifecycle.py index 482d0853c6..5af70f60e1 100644 --- a/snapcraft/parts/lifecycle.py +++ b/snapcraft/parts/lifecycle.py @@ -16,6 +16,7 @@ """Parts lifecycle preparation and execution.""" +import os import subprocess from dataclasses import dataclass from pathlib import Path @@ -131,7 +132,6 @@ def run(command_name: str, parsed_args: "argparse.Namespace") -> None: yaml_data = process_yaml(snap_project.project_file) parse_info = _extract_parse_info(yaml_data) - # argument --provider is only supported by legacy snapcraft if parsed_args.provider: raise errors.SnapcraftError("Option --provider is not supported.") @@ -165,7 +165,11 @@ def _run_command( if parsed_args.use_lxd and providers.get_platform_default_provider() == "lxd": emit.message("LXD is used by default on this platform.", intermediate=True) - if not managed_mode and not parsed_args.destructive_mode: + if ( + not managed_mode + and not parsed_args.destructive_mode + and not os.getenv("SNAPCRAFT_BUILD_ENVIRONMENT") == "host" + ): if command_name == "clean" and not part_names: _clean_provider(project, parsed_args) else: diff --git a/snapcraft/providers/_get_provider.py b/snapcraft/providers/_get_provider.py index a623068b20..1f8e8a4338 100644 --- a/snapcraft/providers/_get_provider.py +++ b/snapcraft/providers/_get_provider.py @@ -16,6 +16,7 @@ """Build environment provider support for snapcraft.""" +import os import sys from typing import Optional @@ -33,11 +34,17 @@ def get_provider(provider: Optional[str] = None) -> Provider: (1) use provider specified in the function argument, (2) use provider specified with snap configuration if running as snap, - (3) default to platform default (LXD on Linux). + (3) get the provider from the environment if valid, + (4) default to platform default (LXD on Linux). :return: Provider instance. """ - if provider is None: + env_provider = os.getenv("SNAPCRAFT_BUILD_ENVIRONMENT") + env_provider_is_valid = env_provider in ("lxd", "multipass") + + if provider is None and env_provider_is_valid: + provider = env_provider + elif provider is None: provider = get_platform_default_provider() if provider == "lxd": From dedb44ab619438da4e1586fb2077b5476dfc1d15 Mon Sep 17 00:00:00 2001 From: Sergio Schvezov Date: Mon, 25 Apr 2022 18:50:24 -0300 Subject: [PATCH 112/167] multipass provider: emit.pause to confirm install Signed-off-by: Sergio Schvezov --- snapcraft/providers/_multipass.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/snapcraft/providers/_multipass.py b/snapcraft/providers/_multipass.py index 007750f201..470f2063da 100644 --- a/snapcraft/providers/_multipass.py +++ b/snapcraft/providers/_multipass.py @@ -21,6 +21,7 @@ import pathlib from typing import Generator, List +from craft_cli import emit from craft_providers import Executor, bases, multipass from craft_providers.multipass.errors import MultipassError @@ -91,11 +92,13 @@ def ensure_provider_is_available(cls) -> None: :raises ProviderError: if provider is not available. """ if not multipass.is_installed(): - if confirm_with_user( - "Multipass is required, but not installed. Do you wish to install Multipass " - "and configure it with the defaults?", - default=False, - ): + with emit.pause(): + confirmation = confirm_with_user( + "Multipass is required, but not installed. Do you wish to install Multipass " + "and configure it with the defaults?", + default=False, + ) + if confirmation: try: multipass.install() except multipass.MultipassInstallationError as error: From cd36e3d68174d91012304bb33b58ccbdc5678273 Mon Sep 17 00:00:00 2001 From: Sergio Schvezov Date: Mon, 25 Apr 2022 21:56:28 -0300 Subject: [PATCH 113/167] parts: extra_snap_packages & extra_build_packages Hook the entries up to the LifeCycleManager and add the base if there as extra_snap_packages Signed-off-by: Sergio Schvezov --- requirements-devel.txt | 2 +- requirements.txt | 2 +- snapcraft/parts/lifecycle.py | 1 + snapcraft/parts/parts.py | 11 +++- tests/unit/parts/test_parts.py | 98 +++++++++++++++++++++++++++++++++- 5 files changed, 110 insertions(+), 4 deletions(-) diff --git a/requirements-devel.txt b/requirements-devel.txt index c8ffea17cb..308ff6e086 100644 --- a/requirements-devel.txt +++ b/requirements-devel.txt @@ -11,7 +11,7 @@ codespell==2.1.0 coverage==6.3.2 craft-cli==0.4.0 craft-grammar==1.1.1 -craft-parts==1.4.2 +craft-parts==1.5.1 craft-providers==1.2.0 craft-store==2.1.0 cryptography==3.4 diff --git a/requirements.txt b/requirements.txt index 5c5015aac5..a76d5033a2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,7 +7,7 @@ charset-normalizer==2.0.12 click==8.1.2 craft-cli==0.4.0 craft-grammar==1.1.1 -craft-parts==1.4.2 +craft-parts==1.5.1 craft-providers==1.2.0 craft-store==2.1.0 cryptography==3.4 diff --git a/snapcraft/parts/lifecycle.py b/snapcraft/parts/lifecycle.py index 5af70f60e1..624569444c 100644 --- a/snapcraft/parts/lifecycle.py +++ b/snapcraft/parts/lifecycle.py @@ -187,6 +187,7 @@ def _run_command( project.parts, work_dir=work_dir, assets_dir=assets_dir, + base=project.base, package_repositories=project.package_repositories, part_names=part_names, adopt_info=project.adopt_info, diff --git a/snapcraft/parts/parts.py b/snapcraft/parts/parts.py index 2dffd08d83..63b4ad6b18 100644 --- a/snapcraft/parts/parts.py +++ b/snapcraft/parts/parts.py @@ -42,6 +42,7 @@ class PartsLifecycle: :param all_parts: A dictionary containing the parts defined in the project. :param work_dir: The working directory for parts processing. :param assets_dir: The directory containing project assets. + :param base: the base to build for. :param adopt_info: The name of the part containing metadata do adopt. :raises PartsLifecycleError: On error initializing the parts lifecycle. @@ -53,6 +54,7 @@ def __init__( *, work_dir: pathlib.Path, assets_dir: pathlib.Path, + base: Optional[str], package_repositories: List[Dict[str, Any]], part_names: Optional[List[str]], adopt_info: Optional[str], @@ -76,6 +78,9 @@ def __init__( # Install pre-requisite packages for apt-key, if not installed. # FIXME: package names should be plataform-specific extra_build_packages.extend(["gnupg", "dirmngr"]) + extra_snap_packages = [] + if base is not None: + extra_snap_packages.append(base) try: self._lcm = craft_parts.LifecycleManager( @@ -84,6 +89,8 @@ def __init__( work_dir=work_dir, cache_dir=cache_dir, ignore_local_sources=["*.snap"], + extra_build_packages=extra_build_packages, + extra_snap_packages=extra_snap_packages, project_name=project_name, project_vars_part_name=adopt_info, project_vars=project_vars, @@ -193,7 +200,9 @@ def extract_metadata(self) -> List[ExtractedMetadata]: metadata_list.append(metadata) break - emit.message(f"No metadata extracted from {metadata_file}", intermediate=True) + emit.message( + f"No metadata extracted from {metadata_file}", intermediate=True + ) return metadata_list diff --git a/tests/unit/parts/test_parts.py b/tests/unit/parts/test_parts.py index 3391166122..2fa28c4cf5 100644 --- a/tests/unit/parts/test_parts.py +++ b/tests/unit/parts/test_parts.py @@ -15,7 +15,9 @@ # along with this program. If not, see . from pathlib import Path +from unittest.mock import ANY, call +import craft_parts import pytest from snapcraft import errors @@ -30,11 +32,13 @@ def parts_data(): @pytest.mark.parametrize("step_name", ["pull", "overlay", "build", "stage", "prime"]) -def test_parts_lifecycle_run(parts_data, step_name, new_dir, emitter): +def test_parts_lifecycle_run(mocker, parts_data, step_name, new_dir, emitter): + lcm_spy = mocker.spy(craft_parts, "LifecycleManager") lifecycle = PartsLifecycle( parts_data, work_dir=new_dir, assets_dir=new_dir, + base="core22", part_names=[], package_repositories=[], adopt_info=None, @@ -45,6 +49,20 @@ def test_parts_lifecycle_run(parts_data, step_name, new_dir, emitter): lifecycle.run(step_name) assert lifecycle.prime_dir == Path(new_dir, "prime") assert lifecycle.prime_dir.is_dir() + assert lcm_spy.mock_calls == [ + call( + {"parts": {"p1": {"plugin": "nil"}}}, + application_name="snapcraft", + work_dir=ANY, + cache_dir=ANY, + ignore_local_sources=["*.snap"], + extra_build_packages=[], + extra_snap_packages=["core22"], + project_name="test-project", + project_vars_part_name=None, + project_vars={"version": "1", "grade": "stable"}, + ) + ] emitter.assert_recorded([f"Executing parts lifecycle: {step_name} p1"]) @@ -53,6 +71,7 @@ def test_parts_lifecycle_run_bad_step(parts_data, new_dir): parts_data, work_dir=new_dir, assets_dir=new_dir, + base="core22", part_names=[], package_repositories=[], adopt_info=None, @@ -70,6 +89,7 @@ def test_parts_lifecycle_run_internal_error(parts_data, new_dir, mocker): parts_data, work_dir=new_dir, assets_dir=new_dir, + base="core22", part_names=[], package_repositories=[], adopt_info=None, @@ -88,6 +108,7 @@ def test_parts_lifecycle_run_parts_error(new_dir): {"p1": {"plugin": "dump", "source": "foo"}}, work_dir=new_dir, assets_dir=new_dir, + base="core22", part_names=[], package_repositories=[], adopt_info=None, @@ -107,6 +128,7 @@ def test_parts_lifecycle_clean(parts_data, new_dir, emitter): parts_data, work_dir=new_dir, assets_dir=new_dir, + base="core22", part_names=[], package_repositories=[], adopt_info=None, @@ -123,6 +145,7 @@ def test_parts_lifecycle_clean_parts(parts_data, new_dir, emitter): parts_data, work_dir=new_dir, assets_dir=new_dir, + base="core22", part_names=[], package_repositories=[], adopt_info=None, @@ -132,3 +155,76 @@ def test_parts_lifecycle_clean_parts(parts_data, new_dir, emitter): ) lifecycle.clean(part_names=["p1"]) emitter.assert_recorded(["Cleaning parts: p1"]) + + +def test_parts_lifecycle_initialize_with_no_base( + mocker, + parts_data, + new_dir, +): + lcm_spy = mocker.spy(craft_parts, "LifecycleManager") + PartsLifecycle( + parts_data, + work_dir=new_dir, + assets_dir=new_dir, + base=None, + part_names=[], + package_repositories=[], + adopt_info=None, + project_name="test-project", + parse_info={}, + project_vars={"version": "1", "grade": "stable"}, + ) + assert lcm_spy.mock_calls == [ + call( + {"parts": {"p1": {"plugin": "nil"}}}, + application_name="snapcraft", + work_dir=ANY, + cache_dir=ANY, + ignore_local_sources=["*.snap"], + extra_build_packages=[], + extra_snap_packages=[], + project_name="test-project", + project_vars_part_name=None, + project_vars={"version": "1", "grade": "stable"}, + ) + ] + + +def test_parts_lifecycle_initialize_with_package_repositories( + mocker, + parts_data, + new_dir, +): + lcm_spy = mocker.spy(craft_parts, "LifecycleManager") + PartsLifecycle( + parts_data, + work_dir=new_dir, + assets_dir=new_dir, + base="core22", + part_names=[], + package_repositories=[ + { + "type": "apt", + "ppa": "test/somerepo", + }, + ], + adopt_info=None, + project_name="test-project", + parse_info={}, + project_vars={"version": "1", "grade": "stable"}, + ) + assert lcm_spy.mock_calls == [ + call( + {"parts": {"p1": {"plugin": "nil"}}}, + application_name="snapcraft", + work_dir=ANY, + cache_dir=ANY, + ignore_local_sources=["*.snap"], + extra_build_packages=["gnupg", "dirmngr"], + extra_snap_packages=["core22"], + project_name="test-project", + project_vars_part_name=None, + project_vars={"version": "1", "grade": "stable"}, + ) + ] From a0d10a16de008bd132b5612b4256165f6809c770 Mon Sep 17 00:00:00 2001 From: Claudio Matsuoka Date: Tue, 26 Apr 2022 10:17:18 -0300 Subject: [PATCH 114/167] projects: field grade is not mandatory Grade is an optional field and assumed as stable if not explicitly set in project or adopted metadata. Signed-off-by: Claudio Matsuoka --- snapcraft/meta/snap_yaml.py | 3 +-- snapcraft/projects.py | 2 +- tests/unit/meta/test_snap_yaml.py | 5 ++--- tests/unit/parts/test_lifecycle.py | 24 ++++++++++++++++++++++-- tests/unit/test_projects.py | 1 - 5 files changed, 26 insertions(+), 9 deletions(-) diff --git a/snapcraft/meta/snap_yaml.py b/snapcraft/meta/snap_yaml.py index 555c590786..58db17778d 100644 --- a/snapcraft/meta/snap_yaml.py +++ b/snapcraft/meta/snap_yaml.py @@ -161,7 +161,6 @@ def write(project: Project, prime_dir: Path, *, arch: str): sockets=app_sockets if app_sockets else None, ) - # FIXME: handle adopted parameters snap_metadata = SnapMetadata( name=project.name, title=project.title, @@ -176,7 +175,7 @@ def write(project: Project, prime_dir: Path, *, arch: str): epoch=project.epoch, apps=snap_apps, confinement=project.confinement, - grade=project.grade, # type: ignore + grade=project.grade or "stable", environment=project.environment, plugs=project.plugs, hooks=project.hooks, diff --git a/snapcraft/projects.py b/snapcraft/projects.py index 30fa0192a4..1fb3397bb8 100644 --- a/snapcraft/projects.py +++ b/snapcraft/projects.py @@ -225,7 +225,7 @@ class ContentPlug(ProjectModel): default_provider: Optional[str] -MANDATORY_ADOPTABLE_FIELDS = ("version", "summary", "description", "grade") +MANDATORY_ADOPTABLE_FIELDS = ("version", "summary", "description") class Project(ProjectModel): diff --git a/tests/unit/meta/test_snap_yaml.py b/tests/unit/meta/test_snap_yaml.py index a1c4346ec2..713e3e15e2 100644 --- a/tests/unit/meta/test_snap_yaml.py +++ b/tests/unit/meta/test_snap_yaml.py @@ -38,7 +38,6 @@ def simple_project(): we live in tweetspace and your description wants to look good in the snap store. - grade: stable confinement: strict parts: @@ -105,7 +104,7 @@ def complex_project(): we live in tweetspace and your description wants to look good in the snap store. - grade: stable + grade: devel confinement: strict environment: @@ -249,7 +248,7 @@ def test_complex_snap_yaml(complex_project, new_dir): listen-stream: 100 socket-mode: 1 confinement: strict - grade: stable + grade: devel environment: GLOBAL_VARIABLE: test-global-variable plugs: diff --git a/tests/unit/parts/test_lifecycle.py b/tests/unit/parts/test_lifecycle.py index 040fd2c0e6..279b04b0a5 100644 --- a/tests/unit/parts/test_lifecycle.py +++ b/tests/unit/parts/test_lifecycle.py @@ -25,7 +25,7 @@ from snapcraft import errors from snapcraft.parts import lifecycle as parts_lifecycle from snapcraft.parts.update_metadata import update_project_metadata -from snapcraft.projects import Project +from snapcraft.projects import MANDATORY_ADOPTABLE_FIELDS, Project _SNAPCRAFT_YAML_FILENAMES = [ "snap/snapcraft.yaml", @@ -391,7 +391,7 @@ def test_lifecycle_pack_metadata_error(cmd, snapcraft_yaml, new_dir, mocker): assert pack_mock.mock_calls == [] -@pytest.mark.parametrize("field", ["version", "summary", "description", "grade"]) +@pytest.mark.parametrize("field", MANDATORY_ADOPTABLE_FIELDS) def test_lifecycle_metadata_empty(field, snapcraft_yaml, new_dir): """Adoptable fields shouldn't be empty after adoption.""" yaml_data = snapcraft_yaml(base="core22") @@ -546,6 +546,26 @@ def test_lifecycle_clean_managed(snapcraft_yaml, project_vars, new_dir, mocker): assert clean_mock.mock_calls == [call(part_names=["part1"])] +def test_lifecycle_adopt_project_vars(snapcraft_yaml, new_dir): + """Adoptable fields shouldn't be empty after adoption.""" + yaml_data = snapcraft_yaml(base="core22") + yaml_data.pop("version") + yaml_data.pop("grade") + yaml_data["adopt-info"] = "part" + project = Project.unmarshal(yaml_data) + + update_project_metadata( + project, + project_vars={"version": "42", "grade": "devel"}, + metadata_list=[], + assets_dir=new_dir, + prime_dir=new_dir, + ) + + assert project.version == "42" + assert project.grade == "devel" + + def test_extract_parse_info(): yaml_data = { "name": "foo", diff --git a/tests/unit/test_projects.py b/tests/unit/test_projects.py index 0d74e5afd2..28cbef9db5 100644 --- a/tests/unit/test_projects.py +++ b/tests/unit/test_projects.py @@ -173,7 +173,6 @@ def test_mandatory_adoptable_fields_definition(self): "version", "summary", "description", - "grade", ) @pytest.mark.parametrize("field", MANDATORY_ADOPTABLE_FIELDS) From 02c7b0c62d49681533affe6a2e87e2c3bbfb51bc Mon Sep 17 00:00:00 2001 From: Sergio Schvezov Date: Tue, 26 Apr 2022 16:52:39 -0300 Subject: [PATCH 115/167] github actions: python 3.10 for linters Solves pylint issue with PyYAML Signed-off-by: Sergio Schvezov --- .github/workflows/tests.yaml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 383f9699d6..21c5b5d003 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -15,10 +15,14 @@ jobs: uses: actions/checkout@v2 with: fetch-depth: 0 + - name: Set up Python 3.10 + uses: actions/setup-python@v2 + with: + python-version: "3.10" - name: Install dependencies run: | sudo apt update - sudo apt install -y python3-pip python3-venv libapt-pkg-dev libyaml-dev xdelta3 shellcheck + sudo apt install -y libapt-pkg-dev libyaml-dev xdelta3 shellcheck pip install -U -r requirements.txt -r requirements-devel.txt pip install . - name: Run black @@ -45,6 +49,8 @@ jobs: sudo snap install --classic pyright make test-pyright - name: Run pylint + env: + SNAPCRAFT_IGNORE_YAML_BINDINGS: "1" run: | make test-pylint - name: Run shellcheck From 39b393fea6d48dcc0c0c7580b1448a3f2916c327 Mon Sep 17 00:00:00 2001 From: Claudio Matsuoka Date: Tue, 26 Apr 2022 15:28:37 -0300 Subject: [PATCH 116/167] repo: get host architecture using non-legacy code Remove legacy hook to obtain host architecture, use OsPlatform instead. Signed-off-by: Claudio Matsuoka --- snapcraft/repo/apt_sources_manager.py | 9 ++------- tests/unit/repo/test_apt_sources_manager.py | 4 ++-- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/snapcraft/repo/apt_sources_manager.py b/snapcraft/repo/apt_sources_manager.py index c0685aefb4..0139a62f6b 100644 --- a/snapcraft/repo/apt_sources_manager.py +++ b/snapcraft/repo/apt_sources_manager.py @@ -23,8 +23,7 @@ from craft_cli import emit -from snapcraft import os_release -from snapcraft_legacy.project._project_options import ProjectOptions +from snapcraft import os_release, utils from . import apt_ppa, package_repository @@ -58,17 +57,13 @@ def _construct_deb822_source( if architectures: arch_text = " ".join(architectures) else: - arch_text = _get_host_arch() + arch_text = utils.get_host_architecture() print(f"Architectures: {arch_text}", file=deb822) return deb822.getvalue() -def _get_host_arch() -> str: - return ProjectOptions().deb_arch - - class AptSourcesManager: """Manage apt source configuration in /etc/apt/sources.list.d. diff --git a/tests/unit/repo/test_apt_sources_manager.py b/tests/unit/repo/test_apt_sources_manager.py index 4d0981cd2f..19d95fcf0f 100644 --- a/tests/unit/repo/test_apt_sources_manager.py +++ b/tests/unit/repo/test_apt_sources_manager.py @@ -42,8 +42,8 @@ def mock_environ_copy(mocker): @pytest.fixture(autouse=True) def mock_host_arch(mocker): - m = mocker.patch("snapcraft.repo.apt_sources_manager.ProjectOptions") - m.return_value.deb_arch = "FAKE-HOST-ARCH" + m = mocker.patch("snapcraft.utils.get_host_architecture") + m.return_value = "FAKE-HOST-ARCH" yield m From 1c4f5ed7271e8142e7a771a844acf10c8ff442a3 Mon Sep 17 00:00:00 2001 From: Sergio Schvezov Date: Tue, 26 Apr 2022 16:35:12 -0300 Subject: [PATCH 117/167] commands: register and names names is backwards compatible with list and list-registered Signed-off-by: Sergio Schvezov --- requirements-devel.txt | 2 +- requirements.txt | 2 +- snapcraft/cli.py | 9 + snapcraft/commands/__init__.py | 10 + snapcraft/commands/names.py | 183 +++++++++++++++ snapcraft/commands/store/client.py | 72 +++++- snapcraft/commands/store/constants.py | 3 + snapcraft_legacy/cli/store.py | 79 +------ tests/legacy/unit/commands/test_list.py | 129 ----------- tests/legacy/unit/commands/test_register.py | 132 ----------- tests/unit/commands/conftest.py | 8 + tests/unit/commands/store/test_client.py | 129 ++++++++++- tests/unit/commands/test_names.py | 240 ++++++++++++++++++++ 13 files changed, 655 insertions(+), 343 deletions(-) create mode 100644 snapcraft/commands/names.py delete mode 100644 tests/legacy/unit/commands/test_list.py delete mode 100644 tests/legacy/unit/commands/test_register.py create mode 100644 tests/unit/commands/test_names.py diff --git a/requirements-devel.txt b/requirements-devel.txt index 308ff6e086..013153b88c 100644 --- a/requirements-devel.txt +++ b/requirements-devel.txt @@ -13,7 +13,7 @@ craft-cli==0.4.0 craft-grammar==1.1.1 craft-parts==1.5.1 craft-providers==1.2.0 -craft-store==2.1.0 +craft-store==2.1.1 cryptography==3.4 Deprecated==1.2.13 dill==0.3.4 diff --git a/requirements.txt b/requirements.txt index a76d5033a2..f27b434d31 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,7 +9,7 @@ craft-cli==0.4.0 craft-grammar==1.1.1 craft-parts==1.5.1 craft-providers==1.2.0 -craft-store==2.1.0 +craft-store==2.1.1 cryptography==3.4 Deprecated==1.2.13 distro==1.7.0 diff --git a/snapcraft/cli.py b/snapcraft/cli.py index 5aad19a637..5afd984f6a 100644 --- a/snapcraft/cli.py +++ b/snapcraft/cli.py @@ -52,6 +52,15 @@ commands.StoreWhoAmICommand, ], ), + craft_cli.CommandGroup( + "Store Snap Names", + [ + commands.StoreRegisterCommand, + commands.StoreNamesCommand, + commands.StoreLegacyListRegisteredCommand, + commands.StoreLegacyListCommand, + ], + ), craft_cli.CommandGroup( "Extensions", [ diff --git a/snapcraft/commands/__init__.py b/snapcraft/commands/__init__.py index 146d854f06..d31efd3c39 100644 --- a/snapcraft/commands/__init__.py +++ b/snapcraft/commands/__init__.py @@ -36,6 +36,12 @@ SnapCommand, StageCommand, ) +from .names import ( + StoreLegacyListCommand, + StoreLegacyListRegisteredCommand, + StoreNamesCommand, + StoreRegisterCommand, +) from .version import VersionCommand __all__ = [ @@ -48,8 +54,12 @@ "SnapCommand", "StageCommand", "StoreLoginCommand", + "StoreNamesCommand", "StoreExportLoginCommand", "StoreLogoutCommand", + "StoreRegisterCommand", + "StoreLegacyListCommand", + "StoreLegacyListRegisteredCommand", "StoreWhoAmICommand", "ExtensionsCommand", "ListExtensionsCommand", diff --git a/snapcraft/commands/names.py b/snapcraft/commands/names.py new file mode 100644 index 0000000000..a46ee31c56 --- /dev/null +++ b/snapcraft/commands/names.py @@ -0,0 +1,183 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright 2022 Canonical Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +"""Snapcraft Store Account management commands.""" + +import operator +import textwrap +from typing import TYPE_CHECKING + +from craft_cli import BaseCommand, emit +from overrides import overrides +from tabulate import tabulate + +from snapcraft import utils + +from . import store + +if TYPE_CHECKING: + import argparse + + +_MESSAGE_REGISTER_PRIVATE = textwrap.dedent( + """\ + Even though this is private snap, you should think carefully about + the choice of name and make sure you are confident nobody else will + have a stronger claim to that particular name. If you are unsure + then we suggest you prefix the name with your developer identity, + As '$username-yoyodyne-www-site-content'.""" +) +_MESSAGE_REGISTER_CONFIRM = textwrap.dedent( + """\ + We always want to ensure that users get the software they expect + for a particular name. + + If needed, we will rename snaps to ensure that a particular name + reflects the software most widely expected by our community. + + For example, most people would expect 'thunderbird' to be published by + Mozilla. They would also expect to be able to get other snaps of + Thunderbird as '$username-thunderbird'. + + Would you say that MOST users will expect {!r} to come from + you, and be the software you intend to publish there?""" +) +_MESSAGE_REGISTER_SUCCESS = "Registered {!r}" +_MESSAGE_REGISTER_NO = "Snap name {!r} not registered" + + +class StoreRegisterCommand(BaseCommand): + """Command to register a snap with the Snap Store.""" + + name = "register" + help_msg = "Register with the store" + overview = textwrap.dedent( + """ + You can use this command to register an available and become the + publisher for this snap.""" + ) + + @overrides + def fill_parser(self, parser: "argparse.ArgumentParser") -> None: + parser.add_argument( + "snap-name", + type=str, + help="The snap name to register", + ) + parser.add_argument( + "--store", + metavar="", + dest="store_id", + type=str, + default=None, + help="Store to register with", + ) + parser.add_argument( + "--private", + action="store_true", + default=False, + help="Register the snap as a private one", + ) + parser.add_argument( + "--yes", + action="store_true", + default=False, + help="Do not ask for confirmation", + ) + + @overrides + def run(self, parsed_args): + # dest does not work when filling the parser so getattr instead + snap_name = getattr(parsed_args, "snap-name") + + if parsed_args.private: + emit.message( + _MESSAGE_REGISTER_PRIVATE.format(snap_name), + intermediate=True, + ) + if parsed_args.yes or utils.confirm_with_user( + _MESSAGE_REGISTER_CONFIRM.format(snap_name) + ): + store.StoreClientCLI().register( + snap_name, is_private=parsed_args.private, store_id=parsed_args.store_id + ) + emit.message(_MESSAGE_REGISTER_SUCCESS.format(snap_name)) + else: + emit.message(_MESSAGE_REGISTER_NO.format(snap_name)) + + +class StoreNamesCommand(BaseCommand): + """Command to list the snap names registered with the current account.""" + + name = "names" + help_msg = "List the names registered to the logged it account" + overview = textwrap.dedent( + """ + Return the list of snap names together with the registration date, + its visibility and any additional notes.""" + ) + + @overrides + def run(self, parsed_args): + account_info = store.StoreClientCLI().get_account_info() + + snaps = [ + ( + name, + info["since"], + "private" if info["private"] else "public", + "-", + ) + for name, info in account_info["snaps"] + .get(store.constants.DEFAULT_SERIES, {}) + .items() + # Presenting only approved snap registrations, which means name + # disputes will be displayed/sorted some other way. + if info["status"] == "Approved" + ] + if not snaps: + emit.message("No registered snaps") + else: + tabulated_snaps = tabulate( + sorted(snaps, key=operator.itemgetter(0)), + headers=["Name", "Since", "Visibility", "Notes"], + tablefmt="plain", + ) + emit.message(tabulated_snaps) + + +class StoreLegacyListCommand(StoreNamesCommand): + """Legacy command to list the snap names registered with the current account.""" + + name = "list" + hidden = True + + @overrides + def run(self, parsed_args): + emit.progress("This command is deprecated: use 'names' instead") + super().run(parsed_args) + + +class StoreLegacyListRegisteredCommand(StoreNamesCommand): + """Legacy command to list the snap names registered with the current account.""" + + name = "list-registered" + hidden = True + + @overrides + def run(self, parsed_args): + emit.progress("This command is deprecated: use 'names' instead") + super().run(parsed_args) diff --git a/snapcraft/commands/store/client.py b/snapcraft/commands/store/client.py index 2cd9730524..bed611bac3 100644 --- a/snapcraft/commands/store/client.py +++ b/snapcraft/commands/store/client.py @@ -22,9 +22,10 @@ from typing import Any, Dict, Optional, Sequence, Tuple import craft_store +import requests from craft_cli import emit -from snapcraft import __version__, utils +from snapcraft import __version__, errors, utils from . import constants @@ -125,6 +126,7 @@ class StoreClientCLI: def __init__(self, ephemeral=False): self.store_client = get_client(ephemeral=ephemeral) + self._base_url = get_store_url() def login( self, @@ -182,3 +184,71 @@ def login( ) return credentials + + def request(self, *args, **kwargs) -> requests.Response: + """Request using the BaseClient and wrap responses that require action. + + Actionable items are those that could prompt a login or registration. + """ + try: + return self.store_client.request(*args, **kwargs) + except craft_store.errors.StoreServerError as store_error: + if ( + store_error.response.status_code + == requests.codes.unauthorized # pylint: disable=no-member + ): + if os.getenv(constants.ENVIRONMENT_STORE_CREDENTIALS): + raise errors.SnapcraftError( + "Provided credentials are no longer valid for the Snap Store. " + "Regenerate them and try again." + ) from store_error + + emit.message( + "You are required to re-login before continuing", + intermediate=True, + ) + self.store_client.logout() + else: + raise + except craft_store.errors.CredentialsUnavailable: + emit.message( + "You are required to login before continuing", intermediate=True + ) + + self.login() + return self.store_client.request(*args, **kwargs) + + def register( + self, + snap_name: str, + *, + is_private: bool = False, + store_id: Optional[str] = None, + ) -> None: + """Register snap_name with the Snap Store. + + :param snap_name: the name of the snap to register with the Snap Store + :param is_private: makes the registered snap a private snap + :param store_id: alternative store to register with + """ + data = dict( + snap_name=snap_name, is_private=is_private, series=constants.DEFAULT_SERIES + ) + if store_id is not None: + data["store"] = store_id + + self.request( + "POST", + self._base_url + "/dev/api/register-name/", + json=data, + ) + + def get_account_info( + self, + ) -> Dict[str, Any]: + """Return account information.""" + return self.request( + "GET", + self._base_url + "/dev/api/account", + headers={"Accept": "application/json"}, + ).json() diff --git a/snapcraft/commands/store/constants.py b/snapcraft/commands/store/constants.py index 907a0b6993..ea74dd58a4 100644 --- a/snapcraft/commands/store/constants.py +++ b/snapcraft/commands/store/constants.py @@ -36,3 +36,6 @@ UBUNTU_ONE_SSO_URL = "https://login.ubuntu.com" """Default Ubuntu One Login URL.""" + +DEFAULT_SERIES = "16" +"""Legacy value for older generation Snap Store APIs.""" diff --git a/snapcraft_legacy/cli/store.py b/snapcraft_legacy/cli/store.py index 7c75ae9b3f..95e9344076 100644 --- a/snapcraft_legacy/cli/store.py +++ b/snapcraft_legacy/cli/store.py @@ -14,18 +14,12 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -import base64 -import contextlib -import functools import json import operator import os -import stat -import sys -from datetime import date, datetime, timedelta +from datetime import date, timedelta from textwrap import dedent from typing import Dict, List, Optional, Set, Union -from urllib.parse import urlparse import click from tabulate import tabulate @@ -40,42 +34,6 @@ from ._metrics import convert_metrics_to_table from ._review import review_snap -_VALID_DATE_FORMATS = [ - "%Y-%m-%d", - "%Y-%m-%dT%H:%M:%SZ", -] - -_MESSAGE_REGISTER_PRIVATE = dedent( - """\ - Even though this is private snap, you should think carefully about - the choice of name and make sure you are confident nobody else will - have a stronger claim to that particular name. If you are unsure - then we suggest you prefix the name with your developer identity, - As '$username-yoyodyne-www-site-content'.""" -) -_MESSAGE_REGISTER_CONFIRM = dedent( - """ - We always want to ensure that users get the software they expect - for a particular name. - - If needed, we will rename snaps to ensure that a particular name - reflects the software most widely expected by our community. - - For example, most people would expect 'thunderbird' to be published by - Mozilla. They would also expect to be able to get other snaps of - Thunderbird as '$username-thunderbird'. - - Would you say that MOST users will expect {!r} to come from - you, and be the software you intend to publish there?""" -) -_MESSAGE_REGISTER_SUCCESS = "Congrats! You are now the publisher of {!r}." -_MESSAGE_REGISTER_NO = dedent( - """ - Thank you! {!r} will remain available. - - In the meantime you can register an alternative name.""" -) - @click.group() def storecli(): @@ -118,30 +76,6 @@ def _human_readable_acls(store_client: storeapi.StoreClient) -> str: ) -@storecli.command() -@click.argument("snap-name", metavar="") -@click.option("--private", is_flag=True, help="Register the snap as a private one") -@click.option("--store", metavar="", help="Store to register with") -@click.option("--yes", is_flag=True) -def register(snap_name, private, store, yes): - """Register with the store. - - You can use this command to register an available and become - the publisher for this snap. - - \b - Examples: - snapcraft register thunderbird - """ - if private: - click.echo(_MESSAGE_REGISTER_PRIVATE.format(snap_name)) - if yes or echo.confirm(_MESSAGE_REGISTER_CONFIRM.format(snap_name)): - snapcraft_legacy.register(snap_name, is_private=private, store_id=store) - click.echo(_MESSAGE_REGISTER_SUCCESS.format(snap_name)) - else: - click.echo(_MESSAGE_REGISTER_NO.format(snap_name)) - - @storecli.command() @click.option( "--release", @@ -649,17 +583,6 @@ def get_channels_for_revision(revision: int) -> List[str]: click.echo_via_pager(tabulated_revisions) -@storecli.command("list") -def list_registered(): - """List snap names registered or shared with you. - - \b - Examples: - snapcraft list - """ - snapcraft_legacy.list_registered() - - @storecli.command() @click.argument("snap-name", metavar="") @click.argument("track_name", metavar="") diff --git a/tests/legacy/unit/commands/test_list.py b/tests/legacy/unit/commands/test_list.py deleted file mode 100644 index f7d089dc2f..0000000000 --- a/tests/legacy/unit/commands/test_list.py +++ /dev/null @@ -1,129 +0,0 @@ -# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- -# -# Copyright (C) 2016-2020 Canonical Ltd -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License version 3 as -# published by the Free Software Foundation. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -from textwrap import dedent - -from testtools.matchers import Contains, Equals - -from . import FAKE_UNAUTHORIZED_ERROR, FakeStoreCommandsBaseTestCase - - -class ListTest(FakeStoreCommandsBaseTestCase): - - command_name = "list" - - def test_command_without_login_must_ask(self): - # TODO: look into why this many calls are done inside snapcraft_legacy.storeapi - self.fake_store_account_info.mock.side_effect = [ - FAKE_UNAUTHORIZED_ERROR, - {"account_id": "abcd", "snaps": dict()}, - {"account_id": "abcd", "snaps": dict()}, - {"account_id": "abcd", "snaps": dict()}, - ] - - result = self.run_command( - [self.command_name], input="user@example.com\nsecret\n" - ) - self.assertThat( - result.output, Contains("You are required to login before continuing.") - ) - - def test_list_empty(self): - self.fake_store_account_info.mock.return_value = { - "account_id": "abcd", - "snaps": dict(), - } - - result = self.run_command([self.command_name]) - - self.assertThat(result.exit_code, Equals(0)) - self.assertThat(result.output, Contains("There are no registered snaps.")) - - def test_list_registered(self): - self.command_name = "list-registered" - self.fake_store_account_info.mock.return_value = { - "account_id": "abcd", - "snaps": dict(), - } - - result = self.run_command([self.command_name]) - - self.assertThat(result.exit_code, Equals(0)) - self.assertThat(result.output, Contains("There are no registered snaps.")) - - def test_registered(self): - self.command_name = "registered" - self.fake_store_account_info.mock.return_value = { - "account_id": "abcd", - "snaps": dict(), - } - - result = self.run_command([self.command_name]) - - self.assertThat(result.exit_code, Equals(0)) - self.assertThat(result.output, Contains("There are no registered snaps.")) - - def test_list_successfully(self): - self.fake_store_account_info.mock.return_value = { - "snaps": { - "16": { - "foo": { - "status": "Approved", - "snap-id": "a_snap_id", - "private": False, - "since": "2016-12-12T01:01:01Z", - "price": "9.99", - }, - "bar": { - "status": "ReviewPending", - "snap-id": "another_snap_id", - "private": True, - "since": "2016-12-12T01:01:01Z", - "price": None, - }, - "baz": { - "status": "Approved", - "snap-id": "yet_another_snap_id", - "private": True, - "since": "2016-12-12T02:02:02Z", - "price": "6.66", - }, - "boing": { - "status": "Approved", - "snap-id": "boing_snap_id", - "private": False, - "since": "2016-12-12T03:03:03Z", - "price": None, - }, - } - } - } - - result = self.run_command([self.command_name]) - - self.assertThat(result.exit_code, Equals(0)) - self.assertThat( - result.output, - Contains( - dedent( - """\ - Name Since Visibility Price Notes - baz 2016-12-12T02:02:02Z private 6.66 - - boing 2016-12-12T03:03:03Z public - - - foo 2016-12-12T01:01:01Z public 9.99 -""" - ) - ), - ) diff --git a/tests/legacy/unit/commands/test_register.py b/tests/legacy/unit/commands/test_register.py deleted file mode 100644 index a30495126e..0000000000 --- a/tests/legacy/unit/commands/test_register.py +++ /dev/null @@ -1,132 +0,0 @@ -# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- -# -# Copyright (C) 2016-2017 Canonical Ltd -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License version 3 as -# published by the Free Software Foundation. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -from unittest import mock - -from simplejson.scanner import JSONDecodeError -from testtools.matchers import Contains, Equals, Not - -from snapcraft_legacy import storeapi - -from . import FAKE_UNAUTHORIZED_ERROR, FakeStoreCommandsBaseTestCase - - -class RegisterTestCase(FakeStoreCommandsBaseTestCase): - def test_register_without_name_must_error(self): - result = self.run_command(["register"]) - - self.assertThat(result.exit_code, Equals(2)) - self.assertThat(result.output, Contains("Usage:")) - - def test_register_without_login_must_ask(self): - self.fake_store_register.mock.side_effect = [ - FAKE_UNAUTHORIZED_ERROR, - None, - ] - - result = self.run_command( - ["register", "snap-name"], input="y\nuser@example.com\nsecret\n" - ) - self.assertThat( - result.output, Contains("You are required to login before continuing.") - ) - - def test_register_name_successfully(self): - result = self.run_command(["register", "test-snap"], input="y\n") - - self.assertThat(result.exit_code, Equals(0)) - self.assertThat(result.output, Contains("Registering test-snap")) - self.assertThat( - result.output, - Contains("Congrats! You are now the publisher of 'test-snap'."), - ) - self.assertThat( - result.output, - Not(Contains("Congratulations! You're now the publisher for 'test-snap'.")), - ) - self.fake_store_register.mock.assert_called_once_with( - "test-snap", is_private=False, series="16", store_id=None - ) - - def test_register_name_to_specific_store_successfully(self): - result = self.run_command( - ["register", "test-snap", "--store", "my-brand"], input="y\n" - ) - - self.assertThat(result.exit_code, Equals(0)) - self.assertThat(result.output, Contains("Registering test-snap")) - self.assertThat( - result.output, - Contains("Congrats! You are now the publisher of 'test-snap'."), - ) - self.assertThat( - result.output, - Not(Contains("Congratulations! You're now the publisher for 'test-snap'.")), - ) - self.fake_store_register.mock.assert_called_once_with( - "test-snap", is_private=False, series="16", store_id="my-brand" - ) - - def test_register_private_name_successfully(self): - result = self.run_command(["register", "test-snap", "--private"], input="y\n") - - self.assertThat(result.exit_code, Equals(0)) - self.assertThat( - result.output, - Contains("Even though this is private snap, you should think carefully"), - ) - self.assertThat(result.output, Contains("Registering test-snap")) - self.assertThat( - result.output, - Contains("Congrats! You are now the publisher of 'test-snap'."), - ) - self.assertThat( - result.output, - Not(Contains("Congratulations! You're now the publisher for 'test-snap'.")), - ) - self.fake_store_register.mock.assert_called_once_with( - "test-snap", is_private=True, series="16", store_id=None - ) - - def test_registration_failed(self): - response = mock.Mock() - response.json.side_effect = JSONDecodeError("mock-fail", "doc", 1) - self.fake_store_register.mock.side_effect = ( - storeapi.errors.StoreRegistrationError("test-snap", response) - ) - - raised = self.assertRaises( - storeapi.errors.StoreRegistrationError, - self.run_command, - ["register", "test-snap"], - input="y\n", - ) - - self.assertThat(str(raised), Equals("Registration failed.")) - - def test_registration_cancelled(self): - response = mock.Mock() - response.json.side_effect = JSONDecodeError("mock-fail", "doc", 1) - self.fake_store_register.mock.side_effect = ( - storeapi.errors.StoreRegistrationError("test-snap", response) - ) - - result = self.run_command(["register", "test-snap"], input="n\n") - - self.assertThat(result.exit_code, Equals(0)) - self.assertThat( - result.output, Contains("Thank you! 'test-snap' will remain available") - ) diff --git a/tests/unit/commands/conftest.py b/tests/unit/commands/conftest.py index 31358dbfa1..77764dcce0 100644 --- a/tests/unit/commands/conftest.py +++ b/tests/unit/commands/conftest.py @@ -24,3 +24,11 @@ def fake_client(mocker): client = mocker.patch("craft_store.BaseClient", autospec=True) mocker.patch("snapcraft.commands.store.client.get_client", return_value=client) return client + + +@pytest.fixture +def fake_confirmation_prompt(mocker): + """Fake the confirmation prompt.""" + return mocker.patch( + "snapcraft.utils.confirm_with_user", return_value=False, autospec=True + ) diff --git a/tests/unit/commands/store/test_client.py b/tests/unit/commands/store/test_client.py index fa3060d43f..dd0c85a68c 100644 --- a/tests/unit/commands/store/test_client.py +++ b/tests/unit/commands/store/test_client.py @@ -22,6 +22,7 @@ import requests from craft_store import endpoints +from snapcraft import errors from snapcraft.commands.store import client from snapcraft.utils import OSPlatform @@ -254,7 +255,7 @@ def test_login_otp(fake_client): @pytest.mark.usefixtures("fake_user_password", "fake_hostname") -def test_with_params(fake_client): +def test_login_with_params(fake_client): client.StoreClientCLI().login( ttl=20, acls=["package_access", "package_push"], @@ -279,3 +280,129 @@ def test_with_params(fake_client): password="fake-password", ) ] + + +########### +# Request # +########### + + +@pytest.mark.usefixtures("fake_user_password", "fake_hostname") +def test_login_from_401_request(fake_client): + fake_client.request.side_effect = [ + craft_store.errors.StoreServerError( + FakeResponse( + status_code=401, + content=json.dumps( + { + "error_list": [ + { + "code": "macaroon-needs-refresh", + "message": "Expired macaroon (age: 1234567 seconds)", + } + ] + } + ), + ) + ), + FakeResponse(status_code=200, content="text"), + ] + + client.StoreClientCLI().request("GET", "http://url.com/path") + + assert fake_client.request.mock_calls == [ + call("GET", "http://url.com/path"), + call("GET", "http://url.com/path"), + ] + assert fake_client.login.mock_calls == [ + call( + ttl=31536000, + permissions=[ + "package_access", + "package_manage", + "package_metrics", + "package_push", + "package_register", + "package_release", + "package_update", + ], + channels=None, + packages=[], + description="snapcraft@fake-host", + email="fake-username@acme.com", + password="fake-password", + ) + ] + + +def test_login_from_401_request_with_env_credentials(monkeypatch, fake_client): + monkeypatch.setenv(client.constants.ENVIRONMENT_STORE_CREDENTIALS, "foo") + fake_client.request.side_effect = [ + craft_store.errors.StoreServerError( + FakeResponse( + status_code=401, + content=json.dumps( + { + "error_list": [ + { + "code": "macaroon-needs-refresh", + "message": "Expired macaroon (age: 1234567 seconds)", + } + ] + } + ), + ) + ), + ] + + with pytest.raises(errors.SnapcraftError) as raised: + client.StoreClientCLI().request("GET", "http://url.com/path") + + assert str(raised.value) == ( + "Provided credentials are no longer valid for the Snap Store. " + "Regenerate them and try again." + ) + + +############ +# Register # +############ + + +@pytest.mark.parametrize("private", [True, False]) +@pytest.mark.parametrize("store_id", [None, "one-store", "other-store"]) +def test_register(fake_client, private, store_id): + client.StoreClientCLI().register("snap", is_private=private, store_id=store_id) + + expected_json = { + "snap_name": "snap", + "is_private": private, + "series": "16", + } + if store_id: + expected_json["store"] = store_id + assert fake_client.request.mock_calls == [ + call( + "POST", + "https://dashboard.snapcraft.io/dev/api/register-name/", + json=expected_json, + ) + ] + + +########################### +# Get Account Information # +########################### + + +def test_get_account_info(fake_client): + client.StoreClientCLI().get_account_info() + + assert fake_client.request.mock_calls == [ + call( + "GET", + "https://dashboard.snapcraft.io/dev/api/account", + headers={"Accept": "application/json"}, + ), + call().json(), + ] diff --git a/tests/unit/commands/test_names.py b/tests/unit/commands/test_names.py new file mode 100644 index 0000000000..e97a3c690c --- /dev/null +++ b/tests/unit/commands/test_names.py @@ -0,0 +1,240 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright 2022 Canonical Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import argparse +from textwrap import dedent +from typing import List +from unittest.mock import ANY, call + +import pytest + +from snapcraft import commands + +############ +# Fixtures # +############ + + +@pytest.fixture +def fake_store_register(mocker): + fake_client = mocker.patch( + "snapcraft.commands.store.StoreClientCLI.register", + autospec=True, + ) + return fake_client + + +@pytest.fixture +def fake_store_get_account_info(mocker): + # reduced payload + data = { + "snaps": { + "16": { + "test-snap-public": { + "private": False, + "since": "2016-07-26T20:18:32Z", + "status": "Approved", + }, + "test-snap-private": { + "private": True, + "since": "2016-07-26T20:18:32Z", + "status": "Approved", + }, + "test-snap-not-approved": { + "private": False, + "since": "2016-07-26T20:18:32Z", + "status": "Dispute", + }, + } + } + } + fake_client = mocker.patch( + "snapcraft.commands.store.StoreClientCLI.get_account_info", + autospec=True, + return_value=data, + ) + return fake_client + + +#################### +# Register Command # +#################### + + +@pytest.mark.usefixtures("memory_keyring") +def test_register_default(emitter, fake_confirmation_prompt, fake_store_register): + fake_confirmation_prompt.return_value = True + + cmd = commands.StoreRegisterCommand(None) + + cmd.run( + argparse.Namespace( + store_id=None, private=False, yes=False, **{"snap-name": "test-snap"} + ) + ) + + assert fake_store_register.mock_calls == [ + call(ANY, "test-snap", is_private=False, store_id=None) + ] + emitter.assert_recorded(["Registered 'test-snap'"]) + assert fake_confirmation_prompt.mock_calls == [ + call( + dedent( + """\ + We always want to ensure that users get the software they expect + for a particular name. + + If needed, we will rename snaps to ensure that a particular name + reflects the software most widely expected by our community. + + For example, most people would expect 'thunderbird' to be published by + Mozilla. They would also expect to be able to get other snaps of + Thunderbird as '$username-thunderbird'. + + Would you say that MOST users will expect 'test-snap' to come from + you, and be the software you intend to publish there?""" + ) + ) + ] + + +@pytest.mark.usefixtures("memory_keyring") +def test_register_yes(emitter, fake_store_register): + cmd = commands.StoreRegisterCommand(None) + + cmd.run( + argparse.Namespace( + store_id=None, private=False, yes=True, **{"snap-name": "test-snap"} + ) + ) + + assert fake_store_register.mock_calls == [ + call(ANY, "test-snap", is_private=False, store_id=None) + ] + emitter.assert_recorded(["Registered 'test-snap'"]) + + +@pytest.mark.usefixtures("memory_keyring") +def test_register_no(emitter, fake_confirmation_prompt, fake_store_register): + cmd = commands.StoreRegisterCommand(None) + + cmd.run( + argparse.Namespace( + store_id=None, private=False, yes=False, **{"snap-name": "test-snap"} + ) + ) + + assert fake_store_register.mock_calls == [] + emitter.assert_recorded(["Snap name 'test-snap' not registered"]) + assert fake_confirmation_prompt.mock_calls == [ + call( + dedent( + """\ + We always want to ensure that users get the software they expect + for a particular name. + + If needed, we will rename snaps to ensure that a particular name + reflects the software most widely expected by our community. + + For example, most people would expect 'thunderbird' to be published by + Mozilla. They would also expect to be able to get other snaps of + Thunderbird as '$username-thunderbird'. + + Would you say that MOST users will expect 'test-snap' to come from + you, and be the software you intend to publish there?""" + ) + ) + ] + + +@pytest.mark.usefixtures("memory_keyring", "fake_confirmation_prompt") +def test_register_private(emitter, fake_store_register): + cmd = commands.StoreRegisterCommand(None) + + cmd.run( + argparse.Namespace( + store_id=None, private=True, yes=False, **{"snap-name": "test-snap"} + ) + ) + + assert fake_store_register.mock_calls == [] + emitter.assert_recorded( + [ + dedent( + """\ + Even though this is private snap, you should think carefully about + the choice of name and make sure you are confident nobody else will + have a stronger claim to that particular name. If you are unsure + then we suggest you prefix the name with your developer identity, + As '$username-yoyodyne-www-site-content'.""" + ), + "Snap name 'test-snap' not registered", + ] + ) + + +@pytest.mark.usefixtures("memory_keyring") +def test_register_store_id(emitter, fake_store_register): + cmd = commands.StoreRegisterCommand(None) + + cmd.run( + argparse.Namespace( + store_id="1234", private=False, yes=True, **{"snap-name": "test-snap"} + ) + ) + + assert fake_store_register.mock_calls == [ + call(ANY, "test-snap", is_private=False, store_id="1234") + ] + emitter.assert_recorded(["Registered 'test-snap'"]) + + +################# +# Names Command # +################# + + +@pytest.mark.parametrize( + "command_class", + [ + commands.StoreNamesCommand, + commands.StoreLegacyListCommand, + commands.StoreLegacyListRegisteredCommand, + ], +) +@pytest.mark.usefixtures("memory_keyring") +def test_names(emitter, fake_store_get_account_info, command_class): + cmd = command_class(None) + + cmd.run( + argparse.Namespace( + store_id="1234", private=False, yes=True, **{"snap-name": "test-snap"} + ) + ) + + assert fake_store_get_account_info.mock_calls == [call(ANY)] + recorded: List[str] = [] + if command_class.hidden: + recorded = ["This command is deprecated: use 'names' instead"] + recorded.append( + dedent( + """\ + Name Since Visibility Notes + test-snap-private 2016-07-26T20:18:32Z private - + test-snap-public 2016-07-26T20:18:32Z public -""" + ) + ) + emitter.assert_recorded(recorded) From 80385919eac9e4a55e9a887085194b9b1790cf29 Mon Sep 17 00:00:00 2001 From: Facundo Batista Date: Tue, 26 Apr 2022 16:58:01 -0300 Subject: [PATCH 118/167] tests: use the Craft CLI pytest fixtures --- requirements-devel.txt | 2 +- requirements.txt | 2 +- tests/unit/cli/test_version.py | 4 +- tests/unit/commands/test_account.py | 14 ++-- tests/unit/commands/test_expand_extensions.py | 48 ++++++----- tests/unit/commands/test_list_extensions.py | 70 ++++++++-------- tests/unit/commands/test_names.py | 30 ++++--- tests/unit/commands/test_version.py | 2 +- tests/unit/conftest.py | 82 ------------------- tests/unit/extensions/test_extensions.py | 5 +- tests/unit/parts/test_parts.py | 6 +- 11 files changed, 88 insertions(+), 177 deletions(-) diff --git a/requirements-devel.txt b/requirements-devel.txt index 013153b88c..04cdb8f9db 100644 --- a/requirements-devel.txt +++ b/requirements-devel.txt @@ -9,7 +9,7 @@ charset-normalizer==2.0.12 click==8.1.2 codespell==2.1.0 coverage==6.3.2 -craft-cli==0.4.0 +craft-cli==0.5.0 craft-grammar==1.1.1 craft-parts==1.5.1 craft-providers==1.2.0 diff --git a/requirements.txt b/requirements.txt index f27b434d31..77aa153f99 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,7 @@ cffi==1.15.0 chardet==4.0.0 charset-normalizer==2.0.12 click==8.1.2 -craft-cli==0.4.0 +craft-cli==0.5.0 craft-grammar==1.1.1 craft-parts==1.5.1 craft-providers==1.2.0 diff --git a/tests/unit/cli/test_version.py b/tests/unit/cli/test_version.py index 087f27887b..c352240894 100644 --- a/tests/unit/cli/test_version.py +++ b/tests/unit/cli/test_version.py @@ -31,10 +31,10 @@ def test_version_command(mocker): def test_version_argument(mocker, emitter): mocker.patch.object(sys, "argv", ["cmd", "--version"]) cli.run() - emitter.assert_recorded([f"snapcraft {__version__}"]) + emitter.assert_message(f"snapcraft {__version__}") def test_version_argument_with_command(mocker, emitter): mocker.patch.object(sys, "argv", ["cmd", "--version", "version"]) cli.run() - emitter.assert_recorded([f"snapcraft {__version__}"]) + emitter.assert_message(f"snapcraft {__version__}") diff --git a/tests/unit/commands/test_account.py b/tests/unit/commands/test_account.py index a933458304..c1b7dfc623 100644 --- a/tests/unit/commands/test_account.py +++ b/tests/unit/commands/test_account.py @@ -54,7 +54,7 @@ def test_login(emitter, fake_store_login): ANY, ) ] - emitter.assert_recorded(["Login successful"]) + emitter.assert_message("Login successful") def test_login_with_file_fails(): @@ -104,7 +104,7 @@ def test_export_login(emitter, fake_store_login): ANY, ) ] - emitter.assert_recorded(["Exported login credentials:\nsecret"]) + emitter.assert_message("Exported login credentials:\nsecret") def test_export_login_file(new_dir, emitter, fake_store_login): @@ -126,7 +126,7 @@ def test_export_login_file(new_dir, emitter, fake_store_login): ANY, ) ] - emitter.assert_recorded(["Exported login credentials to 'target_file'"]) + emitter.assert_message("Exported login credentials to 'target_file'") login_file = new_dir / "target_file" assert login_file.exists() assert login_file.read_text() == "secret" @@ -155,7 +155,7 @@ def test_export_login_with_params(emitter, fake_store_login): ttl=ANY, ) ] - emitter.assert_recorded(["Exported login credentials:\nsecret"]) + emitter.assert_message("Exported login credentials:\nsecret") def test_export_login_with_experimental_fails(): @@ -203,7 +203,7 @@ def test_who(emitter, fake_client): channels: no restrictions expires: 2023-04-22T21:48:57.000Z""" ) - emitter.assert_recorded([expected_message]) + emitter.assert_message(expected_message) def test_who_with_attenuations(emitter, fake_client): @@ -228,7 +228,7 @@ def test_who_with_attenuations(emitter, fake_client): channels: edge, beta expires: 2023-04-22T21:48:57.000Z""" ) - emitter.assert_recorded([expected_message]) + emitter.assert_message(expected_message) ################## @@ -242,4 +242,4 @@ def test_logout(emitter, fake_client): cmd.run(argparse.Namespace()) assert fake_client.logout.mock_calls == [call()] - emitter.assert_recorded(["Credentials cleared"]) + emitter.assert_message("Credentials cleared") diff --git a/tests/unit/commands/test_expand_extensions.py b/tests/unit/commands/test_expand_extensions.py index 3b7efc6ad0..f218df2b8e 100644 --- a/tests/unit/commands/test_expand_extensions.py +++ b/tests/unit/commands/test_expand_extensions.py @@ -50,29 +50,27 @@ def test_command(new_dir, emitter): cmd = ExpandExtensionsCommand(None) cmd.run(Namespace()) - emitter.assert_recorded( - [ - dedent( - """\ - name: test-name - version: '0.1' - summary: testing extensions - description: expand a fake extension - base: core22 - apps: - app1: - command: app1 - plugs: - - fake-plug - parts: - part1: - plugin: nil - after: - - fake-extension/fake-part - fake-extension/fake-part: - plugin: nil - grade: fake-grade - """ - ) - ] + emitter.assert_message( + dedent( + """\ + name: test-name + version: '0.1' + summary: testing extensions + description: expand a fake extension + base: core22 + apps: + app1: + command: app1 + plugs: + - fake-plug + parts: + part1: + plugin: nil + after: + - fake-extension/fake-part + fake-extension/fake-part: + plugin: nil + grade: fake-grade + """ + ) ) diff --git a/tests/unit/commands/test_list_extensions.py b/tests/unit/commands/test_list_extensions.py index 2ea72a60b5..979c07ac18 100644 --- a/tests/unit/commands/test_list_extensions.py +++ b/tests/unit/commands/test_list_extensions.py @@ -27,25 +27,23 @@ def test_command(emitter, command): cmd = command(None) cmd.run(Namespace()) - emitter.assert_recorded( - [ - dedent( - """\ - Extension name Supported bases - ---------------- ----------------- - fake-extension core22 - flutter-beta core18 - flutter-dev core18 - flutter-master core18 - flutter-stable core18 - gnome-3-28 core18 - gnome-3-34 core18 - gnome-3-38 core20 - kde-neon core18, core20 - ros1-noetic core20 - ros2-foxy core20""" - ) - ] + emitter.assert_message( + dedent( + """\ + Extension name Supported bases + ---------------- ----------------- + fake-extension core22 + flutter-beta core18 + flutter-dev core18 + flutter-master core18 + flutter-stable core18 + gnome-3-28 core18 + gnome-3-34 core18 + gnome-3-38 core20 + kde-neon core18, core20 + ros1-noetic core20 + ros2-foxy core20""" + ) ) @@ -54,22 +52,20 @@ def test_command(emitter, command): def test_command_extension_dups(emitter, command): cmd = command(None) cmd.run(Namespace()) - emitter.assert_recorded( - [ - dedent( - """\ - Extension name Supported bases - ---------------- ----------------- - flutter-beta core18 - flutter-dev core18 - flutter-master core18 - flutter-stable core18 - gnome-3-28 core18 - gnome-3-34 core18 - gnome-3-38 core20 - kde-neon core18, core20 - ros1-noetic core20 - ros2-foxy core20, core22""" - ) - ] + emitter.assert_message( + dedent( + """\ + Extension name Supported bases + ---------------- ----------------- + flutter-beta core18 + flutter-dev core18 + flutter-master core18 + flutter-stable core18 + gnome-3-28 core18 + gnome-3-34 core18 + gnome-3-38 core20 + kde-neon core18, core20 + ros1-noetic core20 + ros2-foxy core20, core22""" + ) ) diff --git a/tests/unit/commands/test_names.py b/tests/unit/commands/test_names.py index e97a3c690c..f16d51c979 100644 --- a/tests/unit/commands/test_names.py +++ b/tests/unit/commands/test_names.py @@ -16,7 +16,6 @@ import argparse from textwrap import dedent -from typing import List from unittest.mock import ANY, call import pytest @@ -89,7 +88,7 @@ def test_register_default(emitter, fake_confirmation_prompt, fake_store_register assert fake_store_register.mock_calls == [ call(ANY, "test-snap", is_private=False, store_id=None) ] - emitter.assert_recorded(["Registered 'test-snap'"]) + emitter.assert_message("Registered 'test-snap'") assert fake_confirmation_prompt.mock_calls == [ call( dedent( @@ -124,7 +123,7 @@ def test_register_yes(emitter, fake_store_register): assert fake_store_register.mock_calls == [ call(ANY, "test-snap", is_private=False, store_id=None) ] - emitter.assert_recorded(["Registered 'test-snap'"]) + emitter.assert_message("Registered 'test-snap'") @pytest.mark.usefixtures("memory_keyring") @@ -138,7 +137,7 @@ def test_register_no(emitter, fake_confirmation_prompt, fake_store_register): ) assert fake_store_register.mock_calls == [] - emitter.assert_recorded(["Snap name 'test-snap' not registered"]) + emitter.assert_messages(["Snap name 'test-snap' not registered"]) assert fake_confirmation_prompt.mock_calls == [ call( dedent( @@ -171,18 +170,19 @@ def test_register_private(emitter, fake_store_register): ) assert fake_store_register.mock_calls == [] - emitter.assert_recorded( - [ - dedent( - """\ + emitter.assert_message( + dedent( + """\ Even though this is private snap, you should think carefully about the choice of name and make sure you are confident nobody else will have a stronger claim to that particular name. If you are unsure then we suggest you prefix the name with your developer identity, As '$username-yoyodyne-www-site-content'.""" - ), - "Snap name 'test-snap' not registered", - ] + ), + intermediate=True, + ) + emitter.assert_message( + "Snap name 'test-snap' not registered", ) @@ -199,7 +199,7 @@ def test_register_store_id(emitter, fake_store_register): assert fake_store_register.mock_calls == [ call(ANY, "test-snap", is_private=False, store_id="1234") ] - emitter.assert_recorded(["Registered 'test-snap'"]) + emitter.assert_message("Registered 'test-snap'") ################# @@ -226,10 +226,9 @@ def test_names(emitter, fake_store_get_account_info, command_class): ) assert fake_store_get_account_info.mock_calls == [call(ANY)] - recorded: List[str] = [] if command_class.hidden: - recorded = ["This command is deprecated: use 'names' instead"] - recorded.append( + emitter.assert_progress("This command is deprecated: use 'names' instead") + emitter.assert_message( dedent( """\ Name Since Visibility Notes @@ -237,4 +236,3 @@ def test_names(emitter, fake_store_get_account_info, command_class): test-snap-public 2016-07-26T20:18:32Z public -""" ) ) - emitter.assert_recorded(recorded) diff --git a/tests/unit/commands/test_version.py b/tests/unit/commands/test_version.py index 4c543ddc1c..0f2c11043a 100644 --- a/tests/unit/commands/test_version.py +++ b/tests/unit/commands/test_version.py @@ -23,4 +23,4 @@ def test_version_command(emitter): cmd = VersionCommand(None) cmd.run(Namespace()) - emitter.assert_recorded([f"snapcraft {__version__}"]) + emitter.assert_message(f"snapcraft {__version__}") diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py index 92f2375a0d..c42df88b81 100644 --- a/tests/unit/conftest.py +++ b/tests/unit/conftest.py @@ -14,95 +14,13 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -import os -import tempfile -from pathlib import Path from typing import Any, Dict, Optional, Tuple import pytest -from craft_cli import messages from snapcraft import extensions -# XXX: This can be removed once testing fixtures are provided by craft-cli. -class RecordingEmitter: - """Record what is shown using the emitter and provide a nice API for tests.""" - - def __init__(self): - self.progress = [] - self.message = [] - self.trace = [] - self.emitted = [] - self.raw = [] - - def record(self, level, text): - """Record the text for the specific level and in the general storages.""" - getattr(self, level).append(text) - self.emitted.append(text) - self.raw.append((level, text)) - - def _check(self, expected, storage): - """Really verify messages.""" - for pos, recorded_msg in enumerate(storage): - if recorded_msg == expected[0]: - break - else: - raise AssertionError(f"Initial test message not found in {storage}") - - recorded = storage[pos : pos + len(expected)] # pylint: disable=W0631 - assert recorded == expected - - def assert_recorded(self, expected): - """Verify that the given messages were recorded consecutively.""" - self._check(expected, self.emitted) - - def assert_recorded_raw(self, expected): - """Verify that the given messages (with specific level) were recorded consecutively.""" - self._check(expected, self.raw) - - -@pytest.fixture(autouse=True) -def init_emitter(): - """Ensure emit is always clean, and initted (in test mode). - Note that the `init` is done in the current instance that all modules already - acquired. - """ - # init with a custom log filepath so user directories are not involved here; note that - # we're not using pytest's standard tmp_path as Emitter would write logs there, and in - # effect we would be polluting that temporary directory (potentially messing with - # tests, that may need that empty), so we use another one. - temp_fd, temp_logfile = tempfile.mkstemp(prefix="emitter-logs") - os.close(temp_fd) - temp_logfile = Path(temp_logfile) - - messages.TESTMODE = True - messages.emit.init( - messages.EmitterMode.QUIET, - "test-emitter", - "Hello world", - log_filepath=temp_logfile, - ) - yield - # end machinery (just in case it was not ended before; note it's ok to "double end") - messages.emit.ended_ok() - - -@pytest.fixture -def emitter(monkeypatch): - """Helper to test everything that was shown using craft-cli Emitter.""" - rec = RecordingEmitter() - monkeypatch.setattr( - messages.emit, "message", lambda text, **k: rec.record("message", text) - ) - monkeypatch.setattr( - messages.emit, "progress", lambda text: rec.record("progress", text) - ) - monkeypatch.setattr(messages.emit, "trace", lambda text: rec.record("trace", text)) - - return rec - - @pytest.fixture def fake_extension(): """Basic extension.""" diff --git a/tests/unit/extensions/test_extensions.py b/tests/unit/extensions/test_extensions.py index 063ca16b0f..e1ddb89ce4 100644 --- a/tests/unit/extensions/test_extensions.py +++ b/tests/unit/extensions/test_extensions.py @@ -219,6 +219,7 @@ def test_apply_extension_experimental_with_environment(emitter, monkeypatch): # Should not raise. extensions.apply_extensions(yaml_data, arch="amd64", target_arch="amd64") - emitter.assert_recorded( - ["*EXPERIMENTAL* extension 'fake-extension-experimental' enabled"] + emitter.assert_message( + "*EXPERIMENTAL* extension 'fake-extension-experimental' enabled", + intermediate=True, ) diff --git a/tests/unit/parts/test_parts.py b/tests/unit/parts/test_parts.py index 2fa28c4cf5..ea43c36f26 100644 --- a/tests/unit/parts/test_parts.py +++ b/tests/unit/parts/test_parts.py @@ -63,7 +63,7 @@ def test_parts_lifecycle_run(mocker, parts_data, step_name, new_dir, emitter): project_vars={"version": "1", "grade": "stable"}, ) ] - emitter.assert_recorded([f"Executing parts lifecycle: {step_name} p1"]) + emitter.assert_progress(f"Executing parts lifecycle: {step_name} p1") def test_parts_lifecycle_run_bad_step(parts_data, new_dir): @@ -137,7 +137,7 @@ def test_parts_lifecycle_clean(parts_data, new_dir, emitter): project_vars={"version": "1", "grade": "stable"}, ) lifecycle.clean(part_names=None) - emitter.assert_recorded(["Cleaning all parts"]) + emitter.assert_message("Cleaning all parts", intermediate=True) def test_parts_lifecycle_clean_parts(parts_data, new_dir, emitter): @@ -154,7 +154,7 @@ def test_parts_lifecycle_clean_parts(parts_data, new_dir, emitter): project_vars={"version": "1", "grade": "stable"}, ) lifecycle.clean(part_names=["p1"]) - emitter.assert_recorded(["Cleaning parts: p1"]) + emitter.assert_message("Cleaning parts: p1", intermediate=True) def test_parts_lifecycle_initialize_with_no_base( From 0244836eb7e8bcad658ac944fad1b768430f0e4c Mon Sep 17 00:00:00 2001 From: Claudio Matsuoka Date: Tue, 26 Apr 2022 22:23:49 -0300 Subject: [PATCH 119/167] requirements: unpin pyyaml and update dependencies Unpin mccabe and PyYAML, allow libyaml binding with python 3.10. Signed-off-by: Claudio Matsuoka --- requirements-devel.txt | 14 +++++++------- requirements.txt | 6 +++--- setup.py | 3 +-- 3 files changed, 11 insertions(+), 12 deletions(-) diff --git a/requirements-devel.txt b/requirements-devel.txt index 013153b88c..1d051f0d05 100644 --- a/requirements-devel.txt +++ b/requirements-devel.txt @@ -9,7 +9,7 @@ charset-normalizer==2.0.12 click==8.1.2 codespell==2.1.0 coverage==6.3.2 -craft-cli==0.4.0 +craft-cli==0.5.0 craft-grammar==1.1.1 craft-parts==1.5.1 craft-providers==1.2.0 @@ -20,7 +20,7 @@ dill==0.3.4 distro==1.7.0 docutils==0.18.1 extras==1.0.0 -fixtures==3.0.0 +fixtures==4.0.0 flake8==4.0.1 gnupg==2.3.1 httplib2==0.20.4 @@ -53,7 +53,7 @@ plaster-pastedeploy==0.7 platformdirs==2.5.2 pluggy==1.0.0 progressbar==2.5 -protobuf==3.20.0 +protobuf==3.20.1 psutil==5.9.0 ptyprocess==0.7.0 py==1.11.0 @@ -73,7 +73,7 @@ pymacaroons==0.13.0 pyparsing==3.0.8 pyramid==2.0 pyRFC3339==1.1 -pytest==7.1.1 +pytest==7.1.2 pytest-cov==3.0.0 pytest-mock==3.7.0 pytest-subprocess==1.4.1 @@ -81,7 +81,7 @@ python-dateutil==2.8.2 python-debian==0.1.43 pytz==2022.1 pyxdg==0.27 -PyYAML==5.3 +PyYAML==6.0 raven==6.10.0 requests==2.27.1 requests-toolbelt==0.9.1 @@ -100,10 +100,10 @@ toml==0.10.2 tomli==2.0.1 translationstring==1.4 types-Deprecated==1.2.6 -types-PyYAML==6.0.6 +types-PyYAML==6.0.7 +types-requests==2.27.20 types-setuptools==57.4.14 types-tabulate==0.8.7 -types-requests==2.27.20 types-urllib3==1.26.13 typing-utils==0.1.0 typing_extensions==4.2.0 diff --git a/requirements.txt b/requirements.txt index f27b434d31..4967ea5204 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,7 @@ cffi==1.15.0 chardet==4.0.0 charset-normalizer==2.0.12 click==8.1.2 -craft-cli==0.4.0 +craft-cli==0.5.0 craft-grammar==1.1.1 craft-parts==1.5.1 craft-providers==1.2.0 @@ -31,7 +31,7 @@ oauthlib==3.2.0 overrides==6.1.0 platformdirs==2.5.2 progressbar==2.5 -protobuf==3.20.0 +protobuf==3.20.1 psutil==5.9.0 pycparser==2.21 pydantic==1.9.0 @@ -45,7 +45,7 @@ python-dateutil==2.8.2 python-debian==0.1.43 pytz==2022.1 pyxdg==0.27 -PyYAML==5.3 +PyYAML==6.0 raven==6.10.0 requests==2.27.1 requests-toolbelt==0.9.1 diff --git a/setup.py b/setup.py index 1edfb07d7e..eb5c449222 100755 --- a/setup.py +++ b/setup.py @@ -59,7 +59,6 @@ def recursive_data_files(directory, install_directory): scripts = [] dev_requires = [ - "mccabe<0.7.0", # to resolve version conflict "black", "codespell", "coverage", @@ -113,7 +112,7 @@ def recursive_data_files(directory, install_directory): "pyelftools", "pymacaroons", "pyxdg", - "pyyaml==5.3", + "pyyaml", "raven", "requests-toolbelt", "requests-unixsocket", From 62b890daa8dcf62e21cc8081159c5ea753759ed8 Mon Sep 17 00:00:00 2001 From: Claudio Matsuoka Date: Tue, 26 Apr 2022 23:45:57 -0300 Subject: [PATCH 120/167] tests: adjust regexp to match libyaml error message Signed-off-by: Claudio Matsuoka --- tests/legacy/unit/project/test_project_info.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/legacy/unit/project/test_project_info.py b/tests/legacy/unit/project/test_project_info.py index 29d279c6ee..8b1d5c9713 100644 --- a/tests/legacy/unit/project/test_project_info.py +++ b/tests/legacy/unit/project/test_project_info.py @@ -177,7 +177,7 @@ def test_tab_in_yaml(self): self.assertThat( raised.message, MatchesRegex( - "found a tab character that violate (indentation|intendation)" + "found a tab character that violates? (indentation|intendation)" " on line 5, column 1" ), ) From 21c37843774483be1872cba459106510b7d35bfb Mon Sep 17 00:00:00 2001 From: Claudio Matsuoka Date: Wed, 27 Apr 2022 15:03:17 -0300 Subject: [PATCH 121/167] meta: dump unicode yaml data Set yaml dumping to allow unicode. This fixes formatting of multiline text in e.g. snap.yaml when it contains unicode characters. Signed-off-by: Claudio Matsuoka --- snapcraft/meta/snap_yaml.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/snapcraft/meta/snap_yaml.py b/snapcraft/meta/snap_yaml.py index bdbb77e0ea..1660a1a4c7 100644 --- a/snapcraft/meta/snap_yaml.py +++ b/snapcraft/meta/snap_yaml.py @@ -180,7 +180,11 @@ def write(project: Project, prime_dir: Path, *, arch: str): yaml.add_representer(str, _repr_str, Dumper=yaml.SafeDumper) yaml_data = snap_metadata.yaml( - by_alias=True, exclude_none=True, sort_keys=False, width=1000 + by_alias=True, + exclude_none=True, + allow_unicode=True, + sort_keys=False, + width=1000, ) snap_yaml = meta_dir / "snap.yaml" From 09d2e4479d29d17f76aa8fc6e211f4c3aef83c1d Mon Sep 17 00:00:00 2001 From: Sergio Schvezov Date: Wed, 27 Apr 2022 17:24:49 -0300 Subject: [PATCH 122/167] commands: release and close Signed-off-by: Sergio Schvezov --- snapcraft/cli.py | 4 + snapcraft/commands/__init__.py | 3 + snapcraft/commands/manage.py | 165 +++++++++++ snapcraft/commands/store/client.py | 43 +++ snapcraft_legacy/_store.py | 22 -- snapcraft_legacy/cli/store.py | 150 ---------- snapcraft_legacy/storeapi/_dashboard_api.py | 21 -- snapcraft_legacy/storeapi/_store_client.py | 3 - tests/legacy/unit/commands/test_close.py | 108 ------- tests/legacy/unit/commands/test_release.py | 280 ------------------- tests/legacy/unit/store/test_store_client.py | 76 ----- tests/spread/general/store/task.yaml | 2 +- tests/unit/commands/store/test_client.py | 65 +++++ tests/unit/commands/test_manage.py | 178 ++++++++++++ 14 files changed, 459 insertions(+), 661 deletions(-) create mode 100644 snapcraft/commands/manage.py delete mode 100644 tests/legacy/unit/commands/test_close.py delete mode 100644 tests/legacy/unit/commands/test_release.py create mode 100644 tests/unit/commands/test_manage.py diff --git a/snapcraft/cli.py b/snapcraft/cli.py index 5afd984f6a..efaf9d60ab 100644 --- a/snapcraft/cli.py +++ b/snapcraft/cli.py @@ -61,6 +61,10 @@ commands.StoreLegacyListCommand, ], ), + craft_cli.CommandGroup( + "Store Snap Release Management", + [commands.StoreReleaseCommand, commands.StoreCloseCommand], + ), craft_cli.CommandGroup( "Extensions", [ diff --git a/snapcraft/commands/__init__.py b/snapcraft/commands/__init__.py index d31efd3c39..4d68a9ab12 100644 --- a/snapcraft/commands/__init__.py +++ b/snapcraft/commands/__init__.py @@ -36,6 +36,7 @@ SnapCommand, StageCommand, ) +from .manage import StoreCloseCommand, StoreReleaseCommand from .names import ( StoreLegacyListCommand, StoreLegacyListRegisteredCommand, @@ -53,6 +54,7 @@ "PullCommand", "SnapCommand", "StageCommand", + "StoreCloseCommand", "StoreLoginCommand", "StoreNamesCommand", "StoreExportLoginCommand", @@ -60,6 +62,7 @@ "StoreRegisterCommand", "StoreLegacyListCommand", "StoreLegacyListRegisteredCommand", + "StoreReleaseCommand", "StoreWhoAmICommand", "ExtensionsCommand", "ListExtensionsCommand", diff --git a/snapcraft/commands/manage.py b/snapcraft/commands/manage.py new file mode 100644 index 0000000000..8e29f57ae4 --- /dev/null +++ b/snapcraft/commands/manage.py @@ -0,0 +1,165 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright 2022 Canonical Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +"""Snapcraft Store Account management commands.""" + +import textwrap +from typing import TYPE_CHECKING + +from craft_cli import BaseCommand, emit +from overrides import overrides + +from snapcraft import errors, utils + +from . import store + +if TYPE_CHECKING: + import argparse + + +class StoreReleaseCommand(BaseCommand): + """Command to release a snap on the Snap Store.""" + + name = "release" + help_msg = "Release to the store" + overview = textwrap.dedent( + """ + Release on to the selected store . + is a comma separated list of valid channels on the store. + + The must exist on the store, to see available revisions run + `snapcraft list-revisions `. + + The channel map will be displayed after the operation takes place. To see + the status map at any other time run `snapcraft status `. + + The format for channels is `[/][/]` where + + - is used to have long term release channels. It is implicitly + set to `latest`. If this snap requires one, it can be created by + request by having a conversation on https://forum.snapcraft.io + under the *store* category. + - is mandatory and can be either `stable`, `candidate`, `beta` + or `edge`. + - is optional and dynamically creates a channel with a + specific expiration date. + + Examples: + snapcraft release my-snap 8 stable + snapcraft release my-snap 8 stable/my-branch + snapcraft release my-snap 9 beta,edge + snapcraft release my-snap 9 lts-channel/stable + snapcraft release my-snap 9 lts-channel/stable/my-branch""" + ) + + @overrides + def fill_parser(self, parser: "argparse.ArgumentParser") -> None: + parser.add_argument( + "name", + type=str, + help="The snap name to release", + ) + parser.add_argument( + "revision", + type=int, + help="The revision to release", + ) + parser.add_argument( + "channels", + type=str, + help="The comma separated list of channels to release to", + ) + parser.add_argument( + "--progressive", + dest="progressive_percentage", + type=int, + default=None, + help="set a release progression to a certain percentage [0<=x<=100]", + ) + + @overrides + def run(self, parsed_args): + channels = parsed_args.channels.split(",") + + store.StoreClientCLI().release( + snap_name=parsed_args.name, + revision=parsed_args.revision, + channels=channels, + progressive_percentage=parsed_args.progressive_percentage, + ) + + humanized_channels = utils.humanize_list(channels, conjunction="and") + emit.message( + f"Released {parsed_args.name!r} " + f"revision {parsed_args.revision!r} " + f"to channels: {humanized_channels}" + ) + + +class StoreCloseCommand(BaseCommand): + """Command to close a channel for a snap on the Snap Store.""" + + name = "close" + help_msg = "Close for on the store" + overview = textwrap.dedent( + """ + Closing a channel allows the that is closed to track the + channel that follows it in the channel release chain. + As such closing the 'candidate' channel would make it track the + 'stable' channel. + + Examples: + snapcraft close my-snap --channel beta + """ + ) + + @overrides + def fill_parser(self, parser: "argparse.ArgumentParser") -> None: + parser.add_argument( + "name", + type=str, + help="The snap name to release", + ) + parser.add_argument( + "channel", + type=str, + help="The channel to close", + ) + + @overrides + def run(self, parsed_args): + client = store.StoreClientCLI() + + # Account info request to retrieve the snap-id + account_info = client.get_account_info() + try: + snap_id = account_info["snaps"][store.constants.DEFAULT_SERIES][ + parsed_args.name + ]["snap-id"] + except KeyError as key_error: + emit.trace(f"{key_error!r} no found in {account_info!r}") + raise errors.SnapcraftError( + f"{parsed_args.name!r} not found or not owned by this account" + ) from key_error + + client.close( + snap_id=snap_id, + channel=parsed_args.channel, + ) + + emit.message( + f"Channel {parsed_args.channel!r} for {parsed_args.name!r} is now closed" + ) diff --git a/snapcraft/commands/store/client.py b/snapcraft/commands/store/client.py index bed611bac3..0489e6bc8f 100644 --- a/snapcraft/commands/store/client.py +++ b/snapcraft/commands/store/client.py @@ -252,3 +252,46 @@ def get_account_info( self._base_url + "/dev/api/account", headers={"Accept": "application/json"}, ).json() + + def release( + self, + snap_name: str, + *, + revision: int, + channels: Sequence[str], + progressive_percentage: Optional[int] = None, + ) -> None: + """Register snap_name with the Snap Store. + + :param snap_name: the name of the snap to register with the Snap Store + :param revision: the revision of the snap to release + :param channels: the channels to release to + :param progressive_percentage: enable progressive releases up to a given percentage + """ + data: Dict[str, Any] = { + "name": snap_name, + "revision": str(revision), + "channels": channels, + } + if progressive_percentage is not None and progressive_percentage != 100: + data["progressive"] = { + "percentage": progressive_percentage, + "paused": False, + } + self.request( + "POST", + self._base_url + "/dev/api/snap-release/", + json=data, + ) + + def close(self, snap_id: str, channel: str) -> None: + """Close channel for snap_id. + + :param snap_id: the id for the snap to close + :param channel: the channel to close + """ + self.request( + "POST", + self._base_url + f"/dev/api/snaps/{snap_id}/close", + json={"channels": [channel]}, + ) diff --git a/snapcraft_legacy/_store.py b/snapcraft_legacy/_store.py index a40bd80928..bf8658bcdb 100644 --- a/snapcraft_legacy/_store.py +++ b/snapcraft_legacy/_store.py @@ -337,12 +337,6 @@ class StoreClientCLI(storeapi.StoreClient): # during upload into this class using click. # TODO use an instance of this class directly from snapcraft_legacy.cli.store - @_login_wrapper - def close_channels( - self, *, snap_id: str, channel_names: List[str] - ) -> Dict[str, Any]: - return super().close_channels(snap_id=snap_id, channel_names=channel_names) - @_login_wrapper def get_metrics( self, *, filters: List[MetricsFilter], snap_name: str @@ -385,22 +379,6 @@ def register( ) -> None: super().register(snap_name=snap_name, is_private=is_private, store_id=store_id) - @_login_wrapper - def release( - self, - *, - snap_name: str, - revision: str, - channels: List[str], - progressive_percentage: Optional[int] = None, - ) -> Dict[str, Any]: - return super().release( - snap_name=snap_name, - revision=revision, - channels=channels, - progressive_percentage=progressive_percentage, - ) - @_login_wrapper @_register_wrapper def upload( diff --git a/snapcraft_legacy/cli/store.py b/snapcraft_legacy/cli/store.py index 95e9344076..d0dbb4420a 100644 --- a/snapcraft_legacy/cli/store.py +++ b/snapcraft_legacy/cli/store.py @@ -169,102 +169,6 @@ def upload_metadata(snap_file, force): snapcraft_legacy.upload_metadata(snap_file, force) -@storecli.command() -@click.argument("snap-name", metavar="") -@click.argument("revision", metavar="") -@click.argument("channels", metavar="") -@click.option( - "--progressive", - type=click.IntRange(0, 100), - default=100, - metavar="", - help="set a release progression to a certain percentage.", -) -@click.option( - "--experimental-progressive-releases", - is_flag=True, - help="*EXPERIMENTAL* Enables 'progressive releases'.", - envvar="SNAPCRAFT_EXPERIMENTAL_PROGRESSIVE_RELEASES", -) -def release( - snap_name, - revision, - channels, - progressive: Optional[int], - experimental_progressive_releases: bool, -) -> None: - """Release on to the selected store . - is a comma separated list of valid channels on the - store. - - The must exist on the store, to see available revisions - run `snapcraft list-revisions `. - - The channel map will be displayed after the operation takes place. - To see the status map at any other time run `snapcraft status `. - - The format for channels is `[/][/]` where - - \b - - is used to have long term release channels. It is implicitly - set to `latest`. If this snap requires one, it can be created by - request by having a conversation on https://forum.snapcraft.io - under the *store* category. - - is mandatory and can be either `stable`, `candidate`, `beta` - or `edge`. - - is optional and dynamically creates a channel with a - specific expiration date. - - \b - Examples: - snapcraft release my-snap 8 stable - snapcraft release my-snap 8 stable/my-branch - snapcraft release my-snap 9 beta,edge - snapcraft release my-snap 9 lts-channel/stable - snapcraft release my-snap 9 lts-channel/stable/my-branch - """ - # If progressive is set to 100, treat it as None. - if progressive == 100: - progressive = None - - if progressive is not None and not experimental_progressive_releases: - raise click.UsageError( - "--progressive requires --experimental-progressive-releases." - ) - elif progressive: - os.environ["SNAPCRAFT_EXPERIMENTAL_PROGRESSIVE_RELEASES"] = "Y" - echo.warning("*EXPERIMENTAL* progressive releases in use.") - - store_client_cli = StoreClientCLI() - release_data = store_client_cli.release( - snap_name=snap_name, - revision=revision, - channels=channels.split(","), - progressive_percentage=progressive, - ) - snap_channel_map = store_client_cli.get_snap_channel_map(snap_name=snap_name) - architectures_for_revision = snap_channel_map.get_revision( - int(revision) - ).architectures - tracks = [storeapi.channels.Channel(c).track for c in channels.split(",")] - click.echo( - get_tabulated_channel_map( - snap_channel_map, tracks=tracks, architectures=architectures_for_revision - ) - ) - - opened_channels = release_data.get("opened_channels", []) - if len(opened_channels) == 1: - echo.info(f"The {opened_channels[0]!r} channel is now open.") - elif len(opened_channels) > 1: - channels = ("{!r}".format(channel) for channel in opened_channels[:-1]) - echo.info( - "The {} and {!r} channels are now open.".format( - ", ".join(channels), opened_channels[-1] - ) - ) - - @storecli.command() @click.argument("snap-name", metavar="") @click.option( @@ -368,60 +272,6 @@ def promote(snap_name, from_channel, to_channel, yes): echo.wrapped("Channel promotion cancelled") -@storecli.command() -@click.argument("snap-name", metavar="") -@click.argument("channels", metavar="...", nargs=-1) -def close(snap_name, channels): - """Close for . - Closing a channel allows the that is closed to track the channel - that follows it in the channel release chain. As such closing the - 'candidate' channel would make it track the 'stable' channel. - - The channel map will be displayed after the operation takes place. - - \b - Examples: - snapcraft close my-snap beta - snapcraft close my-snap beta edge - """ - store = storeapi.StoreClient() - account_info = store.get_account_information() - - try: - snap_id = account_info["snaps"][storeapi.constants.DEFAULT_SERIES][snap_name][ - "snap-id" - ] - except KeyError: - raise storeapi.errors.StoreChannelClosingPermissionError( - snap_name, storeapi.constants.DEFAULT_SERIES - ) - - # Returned closed_channels cannot be trusted as it returns risks. - store.close_channels(snap_id=snap_id, channel_names=channels) - if len(channels) == 1: - msg = "The {} channel is now closed.".format(channels[0]) - else: - msg = "The {} and {} channels are now closed.".format( - ", ".join(channels[:-1]), channels[-1] - ) - - snap_channel_map = store.get_snap_channel_map(snap_name=snap_name) - if snap_channel_map.channel_map: - closed_tracks = {storeapi.channels.Channel(c).track for c in channels} - existing_architectures = snap_channel_map.get_existing_architectures() - - click.echo( - get_tabulated_channel_map( - snap_channel_map, - architectures=existing_architectures, - tracks=closed_tracks, - ) - ) - click.echo() - - echo.info(msg) - - @storecli.command() @click.option( "--experimental-progressive-releases", diff --git a/snapcraft_legacy/storeapi/_dashboard_api.py b/snapcraft_legacy/storeapi/_dashboard_api.py index 9a8d5b5488..b9c75506b2 100644 --- a/snapcraft_legacy/storeapi/_dashboard_api.py +++ b/snapcraft_legacy/storeapi/_dashboard_api.py @@ -334,27 +334,6 @@ def snap_status(self, snap_id, series, arch): return response_json - def close_channels(self, snap_id, channel_names): - url = "/dev/api/snaps/{}/close".format(snap_id) - data = {"channels": channel_names} - headers = {"Content-Type": "application/json", "Accept": "application/json"} - - try: - response = self.post(url, json=data, headers=headers) - except craft_store.errors.StoreServerError as craft_error: - raise errors.StoreChannelClosingError(craft_error.response) from craft_error - - try: - results = response.json() - except (JSONDecodeError, KeyError): - logger.debug( - "Invalid response from the server on channel closing:\n" - f"{response.status_code} {response.reason}\n{response.content}" - ) - raise errors.StoreChannelClosingError(response) - - return results["closed_channels"], results["channel_map_tree"] - def sign_developer_agreement(self, latest_tos_accepted=False): data = {"latest_tos_accepted": latest_tos_accepted} try: diff --git a/snapcraft_legacy/storeapi/_store_client.py b/snapcraft_legacy/storeapi/_store_client.py index 5042076f57..cccfe615cd 100644 --- a/snapcraft_legacy/storeapi/_store_client.py +++ b/snapcraft_legacy/storeapi/_store_client.py @@ -248,9 +248,6 @@ def get_validation_sets( ) -> validation_sets.ValidationSets: return self.dashboard.get_validation_sets(name=name, sequence=sequence) - def close_channels(self, snap_id, channel_names): - return self.dashboard.close_channels(snap_id, channel_names) - @classmethod def download( cls, diff --git a/tests/legacy/unit/commands/test_close.py b/tests/legacy/unit/commands/test_close.py deleted file mode 100644 index 81a93212bb..0000000000 --- a/tests/legacy/unit/commands/test_close.py +++ /dev/null @@ -1,108 +0,0 @@ -# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- -# -# Copyright 2016-2021 Canonical Ltd -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License version 3 as -# published by the Free Software Foundation. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -from textwrap import dedent - -import fixtures -from testtools.matchers import Contains, Equals - -import snapcraft_legacy.storeapi.errors -from snapcraft_legacy import storeapi - -from . import FakeStoreCommandsBaseTestCase - - -class CloseCommandTestCase(FakeStoreCommandsBaseTestCase): - def setUp(self): - super().setUp() - - self.useFixture( - fixtures.MockPatchObject( - storeapi._dashboard_api.DashboardAPI, - "close_channels", - return_value=(list(), dict()), - ) - ) - - def test_close_missing_permission(self): - self.fake_store_account_info.mock.return_value = { - "account_id": "abcd", - "snaps": {}, - } - - raised = self.assertRaises( - snapcraft_legacy.storeapi.errors.StoreChannelClosingPermissionError, - self.run_command, - ["close", "foo", "beta"], - ) - - self.assertThat( - str(raised), - Equals( - "Your account lacks permission to close channels for this snap. " - "Make sure the logged in account has upload permissions on " - "'foo' in series '16'." - ), - ) - - def test_close(self): - result = self.run_command(["close", "snap-test", "2.1/candidate"]) - - self.assertThat(result.exit_code, Equals(0)) - self.assertThat( - result.output, - Contains( - dedent( - """\ - Track Arch Channel Version Revision - 2.1 amd64 stable - - - candidate - - - beta 10 19 - edge ↑ ↑ - - The 2.1/candidate channel is now closed.""" - ) - ), - ) - - def test_close_no_revisions(self): - self.channel_map.channel_map = list() - - result = self.run_command(["close", "snap-test", "2.1/candidate"]) - - self.assertThat(result.exit_code, Equals(0)) - self.assertThat( - result.output.strip(), Equals("The 2.1/candidate channel is now closed.") - ) - - def test_close_multiple_channels(self): - result = self.run_command(["close", "snap-test", "2.1/stable", "2.1/edge/test"]) - - self.assertThat(result.exit_code, Equals(0)) - self.assertThat( - result.output, - Contains( - dedent( - """\ - Track Arch Channel Version Revision - 2.1 amd64 stable - - - candidate - - - beta 10 19 - edge ↑ ↑ - - The 2.1/stable and 2.1/edge/test channels are now closed.""" - ) - ), - ) diff --git a/tests/legacy/unit/commands/test_release.py b/tests/legacy/unit/commands/test_release.py deleted file mode 100644 index ba501da1a5..0000000000 --- a/tests/legacy/unit/commands/test_release.py +++ /dev/null @@ -1,280 +0,0 @@ -# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- -# -# Copyright 2016-2020 Canonical Ltd -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License version 3 as -# published by the Free Software Foundation. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -from textwrap import dedent - -from testtools.matchers import Contains, Equals - -from snapcraft_legacy.storeapi.v2.channel_map import ( - MappedChannel, - Progressive, - Revision, - SnapChannel, -) - -from . import FAKE_UNAUTHORIZED_ERROR, FakeStoreCommandsBaseTestCase - - -class ReleaseCommandTestCase(FakeStoreCommandsBaseTestCase): - def setUp(self): - super().setUp() - - self.fake_store_release.mock.return_value = {"opened_channels": ["2.1/beta"]} - - def test_release_without_snap_name_must_raise_exception(self): - result = self.run_command(["release"]) - - self.assertThat(result.exit_code, Equals(2)) - self.assertThat(result.output, Contains("Usage:")) - - def test_release(self): - result = self.run_command(["release", "nil-snap", "19", "2.1/beta"]) - - self.assertThat(result.exit_code, Equals(0)) - self.assertThat( - result.output, - Equals( - dedent( - """\ - Track Arch Channel Version Revision - 2.1 amd64 stable - - - candidate - - - beta 10 19 - edge ↑ ↑ - The '2.1/beta' channel is now open. - """ - ) - ), - ) - self.fake_store_release.mock.assert_called_once_with( - snap_name="nil-snap", - revision="19", - channels=["2.1/beta"], - progressive_percentage=None, - ) - - def test_progressive_release(self): - self.channel_map.channel_map[0].progressive.percentage = 10.0 - self.channel_map.channel_map[0].progressive.current_percentage = 5.0 - - result = self.run_command( - [ - "release", - "nil-snap", - "19", - "2.1/beta", - "--progressive", - "10", - "--experimental-progressive-releases", - ] - ) - - self.assertThat( - result.output, - Equals( - dedent( - """\ - *EXPERIMENTAL* progressive releases in use. - Track Arch Channel Version Revision Progress - 2.1 amd64 stable - - - - candidate - - - - beta - - - - 10 19 5→10% - edge ↑ ↑ - - The '2.1/beta' channel is now open. - """ - ) - ), - ) - self.fake_store_release.mock.assert_called_once_with( - snap_name="nil-snap", - revision="19", - channels=["2.1/beta"], - progressive_percentage=10, - ) - - def test_release_with_branch(self): - self.fake_store_release.mock.return_value = { - "opened_channels": ["stable/hotfix1"] - } - self.channel_map.channel_map.append( - MappedChannel( - channel="2.1/stable/hotfix1", - architecture="amd64", - expiration_date="2020-02-03T20:58:37Z", - revision=20, - progressive=Progressive( - paused=None, percentage=None, current_percentage=None - ), - ) - ) - self.channel_map.revisions.append( - Revision(architectures=["amd64"], revision=20, version="10hotfix") - ) - self.channel_map.snap.channels.append( - SnapChannel( - name="2.1/stable/hotfix1", - track="2.1", - risk="stable", - branch="hotfix1", - fallback="2.1/stable", - ) - ) - - result = self.run_command(["release", "nil-snap", "20", "2.1/stable/hotfix1"]) - - self.assertThat(result.exit_code, Equals(0)) - self.assertThat( - result.output, - Equals( - dedent( - """\ - Track Arch Channel Version Revision Expires at - 2.1 amd64 stable - - - stable/hotfix1 10hotfix 20 2020-02-03T20:58:37Z - candidate - - - beta 10 19 - edge ↑ ↑ - The 'stable/hotfix1' channel is now open. - """ - ) - ), - ) - - self.fake_store_release.mock.assert_called_once_with( - snap_name="nil-snap", - revision="20", - channels=["2.1/stable/hotfix1"], - progressive_percentage=None, - ) - - def test_progressive_release_with_branch(self): - self.fake_store_release.mock.return_value = { - "opened_channels": ["2.1/stable/hotfix1"] - } - self.channel_map.channel_map.append( - MappedChannel( - channel="2.1/stable/hotfix1", - architecture="amd64", - expiration_date="2020-02-03T20:58:37Z", - revision=20, - progressive=Progressive( - paused=None, percentage=80.0, current_percentage=None - ), - ) - ) - self.channel_map.revisions.append( - Revision(architectures=["amd64"], revision=20, version="10hotfix") - ) - self.channel_map.snap.channels.append( - SnapChannel( - name="2.1/stable/hotfix1", - track="2.1", - risk="stable", - branch="hotfix1", - fallback="2.1/stable", - ) - ) - - result = self.run_command( - [ - "release", - "--progressive", - "80", - "--experimental-progressive-releases", - "nil-snap", - "20", - "2.1/stable/hotfix1", - ] - ) - - self.assertThat(result.exit_code, Equals(0)) - self.assertThat( - result.output, - Equals( - dedent( - """\ - *EXPERIMENTAL* progressive releases in use. - Track Arch Channel Version Revision Progress Expires at - 2.1 amd64 stable - - - - stable/hotfix1 10hotfix 20 ?→80% 2020-02-03T20:58:37Z - candidate - - - - beta 10 19 - - edge ↑ ↑ - - The '2.1/stable/hotfix1' channel is now open. - """ - ) - ), - ) - self.fake_store_release.mock.assert_called_once_with( - snap_name="nil-snap", - revision="20", - channels=["2.1/stable/hotfix1"], - progressive_percentage=80, - ) - - def test_progressive_release_with_null_current_percentage(self): - self.channel_map.channel_map[0].progressive.percentage = 10.0 - self.channel_map.channel_map[0].progressive.current_percentage = None - - result = self.run_command( - [ - "release", - "nil-snap", - "19", - "2.1/beta", - "--progressive", - "10", - "--experimental-progressive-releases", - ] - ) - - self.assertThat( - result.output, - Equals( - dedent( - """\ - *EXPERIMENTAL* progressive releases in use. - Track Arch Channel Version Revision Progress - 2.1 amd64 stable - - - - candidate - - - - beta - - - - 10 19 ?→10% - edge ↑ ↑ - - The '2.1/beta' channel is now open. - """ - ) - ), - ) - self.fake_store_release.mock.assert_called_once_with( - snap_name="nil-snap", - revision="19", - channels=["2.1/beta"], - progressive_percentage=10, - ) - - def test_release_without_login_must_ask(self): - self.fake_store_release.mock.side_effect = [ - FAKE_UNAUTHORIZED_ERROR, - {"opened_channels": ["beta"]}, - ] - - result = self.run_command( - ["release", "nil-snap", "19", "beta"], input="user@example.com\nsecret\n" - ) - self.assertThat( - result.output, Contains("You are required to login before continuing.") - ) diff --git a/tests/legacy/unit/store/test_store_client.py b/tests/legacy/unit/store/test_store_client.py index c891dcea29..7c7ab0e2ca 100644 --- a/tests/legacy/unit/store/test_store_client.py +++ b/tests/legacy/unit/store/test_store_client.py @@ -831,82 +831,6 @@ def test_release_to_curly_braced_channel(self): ) -class CloseChannelsTestCase(StoreTestCase): - def setUp(self): - super().setUp() - self.fake_logger = fixtures.FakeLogger(level=logging.DEBUG) - self.useFixture(self.fake_logger) - - def test_close_invalid_data(self): - raised = self.assertRaises( - errors.StoreChannelClosingError, - self.client.close_channels, - "snap-id", - ["invalid"], - ) - self.assertThat( - str(raised), - Equals( - "Could not close channel: The 'channels' field content " "is not valid." - ), - ) - - def test_close_broken_store_plain(self): - # If the contract is broken by the Store, users will be have additional - # debug information available. - raised = self.assertRaises( - errors.StoreChannelClosingError, - self.client.close_channels, - "snap-id", - ["broken-plain"], - ) - self.assertThat(str(raised), Equals("Could not close channel: 200 OK")) - - expected_lines = [ - "Invalid response from the server on channel closing:", - "200 OK", - "b'plain data'", - ] - - actual_lines = [] - for line in self.fake_logger.output.splitlines(): - line = line.strip() - if line in expected_lines: - actual_lines.append(line) - - self.assertThat(actual_lines, Equals(expected_lines)) - - def test_close_successfully(self): - # Successfully closing a channels returns 'closed_channels' - # and 'channel_map_tree' from the Store. - closed_channels, channel_map_tree = self.client.close_channels( - "snap-id", ["beta"] - ) - self.assertThat(closed_channels, Equals(["beta"])) - self.assertThat( - channel_map_tree, - Equals( - { - "latest": { - "16": { - "amd64": [ - {"channel": "stable", "info": "none"}, - {"channel": "candidate", "info": "none"}, - { - "channel": "beta", - "info": "specific", - "revision": 42, - "version": "1.1", - }, - {"channel": "edge", "info": "tracking"}, - ] - } - } - } - ), - ) - - class GetSnapStatusTestCase(StoreTestCase): def setUp(self): super().setUp() diff --git a/tests/spread/general/store/task.yaml b/tests/spread/general/store/task.yaml index 81da481af9..5498c31b48 100644 --- a/tests/spread/general/store/task.yaml +++ b/tests/spread/general/store/task.yaml @@ -74,7 +74,7 @@ execute: | snapcraft release "${snap_name}" 1 edge # Progressive Release - snapcraft release --experimental-progressive-releases --progressive 50 "${snap_name}" 1 candidate + snapcraft release --progressive 50 "${snap_name}" 1 candidate # Close channel snapcraft close "${snap_name}" candidate diff --git a/tests/unit/commands/store/test_client.py b/tests/unit/commands/store/test_client.py index dd0c85a68c..e8828cab52 100644 --- a/tests/unit/commands/store/test_client.py +++ b/tests/unit/commands/store/test_client.py @@ -406,3 +406,68 @@ def test_get_account_info(fake_client): ), call().json(), ] + + +########### +# Release # +########### + + +@pytest.mark.parametrize("progressive_percentage", [None, 100]) +def test_release(fake_client, progressive_percentage): + client.StoreClientCLI().release( + snap_name="snap", + revision=10, + channels=["beta", "edge"], + progressive_percentage=progressive_percentage, + ) + + assert fake_client.request.mock_calls == [ + call( + "POST", + "https://dashboard.snapcraft.io/dev/api/snap-release/", + json={"name": "snap", "revision": "10", "channels": ["beta", "edge"]}, + ) + ] + + +def test_release_progressive(fake_client): + client.StoreClientCLI().release( + snap_name="snap", + revision=10, + channels=["beta", "edge"], + progressive_percentage=88, + ) + + assert fake_client.request.mock_calls == [ + call( + "POST", + "https://dashboard.snapcraft.io/dev/api/snap-release/", + json={ + "name": "snap", + "revision": "10", + "channels": ["beta", "edge"], + "progressive": {"percentage": 88, "paused": False}, + }, + ) + ] + + +######### +# Close # +######### + + +def test_close(fake_client): + client.StoreClientCLI().close( + snap_id="12345", + channel="edge", + ) + + assert fake_client.request.mock_calls == [ + call( + "POST", + "https://dashboard.snapcraft.io/dev/api/snaps/12345/close", + json={"channels": ["edge"]}, + ) + ] diff --git a/tests/unit/commands/test_manage.py b/tests/unit/commands/test_manage.py new file mode 100644 index 0000000000..7f415ab283 --- /dev/null +++ b/tests/unit/commands/test_manage.py @@ -0,0 +1,178 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright 2022 Canonical Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import argparse +from unittest.mock import ANY, call + +import pytest + +from snapcraft import commands, errors + +############ +# Fixtures # +############ + + +@pytest.fixture +def fake_store_release(mocker): + fake_client = mocker.patch( + "snapcraft.commands.store.StoreClientCLI.release", + autospec=True, + ) + return fake_client + + +@pytest.fixture +def fake_store_close(mocker): + fake_client = mocker.patch( + "snapcraft.commands.store.StoreClientCLI.close", + autospec=True, + ) + return fake_client + + +@pytest.fixture +def fake_store_get_account_info(mocker): + # reduced payload + data = { + "snaps": { + "16": { + "test-snap": { + "snap-id": "12345678", + }, + } + } + } + fake_client = mocker.patch( + "snapcraft.commands.store.StoreClientCLI.get_account_info", + autospec=True, + return_value=data, + ) + return fake_client + + +################### +# Release Command # +################### + + +@pytest.mark.usefixtures("memory_keyring") +def test_release(emitter, fake_store_release): + cmd = commands.StoreReleaseCommand(None) + + cmd.run( + argparse.Namespace( + name="test-snap", revision=10, channels="edge", progressive_percentage=None + ) + ) + + assert fake_store_release.mock_calls == [ + call( + ANY, + snap_name="test-snap", + revision=10, + channels=["edge"], + progressive_percentage=None, + ) + ] + emitter.assert_message("Released 'test-snap' revision 10 to channels: 'edge'") + + +@pytest.mark.usefixtures("memory_keyring") +def test_release_multiple_channels(emitter, fake_store_release): + cmd = commands.StoreReleaseCommand(None) + + cmd.run( + argparse.Namespace( + name="test-snap", + revision=10, + channels="edge,latest/stable,1.0/beta", + progressive_percentage=None, + ) + ) + + assert fake_store_release.mock_calls == [ + call( + ANY, + snap_name="test-snap", + revision=10, + channels=["edge", "latest/stable", "1.0/beta"], + progressive_percentage=None, + ) + ] + emitter.assert_message( + "Released 'test-snap' revision 10 to channels: '1.0/beta', 'edge', and 'latest/stable'" + ) + + +@pytest.mark.usefixtures("memory_keyring") +def test_release_progressive(emitter, fake_store_release): + cmd = commands.StoreReleaseCommand(None) + + cmd.run( + argparse.Namespace( + name="test-snap", revision=10, channels="edge", progressive_percentage=10 + ) + ) + + assert fake_store_release.mock_calls == [ + call( + ANY, + snap_name="test-snap", + revision=10, + channels=["edge"], + progressive_percentage=10, + ) + ] + emitter.assert_message("Released 'test-snap' revision 10 to channels: 'edge'") + + +################# +# Close Command # +################# + + +@pytest.mark.usefixtures("memory_keyring", "fake_store_get_account_info") +def test_close(emitter, fake_store_close): + cmd = commands.StoreCloseCommand(None) + + cmd.run(argparse.Namespace(name="test-snap", channel="edge")) + + assert fake_store_close.mock_calls == [ + call( + ANY, + snap_id="12345678", + channel="edge", + ) + ] + emitter.assert_message("Channel 'edge' for 'test-snap' is now closed") + + +@pytest.mark.usefixtures("memory_keyring", "fake_store_get_account_info") +def test_close_no_snap_id(emitter): + cmd = commands.StoreCloseCommand(None) + + with pytest.raises(errors.SnapcraftError) as raised: + cmd.run(argparse.Namespace(name="test-unknown-snap", channel="edge")) + + assert str(raised.value) == ( + "'test-unknown-snap' not found or not owned by this account" + ) + + emitter.assert_trace( + "KeyError('test-unknown-snap') no found in " + "{'snaps': {'16': {'test-snap': {'snap-id': '12345678'}}}}" + ) From 3334e2dcfa430a3c8d180480a23b031a38ccf849 Mon Sep 17 00:00:00 2001 From: Callahan Kovacs Date: Wed, 27 Apr 2022 11:17:51 -0500 Subject: [PATCH 123/167] plugs: install snaps from content plugs --- snapcraft/parts/lifecycle.py | 14 +++++++-- snapcraft/parts/parts.py | 9 ++---- snapcraft/projects.py | 18 +++++++++++ tests/unit/parts/test_lifecycle.py | 47 +++++++++++++++++++++++++++++ tests/unit/parts/test_parts.py | 48 ++++-------------------------- tests/unit/test_projects.py | 13 ++++++++ 6 files changed, 98 insertions(+), 51 deletions(-) diff --git a/snapcraft/parts/lifecycle.py b/snapcraft/parts/lifecycle.py index bffd893c35..50f66af9d6 100644 --- a/snapcraft/parts/lifecycle.py +++ b/snapcraft/parts/lifecycle.py @@ -20,7 +20,7 @@ import subprocess from dataclasses import dataclass from pathlib import Path -from typing import TYPE_CHECKING, Any, Dict, List, cast +from typing import TYPE_CHECKING, Any, Dict, List, Optional, cast from craft_cli import EmitterMode, emit from craft_parts import infos @@ -188,7 +188,6 @@ def _run_command( project.parts, work_dir=work_dir, assets_dir=assets_dir, - base=project.base, package_repositories=project.package_repositories, part_names=part_names, adopt_info=project.adopt_info, @@ -198,6 +197,7 @@ def _run_command( "version": project.version or "", "grade": project.grade or "", }, + extra_build_snaps=_get_extra_build_snaps(project) ) if command_name == "clean": lifecycle.clean(part_names=part_names) @@ -306,3 +306,13 @@ def _get_arch() -> str: machine = infos._get_host_architecture() # pylint: disable=protected-access # FIXME Raise the potential KeyError. return infos._ARCH_TRANSLATIONS[machine]["deb"] # pylint: disable=protected-access + +def _get_extra_build_snaps(project: Project) -> Optional[List[str]]: + """Get list of extra snaps required to build.""" + extra_build_snaps = project.get_content_snaps() + if project.base is not None: + if extra_build_snaps is None: + extra_build_snaps = [project.base] + else: + extra_build_snaps.append(project.base) + return extra_build_snaps diff --git a/snapcraft/parts/parts.py b/snapcraft/parts/parts.py index f51912e837..a691ebb8fb 100644 --- a/snapcraft/parts/parts.py +++ b/snapcraft/parts/parts.py @@ -42,8 +42,8 @@ class PartsLifecycle: :param all_parts: A dictionary containing the parts defined in the project. :param work_dir: The working directory for parts processing. :param assets_dir: The directory containing project assets. - :param base: the base to build for. :param adopt_info: The name of the part containing metadata do adopt. + :param extra_build_snaps: A list of additional build snaps to install. :raises PartsLifecycleError: On error initializing the parts lifecycle. """ @@ -54,13 +54,13 @@ def __init__( *, work_dir: pathlib.Path, assets_dir: pathlib.Path, - base: Optional[str], package_repositories: List[Dict[str, Any]], part_names: Optional[List[str]], adopt_info: Optional[str], parse_info: Dict[str, List[str]], project_name: str, project_vars: Dict[str, str], + extra_build_snaps: Optional[List[str]] = None, ): self._assets_dir = assets_dir self._package_repositories = package_repositories @@ -78,9 +78,6 @@ def __init__( # Install pre-requisite packages for apt-key, if not installed. # FIXME: package names should be plataform-specific extra_build_packages.extend(["gnupg", "dirmngr"]) - extra_snap_packages = [] - if base is not None: - extra_snap_packages.append(base) try: self._lcm = craft_parts.LifecycleManager( @@ -90,7 +87,7 @@ def __init__( cache_dir=cache_dir, ignore_local_sources=["*.snap"], extra_build_packages=extra_build_packages, - extra_snap_packages=extra_snap_packages, + extra_build_snaps=extra_build_snaps, project_name=project_name, project_vars_part_name=adopt_info, project_vars=project_vars, diff --git a/snapcraft/projects.py b/snapcraft/projects.py index 1fb3397bb8..363134806e 100644 --- a/snapcraft/projects.py +++ b/snapcraft/projects.py @@ -408,6 +408,24 @@ def unmarshal(cls, data: Dict[str, Any]) -> "Project": return project + def _get_content_plugs(self) -> List[ContentPlug]: + """Get list of content plugs.""" + if self.plugs is not None: + return [ + plug for plug in self.plugs.values() if isinstance(plug, ContentPlug) + ] + return [] + + def get_content_snaps(self) -> Optional[List[str]]: + """Get list of snaps from ContentPlug `default-provider` fields.""" + content_snaps = [ + x.default_provider + for x in self._get_content_plugs() + if x.default_provider is not None + ] + + return content_snaps if content_snaps else None + class _GrammarAwareModel(pydantic.BaseModel): class Config: diff --git a/tests/unit/parts/test_lifecycle.py b/tests/unit/parts/test_lifecycle.py index 279b04b0a5..8f939d1575 100644 --- a/tests/unit/parts/test_lifecycle.py +++ b/tests/unit/parts/test_lifecycle.py @@ -574,3 +574,50 @@ def test_extract_parse_info(): parse_info = parts_lifecycle._extract_parse_info(yaml_data) assert yaml_data == {"name": "foo", "parts": {"p1": {"plugin": "nil"}, "p2": {}}} assert parse_info == {"p1": "foo/metadata.xml"} + +def test_get_snap_project_no_base(snapcraft_yaml): + project = Project.unmarshal(snapcraft_yaml(base=None)) + + assert parts_lifecycle._get_extra_build_snaps(project) is None + +def test_get_snap_project_with_base(snapcraft_yaml): + project = Project.unmarshal(snapcraft_yaml(base="core22")) + + assert parts_lifecycle._get_extra_build_snaps(project) == ["core22"] + +def test_get_snap_project_with_content_plugs(snapcraft_yaml): + yaml_data = { + "name": "mytest", + "version": "0.1", + "base": "core22", + "summary": "Just some test data", + "description": "This is just some test data.", + "grade": "stable", + "confinement": "strict", + "parts": { + "part1": { + "plugin": "nil" + } + }, + "plugs": { + "test-plug-1": { + "content": "content-interface", + "interface": "content", + "target": "$SNAP/content", + "default-provider": "test-snap-1", + }, + "test-plug-2": { + "content": "content-interface", + "interface": "content", + "target": "$SNAP/content", + "default-provider": "test-snap-2", + }, + }, + } + + project = Project(**yaml_data) + + assert ( + parts_lifecycle._get_extra_build_snaps(project) + == ["test-snap-1", "test-snap-2", "core22"] + ) diff --git a/tests/unit/parts/test_parts.py b/tests/unit/parts/test_parts.py index ea43c36f26..f058c78e04 100644 --- a/tests/unit/parts/test_parts.py +++ b/tests/unit/parts/test_parts.py @@ -33,18 +33,19 @@ def parts_data(): @pytest.mark.parametrize("step_name", ["pull", "overlay", "build", "stage", "prime"]) def test_parts_lifecycle_run(mocker, parts_data, step_name, new_dir, emitter): + mocker.patch("craft_parts.executor.executor.Executor._install_build_snaps") lcm_spy = mocker.spy(craft_parts, "LifecycleManager") lifecycle = PartsLifecycle( parts_data, work_dir=new_dir, assets_dir=new_dir, - base="core22", part_names=[], package_repositories=[], adopt_info=None, project_name="test-project", parse_info={}, project_vars={"version": "1", "grade": "stable"}, + extra_build_snaps=["core22"], ) lifecycle.run(step_name) assert lifecycle.prime_dir == Path(new_dir, "prime") @@ -57,7 +58,7 @@ def test_parts_lifecycle_run(mocker, parts_data, step_name, new_dir, emitter): cache_dir=ANY, ignore_local_sources=["*.snap"], extra_build_packages=[], - extra_snap_packages=["core22"], + extra_build_snaps=["core22"], project_name="test-project", project_vars_part_name=None, project_vars={"version": "1", "grade": "stable"}, @@ -71,7 +72,6 @@ def test_parts_lifecycle_run_bad_step(parts_data, new_dir): parts_data, work_dir=new_dir, assets_dir=new_dir, - base="core22", part_names=[], package_repositories=[], adopt_info=None, @@ -89,7 +89,6 @@ def test_parts_lifecycle_run_internal_error(parts_data, new_dir, mocker): parts_data, work_dir=new_dir, assets_dir=new_dir, - base="core22", part_names=[], package_repositories=[], adopt_info=None, @@ -108,7 +107,6 @@ def test_parts_lifecycle_run_parts_error(new_dir): {"p1": {"plugin": "dump", "source": "foo"}}, work_dir=new_dir, assets_dir=new_dir, - base="core22", part_names=[], package_repositories=[], adopt_info=None, @@ -128,7 +126,6 @@ def test_parts_lifecycle_clean(parts_data, new_dir, emitter): parts_data, work_dir=new_dir, assets_dir=new_dir, - base="core22", part_names=[], package_repositories=[], adopt_info=None, @@ -145,7 +142,6 @@ def test_parts_lifecycle_clean_parts(parts_data, new_dir, emitter): parts_data, work_dir=new_dir, assets_dir=new_dir, - base="core22", part_names=[], package_repositories=[], adopt_info=None, @@ -157,40 +153,6 @@ def test_parts_lifecycle_clean_parts(parts_data, new_dir, emitter): emitter.assert_message("Cleaning parts: p1", intermediate=True) -def test_parts_lifecycle_initialize_with_no_base( - mocker, - parts_data, - new_dir, -): - lcm_spy = mocker.spy(craft_parts, "LifecycleManager") - PartsLifecycle( - parts_data, - work_dir=new_dir, - assets_dir=new_dir, - base=None, - part_names=[], - package_repositories=[], - adopt_info=None, - project_name="test-project", - parse_info={}, - project_vars={"version": "1", "grade": "stable"}, - ) - assert lcm_spy.mock_calls == [ - call( - {"parts": {"p1": {"plugin": "nil"}}}, - application_name="snapcraft", - work_dir=ANY, - cache_dir=ANY, - ignore_local_sources=["*.snap"], - extra_build_packages=[], - extra_snap_packages=[], - project_name="test-project", - project_vars_part_name=None, - project_vars={"version": "1", "grade": "stable"}, - ) - ] - - def test_parts_lifecycle_initialize_with_package_repositories( mocker, parts_data, @@ -201,7 +163,6 @@ def test_parts_lifecycle_initialize_with_package_repositories( parts_data, work_dir=new_dir, assets_dir=new_dir, - base="core22", part_names=[], package_repositories=[ { @@ -213,6 +174,7 @@ def test_parts_lifecycle_initialize_with_package_repositories( project_name="test-project", parse_info={}, project_vars={"version": "1", "grade": "stable"}, + extra_build_snaps=["core22"], ) assert lcm_spy.mock_calls == [ call( @@ -222,7 +184,7 @@ def test_parts_lifecycle_initialize_with_package_repositories( cache_dir=ANY, ignore_local_sources=["*.snap"], extra_build_packages=["gnupg", "dirmngr"], - extra_snap_packages=["core22"], + extra_build_snaps=["core22"], project_name="test-project", project_vars_part_name=None, project_vars={"version": "1", "grade": "stable"}, diff --git a/tests/unit/test_projects.py b/tests/unit/test_projects.py index 28cbef9db5..3cb4c2144c 100644 --- a/tests/unit/test_projects.py +++ b/tests/unit/test_projects.py @@ -480,6 +480,19 @@ def test_project_content_plugs_missing_target(self, project_yaml_data): with pytest.raises(errors.ProjectValidationError, match=error): Project.unmarshal(project_yaml_data(plugs=content_plug)) + def test_project_get_content_snaps(self, project_yaml_data): + content_plug_data = { + "content-interface": { + "interface": "content", + "target": "test-target", + "content": "test-content", + "default-provider": "test-provider", + } + } + + project = Project.unmarshal(project_yaml_data(plugs=content_plug_data)) + assert project.get_content_snaps() == ["test-provider"] + class TestHookValidation: """Validate hooks.""" From d2f2a0d43cc689974512d454d32d1a2dff00da10 Mon Sep 17 00:00:00 2001 From: Sergio Schvezov Date: Wed, 27 Apr 2022 08:54:12 -0300 Subject: [PATCH 124/167] spread: enable core22 Signed-off-by: Sergio Schvezov --- .github/workflows/spread.yml | 1 + spread.yaml | 47 +++++++++++++++++-- .../expected_appstream-desktop.desktop | 0 .../appstream-desktop/expected_snap.yaml | 0 .../snaps/appstream-desktop/appstream-desktop | 0 .../io.snapcraft.appstream.desktop | 0 .../io.snapcraft.appstream.metainfo.xml | 0 .../appstream-desktop/snap/snapcraft.yaml | 0 .../snaps/appstream-desktop/snapcraft.svg | 0 .../core22/appstream-desktop/task.yaml | 7 --- .../core22/clean/snap/snapcraft.yaml | 0 .../{general => }/core22/clean/task.yaml | 0 .../{general => }/core22/craftctl/task.yaml | 0 .../craftctl/test-craftctl-default/Makefile | 0 .../craftctl/test-craftctl-default/hello.c | 0 .../test-craftctl-default/snap/snapcraft.yaml | 0 .../test-craftctl-get-set/snap/snapcraft.yaml | 0 .../core22/environment/task.yaml | 0 .../test-variables/snap/snapcraft.yaml | 0 .../core22/package-repositories/task.yaml | 35 ++++++++++++++ .../snap/keys/FC42E99D.asc | 0 .../snap/snapcraft.yaml | 0 .../test-apt-key-name/snap/keys/FC42E99D.asc | 0 .../test-apt-key-name/snap/snapcraft.yaml | 0 .../test-apt-keyserver/snap/snapcraft.yaml | 0 .../test-apt-path/snap/snapcraft.yaml | 0 .../test-apt-ppa/snap/snapcraft.yaml | 0 .../core22/packing/snap/snapcraft.yaml | 0 .../{general => }/core22/packing/task.yaml | 0 .../scriptlet-failures/snap/snapcraft.yaml | 0 .../{general => }/core22/scriptlets/task.yaml | 0 .../spread/general/classic-patchelf/task.yaml | 10 ++++ .../core22/package-repositories/task.yaml | 46 ------------------ tests/spread/general/sources/task.yaml | 10 ++++ .../spread/general/strict-patchelf/task.yaml | 10 ++++ tests/spread/tools/snapcraft-yaml.sh | 4 +- 36 files changed, 112 insertions(+), 58 deletions(-) rename tests/spread/{general => }/core22/appstream-desktop/expected_appstream-desktop.desktop (100%) rename tests/spread/{general => }/core22/appstream-desktop/expected_snap.yaml (100%) rename tests/spread/{general => }/core22/appstream-desktop/snaps/appstream-desktop/appstream-desktop (100%) rename tests/spread/{general => }/core22/appstream-desktop/snaps/appstream-desktop/io.snapcraft.appstream.desktop (100%) rename tests/spread/{general => }/core22/appstream-desktop/snaps/appstream-desktop/io.snapcraft.appstream.metainfo.xml (100%) rename tests/spread/{general => }/core22/appstream-desktop/snaps/appstream-desktop/snap/snapcraft.yaml (100%) rename tests/spread/{general => }/core22/appstream-desktop/snaps/appstream-desktop/snapcraft.svg (100%) rename tests/spread/{general => }/core22/appstream-desktop/task.yaml (78%) rename tests/spread/{general => }/core22/clean/snap/snapcraft.yaml (100%) rename tests/spread/{general => }/core22/clean/task.yaml (100%) rename tests/spread/{general => }/core22/craftctl/task.yaml (100%) rename tests/spread/{general => }/core22/craftctl/test-craftctl-default/Makefile (100%) rename tests/spread/{general => }/core22/craftctl/test-craftctl-default/hello.c (100%) rename tests/spread/{general => }/core22/craftctl/test-craftctl-default/snap/snapcraft.yaml (100%) rename tests/spread/{general => }/core22/craftctl/test-craftctl-get-set/snap/snapcraft.yaml (100%) rename tests/spread/{general => }/core22/environment/task.yaml (100%) rename tests/spread/{general => }/core22/environment/test-variables/snap/snapcraft.yaml (100%) create mode 100644 tests/spread/core22/package-repositories/task.yaml rename tests/spread/{general => }/core22/package-repositories/test-apt-key-fingerprint/snap/keys/FC42E99D.asc (100%) rename tests/spread/{general => }/core22/package-repositories/test-apt-key-fingerprint/snap/snapcraft.yaml (100%) rename tests/spread/{general => }/core22/package-repositories/test-apt-key-name/snap/keys/FC42E99D.asc (100%) rename tests/spread/{general => }/core22/package-repositories/test-apt-key-name/snap/snapcraft.yaml (100%) rename tests/spread/{general => }/core22/package-repositories/test-apt-keyserver/snap/snapcraft.yaml (100%) rename tests/spread/{general => }/core22/package-repositories/test-apt-path/snap/snapcraft.yaml (100%) rename tests/spread/{general => }/core22/package-repositories/test-apt-ppa/snap/snapcraft.yaml (100%) rename tests/spread/{general => }/core22/packing/snap/snapcraft.yaml (100%) rename tests/spread/{general => }/core22/packing/task.yaml (100%) rename tests/spread/{general => }/core22/scriptlets/scriptlet-failures/snap/snapcraft.yaml (100%) rename tests/spread/{general => }/core22/scriptlets/task.yaml (100%) delete mode 100644 tests/spread/general/core22/package-repositories/task.yaml diff --git a/.github/workflows/spread.yml b/.github/workflows/spread.yml index ac7760c204..7478182844 100644 --- a/.github/workflows/spread.yml +++ b/.github/workflows/spread.yml @@ -40,6 +40,7 @@ jobs: spread-jobs: - google:ubuntu-18.04-64 - google:ubuntu-20.04-64 + - google:ubuntu-22.04-64 steps: - name: Checkout snapcraft diff --git a/spread.yaml b/spread.yaml index 3e1b516320..db30ef2b6a 100644 --- a/spread.yaml +++ b/spread.yaml @@ -38,6 +38,7 @@ backends: # -native is added for clarity and for ubuntu-20.04* to match. - ubuntu-18.04 - ubuntu-20.04 + - ubuntu-22.04 google: key: '$(HOST: echo "$SPREAD_GOOGLE_KEY")' location: snapd-spread/us-east1-b @@ -50,6 +51,9 @@ backends: workers: 6 image: ubuntu-2004-64 storage: 40G + - ubuntu-22.04-64: + workers: 6 + image: ubuntu-2204-64 multipass: type: adhoc @@ -97,6 +101,10 @@ backends: workers: 1 username: root password: ubuntu + - ubuntu-22.04-64: + workers: 1 + username: root + password: ubuntu autopkgtest: type: adhoc @@ -145,6 +153,22 @@ backends: - ubuntu-20.04-arm64: username: ubuntu password: ubuntu + # Jammy + - ubuntu-22.04-amd64: + username: ubuntu + password: ubuntu + - ubuntu-22.04-ppc64el: + username: ubuntu + password: ubuntu + - ubuntu-22.04-armhf: + username: ubuntu + password: ubuntu + - ubuntu-22.04-s390x: + username: ubuntu + password: ubuntu + - ubuntu-22.04-arm64: + username: ubuntu + password: ubuntu exclude: [snaps-cache/] @@ -183,11 +207,12 @@ prepare: | # Remove lxd and lxd-client deb packages as our implementation (pylxd) does not # nicely handle the snap and deb being installed at the same time. apt-get remove --purge --yes lxd lxd-client - # Install and setup the lxd snap - snap install lxd - # Add the ubuntu user to the lxd group. - adduser ubuntu lxd fi + # Install and setup the lxd snap + snap install lxd + # Add the ubuntu user to the lxd group. + adduser ubuntu lxd + lxd init --auto # Hold snap refreshes for 24h. snap set system refresh.hold="$(date --date=tomorrow +%Y-%m-%dT%H:%M:%S%:z)" @@ -223,10 +248,24 @@ prepare: | git commit -m "Testing Commit" popd + # TODO remove once core22 is stable + snap install core22 --edge + restore-each: | "$TOOLS_DIR"/restore.sh suites: + tests/spread/core22/: + summary: core22 specific tests + systems: + - ubuntu-22.04 + - ubuntu-22.04-64 + - ubuntu-22.04-amd64 + - ubuntu-22.04-arm64 + - ubuntu-22.04-armhf + - ubuntu-22.04-s390x + - ubuntu-22.04-ppc64el + # General, core suite tests/spread/general/: summary: tests of snapcraft core functionality diff --git a/tests/spread/general/core22/appstream-desktop/expected_appstream-desktop.desktop b/tests/spread/core22/appstream-desktop/expected_appstream-desktop.desktop similarity index 100% rename from tests/spread/general/core22/appstream-desktop/expected_appstream-desktop.desktop rename to tests/spread/core22/appstream-desktop/expected_appstream-desktop.desktop diff --git a/tests/spread/general/core22/appstream-desktop/expected_snap.yaml b/tests/spread/core22/appstream-desktop/expected_snap.yaml similarity index 100% rename from tests/spread/general/core22/appstream-desktop/expected_snap.yaml rename to tests/spread/core22/appstream-desktop/expected_snap.yaml diff --git a/tests/spread/general/core22/appstream-desktop/snaps/appstream-desktop/appstream-desktop b/tests/spread/core22/appstream-desktop/snaps/appstream-desktop/appstream-desktop similarity index 100% rename from tests/spread/general/core22/appstream-desktop/snaps/appstream-desktop/appstream-desktop rename to tests/spread/core22/appstream-desktop/snaps/appstream-desktop/appstream-desktop diff --git a/tests/spread/general/core22/appstream-desktop/snaps/appstream-desktop/io.snapcraft.appstream.desktop b/tests/spread/core22/appstream-desktop/snaps/appstream-desktop/io.snapcraft.appstream.desktop similarity index 100% rename from tests/spread/general/core22/appstream-desktop/snaps/appstream-desktop/io.snapcraft.appstream.desktop rename to tests/spread/core22/appstream-desktop/snaps/appstream-desktop/io.snapcraft.appstream.desktop diff --git a/tests/spread/general/core22/appstream-desktop/snaps/appstream-desktop/io.snapcraft.appstream.metainfo.xml b/tests/spread/core22/appstream-desktop/snaps/appstream-desktop/io.snapcraft.appstream.metainfo.xml similarity index 100% rename from tests/spread/general/core22/appstream-desktop/snaps/appstream-desktop/io.snapcraft.appstream.metainfo.xml rename to tests/spread/core22/appstream-desktop/snaps/appstream-desktop/io.snapcraft.appstream.metainfo.xml diff --git a/tests/spread/general/core22/appstream-desktop/snaps/appstream-desktop/snap/snapcraft.yaml b/tests/spread/core22/appstream-desktop/snaps/appstream-desktop/snap/snapcraft.yaml similarity index 100% rename from tests/spread/general/core22/appstream-desktop/snaps/appstream-desktop/snap/snapcraft.yaml rename to tests/spread/core22/appstream-desktop/snaps/appstream-desktop/snap/snapcraft.yaml diff --git a/tests/spread/general/core22/appstream-desktop/snaps/appstream-desktop/snapcraft.svg b/tests/spread/core22/appstream-desktop/snaps/appstream-desktop/snapcraft.svg similarity index 100% rename from tests/spread/general/core22/appstream-desktop/snaps/appstream-desktop/snapcraft.svg rename to tests/spread/core22/appstream-desktop/snaps/appstream-desktop/snapcraft.svg diff --git a/tests/spread/general/core22/appstream-desktop/task.yaml b/tests/spread/core22/appstream-desktop/task.yaml similarity index 78% rename from tests/spread/general/core22/appstream-desktop/task.yaml rename to tests/spread/core22/appstream-desktop/task.yaml index 4067e5f8a0..a700e69059 100644 --- a/tests/spread/general/core22/appstream-desktop/task.yaml +++ b/tests/spread/core22/appstream-desktop/task.yaml @@ -1,12 +1,5 @@ summary: Build a snap that tests appstream settings -# This test snap uses core18, and is limited to amd64 arch due to -# architectures specified in expected_snap.yaml. -systems: - - ubuntu-20.04-64 - - ubuntu-20.04-amd64 - - ubuntu-20.04 - environment: SNAP_DIR: snaps/appstream-desktop diff --git a/tests/spread/general/core22/clean/snap/snapcraft.yaml b/tests/spread/core22/clean/snap/snapcraft.yaml similarity index 100% rename from tests/spread/general/core22/clean/snap/snapcraft.yaml rename to tests/spread/core22/clean/snap/snapcraft.yaml diff --git a/tests/spread/general/core22/clean/task.yaml b/tests/spread/core22/clean/task.yaml similarity index 100% rename from tests/spread/general/core22/clean/task.yaml rename to tests/spread/core22/clean/task.yaml diff --git a/tests/spread/general/core22/craftctl/task.yaml b/tests/spread/core22/craftctl/task.yaml similarity index 100% rename from tests/spread/general/core22/craftctl/task.yaml rename to tests/spread/core22/craftctl/task.yaml diff --git a/tests/spread/general/core22/craftctl/test-craftctl-default/Makefile b/tests/spread/core22/craftctl/test-craftctl-default/Makefile similarity index 100% rename from tests/spread/general/core22/craftctl/test-craftctl-default/Makefile rename to tests/spread/core22/craftctl/test-craftctl-default/Makefile diff --git a/tests/spread/general/core22/craftctl/test-craftctl-default/hello.c b/tests/spread/core22/craftctl/test-craftctl-default/hello.c similarity index 100% rename from tests/spread/general/core22/craftctl/test-craftctl-default/hello.c rename to tests/spread/core22/craftctl/test-craftctl-default/hello.c diff --git a/tests/spread/general/core22/craftctl/test-craftctl-default/snap/snapcraft.yaml b/tests/spread/core22/craftctl/test-craftctl-default/snap/snapcraft.yaml similarity index 100% rename from tests/spread/general/core22/craftctl/test-craftctl-default/snap/snapcraft.yaml rename to tests/spread/core22/craftctl/test-craftctl-default/snap/snapcraft.yaml diff --git a/tests/spread/general/core22/craftctl/test-craftctl-get-set/snap/snapcraft.yaml b/tests/spread/core22/craftctl/test-craftctl-get-set/snap/snapcraft.yaml similarity index 100% rename from tests/spread/general/core22/craftctl/test-craftctl-get-set/snap/snapcraft.yaml rename to tests/spread/core22/craftctl/test-craftctl-get-set/snap/snapcraft.yaml diff --git a/tests/spread/general/core22/environment/task.yaml b/tests/spread/core22/environment/task.yaml similarity index 100% rename from tests/spread/general/core22/environment/task.yaml rename to tests/spread/core22/environment/task.yaml diff --git a/tests/spread/general/core22/environment/test-variables/snap/snapcraft.yaml b/tests/spread/core22/environment/test-variables/snap/snapcraft.yaml similarity index 100% rename from tests/spread/general/core22/environment/test-variables/snap/snapcraft.yaml rename to tests/spread/core22/environment/test-variables/snap/snapcraft.yaml diff --git a/tests/spread/core22/package-repositories/task.yaml b/tests/spread/core22/package-repositories/task.yaml new file mode 100644 index 0000000000..376530f079 --- /dev/null +++ b/tests/spread/core22/package-repositories/task.yaml @@ -0,0 +1,35 @@ +summary: Test various package-repository configurations on core22 + +environment: + SNAP/test_apt_key_fingerprint: test-apt-key-fingerprint + SNAP/test_apt_key_name: test-apt-key-name + SNAP/test_apt_keyserver: test-apt-keyserver + SNAP/test_apt_ppa: test-apt-ppa + SNAPCRAFT_BUILD_ENVIRONMENT: "" + +restore: | + cd "$SNAP" + rm -f ./*.snap + snapcraft clean + snapcraft --destructive-mode + +execute: | + cd "$SNAP" + + # No jammy for this ppa yet + if [ "$(basename "$SNAP")" != "test-apt-ppa" ]; then + # Build what we have. + snapcraft --verbose --use-lxd + + # And verify the snap runs as expected. + snap install "${SNAP}"_1.0_*.snap --dangerous + snap_executable="${SNAP}.test-ppa" + [ "$("${snap_executable}")" = "hello!" ] + fi + + # Do it again in destructive mode + snap remove "${SNAP}" + snapcraft --verbose --destructive-mode + snap install "${SNAP}"_1.0_*.snap --dangerous + snap_executable="${SNAP}.test-ppa" + [ "$("${snap_executable}")" = "hello!" ] diff --git a/tests/spread/general/core22/package-repositories/test-apt-key-fingerprint/snap/keys/FC42E99D.asc b/tests/spread/core22/package-repositories/test-apt-key-fingerprint/snap/keys/FC42E99D.asc similarity index 100% rename from tests/spread/general/core22/package-repositories/test-apt-key-fingerprint/snap/keys/FC42E99D.asc rename to tests/spread/core22/package-repositories/test-apt-key-fingerprint/snap/keys/FC42E99D.asc diff --git a/tests/spread/general/core22/package-repositories/test-apt-key-fingerprint/snap/snapcraft.yaml b/tests/spread/core22/package-repositories/test-apt-key-fingerprint/snap/snapcraft.yaml similarity index 100% rename from tests/spread/general/core22/package-repositories/test-apt-key-fingerprint/snap/snapcraft.yaml rename to tests/spread/core22/package-repositories/test-apt-key-fingerprint/snap/snapcraft.yaml diff --git a/tests/spread/general/core22/package-repositories/test-apt-key-name/snap/keys/FC42E99D.asc b/tests/spread/core22/package-repositories/test-apt-key-name/snap/keys/FC42E99D.asc similarity index 100% rename from tests/spread/general/core22/package-repositories/test-apt-key-name/snap/keys/FC42E99D.asc rename to tests/spread/core22/package-repositories/test-apt-key-name/snap/keys/FC42E99D.asc diff --git a/tests/spread/general/core22/package-repositories/test-apt-key-name/snap/snapcraft.yaml b/tests/spread/core22/package-repositories/test-apt-key-name/snap/snapcraft.yaml similarity index 100% rename from tests/spread/general/core22/package-repositories/test-apt-key-name/snap/snapcraft.yaml rename to tests/spread/core22/package-repositories/test-apt-key-name/snap/snapcraft.yaml diff --git a/tests/spread/general/core22/package-repositories/test-apt-keyserver/snap/snapcraft.yaml b/tests/spread/core22/package-repositories/test-apt-keyserver/snap/snapcraft.yaml similarity index 100% rename from tests/spread/general/core22/package-repositories/test-apt-keyserver/snap/snapcraft.yaml rename to tests/spread/core22/package-repositories/test-apt-keyserver/snap/snapcraft.yaml diff --git a/tests/spread/general/core22/package-repositories/test-apt-path/snap/snapcraft.yaml b/tests/spread/core22/package-repositories/test-apt-path/snap/snapcraft.yaml similarity index 100% rename from tests/spread/general/core22/package-repositories/test-apt-path/snap/snapcraft.yaml rename to tests/spread/core22/package-repositories/test-apt-path/snap/snapcraft.yaml diff --git a/tests/spread/general/core22/package-repositories/test-apt-ppa/snap/snapcraft.yaml b/tests/spread/core22/package-repositories/test-apt-ppa/snap/snapcraft.yaml similarity index 100% rename from tests/spread/general/core22/package-repositories/test-apt-ppa/snap/snapcraft.yaml rename to tests/spread/core22/package-repositories/test-apt-ppa/snap/snapcraft.yaml diff --git a/tests/spread/general/core22/packing/snap/snapcraft.yaml b/tests/spread/core22/packing/snap/snapcraft.yaml similarity index 100% rename from tests/spread/general/core22/packing/snap/snapcraft.yaml rename to tests/spread/core22/packing/snap/snapcraft.yaml diff --git a/tests/spread/general/core22/packing/task.yaml b/tests/spread/core22/packing/task.yaml similarity index 100% rename from tests/spread/general/core22/packing/task.yaml rename to tests/spread/core22/packing/task.yaml diff --git a/tests/spread/general/core22/scriptlets/scriptlet-failures/snap/snapcraft.yaml b/tests/spread/core22/scriptlets/scriptlet-failures/snap/snapcraft.yaml similarity index 100% rename from tests/spread/general/core22/scriptlets/scriptlet-failures/snap/snapcraft.yaml rename to tests/spread/core22/scriptlets/scriptlet-failures/snap/snapcraft.yaml diff --git a/tests/spread/general/core22/scriptlets/task.yaml b/tests/spread/core22/scriptlets/task.yaml similarity index 100% rename from tests/spread/general/core22/scriptlets/task.yaml rename to tests/spread/core22/scriptlets/task.yaml diff --git a/tests/spread/general/classic-patchelf/task.yaml b/tests/spread/general/classic-patchelf/task.yaml index f66ed3e39a..19f77df958 100644 --- a/tests/spread/general/classic-patchelf/task.yaml +++ b/tests/spread/general/classic-patchelf/task.yaml @@ -1,5 +1,15 @@ summary: Build a classic snap and validates elf patching +# TODO patchelf'ing not supported in 22.04 +systems: + - -ubuntu-22.04 + - -ubuntu-22.04-64 + - -ubuntu-22.04-amd64 + - -ubuntu-22.04-arm64 + - -ubuntu-22.04-armhf + - -ubuntu-22.04-s390x + - -ubuntu-22.04-ppc64el + environment: SNAP_DIR: ../snaps/classic-patchelf diff --git a/tests/spread/general/core22/package-repositories/task.yaml b/tests/spread/general/core22/package-repositories/task.yaml deleted file mode 100644 index 465d4e4dd3..0000000000 --- a/tests/spread/general/core22/package-repositories/task.yaml +++ /dev/null @@ -1,46 +0,0 @@ -summary: Test various package-repository configurations on core22 - -environment: - SNAP/test_apt_key_fingerprint: test-apt-key-fingerprint - SNAP/test_apt_key_name: test-apt-key-name - SNAP/test_apt_keyserver: test-apt-keyserver - SNAP/test_apt_ppa: test-apt-ppa - SNAPCRAFT_BUILD_ENVIRONMENT: "" - -prepare: | - #shellcheck source=tests/spread/tools/snapcraft-yaml.sh - . "$TOOLS_DIR/snapcraft-yaml.sh" - # set_base "$SNAP/snap/snapcraft.yaml" - snap install core22 --edge - -restore: | - cd "$SNAP" - rm -f ./*.snap - rm -Rf work - - #shellcheck source=tests/spread/tools/snapcraft-yaml.sh - . "$TOOLS_DIR/snapcraft-yaml.sh" - restore_yaml "snap/snapcraft.yaml" - -execute: | - cd "$SNAP" - - if [ "$SPREAD_SYSTEM" = "ubuntu-20.04-64" ]; then - # No jammy for this ppa yet - if [ "$(basename "$SNAP")" != "test-apt-ppa" ]; then - # Build what we have. - snapcraft --verbose --use-lxd - - # And verify the snap runs as expected. - snap install "${SNAP}"_1.0_*.snap --dangerous - snap_executable="${SNAP}.test-ppa" - [ "$("${snap_executable}")" = "hello!" ] - fi - - # Do it again in destructive mode - snap remove "${SNAP}" - snapcraft --verbose --destructive-mode - snap install "${SNAP}"_1.0_*.snap --dangerous - snap_executable="${SNAP}.test-ppa" - [ "$("${snap_executable}")" = "hello!" ] - fi diff --git a/tests/spread/general/sources/task.yaml b/tests/spread/general/sources/task.yaml index 5e0dc3423c..d8be5df4cd 100644 --- a/tests/spread/general/sources/task.yaml +++ b/tests/spread/general/sources/task.yaml @@ -1,5 +1,15 @@ summary: Test pulling different source types +# These are tested on craft-parts for 22.04 and not all of them are available. +systems: + - -ubuntu-22.04 + - -ubuntu-22.04-64 + - -ubuntu-22.04-amd64 + - -ubuntu-22.04-arm64 + - -ubuntu-22.04-armhf + - -ubuntu-22.04-s390x + - -ubuntu-22.04-ppc64el + environment: SNAP_DIR/7z: snaps/7z SNAP_DIR/bzr_commit: snaps/bzr-commit diff --git a/tests/spread/general/strict-patchelf/task.yaml b/tests/spread/general/strict-patchelf/task.yaml index 3aea053a90..6f81b8cf1d 100644 --- a/tests/spread/general/strict-patchelf/task.yaml +++ b/tests/spread/general/strict-patchelf/task.yaml @@ -1,5 +1,15 @@ summary: Build a strict snap and validate elf patching +# TODO patchelf'ing not supported in 22.04 +systems: + - -ubuntu-22.04 + - -ubuntu-22.04-64 + - -ubuntu-22.04-amd64 + - -ubuntu-22.04-arm64 + - -ubuntu-22.04-armhf + - -ubuntu-22.04-s390x + - -ubuntu-22.04-ppc64el + environment: SNAP_DIR: ../snaps/strict-patchelf diff --git a/tests/spread/tools/snapcraft-yaml.sh b/tests/spread/tools/snapcraft-yaml.sh index 0f4be5fdb4..ed4912feea 100755 --- a/tests/spread/tools/snapcraft-yaml.sh +++ b/tests/spread/tools/snapcraft-yaml.sh @@ -2,7 +2,9 @@ get_base() { - if [[ "$SPREAD_SYSTEM" =~ ubuntu-20.04 ]]; then + if [[ "$SPREAD_SYSTEM" =~ ubuntu-22.04 ]]; then + echo "core22" + elif [[ "$SPREAD_SYSTEM" =~ ubuntu-20.04 ]]; then echo "core20" elif [[ "$SPREAD_SYSTEM" =~ ubuntu-18.04 ]]; then echo "core18" From bd178b44364ffa70b184b6411f67c6bce13d2579 Mon Sep 17 00:00:00 2001 From: Claudio Matsuoka Date: Thu, 28 Apr 2022 15:18:25 -0300 Subject: [PATCH 125/167] tests: fix lifecycle tests to run on new directory Fix lifecycle tests to not run on the current directory. Signed-off-by: Claudio Matsuoka --- tests/unit/parts/test_lifecycle.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/unit/parts/test_lifecycle.py b/tests/unit/parts/test_lifecycle.py index 8f939d1575..863caeaeed 100644 --- a/tests/unit/parts/test_lifecycle.py +++ b/tests/unit/parts/test_lifecycle.py @@ -36,7 +36,7 @@ @pytest.fixture -def snapcraft_yaml(): +def snapcraft_yaml(new_dir): def write_file( *, base: str, filename: str = "snap/snapcraft.yaml" ) -> Dict[str, Any]: @@ -575,7 +575,7 @@ def test_extract_parse_info(): assert yaml_data == {"name": "foo", "parts": {"p1": {"plugin": "nil"}, "p2": {}}} assert parse_info == {"p1": "foo/metadata.xml"} -def test_get_snap_project_no_base(snapcraft_yaml): +def test_get_snap_project_no_base(snapcraft_yaml, new_dir): project = Project.unmarshal(snapcraft_yaml(base=None)) assert parts_lifecycle._get_extra_build_snaps(project) is None @@ -585,7 +585,7 @@ def test_get_snap_project_with_base(snapcraft_yaml): assert parts_lifecycle._get_extra_build_snaps(project) == ["core22"] -def test_get_snap_project_with_content_plugs(snapcraft_yaml): +def test_get_snap_project_with_content_plugs(snapcraft_yaml, new_dir): yaml_data = { "name": "mytest", "version": "0.1", From d24c0212146c08794ceda5ce241f7c6946d74a1f Mon Sep 17 00:00:00 2001 From: Sergio Schvezov Date: Mon, 25 Apr 2022 16:11:24 -0300 Subject: [PATCH 126/167] plugins: (new) conda Signed-off-by: Sergio Schvezov --- snapcraft/parts/__init__.py | 4 +- snapcraft/parts/lifecycle.py | 9 +- snapcraft/parts/plugins/__init__.py | 23 ++ snapcraft/parts/plugins/conda_plugin.py | 159 +++++++++++++ snapcraft/parts/plugins/register.py | 26 +++ spread.yaml | 4 + .../build-and-run-hello/conda-hello/hello | 4 + .../conda-hello/snap/snapcraft.yaml | 25 ++ .../craft-parts/build-and-run-hello/task.yaml | 71 ++++++ tests/unit/parts/plugins/__init__.py | 0 tests/unit/parts/plugins/test_conda_plugin.py | 215 ++++++++++++++++++ 11 files changed, 536 insertions(+), 4 deletions(-) create mode 100644 snapcraft/parts/plugins/__init__.py create mode 100644 snapcraft/parts/plugins/conda_plugin.py create mode 100644 snapcraft/parts/plugins/register.py create mode 100755 tests/spread/plugins/craft-parts/build-and-run-hello/conda-hello/hello create mode 100644 tests/spread/plugins/craft-parts/build-and-run-hello/conda-hello/snap/snapcraft.yaml create mode 100644 tests/spread/plugins/craft-parts/build-and-run-hello/task.yaml create mode 100644 tests/unit/parts/plugins/__init__.py create mode 100644 tests/unit/parts/plugins/test_conda_plugin.py diff --git a/snapcraft/parts/__init__.py b/snapcraft/parts/__init__.py index 13e34e8d36..f7e2a9038d 100644 --- a/snapcraft/parts/__init__.py +++ b/snapcraft/parts/__init__.py @@ -16,4 +16,6 @@ """Parts lifecycle processing.""" -from .parts import PartsLifecycle # noqa: F401 +from .parts import PartsLifecycle + +__all__ = ["PartsLifecycle"] diff --git a/snapcraft/parts/lifecycle.py b/snapcraft/parts/lifecycle.py index 50f66af9d6..28424c4c0d 100644 --- a/snapcraft/parts/lifecycle.py +++ b/snapcraft/parts/lifecycle.py @@ -27,11 +27,10 @@ from snapcraft import errors, extensions, pack, providers, utils from snapcraft.meta import snap_yaml -from snapcraft.parts import PartsLifecycle from snapcraft.projects import GrammarAwareProject, Project from snapcraft.providers import capture_logs_from_instance -from . import grammar, yaml_utils +from . import PartsLifecycle, grammar, plugins, yaml_utils from .setup_assets import setup_assets from .update_metadata import update_project_metadata @@ -136,6 +135,9 @@ def run(command_name: str, parsed_args: "argparse.Namespace") -> None: if parsed_args.provider: raise errors.SnapcraftError("Option --provider is not supported.") + # Register our own plugins + plugins.register() + project = Project.unmarshal(yaml_data) _run_command( @@ -197,7 +199,7 @@ def _run_command( "version": project.version or "", "grade": project.grade or "", }, - extra_build_snaps=_get_extra_build_snaps(project) + extra_build_snaps=_get_extra_build_snaps(project), ) if command_name == "clean": lifecycle.clean(part_names=part_names) @@ -307,6 +309,7 @@ def _get_arch() -> str: # FIXME Raise the potential KeyError. return infos._ARCH_TRANSLATIONS[machine]["deb"] # pylint: disable=protected-access + def _get_extra_build_snaps(project: Project) -> Optional[List[str]]: """Get list of extra snaps required to build.""" extra_build_snaps = project.get_content_snaps() diff --git a/snapcraft/parts/plugins/__init__.py b/snapcraft/parts/plugins/__init__.py new file mode 100644 index 0000000000..394653407e --- /dev/null +++ b/snapcraft/parts/plugins/__init__.py @@ -0,0 +1,23 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright 2022 Canonical Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +"""Snapcraft specific plugins.""" + + +from .conda_plugin import CondaPlugin +from .register import register + +__all__ = ["CondaPlugin", "register"] diff --git a/snapcraft/parts/plugins/conda_plugin.py b/snapcraft/parts/plugins/conda_plugin.py new file mode 100644 index 0000000000..47cb0f1c12 --- /dev/null +++ b/snapcraft/parts/plugins/conda_plugin.py @@ -0,0 +1,159 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright 2022 Canonical Ltd. +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License version 3 as published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program. If not, see . + +"""The conda plugin.""" + +import os +import platform +import textwrap +from typing import Any, Dict, List, Optional, Set, cast + +from craft_parts import plugins +from overrides import overrides + +from snapcraft import errors + +_MINICONDA_ARCH_FROM_SNAP_ARCH = { + "i386": "x86", + "amd64": "x86_64", + "armhf": "armv7l", + "ppc64el": "ppc64le", +} +_MINICONDA_ARCH_FROM_PLATFORM = {"x86_64": {"32bit": "x86", "64bit": "x86_64"}} + + +def _get_architecture() -> str: + snap_arch = os.getenv("SNAP_ARCH") + # The first scenario is the general case as snapcraft will be running from the snap. + if snap_arch is not None: + try: + miniconda_arch = _MINICONDA_ARCH_FROM_SNAP_ARCH[snap_arch] + except KeyError as key_error: + raise errors.SnapcraftError( + f"Architecture not supported for conda plugin: {snap_arch!r}" + ) from key_error + # But there may be times when running from a virtualenv while doing development. + else: + machine = platform.machine() + architecture = platform.architecture()[0] + miniconda_arch = _MINICONDA_ARCH_FROM_PLATFORM[machine][architecture] + + return miniconda_arch + + +def _get_miniconda_source(version: str) -> str: + """Return tuple of source_url and source_checksum (if known).""" + arch = _get_architecture() + source = f"https://repo.anaconda.com/miniconda/Miniconda3-{version}-Linux-{arch}.sh" + return source + + +class CondaPluginProperties(plugins.PluginProperties, plugins.PluginModel): + """The part properties used by the conda plugin.""" + + # part properties required by the plugin + conda_packages: Optional[List[str]] = None + conda_python_version: Optional[str] = None + conda_miniconda_version: str = "latest" + + @classmethod + def unmarshal(cls, data: Dict[str, Any]) -> "CondaPluginProperties": + """Populate class attributes from the part specification. + + :param data: A dictionary containing part properties. + + :return: The populated plugin properties data object. + + :raise pydantic.ValidationError: If validation fails. + """ + plugin_data = plugins.extract_plugin_properties( + data, + plugin_name="conda", + ) + return cls(**plugin_data) + + +class CondaPlugin(plugins.Plugin): + """A plugin for conda projects. + + This plugin uses the common plugin keywords as well as those for "sources". + For more information check the 'plugins' topic for the former and the + 'sources' topic for the latter. + + Additionally, this plugin uses the following plugin-specific keywords: + - conda-packages + (list of packages, default: None) + List of packages for conda to install. + - conda-python-version + (str, default: None) + Python version for conda to use (i.e. "3.9"). + - conda-miniconda-version + (str, default: latest) + The version of miniconda to initialize. + """ + + properties_class = CondaPluginProperties + + @overrides + def get_build_snaps(self) -> Set[str]: + return set() + + @overrides + def get_build_packages(self) -> Set[str]: + return set() + + @overrides + def get_build_environment(self) -> Dict[str, str]: + return {"PATH": "${HOME}/miniconda/bin:${PATH}"} + + @staticmethod + def _get_download_miniconda_command(url: str) -> str: + return textwrap.dedent( + f"""\ + if ! [ -e "${{HOME}}/miniconda.sh" ]; then + curl --proto '=https' --tlsv1.2 -sSf {url} > ${{HOME}}/miniconda.sh + chmod 755 ${{HOME}}/miniconda.sh + fi""" + ) + + def _get_deploy_command(self, options) -> str: + conda_target_prefix = f"/snap/{self._part_info.project_name}/current" + + deploy_cmd = [ + f"CONDA_TARGET_PREFIX_OVERRIDE={conda_target_prefix}", + "conda", + "create", + "--prefix", + str(self._part_info.part_install_dir), + "--yes", + ] + if options.conda_python_version: + deploy_cmd.append(f"python={options.conda_python_version}") + + if options.conda_packages: + deploy_cmd.extend(options.conda_packages) + + return " ".join(deploy_cmd) + + @overrides + def get_build_commands(self) -> List[str]: + options = cast(CondaPluginProperties, self._options) + url = _get_miniconda_source(options.conda_miniconda_version) + return [ + self._get_download_miniconda_command(url), + "${HOME}/miniconda.sh -bfp ${HOME}/miniconda", + self._get_deploy_command(options), + ] diff --git a/snapcraft/parts/plugins/register.py b/snapcraft/parts/plugins/register.py new file mode 100644 index 0000000000..a778cbd440 --- /dev/null +++ b/snapcraft/parts/plugins/register.py @@ -0,0 +1,26 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright 2022 Canonical Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +"""Snapcraft provided plugin registration.""" + +import craft_parts + +from .conda_plugin import CondaPlugin + + +def register() -> None: + """Register Snapcraft plugins.""" + craft_parts.plugins.register({"conda": CondaPlugin}) diff --git a/spread.yaml b/spread.yaml index db30ef2b6a..a6efb149d3 100644 --- a/spread.yaml +++ b/spread.yaml @@ -426,6 +426,10 @@ suites: summary: tests of snapcraft's v2 plugins systems: - ubuntu-20.04* + tests/spread/plugins/craft-parts/: + summary: tests of snapcraft's craft-part's based plugins + systems: + - ubuntu-22.04* # Extensions tests tests/spread/extensions/: diff --git a/tests/spread/plugins/craft-parts/build-and-run-hello/conda-hello/hello b/tests/spread/plugins/craft-parts/build-and-run-hello/conda-hello/hello new file mode 100755 index 0000000000..15525d46ed --- /dev/null +++ b/tests/spread/plugins/craft-parts/build-and-run-hello/conda-hello/hello @@ -0,0 +1,4 @@ +#!/usr/bin/env ipython3 + +print('hello world') + diff --git a/tests/spread/plugins/craft-parts/build-and-run-hello/conda-hello/snap/snapcraft.yaml b/tests/spread/plugins/craft-parts/build-and-run-hello/conda-hello/snap/snapcraft.yaml new file mode 100644 index 0000000000..20254a51f5 --- /dev/null +++ b/tests/spread/plugins/craft-parts/build-and-run-hello/conda-hello/snap/snapcraft.yaml @@ -0,0 +1,25 @@ +name: conda-hello +version: '1.0' +summary: Hello world using ipython from conda packages +description: | + Leverage conda-packages to install ipython and use it to say "hello world". + +grade: devel +base: core22 +confinement: strict + +apps: + conda-hello: + command: + hello + +parts: + ipython: + plugin: conda + conda-miniconda-version: 4.6.14 + conda-packages: + - ipython + conda-python-version: "3.9" + hello: + plugin: dump + source: . diff --git a/tests/spread/plugins/craft-parts/build-and-run-hello/task.yaml b/tests/spread/plugins/craft-parts/build-and-run-hello/task.yaml new file mode 100644 index 0000000000..9a681c1ec7 --- /dev/null +++ b/tests/spread/plugins/craft-parts/build-and-run-hello/task.yaml @@ -0,0 +1,71 @@ +summary: >- + Build, clean, build, modify and rebuild, and run hello + with different plugin configurations + +environment: + SNAP/conda: conda-hello + +prepare: | + #shellcheck source=tests/spread/tools/snapcraft-yaml.sh + . "$TOOLS_DIR/snapcraft-yaml.sh" + set_base "${SNAP}/snap/snapcraft.yaml" + +restore: | + cd "${SNAP}" + snapcraft clean + rm -f ./*.snap + + # Undo changes to hello + [ -f hello ] && git checkout hello + [ -f hello.c ] && git checkout hello.c + [ -f subdir/hello.c ] && git checkout subdir/hello.c + [ -f hello.js ] && git checkout hello.js + [ -f main.go ] && git checkout main.go + [ -f src/hello.cpp ] && git checkout src/hello.cpp + [ -f src/main.rs ] && git checkout src/main.rs + [ -f lib/src/lib.rs ] && git checkout lib/src/lib.rs + + #shellcheck source=tests/spread/tools/snapcraft-yaml.sh + . "$TOOLS_DIR/snapcraft-yaml.sh" + restore_yaml "snap/snapcraft.yaml" + +execute: | + cd "${SNAP}" + + # Build what we have and verify the snap runs as expected. + snapcraft + snap install "${SNAP}"_1.0_*.snap --dangerous + [ "$($SNAP)" = "hello world" ] + + # Clean the hello part, then build and run again. + snapcraft clean hello + snapcraft + snap install "${SNAP}"_1.0_*.snap --dangerous + [ "$($SNAP)" = "hello world" ] + + # Make sure that what we built runs with the changes applied. + if [ -f hello ]; then + modified_file=hello + elif [ -f hello.c ]; then + modified_file=hello.c + elif [ -f subdir/hello.c ]; then + modified_file=subdir/hello.c + elif [ -f hello.js ]; then + modified_file=hello.js + elif [ -f main.go ]; then + modified_file=main.go + elif [ -f src/hello.cpp ]; then + modified_file=src/hello.cpp + elif [ -f src/main.rs ]; then + modified_file=src/main.rs + elif [ -f say/src/lib.rs ]; then + modified_file=say/src/lib.rs + else + FATAL "Cannot setup ${SNAP} for rebuilding" + fi + + sed -i "${modified_file}" -e 's/hello world/hello rebuilt world/' + + snapcraft + snap install "${SNAP}"_1.0_*.snap --dangerous + [ "$($SNAP)" = "hello rebuilt world" ] diff --git a/tests/unit/parts/plugins/__init__.py b/tests/unit/parts/plugins/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/unit/parts/plugins/test_conda_plugin.py b/tests/unit/parts/plugins/test_conda_plugin.py new file mode 100644 index 0000000000..ace3ca9f65 --- /dev/null +++ b/tests/unit/parts/plugins/test_conda_plugin.py @@ -0,0 +1,215 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright 2022 Canonical Ltd. +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License version 3 as published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program. If not, see . + +import os + +import pytest +from craft_parts import Part, PartInfo, ProjectInfo +from pydantic import ValidationError + +from snapcraft import errors +from snapcraft.parts.plugins import CondaPlugin +from snapcraft.parts.plugins.conda_plugin import _get_miniconda_source + + +@pytest.fixture(autouse=True) +def part_info(new_dir): + yield PartInfo( + project_info=ProjectInfo( + application_name="test", project_name="test-snap", cache_dir=new_dir + ), + part=Part("my-part", {}), + ) + + +@pytest.fixture +def fake_platform(monkeypatch): + if os.getenv("SNAP_ARCH"): + monkeypatch.delenv("SNAP_ARCH") + monkeypatch.setattr("platform.machine", lambda: "x86_64") + monkeypatch.setattr("platform.architecture", lambda: ("64bit", "ELF")) + + +@pytest.mark.usefixtures("fake_platform") +class TestPluginCondaPlugin: + """Conda plugin tests.""" + + def test_get_build_snaps(self, part_info): + properties = CondaPlugin.properties_class.unmarshal({}) + plugin = CondaPlugin(properties=properties, part_info=part_info) + assert plugin.get_build_snaps() == set() + + def test_get_build_packages(self, part_info): + properties = CondaPlugin.properties_class.unmarshal({}) + plugin = CondaPlugin(properties=properties, part_info=part_info) + assert plugin.get_build_packages() == set() + + def test_get_build_environment(self, part_info): + properties = CondaPlugin.properties_class.unmarshal({}) + plugin = CondaPlugin(properties=properties, part_info=part_info) + + assert plugin.get_build_environment() == { + "PATH": "${HOME}/miniconda/bin:${PATH}" + } + + def test_get_build_commands(self, part_info): + properties = CondaPlugin.properties_class.unmarshal({}) + plugin = CondaPlugin(properties=properties, part_info=part_info) + + assert plugin.get_build_commands() == [ + 'if ! [ -e "${HOME}/miniconda.sh" ]; then\n' + " curl --proto '=https' --tlsv1.2 -sSf " + "https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh " + "> ${HOME}/miniconda.sh\n" + " chmod 755 ${HOME}/miniconda.sh\n" + "fi", + "${HOME}/miniconda.sh -bfp ${HOME}/miniconda", + ( + "CONDA_TARGET_PREFIX_OVERRIDE=/snap/test-snap/current conda create --prefix " + f"{plugin._part_info.part_install_dir!s} " + "--yes" + ), + ] + + def test_get_build_commands_conda_packages(self, part_info): + properties = CondaPlugin.properties_class.unmarshal( + {"conda-packages": ["test-package-1", "test-package-2"]} + ) + plugin = CondaPlugin(properties=properties, part_info=part_info) + + assert plugin.get_build_commands() == [ + 'if ! [ -e "${HOME}/miniconda.sh" ]; then\n' + " curl --proto '=https' --tlsv1.2 -sSf " + "https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh " + "> ${HOME}/miniconda.sh\n" + " chmod 755 ${HOME}/miniconda.sh\n" + "fi", + "${HOME}/miniconda.sh -bfp ${HOME}/miniconda", + ( + "CONDA_TARGET_PREFIX_OVERRIDE=/snap/test-snap/current conda create --prefix " + f"{plugin._part_info.part_install_dir!s} " + "--yes " + "test-package-1 test-package-2" + ), + ] + + @pytest.mark.parametrize("value", [None, []]) + def test_get_build_commands_conda_packages_empty(self, part_info, value): + properties = CondaPlugin.properties_class.unmarshal({"conda-packages": value}) + plugin = CondaPlugin(properties=properties, part_info=part_info) + + assert plugin.get_build_commands() == [ + 'if ! [ -e "${HOME}/miniconda.sh" ]; then\n' + " curl --proto '=https' --tlsv1.2 -sSf " + "https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh " + "> ${HOME}/miniconda.sh\n" + " chmod 755 ${HOME}/miniconda.sh\n" + "fi", + "${HOME}/miniconda.sh -bfp ${HOME}/miniconda", + ( + "CONDA_TARGET_PREFIX_OVERRIDE=/snap/test-snap/current conda create --prefix " + f"{plugin._part_info.part_install_dir!s} " + "--yes" + ), + ] + + @pytest.mark.parametrize( + "conda_packages", + ["i am a string", {"i am": "a dictionary"}], + ) + def test_get_build_commands_conda_packages_invalid(self, conda_packages): + with pytest.raises(ValidationError): + CondaPlugin.properties_class.unmarshal({"conda-packages": conda_packages}) + + def test_get_build_commands_conda_python_version(self, part_info): + properties = CondaPlugin.properties_class.unmarshal( + {"conda-python-version": "3.9"} + ) + plugin = CondaPlugin(properties=properties, part_info=part_info) + + assert plugin.get_build_commands() == [ + 'if ! [ -e "${HOME}/miniconda.sh" ]; then\n' + " curl --proto '=https' --tlsv1.2 -sSf " + "https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh " + "> ${HOME}/miniconda.sh\n" + " chmod 755 ${HOME}/miniconda.sh\n" + "fi", + "${HOME}/miniconda.sh -bfp ${HOME}/miniconda", + ( + "CONDA_TARGET_PREFIX_OVERRIDE=/snap/test-snap/current conda create --prefix " + f"{plugin._part_info.part_install_dir!s} " + "--yes python=3.9" + ), + ] + + @pytest.mark.parametrize( + "conda_python_version", + [{"i am": "a dictionary"}, ["i am", "a list"]], + ) + def test_get_build_commands_conda_python_version_invalid( + self, conda_python_version + ): + with pytest.raises(ValidationError): + CondaPlugin.properties_class.unmarshal( + {"conda-python-version": conda_python_version} + ) + + @pytest.mark.parametrize( + "conda_install_prefix", + [{"i am": "a dictionary"}, ["i am", "a list"]], + ) + def test_get_build_commands_conda_install_prefix_invalid( + self, conda_install_prefix + ): + with pytest.raises(ValidationError): + CondaPlugin.properties_class.unmarshal( + {"conda-install-prefix": conda_install_prefix} + ) + + +@pytest.mark.parametrize( + "snap_arch, url_arch", + [ + ("i386", "x86"), + ("amd64", "x86_64"), + ("armhf", "armv7l"), + ("ppc64el", "ppc64le"), + ], +) +def test_get_miniconda(monkeypatch, snap_arch, url_arch): + monkeypatch.setenv("SNAP_ARCH", snap_arch) + + assert _get_miniconda_source("latest") == ( + f"https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-{url_arch}.sh" + ) + + +@pytest.mark.parametrize( + "snap_arch", + ("s390x", "other", "not-supported", "new-arch"), +) +def test_get_miniconda_unsupported_arch( + monkeypatch, + snap_arch, +): + monkeypatch.setenv("SNAP_ARCH", snap_arch) + + with pytest.raises(errors.SnapcraftError) as raised: + _get_miniconda_source("latest") + + assert str(raised.value) == ( + f"Architecture not supported for conda plugin: {snap_arch!r}" + ) From d56fab0c7fe5f6884fc96b4ac7c69c5494f6bd23 Mon Sep 17 00:00:00 2001 From: Sergio Schvezov Date: Thu, 28 Apr 2022 17:47:56 -0300 Subject: [PATCH 127/167] utils: return the userspace architecture Solve the case of a 64 kernel running a 32 bit userpace Signed-off-by: Sergio Schvezov --- snapcraft/utils.py | 17 +++++++++++++++-- tests/unit/test_utils.py | 25 +++++++++++++++---------- 2 files changed, 30 insertions(+), 12 deletions(-) diff --git a/snapcraft/utils.py b/snapcraft/utils.py index bb8a4bf148..9896a1e9eb 100644 --- a/snapcraft/utils.py +++ b/snapcraft/utils.py @@ -54,6 +54,13 @@ def __str__(self) -> str: "AMD64": "amd64", # Windows support } +_32BIT_USERSPACE_ARCHITECTURE = { + "aarch64": "armv7l", + "armv8l": "armv7l", + "ppc64le": "ppc", + "x86_64": "i686", +} + def get_os_platform(filepath=pathlib.Path("/etc/os-release")): """Determine a system/release combo for an OS using /etc/os-release if available.""" @@ -85,8 +92,14 @@ def get_os_platform(filepath=pathlib.Path("/etc/os-release")): def get_host_architecture(): """Get host architecture in deb format suitable for base definition.""" - os_platform = get_os_platform() - return ARCH_TRANSLATIONS.get(os_platform.machine, os_platform.machine) + os_platform_machine = get_os_platform().machine + + if platform.architecture()[0] == "32bit": + userspace = _32BIT_USERSPACE_ARCHITECTURE.get(os_platform_machine) + if userspace: + os_platform_machine = userspace + + return ARCH_TRANSLATIONS.get(os_platform_machine, os_platform_machine) def strtobool(value: str) -> bool: diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py index bd360fbbc2..a9de26a085 100644 --- a/tests/unit/test_utils.py +++ b/tests/unit/test_utils.py @@ -178,20 +178,25 @@ def test_get_os_platform_windows(mocker): @pytest.mark.parametrize( - "platform_arch,deb_arch", + "platform_machine,platform_architecture,deb_arch", [ - ("AMD64", "amd64"), - ("aarch64", "arm64"), - ("armv7l", "armhf"), - ("ppc", "powerpc"), - ("ppc64le", "ppc64el"), - ("x86_64", "amd64"), - ("unknown-arch", "unknown-arch"), + ("AMD64", ("64bit", "ELF"), "amd64"), + ("aarch64", ("64bit", "ELF"), "arm64"), + ("aarch64", ("32bit", "ELF"), "armhf"), + ("armv7l", ("64bit", "ELF"), "armhf"), + ("ppc", ("64bit", "ELF"), "powerpc"), + ("ppc64le", ("64bit", "ELF"), "ppc64el"), + ("x86_64", ("64bit", "ELF"), "amd64"), + ("x86_64", ("32bit", "ELF"), "i386"), + ("unknown-arch", ("64bit", "ELF"), "unknown-arch"), ], ) -def test_get_host_architecture(platform_arch, mocker, deb_arch): +def test_get_host_architecture( + platform_machine, platform_architecture, mocker, deb_arch +): """Test all platform mappings in addition to unknown.""" - mocker.patch("platform.machine", return_value=platform_arch) + mocker.patch("platform.machine", return_value=platform_machine) + mocker.patch("platform.architecture", return_value=platform_architecture) assert utils.get_host_architecture() == deb_arch From e2bfca89db9b03f2ac920a9aded8c9026ae1dce3 Mon Sep 17 00:00:00 2001 From: Claudio Matsuoka Date: Thu, 28 Apr 2022 16:23:56 -0300 Subject: [PATCH 128/167] meta: fix snap yaml generation to match legacy Fix undeclared snap type and app list if no apps defined. Signed-off-by: Claudio Matsuoka --- snapcraft/meta/snap_yaml.py | 6 +++--- snapcraft/projects.py | 2 +- tests/spread/core22/appstream-desktop/expected_snap.yaml | 1 - tests/unit/meta/test_snap_yaml.py | 2 +- tests/unit/parts/test_lifecycle.py | 2 +- tests/unit/test_projects.py | 2 +- 6 files changed, 7 insertions(+), 8 deletions(-) diff --git a/snapcraft/meta/snap_yaml.py b/snapcraft/meta/snap_yaml.py index 1660a1a4c7..79dadd974b 100644 --- a/snapcraft/meta/snap_yaml.py +++ b/snapcraft/meta/snap_yaml.py @@ -99,7 +99,7 @@ class SnapMetadata(YamlModel): summary: str description: str license: Optional[str] - type: str + type: Optional[str] architectures: List[str] base: str assumes: Optional[List[str]] @@ -168,9 +168,9 @@ def write(project: Project, prime_dir: Path, *, arch: str): type=project.type, architectures=[arch], base=cast(str, project.base), - assumes=["command-chain"], + assumes=["command-chain"] if snap_apps else None, epoch=project.epoch, - apps=snap_apps, + apps=snap_apps or None, confinement=project.confinement, grade=project.grade or "stable", environment=project.environment, diff --git a/snapcraft/projects.py b/snapcraft/projects.py index 363134806e..1cd8e1102e 100644 --- a/snapcraft/projects.py +++ b/snapcraft/projects.py @@ -250,7 +250,7 @@ class Project(ProjectModel): website: Optional[str] summary: Optional[constr(max_length=78)] # type: ignore description: Optional[str] - type: Literal["app", "base", "gadget", "kernel", "snapd"] = "app" + type: Optional[Literal["app", "base", "gadget", "kernel", "snapd"]] icon: Optional[str] confinement: Literal["classic", "devmode", "strict"] layout: Optional[Dict[str, Dict[str, Any]]] diff --git a/tests/spread/core22/appstream-desktop/expected_snap.yaml b/tests/spread/core22/appstream-desktop/expected_snap.yaml index ac871c5d85..81a303ae70 100644 --- a/tests/spread/core22/appstream-desktop/expected_snap.yaml +++ b/tests/spread/core22/appstream-desktop/expected_snap.yaml @@ -13,7 +13,6 @@ description: |- Test me please. -type: app architectures: - amd64 base: core22 diff --git a/tests/unit/meta/test_snap_yaml.py b/tests/unit/meta/test_snap_yaml.py index 713e3e15e2..37b84a3660 100644 --- a/tests/unit/meta/test_snap_yaml.py +++ b/tests/unit/meta/test_snap_yaml.py @@ -73,7 +73,6 @@ def test_simple_snap_yaml(simple_project, new_dir): most important story about your snap. Keep it under 100 words though, we live in tweetspace and your description wants to look good in the snap store. - type: app architectures: - arch base: core22 @@ -97,6 +96,7 @@ def complex_project(): name: mytest version: 1.29.3 base: core22 + type: app summary: Single-line elevator pitch for your amazing snap description: | This is my-snap's description. You have a paragraph or two to tell the diff --git a/tests/unit/parts/test_lifecycle.py b/tests/unit/parts/test_lifecycle.py index 863caeaeed..c09e3abed4 100644 --- a/tests/unit/parts/test_lifecycle.py +++ b/tests/unit/parts/test_lifecycle.py @@ -72,7 +72,7 @@ def write_file( "website": None, "summary": "Just some test data", "description": "This is just some test data.", - "type": "app", + "type": None, "confinement": "strict", "icon": None, "layout": None, diff --git a/tests/unit/test_projects.py b/tests/unit/test_projects.py index 3cb4c2144c..1834807cd3 100644 --- a/tests/unit/test_projects.py +++ b/tests/unit/test_projects.py @@ -84,7 +84,7 @@ def test_project_defaults(self, project_yaml_data): assert project.issues is None assert project.source_code is None assert project.website is None - assert project.type == "app" + assert project.type is None assert project.icon is None assert project.layout is None assert project.license is None From 61aaff6fc176045b51011f03ae11a32fcff5136e Mon Sep 17 00:00:00 2001 From: Claudio Matsuoka Date: Thu, 28 Apr 2022 20:32:36 -0300 Subject: [PATCH 129/167] tests: use lxd in clean provider test Tests are ordinarily conducted in destructive mode, enable lxd in provider cleaning tests. Signed-off-by: Claudio Matsuoka --- tests/spread/core22/clean/task.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/spread/core22/clean/task.yaml b/tests/spread/core22/clean/task.yaml index 707ad9b39c..41ce466a19 100644 --- a/tests/spread/core22/clean/task.yaml +++ b/tests/spread/core22/clean/task.yaml @@ -17,6 +17,9 @@ restore: | restore_yaml "snap/snapcraft.yaml" execute: | + # Unset SNAPCRAFT_BUILD_ENVIRONMENT=host. + unset SNAPCRAFT_BUILD_ENVIRONMENT + snapcraft pack snapcraft clean part1 lxc --project=snapcraft list | grep snapcraft-clean From 301aadd2bf06a79d86ee5379975cf88ae4ba4884 Mon Sep 17 00:00:00 2001 From: Sergio Schvezov Date: Fri, 29 Apr 2022 06:31:21 -0300 Subject: [PATCH 130/167] legacy command: remove close and release Signed-off-by: Sergio Schvezov --- snapcraft_legacy/_store.py | 9 +- snapcraft_legacy/cli/store.py | 153 +--------- snapcraft_legacy/storeapi/_dashboard_api.py | 23 +- snapcraft_legacy/storeapi/_store_client.py | 6 +- tests/legacy/unit/commands/test_close.py | 108 ------- tests/legacy/unit/commands/test_release.py | 280 ------------------- tests/legacy/unit/store/test_store_client.py | 77 ----- 7 files changed, 4 insertions(+), 652 deletions(-) delete mode 100644 tests/legacy/unit/commands/test_close.py delete mode 100644 tests/legacy/unit/commands/test_release.py diff --git a/snapcraft_legacy/_store.py b/snapcraft_legacy/_store.py index a40bd80928..bc43e3d5c8 100644 --- a/snapcraft_legacy/_store.py +++ b/snapcraft_legacy/_store.py @@ -26,7 +26,7 @@ from datetime import datetime, timedelta from pathlib import Path from subprocess import Popen -from typing import TYPE_CHECKING, Any, Dict, List, Optional, Sequence, Tuple +from typing import Any, Dict, List, Optional, Sequence, TYPE_CHECKING, Tuple from urllib.parse import urljoin import craft_store @@ -34,7 +34,6 @@ from tabulate import tabulate from snapcraft_legacy import storeapi, yaml_utils - # Ideally we would move stuff into more logical components from snapcraft_legacy.cli import echo from snapcraft_legacy.file_utils import ( @@ -337,12 +336,6 @@ class StoreClientCLI(storeapi.StoreClient): # during upload into this class using click. # TODO use an instance of this class directly from snapcraft_legacy.cli.store - @_login_wrapper - def close_channels( - self, *, snap_id: str, channel_names: List[str] - ) -> Dict[str, Any]: - return super().close_channels(snap_id=snap_id, channel_names=channel_names) - @_login_wrapper def get_metrics( self, *, filters: List[MetricsFilter], snap_name: str diff --git a/snapcraft_legacy/cli/store.py b/snapcraft_legacy/cli/store.py index 95e9344076..82ef79ed04 100644 --- a/snapcraft_legacy/cli/store.py +++ b/snapcraft_legacy/cli/store.py @@ -19,7 +19,7 @@ import os from datetime import date, timedelta from textwrap import dedent -from typing import Dict, List, Optional, Set, Union +from typing import Dict, List, Set, Union import click from tabulate import tabulate @@ -28,7 +28,6 @@ from snapcraft_legacy import formatting_utils, storeapi from snapcraft_legacy._store import StoreClientCLI from snapcraft_legacy.storeapi import metrics as metrics_module - from . import echo from ._channel_map import get_tabulated_channel_map from ._metrics import convert_metrics_to_table @@ -169,102 +168,6 @@ def upload_metadata(snap_file, force): snapcraft_legacy.upload_metadata(snap_file, force) -@storecli.command() -@click.argument("snap-name", metavar="") -@click.argument("revision", metavar="") -@click.argument("channels", metavar="") -@click.option( - "--progressive", - type=click.IntRange(0, 100), - default=100, - metavar="", - help="set a release progression to a certain percentage.", -) -@click.option( - "--experimental-progressive-releases", - is_flag=True, - help="*EXPERIMENTAL* Enables 'progressive releases'.", - envvar="SNAPCRAFT_EXPERIMENTAL_PROGRESSIVE_RELEASES", -) -def release( - snap_name, - revision, - channels, - progressive: Optional[int], - experimental_progressive_releases: bool, -) -> None: - """Release on to the selected store . - is a comma separated list of valid channels on the - store. - - The must exist on the store, to see available revisions - run `snapcraft list-revisions `. - - The channel map will be displayed after the operation takes place. - To see the status map at any other time run `snapcraft status `. - - The format for channels is `[/][/]` where - - \b - - is used to have long term release channels. It is implicitly - set to `latest`. If this snap requires one, it can be created by - request by having a conversation on https://forum.snapcraft.io - under the *store* category. - - is mandatory and can be either `stable`, `candidate`, `beta` - or `edge`. - - is optional and dynamically creates a channel with a - specific expiration date. - - \b - Examples: - snapcraft release my-snap 8 stable - snapcraft release my-snap 8 stable/my-branch - snapcraft release my-snap 9 beta,edge - snapcraft release my-snap 9 lts-channel/stable - snapcraft release my-snap 9 lts-channel/stable/my-branch - """ - # If progressive is set to 100, treat it as None. - if progressive == 100: - progressive = None - - if progressive is not None and not experimental_progressive_releases: - raise click.UsageError( - "--progressive requires --experimental-progressive-releases." - ) - elif progressive: - os.environ["SNAPCRAFT_EXPERIMENTAL_PROGRESSIVE_RELEASES"] = "Y" - echo.warning("*EXPERIMENTAL* progressive releases in use.") - - store_client_cli = StoreClientCLI() - release_data = store_client_cli.release( - snap_name=snap_name, - revision=revision, - channels=channels.split(","), - progressive_percentage=progressive, - ) - snap_channel_map = store_client_cli.get_snap_channel_map(snap_name=snap_name) - architectures_for_revision = snap_channel_map.get_revision( - int(revision) - ).architectures - tracks = [storeapi.channels.Channel(c).track for c in channels.split(",")] - click.echo( - get_tabulated_channel_map( - snap_channel_map, tracks=tracks, architectures=architectures_for_revision - ) - ) - - opened_channels = release_data.get("opened_channels", []) - if len(opened_channels) == 1: - echo.info(f"The {opened_channels[0]!r} channel is now open.") - elif len(opened_channels) > 1: - channels = ("{!r}".format(channel) for channel in opened_channels[:-1]) - echo.info( - "The {} and {!r} channels are now open.".format( - ", ".join(channels), opened_channels[-1] - ) - ) - - @storecli.command() @click.argument("snap-name", metavar="") @click.option( @@ -368,60 +271,6 @@ def promote(snap_name, from_channel, to_channel, yes): echo.wrapped("Channel promotion cancelled") -@storecli.command() -@click.argument("snap-name", metavar="") -@click.argument("channels", metavar="...", nargs=-1) -def close(snap_name, channels): - """Close for . - Closing a channel allows the that is closed to track the channel - that follows it in the channel release chain. As such closing the - 'candidate' channel would make it track the 'stable' channel. - - The channel map will be displayed after the operation takes place. - - \b - Examples: - snapcraft close my-snap beta - snapcraft close my-snap beta edge - """ - store = storeapi.StoreClient() - account_info = store.get_account_information() - - try: - snap_id = account_info["snaps"][storeapi.constants.DEFAULT_SERIES][snap_name][ - "snap-id" - ] - except KeyError: - raise storeapi.errors.StoreChannelClosingPermissionError( - snap_name, storeapi.constants.DEFAULT_SERIES - ) - - # Returned closed_channels cannot be trusted as it returns risks. - store.close_channels(snap_id=snap_id, channel_names=channels) - if len(channels) == 1: - msg = "The {} channel is now closed.".format(channels[0]) - else: - msg = "The {} and {} channels are now closed.".format( - ", ".join(channels[:-1]), channels[-1] - ) - - snap_channel_map = store.get_snap_channel_map(snap_name=snap_name) - if snap_channel_map.channel_map: - closed_tracks = {storeapi.channels.Channel(c).track for c in channels} - existing_architectures = snap_channel_map.get_existing_architectures() - - click.echo( - get_tabulated_channel_map( - snap_channel_map, - architectures=existing_architectures, - tracks=closed_tracks, - ) - ) - click.echo() - - echo.info(msg) - - @storecli.command() @click.option( "--experimental-progressive-releases", diff --git a/snapcraft_legacy/storeapi/_dashboard_api.py b/snapcraft_legacy/storeapi/_dashboard_api.py index 9a8d5b5488..6e829448dd 100644 --- a/snapcraft_legacy/storeapi/_dashboard_api.py +++ b/snapcraft_legacy/storeapi/_dashboard_api.py @@ -18,8 +18,8 @@ import logging from typing import Any, Dict, List, Optional from urllib.parse import urlencode, urljoin -import craft_store +import craft_store import requests from simplejson.scanner import JSONDecodeError @@ -334,27 +334,6 @@ def snap_status(self, snap_id, series, arch): return response_json - def close_channels(self, snap_id, channel_names): - url = "/dev/api/snaps/{}/close".format(snap_id) - data = {"channels": channel_names} - headers = {"Content-Type": "application/json", "Accept": "application/json"} - - try: - response = self.post(url, json=data, headers=headers) - except craft_store.errors.StoreServerError as craft_error: - raise errors.StoreChannelClosingError(craft_error.response) from craft_error - - try: - results = response.json() - except (JSONDecodeError, KeyError): - logger.debug( - "Invalid response from the server on channel closing:\n" - f"{response.status_code} {response.reason}\n{response.content}" - ) - raise errors.StoreChannelClosingError(response) - - return results["closed_channels"], results["channel_map_tree"] - def sign_developer_agreement(self, latest_tos_accepted=False): data = {"latest_tos_accepted": latest_tos_accepted} try: diff --git a/snapcraft_legacy/storeapi/_store_client.py b/snapcraft_legacy/storeapi/_store_client.py index 5042076f57..b3bfb686b3 100644 --- a/snapcraft_legacy/storeapi/_store_client.py +++ b/snapcraft_legacy/storeapi/_store_client.py @@ -18,13 +18,12 @@ import os import platform from time import sleep -from typing import Any, Dict, Iterable, List, Optional, Sequence, Union +from typing import Any, Dict, List, Optional, Sequence, Union import craft_store import requests from snapcraft_legacy.internal.indicators import download_requests_stream - from . import _upload, agent, constants, errors, metrics from ._dashboard_api import DashboardAPI from ._snap_api import SnapAPI @@ -248,9 +247,6 @@ def get_validation_sets( ) -> validation_sets.ValidationSets: return self.dashboard.get_validation_sets(name=name, sequence=sequence) - def close_channels(self, snap_id, channel_names): - return self.dashboard.close_channels(snap_id, channel_names) - @classmethod def download( cls, diff --git a/tests/legacy/unit/commands/test_close.py b/tests/legacy/unit/commands/test_close.py deleted file mode 100644 index 81a93212bb..0000000000 --- a/tests/legacy/unit/commands/test_close.py +++ /dev/null @@ -1,108 +0,0 @@ -# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- -# -# Copyright 2016-2021 Canonical Ltd -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License version 3 as -# published by the Free Software Foundation. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -from textwrap import dedent - -import fixtures -from testtools.matchers import Contains, Equals - -import snapcraft_legacy.storeapi.errors -from snapcraft_legacy import storeapi - -from . import FakeStoreCommandsBaseTestCase - - -class CloseCommandTestCase(FakeStoreCommandsBaseTestCase): - def setUp(self): - super().setUp() - - self.useFixture( - fixtures.MockPatchObject( - storeapi._dashboard_api.DashboardAPI, - "close_channels", - return_value=(list(), dict()), - ) - ) - - def test_close_missing_permission(self): - self.fake_store_account_info.mock.return_value = { - "account_id": "abcd", - "snaps": {}, - } - - raised = self.assertRaises( - snapcraft_legacy.storeapi.errors.StoreChannelClosingPermissionError, - self.run_command, - ["close", "foo", "beta"], - ) - - self.assertThat( - str(raised), - Equals( - "Your account lacks permission to close channels for this snap. " - "Make sure the logged in account has upload permissions on " - "'foo' in series '16'." - ), - ) - - def test_close(self): - result = self.run_command(["close", "snap-test", "2.1/candidate"]) - - self.assertThat(result.exit_code, Equals(0)) - self.assertThat( - result.output, - Contains( - dedent( - """\ - Track Arch Channel Version Revision - 2.1 amd64 stable - - - candidate - - - beta 10 19 - edge ↑ ↑ - - The 2.1/candidate channel is now closed.""" - ) - ), - ) - - def test_close_no_revisions(self): - self.channel_map.channel_map = list() - - result = self.run_command(["close", "snap-test", "2.1/candidate"]) - - self.assertThat(result.exit_code, Equals(0)) - self.assertThat( - result.output.strip(), Equals("The 2.1/candidate channel is now closed.") - ) - - def test_close_multiple_channels(self): - result = self.run_command(["close", "snap-test", "2.1/stable", "2.1/edge/test"]) - - self.assertThat(result.exit_code, Equals(0)) - self.assertThat( - result.output, - Contains( - dedent( - """\ - Track Arch Channel Version Revision - 2.1 amd64 stable - - - candidate - - - beta 10 19 - edge ↑ ↑ - - The 2.1/stable and 2.1/edge/test channels are now closed.""" - ) - ), - ) diff --git a/tests/legacy/unit/commands/test_release.py b/tests/legacy/unit/commands/test_release.py deleted file mode 100644 index ba501da1a5..0000000000 --- a/tests/legacy/unit/commands/test_release.py +++ /dev/null @@ -1,280 +0,0 @@ -# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- -# -# Copyright 2016-2020 Canonical Ltd -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License version 3 as -# published by the Free Software Foundation. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -from textwrap import dedent - -from testtools.matchers import Contains, Equals - -from snapcraft_legacy.storeapi.v2.channel_map import ( - MappedChannel, - Progressive, - Revision, - SnapChannel, -) - -from . import FAKE_UNAUTHORIZED_ERROR, FakeStoreCommandsBaseTestCase - - -class ReleaseCommandTestCase(FakeStoreCommandsBaseTestCase): - def setUp(self): - super().setUp() - - self.fake_store_release.mock.return_value = {"opened_channels": ["2.1/beta"]} - - def test_release_without_snap_name_must_raise_exception(self): - result = self.run_command(["release"]) - - self.assertThat(result.exit_code, Equals(2)) - self.assertThat(result.output, Contains("Usage:")) - - def test_release(self): - result = self.run_command(["release", "nil-snap", "19", "2.1/beta"]) - - self.assertThat(result.exit_code, Equals(0)) - self.assertThat( - result.output, - Equals( - dedent( - """\ - Track Arch Channel Version Revision - 2.1 amd64 stable - - - candidate - - - beta 10 19 - edge ↑ ↑ - The '2.1/beta' channel is now open. - """ - ) - ), - ) - self.fake_store_release.mock.assert_called_once_with( - snap_name="nil-snap", - revision="19", - channels=["2.1/beta"], - progressive_percentage=None, - ) - - def test_progressive_release(self): - self.channel_map.channel_map[0].progressive.percentage = 10.0 - self.channel_map.channel_map[0].progressive.current_percentage = 5.0 - - result = self.run_command( - [ - "release", - "nil-snap", - "19", - "2.1/beta", - "--progressive", - "10", - "--experimental-progressive-releases", - ] - ) - - self.assertThat( - result.output, - Equals( - dedent( - """\ - *EXPERIMENTAL* progressive releases in use. - Track Arch Channel Version Revision Progress - 2.1 amd64 stable - - - - candidate - - - - beta - - - - 10 19 5→10% - edge ↑ ↑ - - The '2.1/beta' channel is now open. - """ - ) - ), - ) - self.fake_store_release.mock.assert_called_once_with( - snap_name="nil-snap", - revision="19", - channels=["2.1/beta"], - progressive_percentage=10, - ) - - def test_release_with_branch(self): - self.fake_store_release.mock.return_value = { - "opened_channels": ["stable/hotfix1"] - } - self.channel_map.channel_map.append( - MappedChannel( - channel="2.1/stable/hotfix1", - architecture="amd64", - expiration_date="2020-02-03T20:58:37Z", - revision=20, - progressive=Progressive( - paused=None, percentage=None, current_percentage=None - ), - ) - ) - self.channel_map.revisions.append( - Revision(architectures=["amd64"], revision=20, version="10hotfix") - ) - self.channel_map.snap.channels.append( - SnapChannel( - name="2.1/stable/hotfix1", - track="2.1", - risk="stable", - branch="hotfix1", - fallback="2.1/stable", - ) - ) - - result = self.run_command(["release", "nil-snap", "20", "2.1/stable/hotfix1"]) - - self.assertThat(result.exit_code, Equals(0)) - self.assertThat( - result.output, - Equals( - dedent( - """\ - Track Arch Channel Version Revision Expires at - 2.1 amd64 stable - - - stable/hotfix1 10hotfix 20 2020-02-03T20:58:37Z - candidate - - - beta 10 19 - edge ↑ ↑ - The 'stable/hotfix1' channel is now open. - """ - ) - ), - ) - - self.fake_store_release.mock.assert_called_once_with( - snap_name="nil-snap", - revision="20", - channels=["2.1/stable/hotfix1"], - progressive_percentage=None, - ) - - def test_progressive_release_with_branch(self): - self.fake_store_release.mock.return_value = { - "opened_channels": ["2.1/stable/hotfix1"] - } - self.channel_map.channel_map.append( - MappedChannel( - channel="2.1/stable/hotfix1", - architecture="amd64", - expiration_date="2020-02-03T20:58:37Z", - revision=20, - progressive=Progressive( - paused=None, percentage=80.0, current_percentage=None - ), - ) - ) - self.channel_map.revisions.append( - Revision(architectures=["amd64"], revision=20, version="10hotfix") - ) - self.channel_map.snap.channels.append( - SnapChannel( - name="2.1/stable/hotfix1", - track="2.1", - risk="stable", - branch="hotfix1", - fallback="2.1/stable", - ) - ) - - result = self.run_command( - [ - "release", - "--progressive", - "80", - "--experimental-progressive-releases", - "nil-snap", - "20", - "2.1/stable/hotfix1", - ] - ) - - self.assertThat(result.exit_code, Equals(0)) - self.assertThat( - result.output, - Equals( - dedent( - """\ - *EXPERIMENTAL* progressive releases in use. - Track Arch Channel Version Revision Progress Expires at - 2.1 amd64 stable - - - - stable/hotfix1 10hotfix 20 ?→80% 2020-02-03T20:58:37Z - candidate - - - - beta 10 19 - - edge ↑ ↑ - - The '2.1/stable/hotfix1' channel is now open. - """ - ) - ), - ) - self.fake_store_release.mock.assert_called_once_with( - snap_name="nil-snap", - revision="20", - channels=["2.1/stable/hotfix1"], - progressive_percentage=80, - ) - - def test_progressive_release_with_null_current_percentage(self): - self.channel_map.channel_map[0].progressive.percentage = 10.0 - self.channel_map.channel_map[0].progressive.current_percentage = None - - result = self.run_command( - [ - "release", - "nil-snap", - "19", - "2.1/beta", - "--progressive", - "10", - "--experimental-progressive-releases", - ] - ) - - self.assertThat( - result.output, - Equals( - dedent( - """\ - *EXPERIMENTAL* progressive releases in use. - Track Arch Channel Version Revision Progress - 2.1 amd64 stable - - - - candidate - - - - beta - - - - 10 19 ?→10% - edge ↑ ↑ - - The '2.1/beta' channel is now open. - """ - ) - ), - ) - self.fake_store_release.mock.assert_called_once_with( - snap_name="nil-snap", - revision="19", - channels=["2.1/beta"], - progressive_percentage=10, - ) - - def test_release_without_login_must_ask(self): - self.fake_store_release.mock.side_effect = [ - FAKE_UNAUTHORIZED_ERROR, - {"opened_channels": ["beta"]}, - ] - - result = self.run_command( - ["release", "nil-snap", "19", "beta"], input="user@example.com\nsecret\n" - ) - self.assertThat( - result.output, Contains("You are required to login before continuing.") - ) diff --git a/tests/legacy/unit/store/test_store_client.py b/tests/legacy/unit/store/test_store_client.py index c891dcea29..ea888fe373 100644 --- a/tests/legacy/unit/store/test_store_client.py +++ b/tests/legacy/unit/store/test_store_client.py @@ -20,7 +20,6 @@ import tempfile from textwrap import dedent from unittest import mock -import craft_store import fixtures import pytest @@ -831,82 +830,6 @@ def test_release_to_curly_braced_channel(self): ) -class CloseChannelsTestCase(StoreTestCase): - def setUp(self): - super().setUp() - self.fake_logger = fixtures.FakeLogger(level=logging.DEBUG) - self.useFixture(self.fake_logger) - - def test_close_invalid_data(self): - raised = self.assertRaises( - errors.StoreChannelClosingError, - self.client.close_channels, - "snap-id", - ["invalid"], - ) - self.assertThat( - str(raised), - Equals( - "Could not close channel: The 'channels' field content " "is not valid." - ), - ) - - def test_close_broken_store_plain(self): - # If the contract is broken by the Store, users will be have additional - # debug information available. - raised = self.assertRaises( - errors.StoreChannelClosingError, - self.client.close_channels, - "snap-id", - ["broken-plain"], - ) - self.assertThat(str(raised), Equals("Could not close channel: 200 OK")) - - expected_lines = [ - "Invalid response from the server on channel closing:", - "200 OK", - "b'plain data'", - ] - - actual_lines = [] - for line in self.fake_logger.output.splitlines(): - line = line.strip() - if line in expected_lines: - actual_lines.append(line) - - self.assertThat(actual_lines, Equals(expected_lines)) - - def test_close_successfully(self): - # Successfully closing a channels returns 'closed_channels' - # and 'channel_map_tree' from the Store. - closed_channels, channel_map_tree = self.client.close_channels( - "snap-id", ["beta"] - ) - self.assertThat(closed_channels, Equals(["beta"])) - self.assertThat( - channel_map_tree, - Equals( - { - "latest": { - "16": { - "amd64": [ - {"channel": "stable", "info": "none"}, - {"channel": "candidate", "info": "none"}, - { - "channel": "beta", - "info": "specific", - "revision": 42, - "version": "1.1", - }, - {"channel": "edge", "info": "tracking"}, - ] - } - } - } - ), - ) - - class GetSnapStatusTestCase(StoreTestCase): def setUp(self): super().setUp() From 7b014684a49fd69ef526a2959616c8ee9c28634a Mon Sep 17 00:00:00 2001 From: Sergio Schvezov Date: Fri, 29 Apr 2022 16:55:54 -0300 Subject: [PATCH 131/167] ci: disable Python 3.9 unit testing Signed-off-by: Sergio Schvezov --- .github/workflows/tests.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 21c5b5d003..3ef064050d 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -61,7 +61,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.8", "3.9", "3.10"] + python-version: ["3.8", "3.10"] runs-on: ubuntu-20.04 steps: From e2ff471b96e4b9aaa93bda460b0977045beddcce Mon Sep 17 00:00:00 2001 From: Claudio Matsuoka Date: Wed, 27 Apr 2022 20:01:46 -0300 Subject: [PATCH 132/167] parts: run debug shell on build environment Handle arguments --debug, --shell and --shell-after to run a shell on the build environment (tipically on instance) in case of a parts error or to replace a lifecycle step. Signed-off-by: Claudio Matsuoka --- snapcraft/commands/lifecycle.py | 16 +++ snapcraft/parts/lifecycle.py | 14 ++- snapcraft/parts/parts.py | 59 +++++++-- tests/unit/cli/test_default_command.py | 4 + tests/unit/cli/test_lifecycle.py | 126 ++++++++++++++++++- tests/unit/parts/test_lifecycle.py | 168 +++++++++++++++++++++++-- 6 files changed, 362 insertions(+), 25 deletions(-) diff --git a/snapcraft/commands/lifecycle.py b/snapcraft/commands/lifecycle.py index d5a61c891a..0584426b30 100644 --- a/snapcraft/commands/lifecycle.py +++ b/snapcraft/commands/lifecycle.py @@ -43,6 +43,12 @@ def fill_parser(self, parser: "argparse.ArgumentParser") -> None: action="store_true", help="Use LXD to build", ) + parser.add_argument( + "--debug", + action="store_true", + help="Shell into the environment if the build fails", + ) + # --enable-experimental-extensions is only available in legacy parser.add_argument( "--enable-experimental-extensions", @@ -89,6 +95,16 @@ def fill_parser(self, parser: "argparse.ArgumentParser") -> None: nargs="*", help="Optional list of parts to process", ) + parser.add_argument( + "--shell", + action="store_true", + help="Shell into the environment in lieu of the step to run.", + ) + parser.add_argument( + "--shell-after", + action="store_true", + help="Shell into the environment after the step has run.", + ) class PullCommand(_LifecycleStepCommand): diff --git a/snapcraft/parts/lifecycle.py b/snapcraft/parts/lifecycle.py index 28424c4c0d..55166ad9ea 100644 --- a/snapcraft/parts/lifecycle.py +++ b/snapcraft/parts/lifecycle.py @@ -205,7 +205,12 @@ def _run_command( lifecycle.clean(part_names=part_names) return - lifecycle.run(step_name) + lifecycle.run( + step_name, + debug=parsed_args.debug, + shell=getattr(parsed_args, "shell", False), + shell_after=getattr(parsed_args, "shell_after", False), + ) # Extract metadata and generate snap.yaml project_vars = lifecycle.project_vars @@ -284,6 +289,13 @@ def _run_in_provider( elif emit.get_mode() == EmitterMode.TRACE: cmd.append("--trace") + if parsed_args.debug: + cmd.append("--debug") + if getattr(parsed_args, "shell", False): + cmd.append("--shell") + if getattr(parsed_args, "shell_after", False): + cmd.append("--shell-after") + output_dir = utils.get_managed_environment_project_path() emit.progress("Launching instance...") diff --git a/snapcraft/parts/parts.py b/snapcraft/parts/parts.py index a691ebb8fb..2344fcc74d 100644 --- a/snapcraft/parts/parts.py +++ b/snapcraft/parts/parts.py @@ -17,6 +17,7 @@ """Craft-parts lifecycle wrapper.""" import pathlib +import subprocess from typing import Any, Dict, List, Optional import craft_parts @@ -113,7 +114,14 @@ def project_vars(self) -> Dict[str, str]: "grade": self._lcm.project_info.get_project_var("grade"), } - def run(self, step_name: str) -> None: + def run( + self, + step_name: str, + *, + debug: bool = False, + shell: bool = False, + shell_after: bool = False, + ) -> None: """Run the parts lifecycle. :param target_step: The final step to execute. @@ -125,19 +133,19 @@ def run(self, step_name: str) -> None: if not target_step: raise RuntimeError(f"Invalid target step {step_name!r}") - try: - actions = self._lcm.plan(target_step, part_names=self._part_names) - - emit.progress("Installing package repositories...") + if shell: + # convert shell to shell_after for the previous step + previous_steps = target_step.previous_steps() + target_step = previous_steps[-1] if previous_steps else None + shell_after = True - if self._package_repositories: - refresh_required = repo.install( - self._package_repositories, key_assets=self._assets_dir / "keys" - ) - if refresh_required: - self._lcm.refresh_packages_list() + try: + if target_step: + actions = self._lcm.plan(target_step, part_names=self._part_names) + else: + actions = [] - emit.message("Installed package repositories", intermediate=True) + self._install_package_repositories() emit.progress("Executing parts lifecycle...") @@ -149,17 +157,34 @@ def run(self, step_name: str) -> None: aex.execute(action, stdout=stream, stderr=stream) emit.message(f"Executed: {message}", intermediate=True) + if shell_after: + _launch_shell() + emit.message("Executed parts lifecycle", intermediate=True) except RuntimeError as err: raise RuntimeError(f"Parts processing internal error: {err}") from err except OSError as err: + if debug: + _launch_shell() msg = err.strerror if err.filename: msg = f"{err.filename}: {msg}" raise errors.PartsLifecycleError(msg) from err except Exception as err: + if debug: + _launch_shell() raise errors.PartsLifecycleError(str(err)) from err + def _install_package_repositories(self): + emit.progress("Installing package repositories...") + if self._package_repositories: + refresh_required = repo.install( + self._package_repositories, key_assets=self._assets_dir / "keys" + ) + if refresh_required: + self._lcm.refresh_packages_list() + emit.message("Installed package repositories", intermediate=True) + def clean(self, *, part_names: Optional[List[str]] = None) -> None: """Remove lifecycle artifacts. @@ -204,6 +229,16 @@ def extract_metadata(self) -> List[ExtractedMetadata]: return metadata_list +def _launch_shell(*, cwd: Optional[pathlib.Path] = None) -> None: + """Launch a user shell for debugging environment. + + :param cwd: Working directory to start user in. + """ + emit.message("Launching shell on build environment...", intermediate=True) + with emit.pause(): + subprocess.run(["bash"], check=False, cwd=cwd) + + def _action_message(action: craft_parts.Action) -> str: msg = { Step.PULL: { diff --git a/tests/unit/cli/test_default_command.py b/tests/unit/cli/test_default_command.py index 30eab5ac93..4cc22ea845 100644 --- a/tests/unit/cli/test_default_command.py +++ b/tests/unit/cli/test_default_command.py @@ -30,6 +30,7 @@ def test_default_command(mocker): assert mock_pack_cmd.mock_calls == [ call( argparse.Namespace( + debug=False, directory=None, output=None, destructive_mode=False, @@ -53,6 +54,7 @@ def test_default_command_destructive_mode(mocker): argparse.Namespace( directory=None, output=None, + debug=False, destructive_mode=True, use_lxd=False, enable_experimental_extensions=False, @@ -74,6 +76,7 @@ def test_default_command_use_lxd(mocker): argparse.Namespace( directory=None, output=None, + debug=False, destructive_mode=False, use_lxd=True, enable_experimental_extensions=False, @@ -96,6 +99,7 @@ def test_default_command_output(mocker, option): argparse.Namespace( directory=None, output="name", + debug=False, destructive_mode=False, use_lxd=False, enable_experimental_extensions=False, diff --git a/tests/unit/cli/test_lifecycle.py b/tests/unit/cli/test_lifecycle.py index 5bcf5a45fc..3e72a4ecf3 100644 --- a/tests/unit/cli/test_lifecycle.py +++ b/tests/unit/cli/test_lifecycle.py @@ -40,7 +40,10 @@ def test_lifecycle_command(cmd, run_method, mocker): call( argparse.Namespace( parts=[], + debug=False, destructive_mode=False, + shell=False, + shell_after=False, use_lxd=False, enable_experimental_extensions=False, enable_developer_debug=False, @@ -78,7 +81,10 @@ def test_lifecycle_command_arguments(cmd, run_method, mocker): call( argparse.Namespace( parts=["part1", "part2"], + debug=False, destructive_mode=False, + shell=False, + shell_after=False, use_lxd=False, enable_experimental_extensions=False, enable_developer_debug=False, @@ -117,7 +123,10 @@ def test_lifecycle_command_arguments_destructive_mode(cmd, run_method, mocker): call( argparse.Namespace( parts=["part1", "part2"], + debug=False, destructive_mode=True, + shell=False, + shell_after=False, use_lxd=False, enable_experimental_extensions=False, enable_developer_debug=False, @@ -156,7 +165,10 @@ def test_lifecycle_command_arguments_use_lxd(cmd, run_method, mocker): call( argparse.Namespace( parts=["part1", "part2"], + debug=False, destructive_mode=False, + shell=False, + shell_after=False, use_lxd=True, enable_experimental_extensions=False, enable_developer_debug=False, @@ -168,6 +180,87 @@ def test_lifecycle_command_arguments_use_lxd(cmd, run_method, mocker): ] +@pytest.mark.parametrize( + "cmd,run_method", + [ + ("pull", "snapcraft.commands.lifecycle.PullCommand.run"), + ("build", "snapcraft.commands.lifecycle.BuildCommand.run"), + ("stage", "snapcraft.commands.lifecycle.StageCommand.run"), + ("prime", "snapcraft.commands.lifecycle.PrimeCommand.run"), + ], +) +def test_lifecycle_command_arguments_debug(cmd, run_method, mocker): + mocker.patch.object( + sys, + "argv", + [ + "cmd", + cmd, + "--debug", + ], + ) + mock_lifecycle_cmd = mocker.patch(run_method) + cli.run() + assert mock_lifecycle_cmd.mock_calls == [ + call( + argparse.Namespace( + parts=[], + debug=True, + destructive_mode=False, + shell=False, + shell_after=False, + use_lxd=False, + enable_experimental_extensions=False, + enable_developer_debug=False, + enable_experimental_target_arch=False, + target_arch=None, + provider=None, + ) + ) + ] + + +@pytest.mark.parametrize( + "cmd,run_method", + [ + ("pull", "snapcraft.commands.lifecycle.PullCommand.run"), + ("build", "snapcraft.commands.lifecycle.BuildCommand.run"), + ("stage", "snapcraft.commands.lifecycle.StageCommand.run"), + ("prime", "snapcraft.commands.lifecycle.PrimeCommand.run"), + ], +) +def test_lifecycle_command_arguments_shell(cmd, run_method, mocker): + mocker.patch.object( + sys, + "argv", + [ + "cmd", + cmd, + "--shell", + "--shell-after", + ], + ) + mock_lifecycle_cmd = mocker.patch(run_method) + cli.run() + assert mock_lifecycle_cmd.mock_calls == [ + call( + argparse.Namespace( + parts=[], + debug=False, + destructive_mode=False, + shell=True, + shell_after=True, + use_lxd=False, + enable_experimental_extensions=False, + enable_developer_debug=False, + enable_experimental_target_arch=False, + target_arch=None, + provider=None, + ) + ) + ] + + def test_lifecycle_command_pack(mocker): mocker.patch.object( sys, @@ -181,6 +274,7 @@ def test_lifecycle_command_pack(mocker): argparse.Namespace( directory=None, output=None, + debug=False, destructive_mode=False, use_lxd=False, enable_experimental_extensions=False, @@ -206,6 +300,7 @@ def test_lifecycle_command_pack_destructive_mode(mocker): argparse.Namespace( directory=None, output=None, + debug=False, destructive_mode=True, use_lxd=False, enable_experimental_extensions=False, @@ -231,6 +326,7 @@ def test_lifecycle_command_pack_use_lxd(mocker): argparse.Namespace( directory=None, output=None, + debug=False, destructive_mode=False, use_lxd=True, enable_experimental_extensions=False, @@ -243,6 +339,32 @@ def test_lifecycle_command_pack_use_lxd(mocker): ] +def test_lifecycle_command_pack_debug(mocker): + mocker.patch.object( + sys, + "argv", + ["cmd", "pack", "--debug"], + ) + mock_pack_cmd = mocker.patch("snapcraft.commands.lifecycle.PackCommand.run") + cli.run() + assert mock_pack_cmd.mock_calls == [ + call( + argparse.Namespace( + directory=None, + output=None, + debug=True, + destructive_mode=False, + use_lxd=False, + enable_experimental_extensions=False, + enable_developer_debug=False, + enable_experimental_target_arch=False, + target_arch=None, + provider=None, + ) + ) + ] + + @pytest.mark.parametrize("option", ["-o", "--output"]) def test_lifecycle_command_pack_output(mocker, option): mocker.patch.object(sys, "argv", ["cmd", "pack", option, "name"]) @@ -253,6 +375,7 @@ def test_lifecycle_command_pack_output(mocker, option): argparse.Namespace( directory=None, output="name", + debug=False, destructive_mode=False, use_lxd=False, enable_experimental_extensions=False, @@ -272,9 +395,10 @@ def test_lifecycle_command_pack_directory(mocker): assert mock_pack_cmd.mock_calls == [ call( argparse.Namespace( + debug=False, + destructive_mode=False, directory="name", output=None, - destructive_mode=False, use_lxd=False, enable_experimental_extensions=False, enable_developer_debug=False, diff --git a/tests/unit/parts/test_lifecycle.py b/tests/unit/parts/test_lifecycle.py index c09e3abed4..09e6cbf348 100644 --- a/tests/unit/parts/test_lifecycle.py +++ b/tests/unit/parts/test_lifecycle.py @@ -21,6 +21,7 @@ from unittest.mock import PropertyMock, call import pytest +from craft_parts import Action, Step from snapcraft import errors from snapcraft.parts import lifecycle as parts_lifecycle @@ -198,23 +199,36 @@ def test_lifecycle_legacy_run_provider(cmd, snapcraft_yaml, new_dir, mocker): ("prime", "prime"), ], ) +@pytest.mark.parametrize("debug_shell", [None, "debug", "shell", "shell_after"]) def test_lifecycle_run_command_step( - cmd, step, snapcraft_yaml, project_vars, new_dir, mocker + cmd, step, debug_shell, snapcraft_yaml, project_vars, new_dir, mocker ): project = Project.unmarshal(snapcraft_yaml(base="core22")) run_mock = mocker.patch("snapcraft.parts.PartsLifecycle.run") mocker.patch("snapcraft.meta.snap_yaml.write") pack_mock = mocker.patch("snapcraft.pack.pack_snap") + parsed_args = argparse.Namespace( + debug=False, + destructive_mode=True, + shell=False, + shell_after=False, + use_lxd=False, + parts=[], + ) + + if debug_shell: + setattr(parsed_args, debug_shell, True) + parts_lifecycle._run_command( - cmd, - project=project, - parse_info={}, - assets_dir=Path(), - parsed_args=argparse.Namespace(destructive_mode=True, use_lxd=False, parts=[]), + cmd, project=project, parse_info={}, assets_dir=Path(), parsed_args=parsed_args ) - assert run_mock.mock_calls == [call(step)] + call_args = {"debug": False, "shell": False, "shell_after": False} + if debug_shell: + call_args[debug_shell] = True + + assert run_mock.mock_calls == [call(step, **call_args)] assert pack_mock.mock_calls == [] @@ -233,13 +247,18 @@ def test_lifecycle_run_command_pack(cmd, snapcraft_yaml, project_vars, new_dir, parsed_args=argparse.Namespace( directory=None, output=None, + debug=False, destructive_mode=True, + shell=False, + shell_after=False, use_lxd=False, parts=[], ), ) - assert run_mock.mock_calls == [call("prime")] + assert run_mock.mock_calls == [ + call("prime", debug=False, shell=False, shell_after=False) + ] assert pack_mock.mock_calls == [ call(new_dir / "prime", output=None, compression="xz") ] @@ -268,14 +287,19 @@ def test_lifecycle_pack_destructive_mode( parsed_args=argparse.Namespace( directory=None, output=None, + debug=False, destructive_mode=True, + shell=False, + shell_after=False, use_lxd=False, parts=[], ), ) assert run_in_provider_mock.mock_calls == [] - assert run_mock.mock_calls == [call("prime")] + assert run_mock.mock_calls == [ + call("prime", debug=False, shell=False, shell_after=False) + ] assert pack_mock.mock_calls == [ call(new_dir / "home/prime", output=None, compression="xz") ] @@ -302,14 +326,19 @@ def test_lifecycle_pack_managed(cmd, snapcraft_yaml, project_vars, new_dir, mock parsed_args=argparse.Namespace( directory=None, output=None, + debug=False, destructive_mode=False, + shell=False, + shell_after=False, use_lxd=False, parts=[], ), ) assert run_in_provider_mock.mock_calls == [] - assert run_mock.mock_calls == [call("prime")] + assert run_mock.mock_calls == [ + call("prime", debug=False, shell=False, shell_after=False) + ] assert pack_mock.mock_calls == [ call(new_dir / "home/prime", output=None, compression="xz") ] @@ -378,7 +407,10 @@ def test_lifecycle_pack_metadata_error(cmd, snapcraft_yaml, new_dir, mocker): parsed_args=argparse.Namespace( directory=None, output=None, + debug=False, destructive_mode=False, + shell=False, + shell_after=False, use_lxd=False, parts=[], ), @@ -387,7 +419,9 @@ def test_lifecycle_pack_metadata_error(cmd, snapcraft_yaml, new_dir, mocker): assert str(raised.value) == ( "error setting grade: unexpected value; permitted: 'stable', 'devel'" ) - assert run_mock.mock_calls == [call("prime")] + assert run_mock.mock_calls == [ + call("prime", debug=False, shell=False, shell_after=False) + ] assert pack_mock.mock_calls == [] @@ -546,6 +580,118 @@ def test_lifecycle_clean_managed(snapcraft_yaml, project_vars, new_dir, mocker): assert clean_mock.mock_calls == [call(part_names=["part1"])] +@pytest.mark.parametrize("cmd", ["pull", "build", "stage", "prime", "pack", "snap"]) +def test_lifecycle_debug_shell(snapcraft_yaml, cmd, new_dir, mocker): + """Adoptable fields shouldn't be empty after adoption.""" + mocker.patch("craft_parts.executor.Executor.execute", side_effect=Exception) + mock_shell = mocker.patch("subprocess.run") + project = Project.unmarshal(snapcraft_yaml(base="core22")) + + with pytest.raises(errors.PartsLifecycleError): + parts_lifecycle._run_command( + cmd, + project=project, + parse_info={}, + assets_dir=Path(), + parsed_args=argparse.Namespace( + directory=None, + output=None, + debug=True, + destructive_mode=True, + shell=False, + shell_after=False, + use_lxd=False, + parts=["part1"], + ), + ) + + assert mock_shell.mock_calls == [call(["bash"], check=False, cwd=None)] + + +@pytest.mark.parametrize("cmd", ["pull", "build", "stage", "prime"]) +def test_lifecycle_shell(snapcraft_yaml, cmd, new_dir, mocker): + """Adoptable fields shouldn't be empty after adoption.""" + last_step = None + + def _fake_execute(_, action: Action, **kwargs): # pylint: disable=unused-argument + nonlocal last_step + last_step = action.step + + mocker.patch("craft_parts.executor.Executor.execute", new=_fake_execute) + mock_shell = mocker.patch("subprocess.run") + project = Project.unmarshal(snapcraft_yaml(base="core22")) + + parts_lifecycle._run_command( + cmd, + project=project, + parse_info={}, + assets_dir=Path(), + parsed_args=argparse.Namespace( + directory=None, + output=None, + debug=False, + destructive_mode=True, + shell=True, + shell_after=False, + use_lxd=False, + parts=["part1"], + ), + ) + + expected_last_step = None + if cmd == "build": + expected_last_step = Step.OVERLAY + if cmd == "stage": + expected_last_step = Step.BUILD + if cmd == "prime": + expected_last_step = Step.STAGE + + assert last_step == expected_last_step + assert mock_shell.mock_calls == [call(["bash"], check=False, cwd=None)] + + +@pytest.mark.parametrize("cmd", ["pull", "build", "stage", "prime"]) +def test_lifecycle_shell_after(snapcraft_yaml, cmd, new_dir, mocker): + """Adoptable fields shouldn't be empty after adoption.""" + last_step = None + + def _fake_execute(_, action: Action, **kwargs): # pylint: disable=unused-argument + nonlocal last_step + last_step = action.step + + mocker.patch("craft_parts.executor.Executor.execute", new=_fake_execute) + mock_shell = mocker.patch("subprocess.run") + project = Project.unmarshal(snapcraft_yaml(base="core22")) + + parts_lifecycle._run_command( + cmd, + project=project, + parse_info={}, + assets_dir=Path(), + parsed_args=argparse.Namespace( + directory=None, + output=None, + debug=False, + destructive_mode=True, + shell=False, + shell_after=True, + use_lxd=False, + parts=["part1"], + ), + ) + + expected_last_step = Step.PULL + if cmd == "build": + expected_last_step = Step.BUILD + if cmd == "stage": + expected_last_step = Step.STAGE + if cmd == "prime": + expected_last_step = Step.PRIME + + assert last_step == expected_last_step + assert mock_shell.mock_calls == [call(["bash"], check=False, cwd=None)] + + def test_lifecycle_adopt_project_vars(snapcraft_yaml, new_dir): """Adoptable fields shouldn't be empty after adoption.""" yaml_data = snapcraft_yaml(base="core22") From 0f3068261e0b8381770e9700ca96f2ed1c1b8bb9 Mon Sep 17 00:00:00 2001 From: Claudio Matsuoka Date: Thu, 28 Apr 2022 21:57:06 -0300 Subject: [PATCH 133/167] commands: make options shell and shell-after exclusive Using --shell and --shell-after at the same time will result in --shell-after not being executed, so make the options mutually exclusive. Signed-off-by: Claudio Matsuoka --- snapcraft/commands/lifecycle.py | 6 +++-- tests/unit/cli/test_lifecycle.py | 41 +++++++++++++++++++++++++++++++- 2 files changed, 44 insertions(+), 3 deletions(-) diff --git a/snapcraft/commands/lifecycle.py b/snapcraft/commands/lifecycle.py index 0584426b30..54de39e1f9 100644 --- a/snapcraft/commands/lifecycle.py +++ b/snapcraft/commands/lifecycle.py @@ -95,12 +95,14 @@ def fill_parser(self, parser: "argparse.ArgumentParser") -> None: nargs="*", help="Optional list of parts to process", ) - parser.add_argument( + + group = parser.add_mutually_exclusive_group() + group.add_argument( "--shell", action="store_true", help="Shell into the environment in lieu of the step to run.", ) - parser.add_argument( + group.add_argument( "--shell-after", action="store_true", help="Shell into the environment after the step has run.", diff --git a/tests/unit/cli/test_lifecycle.py b/tests/unit/cli/test_lifecycle.py index 3e72a4ecf3..9f4daa2204 100644 --- a/tests/unit/cli/test_lifecycle.py +++ b/tests/unit/cli/test_lifecycle.py @@ -237,7 +237,6 @@ def test_lifecycle_command_arguments_shell(cmd, run_method, mocker): "cmd", cmd, "--shell", - "--shell-after", ], ) mock_lifecycle_cmd = mocker.patch(run_method) @@ -249,6 +248,46 @@ def test_lifecycle_command_arguments_shell(cmd, run_method, mocker): debug=False, destructive_mode=False, shell=True, + shell_after=False, + use_lxd=False, + enable_experimental_extensions=False, + enable_developer_debug=False, + enable_experimental_target_arch=False, + target_arch=None, + provider=None, + ) + ) + ] + + +@pytest.mark.parametrize( + "cmd,run_method", + [ + ("pull", "snapcraft.commands.lifecycle.PullCommand.run"), + ("build", "snapcraft.commands.lifecycle.BuildCommand.run"), + ("stage", "snapcraft.commands.lifecycle.StageCommand.run"), + ("prime", "snapcraft.commands.lifecycle.PrimeCommand.run"), + ], +) +def test_lifecycle_command_arguments_shell_after(cmd, run_method, mocker): + mocker.patch.object( + sys, + "argv", + [ + "cmd", + cmd, + "--shell-after", + ], + ) + mock_lifecycle_cmd = mocker.patch(run_method) + cli.run() + assert mock_lifecycle_cmd.mock_calls == [ + call( + argparse.Namespace( + parts=[], + debug=False, + destructive_mode=False, + shell=False, shell_after=True, use_lxd=False, enable_experimental_extensions=False, From 01b23fbf44a4fe9f17133ffae74dc5feb72981a2 Mon Sep 17 00:00:00 2001 From: Claudio Matsuoka Date: Fri, 29 Apr 2022 17:16:49 -0300 Subject: [PATCH 134/167] tests: disable package installation in lifecycle tests Signed-off-by: Claudio Matsuoka --- tests/unit/parts/test_lifecycle.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/unit/parts/test_lifecycle.py b/tests/unit/parts/test_lifecycle.py index 09e6cbf348..f34089e421 100644 --- a/tests/unit/parts/test_lifecycle.py +++ b/tests/unit/parts/test_lifecycle.py @@ -36,6 +36,12 @@ ] +@pytest.fixture(autouse=True) +def disable_install(mocker): + mocker.patch("craft_parts.packages.Repository.install_packages") + mocker.patch("craft_parts.packages.snaps.install_snaps") + + @pytest.fixture def snapcraft_yaml(new_dir): def write_file( From 25a86fc6fc892f9a9506f527155a8ee458cd9eae Mon Sep 17 00:00:00 2001 From: Sergio Schvezov Date: Fri, 29 Apr 2022 15:51:11 -0300 Subject: [PATCH 135/167] commands: status and list-tracks These two commands use the same Store API Signed-off-by: Sergio Schvezov --- snapcraft/cli.py | 13 +- snapcraft/commands/__init__.py | 4 + snapcraft/commands/status.py | 424 ++++++++++++ snapcraft/commands/store/__init__.py | 2 + .../commands/store}/channel_map.py | 234 ++++++- snapcraft/commands/store/client.py | 14 +- snapcraft_legacy/_store.py | 5 - snapcraft_legacy/cli/store.py | 84 +-- snapcraft_legacy/storeapi/_dashboard_api.py | 16 +- snapcraft_legacy/storeapi/_store_client.py | 5 +- snapcraft_legacy/storeapi/v2/_api_schema.py | 197 ------ tests/legacy/unit/commands/__init__.py | 115 ---- .../legacy/unit/commands/test_list_tracks.py | 59 -- .../unit/commands/test_set_default_track.py | 20 - tests/legacy/unit/commands/test_status.py | 467 -------------- tests/legacy/unit/store/test_store_client.py | 10 +- .../legacy/unit/store/v2/test_channel_map.py | 396 ------------ tests/unit/commands/store/test_channel_map.py | 386 +++++++++++ tests/unit/commands/store/test_client.py | 110 ++++ tests/unit/commands/test_status.py | 605 ++++++++++++++++++ 20 files changed, 1783 insertions(+), 1383 deletions(-) create mode 100644 snapcraft/commands/status.py rename {snapcraft_legacy/storeapi/v2 => snapcraft/commands/store}/channel_map.py (55%) delete mode 100644 tests/legacy/unit/commands/test_list_tracks.py delete mode 100644 tests/legacy/unit/commands/test_status.py delete mode 100644 tests/legacy/unit/store/v2/test_channel_map.py create mode 100644 tests/unit/commands/store/test_channel_map.py create mode 100644 tests/unit/commands/test_status.py diff --git a/snapcraft/cli.py b/snapcraft/cli.py index efaf9d60ab..f2cd5bc0fb 100644 --- a/snapcraft/cli.py +++ b/snapcraft/cli.py @@ -63,7 +63,18 @@ ), craft_cli.CommandGroup( "Store Snap Release Management", - [commands.StoreReleaseCommand, commands.StoreCloseCommand], + [ + commands.StoreReleaseCommand, + commands.StoreCloseCommand, + commands.StoreStatusCommand, + ], + ), + craft_cli.CommandGroup( + "Store Snap Tracks", + [ + commands.StoreListTracksCommand, + commands.StoreTracksCommand, # hidden (alias to list-tracks) + ], ), craft_cli.CommandGroup( "Extensions", diff --git a/snapcraft/commands/__init__.py b/snapcraft/commands/__init__.py index 4d68a9ab12..5cd6740798 100644 --- a/snapcraft/commands/__init__.py +++ b/snapcraft/commands/__init__.py @@ -43,6 +43,7 @@ StoreNamesCommand, StoreRegisterCommand, ) +from .status import StoreListTracksCommand, StoreStatusCommand, StoreTracksCommand from .version import VersionCommand __all__ = [ @@ -58,11 +59,14 @@ "StoreLoginCommand", "StoreNamesCommand", "StoreExportLoginCommand", + "StoreListTracksCommand", "StoreLogoutCommand", "StoreRegisterCommand", "StoreLegacyListCommand", "StoreLegacyListRegisteredCommand", "StoreReleaseCommand", + "StoreStatusCommand", + "StoreTracksCommand", "StoreWhoAmICommand", "ExtensionsCommand", "ListExtensionsCommand", diff --git a/snapcraft/commands/status.py b/snapcraft/commands/status.py new file mode 100644 index 0000000000..17f6ad06cd --- /dev/null +++ b/snapcraft/commands/status.py @@ -0,0 +1,424 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright 2022 Canonical Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +"""Snapcraft Store Account management commands.""" +import itertools +import operator +import textwrap +from collections import OrderedDict +from typing import TYPE_CHECKING, List, Optional, Sequence, Tuple, cast + +from craft_cli import BaseCommand, emit +from overrides import overrides +from tabulate import tabulate +from typing_extensions import Final + +from snapcraft.commands import store +from snapcraft.commands.store.channel_map import ( + ChannelMap, + MappedChannel, + Revision, + SnapChannel, +) + +if TYPE_CHECKING: + import argparse + + +class StoreStatusCommand(BaseCommand): + """Command to check the status of a snap on the Snap Store.""" + + name = "status" + help_msg = "Show the status of a snap on the Snap Store" + overview = textwrap.dedent( + """ + Show the status of a snap on the Snap Store. + The name must be accessible from the requesting account by being + the owner or a collaborator of the snap.""" + ) + + @overrides + def fill_parser(self, parser: "argparse.ArgumentParser") -> None: + parser.add_argument( + "name", + type=str, + help="Get the status on a snap from the Snap Store", + ) + parser.add_argument( + "--arch", + metavar="", + type=str, + nargs="?", + help="Limit the status report to the requested architectures", + ) + parser.add_argument( + "--track", + metavar="", + type=str, + nargs="?", + help="Limit the status report to the requested tracks", + ) + + @overrides + def run(self, parsed_args): + snap_channel_map = store.StoreClientCLI().get_channel_map( + snap_name=parsed_args.name + ) + + existing_architectures = snap_channel_map.get_existing_architectures() + if not snap_channel_map.channel_map: + emit.message("This snap has no released revisions") + return + + architectures = existing_architectures + if parsed_args.arch: + architectures = set(parsed_args.arch) + for architecture in architectures.copy(): + if architecture not in existing_architectures: + emit.progress(f"No revisions for architecture {architecture!r}") + architectures.remove(architecture) + + # If we have no revisions for any of the architectures requested, there's + # nothing to do here. + if not architectures: + return + + tracks: List[str] = [] + if parsed_args.track: + tracks = cast(list, parsed_args.track) + existing_tracks = { + s.track for s in snap_channel_map.snap.channels if s.track in tracks + } + for track in set(tracks) - existing_tracks: + emit.progress(f"No revisions for track {track!r}") + tracks = list(existing_tracks) + + # If we have no revisions in any of the tracks requested, there's + # nothing to do here. + if not tracks: + return + + emit.message( + get_tabulated_channel_map( + snap_channel_map, + architectures=architectures, + tracks=tracks, + ) + ) + + +class _HINTS: + CLOSED: Final[str] = "-" + FOLLOWING: Final[str] = "↑" + NO_PROGRESS: Final[str] = "-" + PROGRESSING_TO: Final[str] = "→" + UNKNOWN: Final[str] = "?" + + +def _get_channel_order(snap_channels, tracks: Sequence[str]) -> OrderedDict: + channel_order: OrderedDict = OrderedDict() + + if tracks: + snap_channels = [s for s in snap_channels if s.track in tracks] + + for snap_channel in snap_channels: + if snap_channel.track not in channel_order: + channel_order[snap_channel.track] = [] + if snap_channel.fallback is None: + channel_order[snap_channel.track].append(snap_channel.name) + else: + try: + channel_order[snap_channel.track].insert( + channel_order[snap_channel.track].index(snap_channel.fallback) + 1, + snap_channel.name, + ) + except ValueError: + channel_order[snap_channel.track].append(snap_channel.name) + + return channel_order + + +def _get_channel_line( + *, + mapped_channel: Optional[MappedChannel], + revision: Optional[Revision], + channel_info: SnapChannel, + hint: str, + progress_string: str, +) -> List[str]: + version_string = hint + revision_string = hint + expiration_date_string = "" + channel_string = channel_info.risk + + if revision is not None: + version_string = revision.version + revision_string = f"{revision.revision}" + + if mapped_channel is not None: + if channel_info.branch is None and mapped_channel.progressive.percentage: + channel_string = "" + elif channel_info.branch is not None: + channel_string = f"{channel_info.risk}/{channel_info.branch}" + if mapped_channel.expiration_date is not None: + expiration_date_string = mapped_channel.expiration_date + + return [ + channel_string, + version_string, + revision_string, + progress_string, + expiration_date_string, + ] + + +def _get_channel_lines_for_channel( # noqa: C901 # pylint: disable=too-many-locals + snap_channel_map: ChannelMap, + channel_name: str, + architecture: str, + current_tick: str, +) -> Tuple[str, List[List[str]]]: + channel_lines: List[List[str]] = [] + + channel_info = snap_channel_map.get_channel_info(channel_name) + + try: + progressive_mapped_channel: Optional[ + MappedChannel + ] = snap_channel_map.get_mapped_channel( + channel_name=channel_name, architecture=architecture, progressive=True + ) + except ValueError: + progressive_mapped_channel = None + + if progressive_mapped_channel is not None: + progressive_revision = snap_channel_map.get_revision( + progressive_mapped_channel.revision + ) + + if progressive_mapped_channel.progressive.percentage is None: + raise RuntimeError("Unexpected null progressive percentage") + percentage = progressive_mapped_channel.progressive.percentage + + if progressive_mapped_channel.progressive.current_percentage is None: + current_percentage_fmt = _HINTS.UNKNOWN + remaining_percentage_fmt = _HINTS.UNKNOWN + else: + current_percentage = ( + progressive_mapped_channel.progressive.current_percentage + ) + current_percentage_fmt = f"{current_percentage:.0f}" + remaining_percentage_fmt = f"{100 - current_percentage:.0f}" + + progressive_mapped_channel_line = _get_channel_line( + mapped_channel=progressive_mapped_channel, + revision=progressive_revision, + channel_info=channel_info, + hint=current_tick, + progress_string=f"{current_percentage_fmt}{_HINTS.PROGRESSING_TO}{percentage:.0f}%", + ) + # Setup progress for the actually released revision, this needs to be + # calculated. But only show it if the channel is open. + progress_string = ( + f"{remaining_percentage_fmt}{_HINTS.PROGRESSING_TO}{100 - percentage:.0f}%" + ) + else: + progressive_mapped_channel_line = [] + progress_string = _HINTS.NO_PROGRESS + + try: + mapped_channel: Optional[MappedChannel] = snap_channel_map.get_mapped_channel( + channel_name=channel_name, architecture=architecture, progressive=False + ) + except ValueError: + mapped_channel = None + + next_tick = current_tick + if mapped_channel is not None: + revision = snap_channel_map.get_revision(mapped_channel.revision) + channel_lines.append( + _get_channel_line( + mapped_channel=mapped_channel, + revision=revision, + channel_info=channel_info, + hint=current_tick, + progress_string=progress_string, + ) + ) + if channel_info.branch is None: + next_tick = _HINTS.FOLLOWING + # Show an empty entry if there is no specific channel information, but + # only for / (ignoring /). + elif channel_info.branch is None: + channel_lines.append( + _get_channel_line( + mapped_channel=None, + revision=None, + channel_info=channel_info, + hint=current_tick, + progress_string=_HINTS.NO_PROGRESS + if current_tick == _HINTS.CLOSED + else progress_string, + ) + ) + + if progressive_mapped_channel is not None: + channel_lines.append(progressive_mapped_channel_line) + if channel_info.branch is None: + next_tick = _HINTS.FOLLOWING + + return next_tick, channel_lines + + +def _has_channels_for_architecture( + snap_channel_map, architecture: str, channels: List[str] +) -> bool: + progressive = (False, True) + # channel_query = (channel_name, progressive) + for channel_query in itertools.product(channels, progressive): + try: + snap_channel_map.get_mapped_channel( + channel_name=channel_query[0], + architecture=architecture, + progressive=channel_query[1], + ) + found_architecture = True + break + except ValueError: + continue + else: + found_architecture = False + + return found_architecture + + +def get_tabulated_channel_map( # pylint: disable=too-many-branches, too-many-locals # noqa: C901 + snap_channel_map, + *, + architectures: Sequence[str], + tracks: Sequence[str], +): + """Return a tabulated channel map.""" + channel_order = _get_channel_order(snap_channel_map.snap.channels, tracks) + + channel_lines = [] + for track_name in channel_order: + track_mentioned = False + for architecture in sorted(architectures): + if not _has_channels_for_architecture( + snap_channel_map, architecture, channel_order[track_name] + ): + continue + architecture_mentioned = False + next_tick = _HINTS.CLOSED + for channel_name in channel_order[track_name]: + if not track_mentioned: + track_mentioned = True + track_string = track_name + else: + track_string = "" + + if not architecture_mentioned: + architecture_mentioned = True + architecture_string = architecture + else: + architecture_string = "" + + next_tick, parsed_channels = _get_channel_lines_for_channel( + snap_channel_map, channel_name, architecture, next_tick + ) + for channel_line in parsed_channels: + channel_lines.append( + [track_string, architecture_string] + channel_line + ) + track_string = "" + architecture_string = "" + + headers = ["Track", "Arch", "Channel", "Version", "Revision", "Progress"] + expires_column = 6 + + if any(line[expires_column] != "" for line in channel_lines): + headers.append("Expires at") + for index, _ in enumerate(channel_lines): + if not channel_lines[index][expires_column]: + channel_lines[index][expires_column] = "-" + else: + headers.append("") + + return tabulate(channel_lines, numalign="left", headers=headers, tablefmt="plain") + + +class StoreListTracksCommand(BaseCommand): + """Command to list the tracks from a snap on the Snap Store.""" + + name = "list-tracks" + help_msg = "Show the available tracks for a snap on the Snap Store" + overview = textwrap.dedent( + """ + Track status, creation dates and version patterns are returned alongside the + track names in a space formatted table. + + Possible Status values are: + + - active, visible tracks available for installation + - default, the default track to install from when not explicit + - hidden, tracks available for installation but unlisted + - closed, tracks that are no longer available to install from + + A version pattern is a regular expression that restricts a snap revision + from being released to a track if the version string set does not match.""" + ) + + @overrides + def fill_parser(self, parser: "argparse.ArgumentParser") -> None: + parser.add_argument( + "name", + type=str, + help="The snap name to request the information from on the Snap Store", + ) + + @overrides + def run(self, parsed_args): + snap_channel_map = store.StoreClientCLI().get_channel_map( + snap_name=parsed_args.name + ) + + # Iterate over the entries, replace None with - for consistent presentation + track_table: List[List[str]] = [ + [ + track.name, + track.status, + track.creation_date if track.creation_date else "-", + track.version_pattern if track.version_pattern else "-", + ] + for track in snap_channel_map.snap.tracks + ] + + emit.message( + tabulate( + # Sort by "creation-date". + sorted(track_table, key=operator.itemgetter(2)), + headers=["Name", "Status", "Creation-Date", "Version-Pattern"], + tablefmt="plain", + ) + ) + + +class StoreTracksCommand(StoreListTracksCommand): + """Command alias to list the tracks from a snap on the Snap Store.""" + + name = "tracks" + hidden = True diff --git a/snapcraft/commands/store/__init__.py b/snapcraft/commands/store/__init__.py index 477f902726..f5c26953b6 100644 --- a/snapcraft/commands/store/__init__.py +++ b/snapcraft/commands/store/__init__.py @@ -18,9 +18,11 @@ from . import constants +from .channel_map import ChannelMap from .client import StoreClientCLI __all__ = [ + "ChannelMap", "StoreClientCLI", "constants", ] diff --git a/snapcraft_legacy/storeapi/v2/channel_map.py b/snapcraft/commands/store/channel_map.py similarity index 55% rename from snapcraft_legacy/storeapi/v2/channel_map.py rename to snapcraft/commands/store/channel_map.py index 971b6d8b7b..f4e37fa0f5 100644 --- a/snapcraft_legacy/storeapi/v2/channel_map.py +++ b/snapcraft/commands/store/channel_map.py @@ -14,13 +14,8 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -from typing import Any, Dict, List, Optional, Set - -import jsonschema - -from ._api_schema import CHANNEL_MAP_JSONSCHEMA +"""Channel Map API representation. -""" This module holds representations for results for the v2 channel-map API endpoint provided by the Snap Store. @@ -28,12 +23,17 @@ https://dashboard.snapcraft.io/docs/v2/en/snaps.html#snap-channel-map """ +from typing import Any, Dict, List, Optional, Set + +import jsonschema + class Progressive: """Represent Progressive information for a MappedChannel.""" @classmethod def unmarshal(cls, payload: Dict[str, Any]) -> "Progressive": + """Unmarshal payload into a Progressive.""" jsonschema.validate( payload, CHANNEL_MAP_JSONSCHEMA["properties"]["channel-map"]["items"]["properties"][ @@ -47,6 +47,7 @@ def unmarshal(cls, payload: Dict[str, Any]) -> "Progressive": ) def marshal(self) -> Dict[str, Any]: + """Marshal this Progressive into a dict.""" return { "paused": self.paused, "percentage": self.percentage, @@ -54,6 +55,7 @@ def marshal(self) -> Dict[str, Any]: } def __repr__(self) -> str: + """Repr for Progressive.""" return f"<{self.__class__.__name__}: {self.current_percentage!r}=>{self.percentage!r}>" def __init__( @@ -73,6 +75,7 @@ class MappedChannel: @classmethod def unmarshal(cls, payload: Dict[str, Any]) -> "MappedChannel": + """Unmarshal payload into a MappedChannel.""" jsonschema.validate( payload, CHANNEL_MAP_JSONSCHEMA["properties"]["channel-map"]["items"] ) @@ -85,6 +88,7 @@ def unmarshal(cls, payload: Dict[str, Any]) -> "MappedChannel": ) def marshal(self) -> Dict[str, Any]: + """Marshal this MappedChannel into a dict.""" return { "channel": self.channel, "revision": self.revision, @@ -94,7 +98,12 @@ def marshal(self) -> Dict[str, Any]: } def __repr__(self) -> str: - return f"<{self.__class__.__name__}: {self.channel!r} for revision {self.revision!r} and architecture {self.architecture!r}>" + """Repr for MappedChannel.""" + return ( + f"<{self.__class__.__name__}: " + f"{self.channel!r} for revision {self.revision!r} and " + f"architecture {self.architecture!r}>" + ) def __init__( self, @@ -117,6 +126,7 @@ class Revision: @classmethod def unmarshal(cls, payload: Dict[str, Any]) -> "Revision": + """Unmarshal payload into a Revision.""" jsonschema.validate( payload, CHANNEL_MAP_JSONSCHEMA["properties"]["revisions"]["items"] ) @@ -127,6 +137,7 @@ def unmarshal(cls, payload: Dict[str, Any]) -> "Revision": ) def marshal(self) -> Dict[str, Any]: + """Marshal this Revision into a dict.""" return { "revision": self.revision, "version": self.version, @@ -134,7 +145,11 @@ def marshal(self) -> Dict[str, Any]: } def __repr__(self) -> str: - return f"<{self.__class__.__name__}: {self.revision!r} for version {self.version!r} and architectures {self.architectures!r}>" + """Repr for Revision.""" + return ( + f"<{self.__class__.__name__}: {self.revision!r} " + f"for version {self.version!r} and architectures {self.architectures!r}>" + ) def __init__( self, *, revision: int, version: str, architectures: List[str] @@ -149,6 +164,7 @@ class SnapChannel: @classmethod def unmarshal(cls, payload: Dict[str, Any]) -> "SnapChannel": + """Unmarshal payload into a SnapChannel.""" jsonschema.validate( payload, CHANNEL_MAP_JSONSCHEMA["properties"]["snap"]["properties"]["channels"][ @@ -164,6 +180,7 @@ def unmarshal(cls, payload: Dict[str, Any]) -> "SnapChannel": ) def marshal(self) -> Dict[str, Any]: + """Marshal this SnapChannel into a dict.""" return { "name": self.name, "track": self.track, @@ -173,6 +190,7 @@ def marshal(self) -> Dict[str, Any]: } def __repr__(self) -> str: + """Repr for SnapChannel.""" return f"<{self.__class__.__name__}: {self.name!r}>" def __init__( @@ -196,6 +214,7 @@ class SnapTrack: @classmethod def unmarshal(cls, payload: Dict[str, Any]) -> "SnapTrack": + """Unmarshal payload into a SnapTrack.""" jsonschema.validate( payload, CHANNEL_MAP_JSONSCHEMA["properties"]["snap"]["properties"]["tracks"][ @@ -210,6 +229,7 @@ def unmarshal(cls, payload: Dict[str, Any]) -> "SnapTrack": ) def marshal(self) -> Dict[str, Any]: + """Marshal this SnapTrack into a dict.""" return { "name": self.name, "status": self.status, @@ -218,6 +238,7 @@ def marshal(self) -> Dict[str, Any]: } def __repr__(self) -> str: + """Repr for SnapTrack.""" return f"<{self.__class__.__name__}: {self.name!r}>" def __init__( @@ -239,6 +260,7 @@ class Snap: @classmethod def unmarshal(cls, payload: Dict[str, Any]) -> "Snap": + """Unmarshal payload into a Snap.""" jsonschema.validate(payload, CHANNEL_MAP_JSONSCHEMA["properties"]["snap"]) return cls( name=payload["name"], @@ -247,6 +269,7 @@ def unmarshal(cls, payload: Dict[str, Any]) -> "Snap": ) def marshal(self) -> Dict[str, Any]: + """Marshal this Snap into a dict.""" return { "name": self.name, "channels": [sc.marshal() for sc in self.channels], @@ -254,6 +277,7 @@ def marshal(self) -> Dict[str, Any]: } def __repr__(self) -> str: + """Repr for Snap.""" return f"<{self.__class__.__name__}: {self.name!r}>" def __init__( @@ -269,6 +293,7 @@ class ChannelMap: @classmethod def unmarshal(cls, payload: Dict[str, Any]) -> "ChannelMap": + """Unmarshal payload into a ChannelMap.""" jsonschema.validate(payload, CHANNEL_MAP_JSONSCHEMA) return cls( channel_map=[MappedChannel.unmarshal(c) for c in payload["channel-map"]], @@ -277,6 +302,7 @@ def unmarshal(cls, payload: Dict[str, Any]) -> "ChannelMap": ) def marshal(self) -> Dict[str, Any]: + """Marshal this ChannelMap into a dict.""" return { "channel-map": [c.marshal() for c in self.channel_map], "revisions": [r.marshal() for r in self.revisions], @@ -284,6 +310,7 @@ def marshal(self) -> Dict[str, Any]: } def __repr__(self) -> str: + """Repr for ChannelMap.""" return f"<{self.__class__.__name__}: {self.snap.name!r}>" def __init__( @@ -296,6 +323,7 @@ def __init__( def get_mapped_channel( self, *, channel_name: str, architecture: str, progressive: bool ) -> MappedChannel: + """Return the channel for the corresponding attributes.""" channels_with_name = ( cm for cm in self.channel_map if cm.channel == channel_name ) @@ -314,26 +342,208 @@ def get_mapped_channel( try: return channels[0] - except IndexError: + except IndexError as index_error: raise ValueError( - f"No channel mapped to {channel_name!r} for architecture {architecture!r} when progressive is {progressive!r}" - ) + f"No channel mapped to {channel_name!r} for architecture {architecture!r} " + f"when progressive is {progressive!r}" + ) from index_error def get_channel_info(self, channel_name: str) -> SnapChannel: + """Return a SnapChannel for channel_name.""" for snap_channel in self.snap.channels: if snap_channel.name == channel_name: return snap_channel raise ValueError(f"No channel information for {channel_name!r}") def get_revision(self, revision_number: int) -> Revision: + """Return a Revision for revision_number.""" for revision_item in self.revisions: if revision_item.revision == revision_number: return revision_item raise ValueError(f"No revision information for {revision_number!r}") def get_existing_architectures(self) -> Set[str]: - architectures: List[str] = list() + """Return a list of the existing architectures for this map.""" + architectures: List[str] = [] for revision_item in self.revisions: architectures.extend(revision_item.architectures) return set(architectures) + + +CHANNEL_MAP_JSONSCHEMA: Dict[str, Any] = { + "properties": { + "channel-map": { + "items": { + "properties": { + "architecture": {"type": "string"}, + "channel": { + "type": "string", + }, + "expiration-date": { + "format": "date-time", + "type": ["string", "null"], + }, + "progressive": { + "properties": { + "paused": {"type": ["boolean", "null"]}, + "percentage": {"type": ["number", "null"]}, + "current-percentage": {"type": ["number", "null"]}, + }, + "required": ["paused", "percentage", "current-percentage"], + "type": "object", + }, + "revision": {"type": "integer"}, + "when": { + "format": "date-time", + "type": "string", + }, + }, + "required": [ + "architecture", + "channel", + "expiration-date", + "progressive", + "revision", + # "when" + ], + "type": "object", + }, + "minItems": 0, + "type": "array", + }, + "revisions": { + "items": { + "properties": { + "architectures": { + "items": {"type": "string"}, + "minItems": 1, + "type": "array", + }, + "attributes": {"type": "object"}, + "base": {"type": ["string", "null"]}, + "build-url": {"type": ["string", "null"]}, + "confinement": { + "enum": ["strict", "classic", "devmode"], + "type": "string", + }, + "created-at": {"format": "date-time", "type": "string"}, + "epoch": { + "properties": { + "read": { + "items": {"type": "integer"}, + "minItems": 1, + "type": ["array", "null"], + }, + "write": { + "items": {"type": "integer"}, + "minItems": 1, + "type": ["array", "null"], + }, + }, + "required": ["read", "write"], + "type": "object", + }, + "grade": {"enum": ["stable", "devel"], "type": "string"}, + "revision": {"type": "integer"}, + "sha3-384": {"type": "string"}, + "size": {"type": "integer"}, + "version": {"type": "string"}, + }, + "required": [ + "architectures", + # "attributes", + # "base", + # "build-url", + # "confinement", + # "created-at", + # "epoch", + # "grade", + "revision", + # "sha3-384", + # "size", + # "status", + "version", + ], + "type": "object", + }, + "minItems": 0, + "type": "array", + }, + "snap": { + "introduced_at": 6, + "properties": { + "channels": { + "introduced_at": 9, + "items": { + "properties": { + "branch": { + "type": ["string", "null"], + }, + "fallback": { + "type": ["string", "null"], + }, + "name": { + "type": "string", + }, + "risk": { + "type": "string", + }, + "track": { + "type": "string", + }, + }, + "required": ["name", "track", "risk", "branch", "fallback"], + "type": "object", + }, + "minItems": 1, + "type": "array", + }, + "default-track": { + "type": ["string", "null"], + }, + "id": { + "type": "string", + }, + "name": {"type": "string"}, + "private": { + "type": "boolean", + }, + "tracks": { + "introduced_at": 9, + "items": { + "properties": { + "creation-date": { + "format": "date-time", + "type": ["string", "null"], + }, + "name": { + "type": "string", + }, + "version-pattern": { + "type": ["string", "null"], + }, + }, + # pattern is documented as required but is not returned, + # version-pattern is returned instead. + "required": ["name", "creation-date", "version-pattern"], + "type": "object", + }, + "minItems": 1, + "type": "array", + }, + }, + "required": [ + # "id", + "channels", + # "default-track", + "name", + # "private", + # "tracks" + ], + "type": "object", + }, + }, + "required": ["channel-map", "revisions", "snap"], + "type": "object", +} diff --git a/snapcraft/commands/store/client.py b/snapcraft/commands/store/client.py index 0489e6bc8f..fb87a1196c 100644 --- a/snapcraft/commands/store/client.py +++ b/snapcraft/commands/store/client.py @@ -27,7 +27,7 @@ from snapcraft import __version__, errors, utils -from . import constants +from . import channel_map, constants _TESTING_ENV_PREFIXES = ["TRAVIS", "AUTOPKGTEST_TMP"] @@ -243,6 +243,18 @@ def register( json=data, ) + def get_channel_map(self, *, snap_name: str) -> channel_map.ChannelMap: + """Return the channel map for snap_name.""" + response = self.request( + "GET", + self._base_url + f"/api/v2/snaps/{snap_name}/channel-map", + headers={ + "Accept": "application/json", + }, + ) + + return channel_map.ChannelMap.unmarshal(response.json()) + def get_account_info( self, ) -> Dict[str, Any]: diff --git a/snapcraft_legacy/_store.py b/snapcraft_legacy/_store.py index b9169f0001..9920ef369d 100644 --- a/snapcraft_legacy/_store.py +++ b/snapcraft_legacy/_store.py @@ -56,7 +56,6 @@ if TYPE_CHECKING: from snapcraft_legacy.storeapi._status_tracker import StatusTracker - from snapcraft_legacy.storeapi.v2.channel_map import ChannelMap from snapcraft_legacy.storeapi.v2.releases import Releases @@ -346,10 +345,6 @@ def get_metrics( def get_snap_releases(self, *, snap_name: str) -> "Releases": return super().get_snap_releases(snap_name=snap_name) - @_login_wrapper - def get_snap_channel_map(self, *, snap_name: str) -> "ChannelMap": - return super().get_snap_channel_map(snap_name=snap_name) - @_login_wrapper def get_account_information(self) -> Dict[str, Any]: return super().get_account_information() diff --git a/snapcraft_legacy/cli/store.py b/snapcraft_legacy/cli/store.py index 82ef79ed04..b125bb7d08 100644 --- a/snapcraft_legacy/cli/store.py +++ b/snapcraft_legacy/cli/store.py @@ -29,7 +29,6 @@ from snapcraft_legacy._store import StoreClientCLI from snapcraft_legacy.storeapi import metrics as metrics_module from . import echo -from ._channel_map import get_tabulated_channel_map from ._metrics import convert_metrics_to_table from ._review import review_snap @@ -120,18 +119,6 @@ def upload(snap_file, release): snap_name, snap_revision = snapcraft_legacy.upload(snap_file, channel_list) echo.info("Revision {!r} of {!r} created.".format(snap_revision, snap_name)) - if channel_list: - store_client_cli = StoreClientCLI() - snap_channel_map = store_client_cli.get_snap_channel_map(snap_name=snap_name) - - click.echo( - get_tabulated_channel_map( - snap_channel_map, - architectures=snap_channel_map.get_revision( - snap_revision - ).architectures, - ) - ) @storecli.command("upload-metadata") @@ -258,14 +245,8 @@ def promote(snap_name, from_channel, to_channel, yes): revision=str(c.revision), channels=[str(parsed_to_channel)], ) - snap_channel_map = store.get_snap_channel_map(snap_name=snap_name) - existing_architectures = snap_channel_map.get_existing_architectures() - click.echo( - get_tabulated_channel_map( - snap_channel_map, - tracks=[parsed_to_channel.track], - architectures=existing_architectures, - ) + echo.wrapped( + f"Promotion from {parsed_from_channel} to {parsed_to_channel} complete" ) else: echo.wrapped("Channel promotion cancelled") @@ -442,73 +423,12 @@ def set_default_track(snap_name: str, track_name: str): """ store_client_cli = StoreClientCLI() - # Client-side check to verify that the selected track exists. - snap_channel_map = store_client_cli.get_snap_channel_map(snap_name=snap_name) - active_tracks = [ - track.name - for track in snap_channel_map.snap.tracks - if track.status in ("default", "active") - ] - if track_name not in active_tracks: - echo.exit_error( - brief=f"The specified track {track_name!r} does not exist for {snap_name!r}.", - resolution=f"Ensure the {track_name!r} track exists for the {snap_name!r} snap and try again.", - details="Valid tracks for {!r}: {}.".format( - snap_name, ", ".join([f"{t!r}" for t in active_tracks]) - ), - ) - metadata = dict(default_track=track_name) store_client_cli.upload_metadata(snap_name=snap_name, metadata=metadata, force=True) echo.info(f"Default track for {snap_name!r} set to {track_name!r}.") -@storecli.command() -@click.argument("snap-name", metavar="") -def list_tracks(snap_name: str) -> None: - """List channel tracks for . - - This command has an alias of `tracks`. - - Track status, creation dates and version patterns are returned alongside - the track names in a space formatted table. - - Possible Status values are: - - \b - - active, visible tracks available for installation - - default, the default track to install from when not explicit - - hidden, tracks available for installation but unlisted - - closed, tracks that are no longer available to install from - - A version pattern is a regular expression that restricts a snap revision - from being released to a track if the version string set does not match. - """ - store_client_cli = StoreClientCLI() - snap_channel_map = store_client_cli.get_snap_channel_map(snap_name=snap_name) - - # Iterate over the entries, replace None with - for consistent presentation - track_table: List[List[str]] = [ - [ - track.name, - track.status, - track.creation_date if track.creation_date else "-", - track.version_pattern if track.version_pattern else "-", - ] - for track in snap_channel_map.snap.tracks - ] - - click.echo( - tabulate( - # Sort by "creation-date". - sorted(track_table, key=operator.itemgetter(2)), - headers=["Name", "Status", "Creation-Date", "Version-Pattern"], - tablefmt="plain", - ) - ) - - _YESTERDAY = str(date.today() - timedelta(days=1)) diff --git a/snapcraft_legacy/storeapi/_dashboard_api.py b/snapcraft_legacy/storeapi/_dashboard_api.py index 6e829448dd..cf66abece7 100644 --- a/snapcraft_legacy/storeapi/_dashboard_api.py +++ b/snapcraft_legacy/storeapi/_dashboard_api.py @@ -26,7 +26,7 @@ from . import _metadata, constants, errors, metrics from ._requests import Requests from ._status_tracker import StatusTracker -from .v2 import channel_map, releases, validation_sets, whoami +from .v2 import releases, validation_sets, whoami logger = logging.getLogger(__name__) @@ -352,20 +352,6 @@ def sign_developer_agreement(self, latest_tos_accepted=False): return response.json() - def get_snap_channel_map(self, *, snap_name: str) -> channel_map.ChannelMap: - try: - response = self.get( - f"/api/v2/snaps/{snap_name}/channel-map", - headers={ - "Content-Type": "application/json", - "Accept": "application/json", - }, - ) - except craft_store.errors.StoreServerError as store_error: - raise errors.StoreSnapChannelMapError(snap_name=snap_name) from store_error - - return channel_map.ChannelMap.unmarshal(response.json()) - def get_metrics( self, filters: List[metrics.MetricsFilter], snap_name: str ) -> metrics.MetricsResults: diff --git a/snapcraft_legacy/storeapi/_store_client.py b/snapcraft_legacy/storeapi/_store_client.py index b3bfb686b3..9d8178b584 100644 --- a/snapcraft_legacy/storeapi/_store_client.py +++ b/snapcraft_legacy/storeapi/_store_client.py @@ -29,7 +29,7 @@ from ._snap_api import SnapAPI from ._up_down_client import UpDownClient from .constants import DEFAULT_SERIES -from .v2 import channel_map, releases, validation_sets, whoami +from .v2 import releases, validation_sets, whoami logger = logging.getLogger(__name__) @@ -218,9 +218,6 @@ def get_snap_status(self, snap_name, arch=None): return response - def get_snap_channel_map(self, *, snap_name: str) -> channel_map.ChannelMap: - return self.dashboard.get_snap_channel_map(snap_name=snap_name) - def get_metrics( self, *, diff --git a/snapcraft_legacy/storeapi/v2/_api_schema.py b/snapcraft_legacy/storeapi/v2/_api_schema.py index 8dc478ef6f..34bf16f492 100644 --- a/snapcraft_legacy/storeapi/v2/_api_schema.py +++ b/snapcraft_legacy/storeapi/v2/_api_schema.py @@ -144,203 +144,6 @@ "type": "object", } - -CHANNEL_MAP_JSONSCHEMA: Dict[str, Any] = { - "properties": { - "channel-map": { - "items": { - "properties": { - "architecture": {"type": "string"}, - "channel": { - "description": 'The channel name, including "latest/" for the latest track.', - "type": "string", - }, - "expiration-date": { - "description": "The date when this release expires, in RFC 3339 format. If null, the release does not expire.", - "format": "date-time", - "type": ["string", "null"], - }, - "progressive": { - "properties": { - "paused": {"type": ["boolean", "null"]}, - "percentage": {"type": ["number", "null"]}, - "current-percentage": {"type": ["number", "null"]}, - }, - "required": ["paused", "percentage", "current-percentage"], - "type": "object", - }, - "revision": {"type": "integer"}, - "when": { - "description": "The date when this release was made, in RFC 3339 format.", - "format": "date-time", - "type": "string", - }, - }, - "required": [ - "architecture", - "channel", - "expiration-date", - "progressive", - "revision", - # "when" - ], - "type": "object", - }, - "minItems": 0, - "type": "array", - }, - "revisions": { - "items": { - "properties": { - "architectures": { - "items": {"type": "string"}, - "minItems": 1, - "type": "array", - }, - "attributes": {"type": "object"}, - "base": {"type": ["string", "null"]}, - "build-url": {"type": ["string", "null"]}, - "confinement": { - "enum": ["strict", "classic", "devmode"], - "type": "string", - }, - "created-at": {"format": "date-time", "type": "string"}, - "epoch": { - "properties": { - "read": { - "items": {"type": "integer"}, - "minItems": 1, - "type": ["array", "null"], - }, - "write": { - "items": {"type": "integer"}, - "minItems": 1, - "type": ["array", "null"], - }, - }, - "required": ["read", "write"], - "type": "object", - }, - "grade": {"enum": ["stable", "devel"], "type": "string"}, - "revision": {"type": "integer"}, - "sha3-384": {"type": "string"}, - "size": {"type": "integer"}, - "version": {"type": "string"}, - }, - "required": [ - "architectures", - # "attributes", - # "base", - # "build-url", - # "confinement", - # "created-at", - # "epoch", - # "grade", - "revision", - # "sha3-384", - # "size", - # "status", - "version", - ], - "type": "object", - }, - "minItems": 0, - "type": "array", - }, - "snap": { - "description": "Metadata about the requested snap.", - "introduced_at": 6, - "properties": { - "channels": { - "description": "The list of most relevant channels for this snap. Branches are only included if there is a release for it.", - "introduced_at": 9, - "items": { - "description": "A list of channels and their metadata for the requested snap.", - "properties": { - "branch": { - "description": "The branch name for this channel, can be null.", - "type": ["string", "null"], - }, - "fallback": { - "description": "The name of the channel that this channel would fall back to if there were no releases in it. If null, this channel has no fallback channel.", - "type": ["string", "null"], - }, - "name": { - "description": 'The channel name, including "latest/" for the latest track.', - "type": "string", - }, - "risk": { - "description": "The risk name for this channel.", - "type": "string", - }, - "track": { - "description": "The track name for this channel.", - "type": "string", - }, - }, - "required": ["name", "track", "risk", "branch", "fallback"], - "type": "object", - }, - "minItems": 1, - "type": "array", - }, - "default-track": { - "description": "The default track name for this snap. If no default track is set, this value is null.", - "type": ["string", "null"], - }, - "id": { - "description": "The snap ID for this snap package.", - "type": "string", - }, - "name": {"description": "The snap package name.", "type": "string"}, - "private": { - "description": "Whether this snap is private or not.", - "type": "boolean", - }, - "tracks": { - "description": "An ordered list of most relevant tracks for this snap.", - "introduced_at": 9, - "items": { - "description": "An ordered list of tracks and their metadata for this snap.", - "properties": { - "creation-date": { - "description": "The track creation date, in ISO 8601 format.", - "format": "date-time", - "type": ["string", "null"], - }, - "name": { - "description": "The track name.", - "type": "string", - }, - "version-pattern": { - "description": "A Python regex to validate the versions being released to this track. If null, no validation is enforced.", - "type": ["string", "null"], - }, - }, - # pattern is documented as required but is not returned, - # version-pattern is returned instead. - "required": ["name", "creation-date", "version-pattern"], - "type": "object", - }, - "minItems": 1, - "type": "array", - }, - }, - "required": [ - # "id", - "channels", - # "default-track", - "name", - # "private", - # "tracks" - ], - "type": "object", - }, - }, - "required": ["channel-map", "revisions", "snap"], - "type": "object", -} - # Version 27, found at https://dashboard.snapcraft.io/docs/v2/en/tokens.html#api-tokens-whoami WHOAMI_JSONSCHEMA: Dict[str, Any] = { "properties": { diff --git a/tests/legacy/unit/commands/__init__.py b/tests/legacy/unit/commands/__init__.py index b472287eab..32d89ad0bb 100644 --- a/tests/legacy/unit/commands/__init__.py +++ b/tests/legacy/unit/commands/__init__.py @@ -29,7 +29,6 @@ from snapcraft_legacy import storeapi from snapcraft_legacy.cli._runner import run from snapcraft_legacy.storeapi import metrics -from snapcraft_legacy.storeapi.v2.channel_map import ChannelMap from snapcraft_legacy.storeapi.v2.releases import Releases from tests.legacy import fixture_setup, unit @@ -239,120 +238,6 @@ def setUp(self): ) self.useFixture(self.fake_store_register_key) - # channel-map endpoint - self.channel_map = ChannelMap.unmarshal( - { - "channel-map": [ - { - "architecture": "amd64", - "channel": "2.1/beta", - "expiration-date": None, - "revision": 19, - "progressive": { - "paused": None, - "percentage": None, - "current-percentage": None, - }, - }, - { - "architecture": "amd64", - "channel": "2.0/beta", - "expiration-date": None, - "revision": 18, - "progressive": { - "paused": None, - "percentage": None, - "current-percentage": None, - }, - }, - ], - "revisions": [ - {"architectures": ["amd64"], "revision": 19, "version": "10"}, - {"architectures": ["amd64"], "revision": 18, "version": "10"}, - ], - "snap": { - "name": "snap-test", - "channels": [ - { - "branch": None, - "fallback": None, - "name": "2.1/stable", - "risk": "stable", - "track": "2.1", - }, - { - "branch": None, - "fallback": "2.1/stable", - "name": "2.1/candidate", - "risk": "candidate", - "track": "2.1", - }, - { - "branch": None, - "fallback": "2.1/candidate", - "name": "2.1/beta", - "risk": "beta", - "track": "2.1", - }, - { - "branch": None, - "fallback": "2.1/beta", - "name": "2.1/edge", - "risk": "edge", - "track": "2.1", - }, - { - "branch": None, - "fallback": None, - "name": "2.0/stable", - "risk": "stable", - "track": "2.0", - }, - { - "branch": None, - "fallback": "2.0/stable", - "name": "2.0/candidate", - "risk": "candidate", - "track": "2.0", - }, - { - "branch": None, - "fallback": "2.0/candidate", - "name": "2.0/beta", - "risk": "beta", - "track": "2.0", - }, - { - "branch": None, - "fallback": "2.0/beta", - "name": "2.0/edge", - "risk": "edge", - "track": "2.0", - }, - ], - "default-track": "2.1", - "tracks": [ - { - "name": "2.0", - "status": "default", - "creation-date": "2019-10-17T14:11:59Z", - "version-pattern": "2\\.*", - }, - { - "name": "latest", - "status": "active", - "creation-date": None, - "version-pattern": None, - }, - ], - }, - } - ) - self.fake_store_get_snap_channel_map = fixtures.MockPatchObject( - storeapi.StoreClient, "get_snap_channel_map", return_value=self.channel_map - ) - self.useFixture(self.fake_store_get_snap_channel_map) - self.metrics = metrics.MetricsResults( metrics=[ metrics.MetricResults( diff --git a/tests/legacy/unit/commands/test_list_tracks.py b/tests/legacy/unit/commands/test_list_tracks.py deleted file mode 100644 index 6db06bc8ce..0000000000 --- a/tests/legacy/unit/commands/test_list_tracks.py +++ /dev/null @@ -1,59 +0,0 @@ -# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- -# -# Copyright 2020 Canonical Ltd -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License version 3 as -# published by the Free Software Foundation. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -from textwrap import dedent - -from testtools.matchers import Contains, Equals - -from . import FAKE_UNAUTHORIZED_ERROR, FakeStoreCommandsBaseTestCase - - -class ListTracksCommandTestCase(FakeStoreCommandsBaseTestCase): - def test_list_tracks_without_snap_raises_exception(self): - result = self.run_command(["list-tracks"]) - - self.assertThat(result.exit_code, Equals(2)) - self.assertThat(result.output, Contains("Usage:")) - - def test_list_tracks_without_login_must_ask(self): - self.fake_store_get_snap_channel_map.mock.side_effect = [ - FAKE_UNAUTHORIZED_ERROR, - self.channel_map, - ] - - result = self.run_command( - ["list-tracks", "snap-test"], input="user@example.com\nsecret\n" - ) - self.assertThat( - result.output, Contains("You are required to login before continuing.") - ) - - def test_list_tracks(self): - result = self.run_command(["list-tracks", "snap-test"]) - - self.assertThat(result.exit_code, Equals(0)) - self.assertThat( - result.output, - Equals( - dedent( - """\ - Name Status Creation-Date Version-Pattern - latest active - - - 2.0 default 2019-10-17T14:11:59Z 2\\.* - """ - ) - ), - ) diff --git a/tests/legacy/unit/commands/test_set_default_track.py b/tests/legacy/unit/commands/test_set_default_track.py index 31008d4be0..91966e15f0 100644 --- a/tests/legacy/unit/commands/test_set_default_track.py +++ b/tests/legacy/unit/commands/test_set_default_track.py @@ -17,9 +17,7 @@ import fixtures from testtools.matchers import Contains, Equals -import snapcraft_legacy from snapcraft_legacy import storeapi - from . import FAKE_UNAUTHORIZED_ERROR, FakeStoreCommandsBaseTestCase @@ -59,21 +57,3 @@ def test_set_default_track(self): self.fake_metadata.mock.assert_called_once_with( snap_name="snap-test", metadata=dict(default_track="2.0"), force=True ) - - def test_invalid_track_fails(self): - mock_wrap = self.useFixture( - fixtures.MockPatch( - "snapcraft_legacy.cli.echo.exit_error", - wraps=snapcraft_legacy.cli.echo.exit_error, - ) - ).mock - - result = self.run_command(["set-default-track", "snap-test", "3.0"]) - - self.assertThat(result.exit_code, Equals(2)) - self.assertThat(result.output, Contains("'2.0', 'latest'")) - mock_wrap.assert_called_once_with( - brief="The specified track '3.0' does not exist for 'snap-test'.", - details="Valid tracks for 'snap-test': '2.0', 'latest'.", - resolution="Ensure the '3.0' track exists for the 'snap-test' snap and try again.", - ) diff --git a/tests/legacy/unit/commands/test_status.py b/tests/legacy/unit/commands/test_status.py deleted file mode 100644 index cd1f52dba1..0000000000 --- a/tests/legacy/unit/commands/test_status.py +++ /dev/null @@ -1,467 +0,0 @@ -# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- -# -# Copyright 2016-2020 Canonical Ltd -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License version 3 as -# published by the Free Software Foundation. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -from textwrap import dedent - -from testtools.matchers import Contains, Equals - -from snapcraft_legacy.storeapi.v2.channel_map import ( - MappedChannel, - Progressive, - Revision, - SnapChannel, -) - -from . import FAKE_UNAUTHORIZED_ERROR, FakeStoreCommandsBaseTestCase - - -class StatusCommandTestCase(FakeStoreCommandsBaseTestCase): - def test_status_without_snap_raises_exception(self): - result = self.run_command(["status"]) - - self.assertThat(result.exit_code, Equals(2)) - self.assertThat(result.output, Contains("Usage:")) - - def test_status_without_login_must_ask(self): - self.fake_store_get_snap_channel_map.mock.side_effect = [ - FAKE_UNAUTHORIZED_ERROR, - self.channel_map, - ] - - result = self.run_command( - ["status", "snap-test"], input="user@example.com\nsecret\n" - ) - self.assertThat( - result.output, Contains("You are required to login before continuing.") - ) - - def test_status(self): - result = self.run_command(["status", "snap-test"]) - - self.assertThat(result.exit_code, Equals(0)) - self.assertThat( - result.output, - Equals( - dedent( - """\ - Track Arch Channel Version Revision - 2.1 amd64 stable - - - candidate - - - beta 10 19 - edge ↑ ↑ - 2.0 amd64 stable - - - candidate - - - beta 10 18 - edge ↑ ↑ - """ - ) - ), - ) - - def test_status_following(self): - self.channel_map.channel_map = [ - MappedChannel( - channel="2.1/stable", - architecture="amd64", - expiration_date="2020-02-03T20:58:37Z", - revision=20, - progressive=Progressive( - paused=None, percentage=None, current_percentage=None - ), - ) - ] - self.channel_map.revisions.append( - Revision(architectures=["amd64"], revision=20, version="10") - ) - - result = self.run_command(["status", "snap-test"]) - - self.assertThat(result.exit_code, Equals(0)) - self.assertThat( - result.output, - Equals( - dedent( - """\ - Track Arch Channel Version Revision - 2.1 amd64 stable 10 20 - candidate ↑ ↑ - beta ↑ ↑ - edge ↑ ↑ - """ - ) - ), - ) - - def test_status_no_releases(self): - self.channel_map.channel_map = [] - - result = self.run_command(["status", "snap-test"]) - - self.assertThat(result.exit_code, Equals(0)) - self.assertThat( - result.output.strip(), Equals("This snap has no released revisions.") - ) - - def test_progressive_status(self): - self.channel_map.channel_map.append( - MappedChannel( - channel="2.1/beta", - architecture="amd64", - expiration_date="2020-02-03T20:58:37Z", - revision=20, - progressive=Progressive( - paused=None, percentage=10.0, current_percentage=7.2 - ), - ) - ) - self.channel_map.revisions.append( - Revision(architectures=["amd64"], revision=20, version="11") - ) - - result = self.run_command( - ["status", "snap-test", "--experimental-progressive-releases"] - ) - - self.assertThat(result.exit_code, Equals(0)) - self.assertThat( - result.output, - Equals( - dedent( - """\ - *EXPERIMENTAL* progressive releases in use. - Track Arch Channel Version Revision Progress - 2.1 amd64 stable - - - - candidate - - - - beta 10 19 93→90% - 11 20 7→10% - edge ↑ ↑ - - 2.0 amd64 stable - - - - candidate - - - - beta 10 18 - - edge ↑ ↑ - - """ - ) - ), - ) - - def test_status_by_arch(self): - self.channel_map.channel_map.append( - MappedChannel( - channel="2.1/beta", - architecture="s390x", - expiration_date=None, - revision=99, - progressive=Progressive( - paused=None, percentage=None, current_percentage=None - ), - ) - ) - self.channel_map.revisions.append( - Revision(architectures=["s390x"], revision=99, version="10") - ) - - result = self.run_command(["status", "snap-test", "--arch=s390x"]) - - self.assertThat(result.exit_code, Equals(0)) - self.assertThat( - result.output, - Equals( - dedent( - """\ - Track Arch Channel Version Revision - 2.1 s390x stable - - - candidate - - - beta 10 99 - edge ↑ ↑ - """ - ) - ), - ) - - def test_status_by_multiple_arch(self): - self.channel_map.channel_map.append( - MappedChannel( - channel="2.1/beta", - architecture="s390x", - expiration_date=None, - revision=98, - progressive=Progressive( - paused=None, percentage=None, current_percentage=None - ), - ) - ) - self.channel_map.channel_map.append( - MappedChannel( - channel="2.1/beta", - architecture="arm64", - expiration_date=None, - revision=99, - progressive=Progressive( - paused=None, percentage=None, current_percentage=None - ), - ) - ) - self.channel_map.revisions.append( - Revision(architectures=["s390x"], revision=98, version="10") - ) - self.channel_map.revisions.append( - Revision(architectures=["arm64"], revision=99, version="10") - ) - - result = self.run_command( - ["status", "snap-test", "--arch=s390x", "--arch=arm64"] - ) - - self.assertThat(result.exit_code, Equals(0)) - self.assertThat( - result.output, - Equals( - dedent( - """\ - Track Arch Channel Version Revision - 2.1 arm64 stable - - - candidate - - - beta 10 99 - edge ↑ ↑ - s390x stable - - - candidate - - - beta 10 98 - edge ↑ ↑ - """ - ) - ), - ) - - def test_status_by_track(self): - result = self.run_command(["status", "snap-test", "--track=2.0"]) - - self.assertThat(result.exit_code, Equals(0)) - self.assertThat( - result.output, - Equals( - dedent( - """\ - Track Arch Channel Version Revision - 2.0 amd64 stable - - - candidate - - - beta 10 18 - edge ↑ ↑ - """ - ) - ), - ) - - def test_status_by_multiple_track(self): - result = self.run_command(["status", "snap-test", "--track=2.0", "--track=2.1"]) - - self.assertThat(result.exit_code, Equals(0)) - self.assertThat( - result.output, - Equals( - dedent( - """\ - Track Arch Channel Version Revision - 2.1 amd64 stable - - - candidate - - - beta 10 19 - edge ↑ ↑ - 2.0 amd64 stable - - - candidate - - - beta 10 18 - edge ↑ ↑ - """ - ) - ), - ) - - def test_status_by_track_and_arch(self): - self.channel_map.channel_map.append( - MappedChannel( - channel="2.1/beta", - architecture="s390x", - expiration_date=None, - revision=99, - progressive=Progressive( - paused=None, percentage=None, current_percentage=None - ), - ) - ) - self.channel_map.channel_map.append( - MappedChannel( - channel="2.0/beta", - architecture="s390x", - expiration_date=None, - revision=98, - progressive=Progressive( - paused=None, percentage=None, current_percentage=None - ), - ) - ) - self.channel_map.revisions.append( - Revision(architectures=["s390x"], revision=99, version="10") - ) - self.channel_map.revisions.append( - Revision(architectures=["s390x"], revision=98, version="10") - ) - - result = self.run_command( - ["status", "snap-test", "--arch=s390x", "--track=2.0"] - ) - - self.assertThat(result.exit_code, Equals(0)) - self.assertThat( - result.output, - Equals( - dedent( - """\ - Track Arch Channel Version Revision - 2.0 s390x stable - - - candidate - - - beta 10 98 - edge ↑ ↑ - """ - ) - ), - ) - - def test_status_including_branch(self): - self.channel_map.channel_map.append( - MappedChannel( - channel="2.1/stable/hotfix1", - architecture="amd64", - expiration_date="2020-02-03T20:58:37Z", - revision=20, - progressive=Progressive( - paused=None, percentage=None, current_percentage=None - ), - ) - ) - self.channel_map.revisions.append( - Revision(architectures=["amd64"], revision=20, version="10hotfix") - ) - self.channel_map.snap.channels.append( - SnapChannel( - name="2.1/stable/hotfix1", - track="2.1", - risk="stable", - branch="hotfix1", - fallback="2.1/stable", - ) - ) - - result = self.run_command(["status", "snap-test"]) - - self.assertThat(result.exit_code, Equals(0)) - self.assertThat( - result.output, - Equals( - dedent( - """\ - Track Arch Channel Version Revision Expires at - 2.1 amd64 stable - - - stable/hotfix1 10hotfix 20 2020-02-03T20:58:37Z - candidate - - - beta 10 19 - edge ↑ ↑ - 2.0 amd64 stable - - - candidate - - - beta 10 18 - edge ↑ ↑ - """ - ) - ), - ) - - def test_progressive_status_including_branch(self): - self.channel_map.channel_map.append( - MappedChannel( - channel="2.1/stable/hotfix1", - architecture="amd64", - expiration_date="2020-02-03T20:58:37Z", - revision=20, - progressive=Progressive( - paused=None, percentage=20.0, current_percentage=12.3 - ), - ) - ) - self.channel_map.revisions.append( - Revision(architectures=["amd64"], revision=20, version="10hotfix") - ) - self.channel_map.snap.channels.append( - SnapChannel( - name="2.1/stable/hotfix1", - track="2.1", - risk="stable", - branch="hotfix1", - fallback="2.1/stable", - ) - ) - - result = self.run_command( - ["status", "snap-test", "--experimental-progressive-releases"] - ) - - self.assertThat(result.exit_code, Equals(0)) - self.assertThat( - result.output, - Equals( - dedent( - """\ - *EXPERIMENTAL* progressive releases in use. - Track Arch Channel Version Revision Progress Expires at - 2.1 amd64 stable - - - - stable/hotfix1 10hotfix 20 12→20% 2020-02-03T20:58:37Z - candidate - - - - beta 10 19 - - edge ↑ ↑ - - 2.0 amd64 stable - - - - candidate - - - - beta 10 18 - - edge ↑ ↑ - - """ - ) - ), - ) - - def test_progressive_status_with_null_current_percentage(self): - self.channel_map.channel_map[0].progressive.percentage = 10.0 - self.channel_map.channel_map[0].progressive.current_percentage = None - - result = self.run_command( - ["status", "snap-test", "--experimental-progressive-releases"] - ) - - self.assertThat(result.exit_code, Equals(0)) - self.assertThat( - result.output, - Equals( - dedent( - """\ - *EXPERIMENTAL* progressive releases in use. - Track Arch Channel Version Revision Progress - 2.1 amd64 stable - - - - candidate - - - - beta - - - - 10 19 ?→10% - edge ↑ ↑ - - 2.0 amd64 stable - - - - candidate - - - - beta 10 18 - - edge ↑ ↑ - - """ - ) - ), - ) diff --git a/tests/legacy/unit/store/test_store_client.py b/tests/legacy/unit/store/test_store_client.py index ea888fe373..42355fffd0 100644 --- a/tests/legacy/unit/store/test_store_client.py +++ b/tests/legacy/unit/store/test_store_client.py @@ -36,7 +36,7 @@ import tests.legacy from snapcraft_legacy import storeapi from snapcraft_legacy.storeapi import errors, metrics -from snapcraft_legacy.storeapi.v2 import channel_map, releases, validation_sets, whoami +from snapcraft_legacy.storeapi.v2 import releases, validation_sets, whoami from tests.legacy import fixture_setup, unit @@ -941,14 +941,6 @@ def test_get_snap_status_no_id(self): self.assertThat(e.snap_name, Equals("no-id")) -class SnapChannelMapTest(StoreTestCase): - def test_get_snap_channel_map(self): - self.assertThat( - self.client.get_snap_channel_map(snap_name="basic"), - IsInstance(channel_map.ChannelMap), - ) - - class SnapReleasesTest(StoreTestCase): def test_get_snap_releases(self): self.assertThat( diff --git a/tests/legacy/unit/store/v2/test_channel_map.py b/tests/legacy/unit/store/v2/test_channel_map.py deleted file mode 100644 index 239c897a74..0000000000 --- a/tests/legacy/unit/store/v2/test_channel_map.py +++ /dev/null @@ -1,396 +0,0 @@ -# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- -# -# Copyright (C) 2020 Canonical Ltd -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License version 3 as -# published by the Free Software Foundation. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -import pytest -from testtools.matchers import Equals, HasLength, Is, IsInstance - -from snapcraft_legacy.storeapi.v2 import channel_map -from tests.legacy import unit - - -class ProgressiveTest(unit.TestCase): - def test_progressive(self): - payload = {"paused": False, "percentage": 83.3, "current-percentage": 32.1} - - p = channel_map.Progressive.unmarshal(payload) - - self.expectThat(repr(p), Equals(f"83.3>")) - self.expectThat(p.paused, Equals(payload["paused"])) - self.expectThat(p.percentage, Equals(payload["percentage"])) - self.expectThat(p.current_percentage, Equals(payload["current-percentage"])) - self.expectThat(p.marshal(), Equals(payload)) - - def test_none(self): - payload = {"paused": None, "percentage": None, "current-percentage": None} - - p = channel_map.Progressive.unmarshal(payload) - - self.expectThat(repr(p), Equals(f"None>")) - self.expectThat(p.paused, Equals(payload["paused"])) - self.expectThat(p.percentage, Equals(payload["percentage"])) - self.expectThat(p.current_percentage, Equals(payload["current-percentage"])) - self.expectThat(p.marshal(), Equals(payload)) - - -class MappedChannelTest(unit.TestCase): - def setUp(self): - super().setUp() - - self.payload = { - "architecture": "amd64", - "channel": "latest/stable", - "expiration-date": None, - "progressive": { - "paused": None, - "percentage": None, - "current-percentage": None, - }, - "revision": 2, - } - - def test_channel(self): - mc = channel_map.MappedChannel.unmarshal(self.payload) - - self.expectThat( - repr(mc), - Equals( - "" - ), - ) - self.expectThat(mc.channel, Equals(self.payload["channel"])) - self.expectThat(mc.revision, Equals(self.payload["revision"])) - self.expectThat(mc.architecture, Equals(self.payload["architecture"])) - self.expectThat(mc.progressive, IsInstance(channel_map.Progressive)) - self.expectThat(mc.expiration_date, Is(None)) - self.expectThat(mc.marshal(), Equals(self.payload)) - - def test_channel_with_expiration(self): - date_string = "2020-02-11T17:51:40.891996Z" - self.payload.update({"expiration-date": date_string}) - - mc = channel_map.MappedChannel.unmarshal(self.payload) - - self.expectThat( - repr(mc), - Equals( - "" - ), - ) - self.expectThat(mc.channel, Equals(self.payload["channel"])) - self.expectThat(mc.revision, Equals(self.payload["revision"])) - self.expectThat(mc.architecture, Equals(self.payload["architecture"])) - self.expectThat(mc.progressive, IsInstance(channel_map.Progressive)) - self.expectThat(mc.expiration_date, Equals(date_string)) - self.expectThat(mc.marshal(), Equals(self.payload)) - - -class SnapChannelTest(unit.TestCase): - def setUp(self): - super().setUp() - - self.payload = { - "name": "latest/candidate", - "track": "latest", - "risk": "candidate", - "branch": None, - "fallback": None, - } - - def test_channel(self): - sc = channel_map.SnapChannel.unmarshal(self.payload) - - self.expectThat(repr(sc), Equals("")) - self.expectThat(sc.name, Equals(self.payload["name"])) - self.expectThat(sc.track, Equals(self.payload["track"])) - self.expectThat(sc.risk, Equals(self.payload["risk"])) - self.expectThat(sc.branch, Is(None)) - self.expectThat(sc.fallback, Is(None)) - self.expectThat(sc.marshal(), Equals(self.payload)) - - def test_channel_with_branch(self): - self.payload.update({"branch": "test-branch"}) - - sc = channel_map.SnapChannel.unmarshal(self.payload) - - self.expectThat(repr(sc), Equals("")) - self.expectThat(sc.name, Equals(self.payload["name"])) - self.expectThat(sc.track, Equals(self.payload["track"])) - self.expectThat(sc.risk, Equals(self.payload["risk"])) - self.expectThat(sc.branch, Equals(self.payload["branch"])) - self.expectThat(sc.fallback, Is(None)) - self.expectThat(sc.marshal(), Equals(self.payload)) - - def test_channel_with_fallback(self): - self.payload.update({"fallback": "latest/stable"}) - - sc = channel_map.SnapChannel.unmarshal(self.payload) - - self.expectThat(repr(sc), Equals("")) - self.expectThat(sc.name, Equals(self.payload["name"])) - self.expectThat(sc.track, Equals(self.payload["track"])) - self.expectThat(sc.risk, Equals(self.payload["risk"])) - self.expectThat(sc.branch, Is(None)), - self.expectThat(sc.fallback, Equals(self.payload["fallback"])) - self.expectThat(sc.marshal(), Equals(self.payload)) - - -_TRACK_PAYLOADS = [ - { - "name": "latest", - "status": "active", - "creation-date": None, - "version-pattern": None, - }, - { - "name": "1.0", - "status": "default", - "creation-date": "2019-10-17T14:11:59Z", - "version-pattern": "1.*", - }, -] - - -@pytest.mark.parametrize("payload", _TRACK_PAYLOADS) -def test_snap_track(payload): - st = channel_map.SnapTrack.unmarshal(payload) - - assert repr(st) == f"" - assert st.name == payload["name"] - assert st.status == payload["status"] - assert st.creation_date == payload["creation-date"] - assert st.version_pattern == payload["version-pattern"] - assert st.marshal() == payload - - -class RevisionTest(unit.TestCase): - def test_revision(self): - payload = {"revision": 2, "version": "2.0", "architectures": ["amd64", "arm64"]} - - r = channel_map.Revision.unmarshal(payload) - - self.expectThat( - repr(r), - Equals( - "" - ), - ) - self.expectThat(r.revision, Equals(payload["revision"])) - self.expectThat(r.version, Equals(payload["version"])) - self.expectThat(r.architectures, Equals(payload["architectures"])) - self.expectThat(r.marshal(), Equals(payload)) - - -class SnapTest(unit.TestCase): - def test_snap(self): - payload = { - "name": "my-snap", - "channels": [ - { - "name": "latest/stable", - "track": "latest", - "risk": "candidate", - "branch": None, - "fallback": None, - }, - { - "name": "latest/candidate", - "track": "latest", - "risk": "candidate", - "branch": None, - "fallback": "latest/stable", - }, - ], - "tracks": [ - { - "name": "track1", - "creation-date": "2019-10-17T14:11:59Z", - "status": "default", - "version-pattern": None, - }, - { - "name": "track2", - "creation-date": None, - "status": "active", - "version-pattern": None, - }, - ], - } - - s = channel_map.Snap.unmarshal(payload) - - self.expectThat(repr(s), Equals("")) - self.expectThat(s.name, Equals(payload["name"])) - - snap_channels = s.channels - self.expectThat(snap_channels, HasLength(2)) - self.expectThat(snap_channels[0], IsInstance(channel_map.SnapChannel)) - self.expectThat(snap_channels[1], IsInstance(channel_map.SnapChannel)) - - self.expectThat(s.marshal(), Equals(payload)) - - -class ChannelMapTest(unit.TestCase): - def test_channel_map(self): - payload = { - "channel-map": [ - { - "architecture": "amd64", - "channel": "latest/stable", - "expiration-date": None, - "progressive": { - "paused": None, - "percentage": None, - "current-percentage": None, - }, - "revision": 2, - }, - { - "architecture": "amd64", - "channel": "latest/stable", - "expiration-date": None, - "progressive": { - "paused": None, - "percentage": 33.3, - "current-percentage": 12.3, - }, - "revision": 3, - }, - { - "architecture": "arm64", - "channel": "latest/stable", - "expiration-date": None, - "progressive": { - "paused": None, - "percentage": None, - "current-percentage": None, - }, - "revision": 2, - }, - { - "architecture": "i386", - "channel": "latest/stable", - "expiration-date": None, - "progressive": { - "paused": None, - "percentage": None, - "current-percentage": None, - }, - "revision": 4, - }, - ], - "revisions": [ - {"revision": 2, "version": "2.0", "architectures": ["amd64", "arm64"]}, - {"revision": 3, "version": "2.0", "architectures": ["amd64", "arm64"]}, - {"revision": 4, "version": "2.0", "architectures": ["i386"]}, - ], - "snap": { - "name": "my-snap", - "channels": [ - { - "name": "latest/stable", - "track": "latest", - "risk": "candidate", - "branch": None, - "fallback": None, - }, - { - "name": "latest/candidate", - "track": "latest", - "risk": "candidate", - "branch": None, - "fallback": "latest/stable", - }, - ], - "tracks": [ - { - "name": "track1", - "creation-date": "2019-10-17T14:11:59Z", - "status": "default", - "version-pattern": None, - }, - { - "name": "track2", - "creation-date": None, - "status": "active", - "version-pattern": None, - }, - ], - }, - } - - cm = channel_map.ChannelMap.unmarshal(payload) - - # Check "channel-map". - self.expectThat(cm.channel_map, HasLength(4)) - self.expectThat(cm.channel_map[0], IsInstance(channel_map.MappedChannel)) - self.expectThat(cm.channel_map[1], IsInstance(channel_map.MappedChannel)) - self.expectThat(cm.channel_map[2], IsInstance(channel_map.MappedChannel)) - self.expectThat(cm.channel_map[3], IsInstance(channel_map.MappedChannel)) - - # Check "revisions". - self.expectThat(cm.revisions, HasLength(3)) - self.expectThat(cm.revisions[0], IsInstance(channel_map.Revision)) - self.expectThat(cm.revisions[1], IsInstance(channel_map.Revision)) - self.expectThat(cm.revisions[2], IsInstance(channel_map.Revision)) - - # Check "snap". - self.expectThat(cm.snap, IsInstance(channel_map.Snap)) - - # Marshal. - self.expectThat(cm.marshal(), Equals(payload)) - - # Test the get_mapped_channel method. - self.expectThat( - cm.get_mapped_channel( - channel_name="latest/stable", architecture="amd64", progressive=False - ), - Equals(cm.channel_map[0]), - ) - self.expectThat( - cm.get_mapped_channel( - channel_name="latest/stable", architecture="amd64", progressive=True - ), - Equals(cm.channel_map[1]), - ) - self.assertRaises( - ValueError, - cm.get_mapped_channel, - channel_name="latest/stable", - architecture="arm64", - progressive=True, - ) - self.assertRaises( - ValueError, - cm.get_mapped_channel, - channel_name="latest/stable", - architecture="i386", - progressive=True, - ) - - # Test the get_channel_info method. - self.expectThat( - cm.get_channel_info("latest/stable"), Equals(cm.snap.channels[0]) - ) - self.assertRaises(ValueError, cm.get_channel_info, "other-track/stable") - - # Test the get_revision method. - self.expectThat(cm.get_revision(4), Equals(cm.revisions[2])) - self.assertRaises(ValueError, cm.get_revision, 5) - - # Test the get_existing_architectures method. - self.expectThat( - cm.get_existing_architectures(), Equals(set(["arm64", "amd64", "i386"])) - ) diff --git a/tests/unit/commands/store/test_channel_map.py b/tests/unit/commands/store/test_channel_map.py new file mode 100644 index 0000000000..2914c7e61e --- /dev/null +++ b/tests/unit/commands/store/test_channel_map.py @@ -0,0 +1,386 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright (C) 2020, 2022 Canonical Ltd +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import pytest + +from snapcraft.commands.store import channel_map + +############ +# Fixtures # +############ + + +@pytest.fixture +def channel_payload(): + return { + "name": "latest/candidate", + "track": "latest", + "risk": "candidate", + "branch": None, + "fallback": None, + } + + +@pytest.fixture +def mapped_channel_payload(): + return { + "architecture": "amd64", + "channel": "latest/stable", + "expiration-date": None, + "progressive": { + "paused": None, + "percentage": None, + "current-percentage": None, + }, + "revision": 2, + } + + +######### +# Tests # +######### + + +def test_progressive(): + payload = {"paused": False, "percentage": 83.3, "current-percentage": 32.1} + + p = channel_map.Progressive.unmarshal(payload) + + assert repr(p) == "83.3>" + assert p.paused == payload["paused"] + assert p.percentage == payload["percentage"] + assert p.current_percentage == payload["current-percentage"] + assert p.marshal() == payload + + +def test_none(): + payload = {"paused": None, "percentage": None, "current-percentage": None} + + p = channel_map.Progressive.unmarshal(payload) + + assert repr(p) == "None>" + assert p.paused == payload["paused"] + assert p.percentage == payload["percentage"] + assert p.current_percentage == payload["current-percentage"] + assert p.marshal() == payload + + +def test_mapped_channel(mapped_channel_payload): + mc = channel_map.MappedChannel.unmarshal(mapped_channel_payload) + + assert ( + repr(mc) + ) == "" + assert mc.channel == mapped_channel_payload["channel"] + assert mc.revision == mapped_channel_payload["revision"] + assert mc.architecture == mapped_channel_payload["architecture"] + assert isinstance(mc.progressive, channel_map.Progressive) + assert mc.expiration_date is None + assert mc.marshal() == mapped_channel_payload + + +def test_snap_channel_with_expiration(mapped_channel_payload): + date_string = "2020-02-11T17:51:40.891996Z" + mapped_channel_payload.update({"expiration-date": date_string}) + + mc = channel_map.MappedChannel.unmarshal(mapped_channel_payload) + + assert ( + repr(mc) + ) == "" + assert mc.channel == mapped_channel_payload["channel"] + assert mc.revision == mapped_channel_payload["revision"] + assert mc.architecture == mapped_channel_payload["architecture"] + assert isinstance(mc.progressive, channel_map.Progressive) + assert mc.expiration_date == date_string + assert mc.marshal() == mapped_channel_payload + + +def test_snap_channel(channel_payload): + sc = channel_map.SnapChannel.unmarshal(channel_payload) + + assert repr(sc) == "" + assert sc.name == channel_payload["name"] + assert sc.track == channel_payload["track"] + assert sc.risk == channel_payload["risk"] + assert sc.branch is None + assert sc.fallback is None + assert sc.marshal() == channel_payload + + +def test_snap_channel_with_branch(channel_payload): + channel_payload.update({"branch": "test-branch"}) + + sc = channel_map.SnapChannel.unmarshal(channel_payload) + + assert repr(sc) == "" + assert sc.name == channel_payload["name"] + assert sc.track == channel_payload["track"] + assert sc.risk == channel_payload["risk"] + assert sc.branch == channel_payload["branch"] + assert sc.fallback is None + assert sc.marshal() == channel_payload + + +def test_snap_channel_with_fallback(channel_payload): + channel_payload.update({"fallback": "latest/stable"}) + + sc = channel_map.SnapChannel.unmarshal(channel_payload) + + assert repr(sc) == "" + assert sc.name == channel_payload["name"] + assert sc.track == channel_payload["track"] + assert sc.risk == channel_payload["risk"] + assert sc.branch is None + assert sc.fallback == channel_payload["fallback"] + assert sc.marshal() == channel_payload + + +_TRACK_PAYLOADS = [ + { + "name": "latest", + "status": "active", + "creation-date": None, + "version-pattern": None, + }, + { + "name": "1.0", + "status": "default", + "creation-date": "2019-10-17T14:11:59Z", + "version-pattern": "1.*", + }, +] + + +@pytest.mark.parametrize("payload", _TRACK_PAYLOADS) +def test_snap_track(payload): + st = channel_map.SnapTrack.unmarshal(payload) + + assert repr(st) == f"" + assert st.name == payload["name"] + assert st.status == payload["status"] + assert st.creation_date == payload["creation-date"] + assert st.version_pattern == payload["version-pattern"] + assert st.marshal() == payload + + +def test_revision(): + payload = {"revision": 2, "version": "2.0", "architectures": ["amd64", "arm64"]} + + r = channel_map.Revision.unmarshal(payload) + + assert repr(r) == ( + "" + ) + assert r.revision == payload["revision"] + assert r.version == payload["version"] + assert r.architectures == payload["architectures"] + assert r.marshal() == payload + + +def test_snap(): + payload = { + "name": "my-snap", + "channels": [ + { + "name": "latest/stable", + "track": "latest", + "risk": "candidate", + "branch": None, + "fallback": None, + }, + { + "name": "latest/candidate", + "track": "latest", + "risk": "candidate", + "branch": None, + "fallback": "latest/stable", + }, + ], + "tracks": [ + { + "name": "track1", + "creation-date": "2019-10-17T14:11:59Z", + "status": "default", + "version-pattern": None, + }, + { + "name": "track2", + "creation-date": None, + "status": "active", + "version-pattern": None, + }, + ], + } + + s = channel_map.Snap.unmarshal(payload) + + assert repr(s) == "" + assert s.name == payload["name"] + + snap_channels = s.channels + assert len(snap_channels) == 2 + assert isinstance(snap_channels[0], channel_map.SnapChannel) + assert isinstance(snap_channels[1], channel_map.SnapChannel) + + assert s.marshal() == payload + + +def test_channel_map(): + payload = { + "channel-map": [ + { + "architecture": "amd64", + "channel": "latest/stable", + "expiration-date": None, + "progressive": { + "paused": None, + "percentage": None, + "current-percentage": None, + }, + "revision": 2, + }, + { + "architecture": "amd64", + "channel": "latest/stable", + "expiration-date": None, + "progressive": { + "paused": None, + "percentage": 33.3, + "current-percentage": 12.3, + }, + "revision": 3, + }, + { + "architecture": "arm64", + "channel": "latest/stable", + "expiration-date": None, + "progressive": { + "paused": None, + "percentage": None, + "current-percentage": None, + }, + "revision": 2, + }, + { + "architecture": "i386", + "channel": "latest/stable", + "expiration-date": None, + "progressive": { + "paused": None, + "percentage": None, + "current-percentage": None, + }, + "revision": 4, + }, + ], + "revisions": [ + {"revision": 2, "version": "2.0", "architectures": ["amd64", "arm64"]}, + {"revision": 3, "version": "2.0", "architectures": ["amd64", "arm64"]}, + {"revision": 4, "version": "2.0", "architectures": ["i386"]}, + ], + "snap": { + "name": "my-snap", + "channels": [ + { + "name": "latest/stable", + "track": "latest", + "risk": "candidate", + "branch": None, + "fallback": None, + }, + { + "name": "latest/candidate", + "track": "latest", + "risk": "candidate", + "branch": None, + "fallback": "latest/stable", + }, + ], + "tracks": [ + { + "name": "track1", + "creation-date": "2019-10-17T14:11:59Z", + "status": "default", + "version-pattern": None, + }, + { + "name": "track2", + "creation-date": None, + "status": "active", + "version-pattern": None, + }, + ], + }, + } + + cm = channel_map.ChannelMap.unmarshal(payload) + + # Check "channel-map". + assert len(cm.channel_map) == 4 + assert isinstance(cm.channel_map[0], channel_map.MappedChannel) + assert isinstance(cm.channel_map[1], channel_map.MappedChannel) + assert isinstance(cm.channel_map[2], channel_map.MappedChannel) + assert isinstance(cm.channel_map[3], channel_map.MappedChannel) + + # Check "revisions". + assert len(cm.revisions) == 3 + assert isinstance(cm.revisions[0], channel_map.Revision) + assert isinstance(cm.revisions[1], channel_map.Revision) + assert isinstance(cm.revisions[2], channel_map.Revision) + + # Check "snap". + assert isinstance(cm.snap, channel_map.Snap) + + # Marshal. + assert cm.marshal() == payload + + # Test the get_mapped_channel method. + assert ( + cm.get_mapped_channel( + channel_name="latest/stable", architecture="amd64", progressive=False + ) + ) == cm.channel_map[0] + assert ( + cm.get_mapped_channel( + channel_name="latest/stable", architecture="amd64", progressive=True + ) + ) == cm.channel_map[1] + with pytest.raises(ValueError): + cm.get_mapped_channel( + channel_name="latest/stable", + architecture="arm64", + progressive=True, + ) + with pytest.raises(ValueError): + cm.get_mapped_channel( + channel_name="latest/stable", + architecture="i386", + progressive=True, + ) + + # Test the get_channel_info method. + assert cm.get_channel_info("latest/stable") == cm.snap.channels[0] + with pytest.raises(ValueError): + cm.get_channel_info("other-track/stable") + + # Test the get_revision method. + assert cm.get_revision(4) == cm.revisions[2] + with pytest.raises(ValueError): + cm.get_revision(5) + + # Test the get_existing_architectures method. + assert cm.get_existing_architectures() == set(["arm64", "amd64", "i386"]) diff --git a/tests/unit/commands/store/test_client.py b/tests/unit/commands/store/test_client.py index e8828cab52..f405a2d85e 100644 --- a/tests/unit/commands/store/test_client.py +++ b/tests/unit/commands/store/test_client.py @@ -24,10 +24,97 @@ from snapcraft import errors from snapcraft.commands.store import client +from snapcraft.commands.store.channel_map import ChannelMap from snapcraft.utils import OSPlatform from .utils import FakeResponse +############# +# Fixtures # + + +@pytest.fixture +def channel_map_payload(): + return { + "channel-map": [ + { + "architecture": "all", + "channel": "2.1/beta", + "expiration-date": None, + "revision": 1, + "progressive": { + "paused": None, + "percentage": None, + "current-percentage": None, + }, + "when": "2020-02-03T20:58:37Z", + } + ], + "revisions": [ + { + "architectures": [ + "amd64", + "arm64", + "armhf", + "i386", + "s390x", + "ppc64el", + ], + "revision": 1, + "version": "10", + } + ], + "snap": { + "name": "test-snap", + "channels": [ + { + "branch": None, + "fallback": None, + "name": "2.1/stable", + "risk": "stable", + "track": "2.1", + }, + { + "branch": None, + "fallback": "2.1/stable", + "name": "2.1/candidate", + "risk": "candidate", + "track": "2.1", + }, + { + "branch": None, + "fallback": "2.1/candidate", + "name": "2.1/beta", + "risk": "beta", + "track": "2.1", + }, + { + "branch": None, + "fallback": "2.1/beta", + "name": "2.1/edge", + "risk": "edge", + "track": "2.1", + }, + ], + "tracks": [ + { + "name": "latest", + "status": "active", + "creation-date": None, + "version-pattern": None, + }, + { + "name": "1.0", + "status": "default", + "creation-date": "2019-10-17T14:11:59Z", + "version-pattern": "1.*", + }, + ], + "default-track": "2.1", + }, + } + + #################### # User Agent Tests # #################### @@ -471,3 +558,26 @@ def test_close(fake_client): json={"channels": ["edge"]}, ) ] + + +################### +# Get Channel Map # +################### + + +def test_get_channel_map(fake_client, channel_map_payload): + fake_client.request.return_value = FakeResponse( + status_code=200, content=json.dumps(channel_map_payload) + ) + channel_map = client.StoreClientCLI().get_channel_map( + snap_name="test-snap", + ) + assert isinstance(channel_map, ChannelMap) + + assert fake_client.request.mock_calls == [ + call( + "GET", + "https://dashboard.snapcraft.io/api/v2/snaps/test-snap/channel-map", + headers={"Accept": "application/json"}, + ) + ] diff --git a/tests/unit/commands/test_status.py b/tests/unit/commands/test_status.py new file mode 100644 index 0000000000..ce580fda0f --- /dev/null +++ b/tests/unit/commands/test_status.py @@ -0,0 +1,605 @@ +import argparse + +import pytest + +from snapcraft import commands +from snapcraft.commands.store import channel_map + +############ +# Fixtures # +############ + + +@pytest.fixture +def channel_map_result(): + return channel_map.ChannelMap.unmarshal( + { + "channel-map": [ + { + "architecture": "amd64", + "channel": "2.1/beta", + "expiration-date": None, + "revision": 19, + "progressive": { + "paused": None, + "percentage": None, + "current-percentage": None, + }, + }, + { + "architecture": "amd64", + "channel": "2.0/beta", + "expiration-date": None, + "revision": 18, + "progressive": { + "paused": None, + "percentage": None, + "current-percentage": None, + }, + }, + ], + "revisions": [ + {"architectures": ["amd64"], "revision": 19, "version": "10"}, + {"architectures": ["amd64"], "revision": 18, "version": "10"}, + ], + "snap": { + "name": "snap-test", + "channels": [ + { + "branch": None, + "fallback": None, + "name": "2.1/stable", + "risk": "stable", + "track": "2.1", + }, + { + "branch": None, + "fallback": "2.1/stable", + "name": "2.1/candidate", + "risk": "candidate", + "track": "2.1", + }, + { + "branch": None, + "fallback": "2.1/candidate", + "name": "2.1/beta", + "risk": "beta", + "track": "2.1", + }, + { + "branch": None, + "fallback": "2.1/beta", + "name": "2.1/edge", + "risk": "edge", + "track": "2.1", + }, + { + "branch": None, + "fallback": None, + "name": "2.0/stable", + "risk": "stable", + "track": "2.0", + }, + { + "branch": None, + "fallback": "2.0/stable", + "name": "2.0/candidate", + "risk": "candidate", + "track": "2.0", + }, + { + "branch": None, + "fallback": "2.0/candidate", + "name": "2.0/beta", + "risk": "beta", + "track": "2.0", + }, + { + "branch": None, + "fallback": "2.0/beta", + "name": "2.0/edge", + "risk": "edge", + "track": "2.0", + }, + ], + "default-track": "2.1", + "tracks": [ + { + "name": "2.0", + "status": "default", + "creation-date": "2019-10-17T14:11:59Z", + "version-pattern": "2\\.*", + }, + { + "name": "latest", + "status": "active", + "creation-date": None, + "version-pattern": None, + }, + ], + }, + } + ) + + +@pytest.fixture +def fake_store_get_status_map(mocker, channel_map_result): + fake_client = mocker.patch( + "snapcraft.commands.store.StoreClientCLI.get_channel_map", + autospec=True, + return_value=channel_map_result, + ) + return fake_client + + +################## +# Status Command # +################## + + +@pytest.mark.usefixtures("memory_keyring", "fake_store_get_status_map") +def test_default(emitter): + cmd = commands.StoreStatusCommand(None) + + cmd.run( + argparse.Namespace( + name="test-snap", + arch=None, + track=None, + ) + ) + + emitter.assert_message( + "Track Arch Channel Version Revision Progress\n" + "2.1 amd64 stable - - -\n" + " candidate - - -\n" + " beta 10 19 -\n" + " edge ↑ ↑ -\n" + "2.0 amd64 stable - - -\n" + " candidate - - -\n" + " beta 10 18 -\n" + " edge ↑ ↑ -" + ) + + +@pytest.mark.usefixtures("memory_keyring") +def test_following(emitter, fake_store_get_status_map, channel_map_result): + channel_map_result.channel_map = [ + channel_map.MappedChannel( + channel="2.1/stable", + architecture="amd64", + expiration_date="2020-02-03T20:58:37Z", + revision=20, + progressive=channel_map.Progressive( + paused=None, percentage=None, current_percentage=None + ), + ) + ] + channel_map_result.revisions.append( + channel_map.Revision(architectures=["amd64"], revision=20, version="10") + ) + fake_store_get_status_map.return_value = channel_map_result + + cmd = commands.StoreStatusCommand(None) + + cmd.run( + argparse.Namespace( + name="test-snap", + arch=None, + track=None, + ) + ) + + emitter.assert_message( + "Track Arch Channel Version Revision Progress\n" + "2.1 amd64 stable 10 20 -\n" + " candidate ↑ ↑ -\n" + " beta ↑ ↑ -\n" + " edge ↑ ↑ -" + "" + ) + + +@pytest.mark.usefixtures("memory_keyring") +def test_no_releases(emitter, fake_store_get_status_map, channel_map_result): + channel_map_result.channel_map = [] + + cmd = commands.StoreStatusCommand(None) + + cmd.run( + argparse.Namespace( + name="test-snap", + arch=None, + track=None, + ) + ) + + emitter.assert_message("This snap has no released revisions") + + +@pytest.mark.usefixtures("memory_keyring") +def test_progressive(emitter, fake_store_get_status_map, channel_map_result): + channel_map_result.channel_map.append( + channel_map.MappedChannel( + channel="2.1/beta", + architecture="amd64", + expiration_date="2020-02-03T20:58:37Z", + revision=20, + progressive=channel_map.Progressive( + paused=None, percentage=10.0, current_percentage=7.2 + ), + ) + ) + channel_map_result.revisions.append( + channel_map.Revision(architectures=["amd64"], revision=20, version="11") + ) + fake_store_get_status_map.return_value = channel_map_result + + cmd = commands.StoreStatusCommand(None) + + cmd.run( + argparse.Namespace( + name="test-snap", + arch=None, + track=None, + ) + ) + + emitter.assert_message( + "Track Arch Channel Version Revision Progress\n" + "2.1 amd64 stable - - -\n" + " candidate - - -\n" + " beta 10 19 93→90%\n" + " 11 20 7→10%\n" + " edge ↑ ↑ -\n" + "2.0 amd64 stable - - -\n" + " candidate - - -\n" + " beta 10 18 -\n" + " edge ↑ ↑ -" + ) + + +@pytest.mark.usefixtures("memory_keyring") +def test_arch(emitter, fake_store_get_status_map, channel_map_result): + channel_map_result.channel_map.append( + channel_map.MappedChannel( + channel="2.1/beta", + architecture="s390x", + expiration_date=None, + revision=99, + progressive=channel_map.Progressive( + paused=None, percentage=None, current_percentage=None + ), + ) + ) + channel_map_result.revisions.append( + channel_map.Revision(architectures=["s390x"], revision=99, version="10") + ) + fake_store_get_status_map.return_value = channel_map_result + + cmd = commands.StoreStatusCommand(None) + + cmd.run( + argparse.Namespace( + name="test-snap", + arch=["s390x"], + track=None, + ) + ) + + emitter.assert_message( + "Track Arch Channel Version Revision Progress\n" + "2.1 s390x stable - - -\n" + " candidate - - -\n" + " beta 10 99 -\n" + " edge ↑ ↑ -" + ) + + +@pytest.mark.usefixtures("memory_keyring") +def test_multiple_arch(emitter, fake_store_get_status_map, channel_map_result): + channel_map_result.channel_map.append( + channel_map.MappedChannel( + channel="2.1/beta", + architecture="s390x", + expiration_date=None, + revision=98, + progressive=channel_map.Progressive( + paused=None, percentage=None, current_percentage=None + ), + ) + ) + channel_map_result.channel_map.append( + channel_map.MappedChannel( + channel="2.1/beta", + architecture="arm64", + expiration_date=None, + revision=99, + progressive=channel_map.Progressive( + paused=None, percentage=None, current_percentage=None + ), + ) + ) + channel_map_result.revisions.append( + channel_map.Revision(architectures=["s390x"], revision=98, version="10") + ) + channel_map_result.revisions.append( + channel_map.Revision(architectures=["arm64"], revision=99, version="10") + ) + fake_store_get_status_map.return_value = channel_map_result + + cmd = commands.StoreStatusCommand(None) + + cmd.run( + argparse.Namespace( + name="test-snap", + arch=["s390x", "arm64"], + track=None, + ) + ) + + emitter.assert_message( + "Track Arch Channel Version Revision Progress\n" + "2.1 arm64 stable - - -\n" + " candidate - - -\n" + " beta 10 99 -\n" + " edge ↑ ↑ -\n" + " s390x stable - - -\n" + " candidate - - -\n" + " beta 10 98 -\n" + " edge ↑ ↑ -" + ) + + +@pytest.mark.usefixtures("memory_keyring") +def test_track(emitter, fake_store_get_status_map): + cmd = commands.StoreStatusCommand(None) + + cmd.run( + argparse.Namespace( + name="test-snap", + arch=None, + track=["2.0"], + ) + ) + + emitter.assert_message( + "Track Arch Channel Version Revision Progress\n" + "2.0 amd64 stable - - -\n" + " candidate - - -\n" + " beta 10 18 -\n" + " edge ↑ ↑ -" + ) + + +@pytest.mark.usefixtures("memory_keyring") +def test_multi_track(emitter, fake_store_get_status_map): + cmd = commands.StoreStatusCommand(None) + + cmd.run( + argparse.Namespace( + name="test-snap", + arch=None, + track=["2.0", "2.1"], + ) + ) + + emitter.assert_message( + "Track Arch Channel Version Revision Progress\n" + "2.1 amd64 stable - - -\n" + " candidate - - -\n" + " beta 10 19 -\n" + " edge ↑ ↑ -\n" + "2.0 amd64 stable - - -\n" + " candidate - - -\n" + " beta 10 18 -\n" + " edge ↑ ↑ -" + ) + + +@pytest.mark.usefixtures("memory_keyring") +def test_arch_and_track(emitter, fake_store_get_status_map, channel_map_result): + channel_map_result.channel_map.append( + channel_map.MappedChannel( + channel="2.1/beta", + architecture="s390x", + expiration_date=None, + revision=99, + progressive=channel_map.Progressive( + paused=None, percentage=None, current_percentage=None + ), + ) + ) + channel_map_result.channel_map.append( + channel_map.MappedChannel( + channel="2.0/beta", + architecture="s390x", + expiration_date=None, + revision=98, + progressive=channel_map.Progressive( + paused=None, percentage=None, current_percentage=None + ), + ) + ) + channel_map_result.revisions.append( + channel_map.Revision(architectures=["s390x"], revision=98, version="10") + ) + channel_map_result.revisions.append( + channel_map.Revision(architectures=["s390x"], revision=99, version="10") + ) + fake_store_get_status_map.return_value = channel_map_result + + cmd = commands.StoreStatusCommand(None) + + cmd.run( + argparse.Namespace( + name="test-snap", + arch=["s390x"], + track=["2.1"], + ) + ) + + emitter.assert_message( + "Track Arch Channel Version Revision Progress\n" + "2.1 s390x stable - - -\n" + " candidate - - -\n" + " beta 10 99 -\n" + " edge ↑ ↑ -" + ) + + +@pytest.mark.usefixtures("memory_keyring") +def test_branch(emitter, fake_store_get_status_map, channel_map_result): + channel_map_result.channel_map.append( + channel_map.MappedChannel( + channel="2.1/stable/hotfix1", + architecture="amd64", + expiration_date="2020-02-03T20:58:37Z", + revision=20, + progressive=channel_map.Progressive( + paused=None, percentage=None, current_percentage=None + ), + ) + ) + channel_map_result.revisions.append( + channel_map.Revision(architectures=["am64"], revision=20, version="10hotfix") + ) + channel_map_result.snap.channels.append( + channel_map.SnapChannel( + name="2.1/stable/hotfix1", + track="2.1", + risk="stable", + branch="hotfix1", + fallback="2.1/stable", + ) + ) + fake_store_get_status_map.return_value = channel_map_result + + cmd = commands.StoreStatusCommand(None) + + cmd.run( + argparse.Namespace( + name="test-snap", + arch=None, + track=None, + ) + ) + + emitter.assert_message( + "Track Arch Channel Version Revision Progress Expires at\n" + "2.1 amd64 stable - - - -\n" + " stable/hotfix1 10hotfix 20 - 2020-02-03T20:58:37Z\n" + " candidate - - - -\n" + " beta 10 19 - -\n" + " edge ↑ ↑ - -\n" + "2.0 amd64 stable - - - -\n" + " candidate - - - -\n" + " beta 10 18 - -\n" + " edge ↑ ↑ - -" + ) + + +@pytest.mark.usefixtures("memory_keyring") +def test_progressive_branch(emitter, fake_store_get_status_map, channel_map_result): + channel_map_result.channel_map.append( + channel_map.MappedChannel( + channel="2.1/stable/hotfix1", + architecture="amd64", + expiration_date="2020-02-03T20:58:37Z", + revision=20, + progressive=channel_map.Progressive( + paused=None, percentage=20.0, current_percentage=12.3 + ), + ) + ) + channel_map_result.revisions.append( + channel_map.Revision(architectures=["am64"], revision=20, version="10hotfix") + ) + channel_map_result.snap.channels.append( + channel_map.SnapChannel( + name="2.1/stable/hotfix1", + track="2.1", + risk="stable", + branch="hotfix1", + fallback="2.1/stable", + ) + ) + fake_store_get_status_map.return_value = channel_map_result + + cmd = commands.StoreStatusCommand(None) + + cmd.run( + argparse.Namespace( + name="test-snap", + arch=None, + track=None, + ) + ) + + emitter.assert_message( + "Track Arch Channel Version Revision Progress Expires at\n" + "2.1 amd64 stable - - - -\n" + " stable/hotfix1 10hotfix 20 12→20% 2020-02-03T20:58:37Z\n" + " candidate - - - -\n" + " beta 10 19 - -\n" + " edge ↑ ↑ - -\n" + "2.0 amd64 stable - - - -\n" + " candidate - - - -\n" + " beta 10 18 - -\n" + " edge ↑ ↑ - -" + ) + + +@pytest.mark.usefixtures("memory_keyring") +def test_progressive_unknown(emitter, fake_store_get_status_map, channel_map_result): + channel_map_result.channel_map[0].progressive.percentage = 10.0 + channel_map_result.channel_map[0].progressive.current_percentage = None + fake_store_get_status_map.return_value = channel_map_result + + cmd = commands.StoreStatusCommand(None) + + cmd.run( + argparse.Namespace( + name="test-snap", + arch=None, + track=None, + ) + ) + + emitter.assert_message( + "Track Arch Channel Version Revision Progress\n" + "2.1 amd64 stable - - -\n" + " candidate - - -\n" + " beta - - -\n" + " 10 19 ?→10%\n" + " edge ↑ ↑ -\n" + "2.0 amd64 stable - - -\n" + " candidate - - -\n" + " beta 10 18 -\n" + " edge ↑ ↑ -" + ) + + +####################### +# List Tracks Command # +####################### + + +@pytest.mark.parametrize( + "command_class", + [ + commands.StoreListTracksCommand, + commands.StoreTracksCommand, + ], +) +@pytest.mark.usefixtures("memory_keyring", "fake_store_get_status_map") +def test_list_tracks(emitter, command_class): + cmd = command_class(None) + + cmd.run(argparse.Namespace(name="test-snap")) + + emitter.assert_message( + "Name Status Creation-Date Version-Pattern\n" + "latest active - -\n" + "2.0 default 2019-10-17T14:11:59Z 2\\.*" + ) From 47f236f749fa865027018f911f7248eea542ce0c Mon Sep 17 00:00:00 2001 From: Claudio Matsuoka Date: Fri, 29 Apr 2022 10:30:03 -0300 Subject: [PATCH 136/167] parts: handle build base Allow projects declaring build-base to set up the correct build environment. For projects of type base, use the declared build-base or fall back to the package name if it's not declared. Signed-off-by: Claudio Matsuoka --- snapcraft/meta/snap_yaml.py | 3 ++- snapcraft/parts/lifecycle.py | 4 ++-- snapcraft/parts/yaml_utils.py | 19 ++++++++++++++----- snapcraft/projects.py | 20 ++++++++++++++++++-- snapcraft/utils.py | 19 +++++++++++++++++++ tests/unit/parts/test_lifecycle.py | 26 +++++++++++++++----------- tests/unit/test_utils.py | 19 +++++++++++++++++++ 7 files changed, 89 insertions(+), 21 deletions(-) diff --git a/snapcraft/meta/snap_yaml.py b/snapcraft/meta/snap_yaml.py index 79dadd974b..4c818d491f 100644 --- a/snapcraft/meta/snap_yaml.py +++ b/snapcraft/meta/snap_yaml.py @@ -101,7 +101,8 @@ class SnapMetadata(YamlModel): license: Optional[str] type: Optional[str] architectures: List[str] - base: str + base: Optional[str] + build_base: Optional[str] assumes: Optional[List[str]] epoch: Optional[str] apps: Optional[Dict[str, SnapApp]] diff --git a/snapcraft/parts/lifecycle.py b/snapcraft/parts/lifecycle.py index 55166ad9ea..18694de309 100644 --- a/snapcraft/parts/lifecycle.py +++ b/snapcraft/parts/lifecycle.py @@ -20,7 +20,7 @@ import subprocess from dataclasses import dataclass from pathlib import Path -from typing import TYPE_CHECKING, Any, Dict, List, Optional, cast +from typing import TYPE_CHECKING, Any, Dict, List, Optional from craft_cli import EmitterMode, emit from craft_parts import infos @@ -302,7 +302,7 @@ def _run_in_provider( with provider.launched_environment( project_name=project.name, project_path=Path().absolute(), - base=cast(str, project.base), + base=project.get_effective_base(), ) as instance: try: with emit.pause(): diff --git a/snapcraft/parts/yaml_utils.py b/snapcraft/parts/yaml_utils.py index 87228128eb..ebc8dde67f 100644 --- a/snapcraft/parts/yaml_utils.py +++ b/snapcraft/parts/yaml_utils.py @@ -21,7 +21,7 @@ import yaml import yaml.error -from snapcraft import errors +from snapcraft import errors, utils def _check_duplicate_keys(node): @@ -78,14 +78,23 @@ def load(filestream: TextIO) -> Dict[str, Any]: :raises LegacyFallback: if the project's base is not core22. """ try: - # TODO: support for build-base. - if yaml.safe_load(filestream)["base"] != "core22": + data = yaml.safe_load(filestream) + build_base = utils.get_effective_base( + base=data.get("base"), + build_base=data.get("build_base"), + project_type=data.get("type"), + name=data.get("name"), + ) + + if build_base is None: + raise errors.LegacyFallback("no base defined") + if build_base != "core22": raise errors.LegacyFallback("base is not core22") - except KeyError as key_error: - raise errors.LegacyFallback("no base defined") from key_error except yaml.error.YAMLError as err: raise errors.SnapcraftError(f"snapcraft.yaml parsing error: {err!s}") from err + filestream.seek(0) + try: return yaml.load(filestream, Loader=_SafeLoader) except yaml.error.YAMLError as err: diff --git a/snapcraft/projects.py b/snapcraft/projects.py index 1cd8e1102e..5f9570fb45 100644 --- a/snapcraft/projects.py +++ b/snapcraft/projects.py @@ -23,7 +23,7 @@ from craft_grammar.models import GrammarSingleEntryDictList, GrammarStr, GrammarStrList from pydantic import conlist, constr -from snapcraft import repo +from snapcraft import repo, utils from snapcraft.errors import ProjectValidationError from snapcraft.parts import validation as parts_validation @@ -299,7 +299,8 @@ def _validate_adoptable_fields(cls, values): @classmethod def _validate_mandatory_base(cls, values): snap_type = values.get("type") - if ("base" in values) ^ (snap_type not in ["base", "kernel", "snapd"]): + base = values.get("base") + if (base is not None) ^ (snap_type not in ["base", "kernel", "snapd"]): raise ValueError( "Snap base must be declared when type is not base, kernel or snapd" ) @@ -426,6 +427,21 @@ def get_content_snaps(self) -> Optional[List[str]]: return content_snaps if content_snaps else None + def get_effective_base(self) -> str: + """Return the base to use to create the snap.""" + base = utils.get_effective_base( + base=self.base, + build_base=self.build_base, + project_type=self.type, + name=self.name, + ) + + # will not happen after schema validation + if base is None: + raise RuntimeError("cannot determine build base") + + return base + class _GrammarAwareModel(pydantic.BaseModel): class Config: diff --git a/snapcraft/utils.py b/snapcraft/utils.py index 9896a1e9eb..a7d59f779c 100644 --- a/snapcraft/utils.py +++ b/snapcraft/utils.py @@ -148,6 +148,25 @@ def get_managed_environment_snap_channel() -> Optional[str]: return os.getenv("SNAPCRAFT_INSTALL_SNAP_CHANNEL") +def get_effective_base( + *, + base: Optional[str], + build_base: Optional[str], + project_type: Optional[str], + name: Optional[str], +) -> Optional[str]: + """Return the base to use to create the snap. + + Returns build-base if set, but if not, name is returned if the + snap is of type base. For all other snaps, the base is returned + as the build-base. + """ + if build_base is not None: + return build_base + + return name if project_type == "base" else base + + def confirm_with_user(prompt_text, default=False) -> bool: """Query user for yes/no answer. diff --git a/tests/unit/parts/test_lifecycle.py b/tests/unit/parts/test_lifecycle.py index f34089e421..eca7c4e724 100644 --- a/tests/unit/parts/test_lifecycle.py +++ b/tests/unit/parts/test_lifecycle.py @@ -727,16 +727,23 @@ def test_extract_parse_info(): assert yaml_data == {"name": "foo", "parts": {"p1": {"plugin": "nil"}, "p2": {}}} assert parse_info == {"p1": "foo/metadata.xml"} + def test_get_snap_project_no_base(snapcraft_yaml, new_dir): - project = Project.unmarshal(snapcraft_yaml(base=None)) + with pytest.raises(errors.ProjectValidationError) as raised: + Project.unmarshal(snapcraft_yaml(base=None)) + + assert str(raised.value) == ( + "Bad snapcraft.yaml content:\n" + "- Snap base must be declared when type is not base, kernel or snapd" + ) - assert parts_lifecycle._get_extra_build_snaps(project) is None def test_get_snap_project_with_base(snapcraft_yaml): project = Project.unmarshal(snapcraft_yaml(base="core22")) assert parts_lifecycle._get_extra_build_snaps(project) == ["core22"] + def test_get_snap_project_with_content_plugs(snapcraft_yaml, new_dir): yaml_data = { "name": "mytest", @@ -746,11 +753,7 @@ def test_get_snap_project_with_content_plugs(snapcraft_yaml, new_dir): "description": "This is just some test data.", "grade": "stable", "confinement": "strict", - "parts": { - "part1": { - "plugin": "nil" - } - }, + "parts": {"part1": {"plugin": "nil"}}, "plugs": { "test-plug-1": { "content": "content-interface", @@ -769,7 +772,8 @@ def test_get_snap_project_with_content_plugs(snapcraft_yaml, new_dir): project = Project(**yaml_data) - assert ( - parts_lifecycle._get_extra_build_snaps(project) - == ["test-snap-1", "test-snap-2", "core22"] - ) + assert parts_lifecycle._get_extra_build_snaps(project) == [ + "test-snap-1", + "test-snap-2", + "core22", + ] diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py index a9de26a085..9ae6c6a4c9 100644 --- a/tests/unit/test_utils.py +++ b/tests/unit/test_utils.py @@ -89,6 +89,25 @@ def test_strtobool_value_error(value: str): ##################### +@pytest.mark.parametrize( + "base,build_base,project_type,name,expected_base", + [ + (None, "build_base", "base", "name", "build_base"), + ("base", "build_base", "base", "name", "build_base"), + (None, None, "base", "name", "name"), + ("base", None, "base", "name", "name"), + (None, None, "other", "name", None), + ("base", "build_base", "other", "name", "build_base"), + ("base", None, "other", "name", "base"), + ], +) +def test_get_effective_base(base, build_base, project_type, name, expected_base): + result = utils.get_effective_base( + base=base, build_base=build_base, project_type=project_type, name=name + ) + assert result == expected_base + + def test_get_os_platform_linux(tmp_path, mocker): """Utilize an /etc/os-release file to determine platform.""" # explicitly add commented and empty lines, for parser robustness From e7b64bdc531288e441536f61e6eb731c5f4310e0 Mon Sep 17 00:00:00 2001 From: Claudio Matsuoka Date: Fri, 29 Apr 2022 17:07:40 -0300 Subject: [PATCH 137/167] requirements: update dependencies Signed-off-by: Claudio Matsuoka --- requirements-devel.txt | 12 ++++++------ requirements.txt | 6 +++--- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/requirements-devel.txt b/requirements-devel.txt index 1d051f0d05..8f12b65338 100644 --- a/requirements-devel.txt +++ b/requirements-devel.txt @@ -6,12 +6,12 @@ certifi==2021.10.8 cffi==1.15.0 chardet==4.0.0 charset-normalizer==2.0.12 -click==8.1.2 +click==8.1.3 codespell==2.1.0 coverage==6.3.2 craft-cli==0.5.0 craft-grammar==1.1.1 -craft-parts==1.5.1 +craft-parts==1.6.0 craft-providers==1.2.0 craft-store==2.1.1 cryptography==3.4 @@ -39,7 +39,7 @@ lazy-object-proxy==1.7.1 lxml==4.8.0 macaroonbakery==1.3.1 mccabe==0.6.1 -mypy==0.942 +mypy==0.950 mypy-extensions==0.4.3 oauthlib==3.2.0 overrides==6.1.0 @@ -99,11 +99,11 @@ tinydb==4.7.0 toml==0.10.2 tomli==2.0.1 translationstring==1.4 -types-Deprecated==1.2.6 +types-Deprecated==1.2.7 types-PyYAML==6.0.7 -types-requests==2.27.20 +types-requests==2.27.24 types-setuptools==57.4.14 -types-tabulate==0.8.7 +types-tabulate==0.8.8 types-urllib3==1.26.13 typing-utils==0.1.0 typing_extensions==4.2.0 diff --git a/requirements.txt b/requirements.txt index 4967ea5204..85f9cd0580 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,10 +4,10 @@ certifi==2021.10.8 cffi==1.15.0 chardet==4.0.0 charset-normalizer==2.0.12 -click==8.1.2 +click==8.1.3 craft-cli==0.5.0 craft-grammar==1.1.1 -craft-parts==1.5.1 +craft-parts==1.6.0 craft-providers==1.2.0 craft-store==2.1.1 cryptography==3.4 @@ -58,7 +58,7 @@ six==1.16.0 tabulate==0.8.9 tinydb==4.7.0 toml==0.10.2 -types-Deprecated==1.2.6 +types-Deprecated==1.2.7 typing-utils==0.1.0 typing_extensions==4.2.0 urllib3==1.26.9 From 92385f6c7c0a97aa0b3882b5ef7a77f90b8c3dce Mon Sep 17 00:00:00 2001 From: Sergio Schvezov Date: Mon, 10 Jan 2022 09:09:13 -0300 Subject: [PATCH 138/167] Merge pull request #3610 from snapcore/bugfix/pull-no-stage-core20 lifecycle: do not stage deps for pull on core20 (CRAFT-725) --- .../internal/lifecycle/_runner.py | 16 +++- .../unit/lifecycle/test_order_core20.py | 81 +++++++++++++++++++ 2 files changed, 95 insertions(+), 2 deletions(-) create mode 100644 tests/legacy/unit/lifecycle/test_order_core20.py diff --git a/snapcraft_legacy/internal/lifecycle/_runner.py b/snapcraft_legacy/internal/lifecycle/_runner.py index 8643a101bf..f7196d6d5d 100644 --- a/snapcraft_legacy/internal/lifecycle/_runner.py +++ b/snapcraft_legacy/internal/lifecycle/_runner.py @@ -290,13 +290,20 @@ def _run_prime(self, part): def _reprime(self, part, hint=""): self._rerun_step(step=steps.PRIME, part=part, progress="Re-priming", hint=hint) - def _prepare_step(self, *, step: steps.Step, part: pluginhandler.PluginHandler): - common.reset_env() + def _handle_part_dependencies( + self, *, step: steps.Step, part: pluginhandler.PluginHandler + ) -> None: + # core20 uses Plugins V2 which does not require staging parts for pull + # like V1 Plugins do. + if self.project._get_build_base() == "core20" and step == steps.PULL: + return + all_dependencies = self.parts_config.get_dependencies(part.name) # Filter dependencies down to only those that need to run the # prerequisite step prerequisite_step = steps.get_dependency_prerequisite_step(step) + dependencies = { p for p in all_dependencies @@ -314,6 +321,11 @@ def _prepare_step(self, *, step: steps.Step, part: pluginhandler.PluginHandler): ) self.run(prerequisite_step, dependency_names) + def _prepare_step(self, *, step: steps.Step, part: pluginhandler.PluginHandler): + common.reset_env() + + self._handle_part_dependencies(step=step, part=part) + # Run the preparation function for this step (if implemented) preparation_function = getattr(part, "prepare_{}".format(step.name), None) if preparation_function: diff --git a/tests/legacy/unit/lifecycle/test_order_core20.py b/tests/legacy/unit/lifecycle/test_order_core20.py new file mode 100644 index 0000000000..7877086086 --- /dev/null +++ b/tests/legacy/unit/lifecycle/test_order_core20.py @@ -0,0 +1,81 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright (C) 2022 Canonical Ltd +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from unittest.mock import call, patch + +import pytest + +from snapcraft_legacy.internal import steps +from snapcraft_legacy.internal.lifecycle._runner import _Executor as Executor +from snapcraft_legacy.internal.meta.snap import Snap +from snapcraft_legacy.project import Project + + +class FakePart: + def __init__(self, name: str = "part1") -> None: + self.name = name + + def should_step_run(self, prerequisite_step): + return True + + +@pytest.fixture +def project_config(): + class Parts: + def get_dependencies(self, part_name: str): + return [FakePart("dep")] + + class Config: + def __init__(self): + self.project = Project() + self.project._snap_meta = Snap( + name="project-name", base="core20", version="1.0", confinement="strict" + ) + self.parts = Parts() + + return Config() + + +@pytest.fixture +def mock_executor_run(): + patcher = patch.object(Executor, "run") + yield patcher.start() + patcher.stop() + + +def test_pull(project_config, mock_executor_run): + executor = Executor(project_config) + + executor._handle_part_dependencies(step=steps.PULL, part=FakePart()) + + assert mock_executor_run.mock_calls == [] + + +@pytest.mark.parametrize("step", [steps.BUILD, steps.STAGE]) +def test_build_stage(step, project_config, mock_executor_run): + executor = Executor(project_config) + + executor._handle_part_dependencies(step=step, part=FakePart()) + + assert mock_executor_run.mock_calls == [call(steps.STAGE, {"dep"})] + + +def test_prime(project_config, mock_executor_run): + executor = Executor(project_config) + + executor._handle_part_dependencies(step=steps.PRIME, part=FakePart()) + + assert mock_executor_run.mock_calls == [call(steps.PRIME, {"dep"})] From 4c966f32a412c185a7b2589e2e11825cab61a314 Mon Sep 17 00:00:00 2001 From: Sergio Schvezov Date: Mon, 10 Jan 2022 19:59:05 -0300 Subject: [PATCH 139/167] Merge pull request #3598 from ppd/kde-neon-not-experimental extensions/kde-neon: declare non-experimental for core20 --- .../internal/project_loader/_extensions/kde_neon.py | 9 ++------- .../unit/project_loader/extensions/test_kde_neon.py | 4 ++-- tests/spread/extensions/kde-neon/task.yaml | 8 +------- 3 files changed, 5 insertions(+), 16 deletions(-) diff --git a/snapcraft_legacy/internal/project_loader/_extensions/kde_neon.py b/snapcraft_legacy/internal/project_loader/_extensions/kde_neon.py index 1851e7ed86..a25f89493f 100644 --- a/snapcraft_legacy/internal/project_loader/_extensions/kde_neon.py +++ b/snapcraft_legacy/internal/project_loader/_extensions/kde_neon.py @@ -17,7 +17,7 @@ # Import types and tell flake8 to ignore the "unused" List. from collections import namedtuple -from typing import Any, Dict, Optional, Tuple +from typing import Any, Dict, Tuple from ._extension import Extension @@ -35,7 +35,7 @@ cmake_args="-DCMAKE_FIND_ROOT_PATH=/snap/kde-frameworks-5-qt-5-15-3-core20-sdk/current", content="kde-frameworks-5-qt-5-15-3-core20-all", provider="kde-frameworks-5-qt-5-15-3-core20", - build_snaps=["kde-frameworks-5-qt-5-15-3-core20-sdk/latest/candidate"], + build_snaps=["kde-frameworks-5-qt-5-15-3-core20-sdk/latest/stable"], ), ) @@ -64,11 +64,6 @@ class ExtensionImpl(Extension): - x11 (https://snapcraft.io/docs/x11-interface) """ - @staticmethod - def is_experimental(base: Optional[str]) -> bool: - # TODO: remove experimental once sdk is on stable - return base == "core20" - @staticmethod def get_supported_bases() -> Tuple[str, ...]: return ("core18", "core20") diff --git a/tests/legacy/unit/project_loader/extensions/test_kde_neon.py b/tests/legacy/unit/project_loader/extensions/test_kde_neon.py index c150875d7e..3fa0bbed62 100644 --- a/tests/legacy/unit/project_loader/extensions/test_kde_neon.py +++ b/tests/legacy/unit/project_loader/extensions/test_kde_neon.py @@ -116,7 +116,7 @@ def test_extension_core20(): assert kde_neon_extension.parts == { "kde-neon-extension": { "build-packages": ["g++"], - "build-snaps": ["kde-frameworks-5-qt-5-15-3-core20-sdk/latest/candidate"], + "build-snaps": ["kde-frameworks-5-qt-5-15-3-core20-sdk/latest/stable"], "make-parameters": ["PLATFORM_PLUG=kde-frameworks-5-qt-5-15-3-core20"], "plugin": "make", "source": "$SNAPCRAFT_EXTENSIONS_DIR/desktop", @@ -137,7 +137,7 @@ def test_experimental_core20(): kde_neon_extension = ExtensionImpl( extension_name="kde-neon", yaml_data=dict(base="core20") ) - assert kde_neon_extension.is_experimental(base="core20") is True + assert kde_neon_extension.is_experimental(base="core20") is False def test_experimental_core18(): diff --git a/tests/spread/extensions/kde-neon/task.yaml b/tests/spread/extensions/kde-neon/task.yaml index 3142729009..bf61ae3abd 100644 --- a/tests/spread/extensions/kde-neon/task.yaml +++ b/tests/spread/extensions/kde-neon/task.yaml @@ -35,13 +35,7 @@ restore: | execute: | cd "$SNAP_DIR" - - if [[ "$SPREAD_SYSTEM" =~ ubuntu-20.04 ]]; then - output="$(snapcraft --enable-experimental-extensions)" - else - output="$(snapcraft)" - fi - + output="$(snapcraft)" snap install neon-hello_*.snap --dangerous [ "$(neon-hello)" = "hello world" ] From 44d1af17212f92cae7127fea6b76d508d18c639b Mon Sep 17 00:00:00 2001 From: Sergio Schvezov Date: Tue, 11 Jan 2022 10:03:52 -0300 Subject: [PATCH 140/167] Merge pull request #3611 from kenvandine/libdrm_layout extensions: add layout for libdrm --- .../internal/project_loader/_extensions/_flutter_meta.py | 3 ++- .../internal/project_loader/_extensions/gnome_3_28.py | 1 + .../internal/project_loader/_extensions/gnome_3_34.py | 1 + .../internal/project_loader/_extensions/gnome_3_38.py | 1 + tests/legacy/unit/project_loader/extensions/test_flutter.py | 3 ++- tests/legacy/unit/project_loader/extensions/test_gnome_3_28.py | 3 +++ tests/legacy/unit/project_loader/extensions/test_gnome_3_34.py | 3 +++ tests/legacy/unit/project_loader/extensions/test_gnome_3_38.py | 3 +++ 8 files changed, 16 insertions(+), 2 deletions(-) diff --git a/snapcraft_legacy/internal/project_loader/_extensions/_flutter_meta.py b/snapcraft_legacy/internal/project_loader/_extensions/_flutter_meta.py index 51b5bb2a10..5ba3efef9a 100644 --- a/snapcraft_legacy/internal/project_loader/_extensions/_flutter_meta.py +++ b/snapcraft_legacy/internal/project_loader/_extensions/_flutter_meta.py @@ -139,7 +139,8 @@ def __init__(self, *, extension_name: str, yaml_data: Dict[str, Any]) -> None: "layout": { "/usr/share/xml/iso-codes": { "bind": "$SNAP/gnome-platform/usr/share/xml/iso-codes" - } + }, + "/usr/share/libdrm": {"bind": "$SNAP/gnome-platform/usr/share/libdrm"}, }, } diff --git a/snapcraft_legacy/internal/project_loader/_extensions/gnome_3_28.py b/snapcraft_legacy/internal/project_loader/_extensions/gnome_3_28.py index d19c9f7563..fcd71535a6 100644 --- a/snapcraft_legacy/internal/project_loader/_extensions/gnome_3_28.py +++ b/snapcraft_legacy/internal/project_loader/_extensions/gnome_3_28.py @@ -102,6 +102,7 @@ def __init__(self, *, extension_name: str, yaml_data: Dict[str, Any]) -> None: "/usr/share/xml/iso-codes": { "bind": "$SNAP/gnome-platform/usr/share/xml/iso-codes" }, + "/usr/share/libdrm": {"bind": "$SNAP/gnome-platform/usr/share/libdrm"}, }, } diff --git a/snapcraft_legacy/internal/project_loader/_extensions/gnome_3_34.py b/snapcraft_legacy/internal/project_loader/_extensions/gnome_3_34.py index e1155942e0..48d866d589 100644 --- a/snapcraft_legacy/internal/project_loader/_extensions/gnome_3_34.py +++ b/snapcraft_legacy/internal/project_loader/_extensions/gnome_3_34.py @@ -105,6 +105,7 @@ def __init__(self, *, extension_name: str, yaml_data: Dict[str, Any]) -> None: "/usr/share/xml/iso-codes": { "bind": "$SNAP/gnome-platform/usr/share/xml/iso-codes" }, + "/usr/share/libdrm": {"bind": "$SNAP/gnome-platform/usr/share/libdrm"}, }, } diff --git a/snapcraft_legacy/internal/project_loader/_extensions/gnome_3_38.py b/snapcraft_legacy/internal/project_loader/_extensions/gnome_3_38.py index cbb87d1d4d..beac6b3e60 100644 --- a/snapcraft_legacy/internal/project_loader/_extensions/gnome_3_38.py +++ b/snapcraft_legacy/internal/project_loader/_extensions/gnome_3_38.py @@ -105,6 +105,7 @@ def __init__(self, *, extension_name: str, yaml_data: Dict[str, Any]) -> None: "/usr/share/xml/iso-codes": { "bind": "$SNAP/gnome-platform/usr/share/xml/iso-codes" }, + "/usr/share/libdrm": {"bind": "$SNAP/gnome-platform/usr/share/libdrm"}, }, } diff --git a/tests/legacy/unit/project_loader/extensions/test_flutter.py b/tests/legacy/unit/project_loader/extensions/test_flutter.py index 07998fbae2..bbb7b7ecb1 100644 --- a/tests/legacy/unit/project_loader/extensions/test_flutter.py +++ b/tests/legacy/unit/project_loader/extensions/test_flutter.py @@ -64,7 +64,8 @@ def test_extension(extension_class): "layout": { "/usr/share/xml/iso-codes": { "bind": "$SNAP/gnome-platform/usr/share/xml/iso-codes" - } + }, + "/usr/share/libdrm": {"bind": "$SNAP/gnome-platform/usr/share/libdrm"}, }, } diff --git a/tests/legacy/unit/project_loader/extensions/test_gnome_3_28.py b/tests/legacy/unit/project_loader/extensions/test_gnome_3_28.py index ef1430f676..b54f5aa2d2 100644 --- a/tests/legacy/unit/project_loader/extensions/test_gnome_3_28.py +++ b/tests/legacy/unit/project_loader/extensions/test_gnome_3_28.py @@ -74,6 +74,9 @@ def test_extension(self): "/usr/share/xml/iso-codes": { "bind": "$SNAP/gnome-platform/usr/share/xml/iso-codes" }, + "/usr/share/libdrm": { + "bind": "$SNAP/gnome-platform/usr/share/libdrm" + }, }, } ), diff --git a/tests/legacy/unit/project_loader/extensions/test_gnome_3_34.py b/tests/legacy/unit/project_loader/extensions/test_gnome_3_34.py index 03f89ad9de..b031f6393b 100644 --- a/tests/legacy/unit/project_loader/extensions/test_gnome_3_34.py +++ b/tests/legacy/unit/project_loader/extensions/test_gnome_3_34.py @@ -77,6 +77,9 @@ def test_extension(self): "/usr/share/xml/iso-codes": { "bind": "$SNAP/gnome-platform/usr/share/xml/iso-codes" }, + "/usr/share/libdrm": { + "bind": "$SNAP/gnome-platform/usr/share/libdrm" + }, }, } ), diff --git a/tests/legacy/unit/project_loader/extensions/test_gnome_3_38.py b/tests/legacy/unit/project_loader/extensions/test_gnome_3_38.py index d2a4b9a18e..953ba897e3 100644 --- a/tests/legacy/unit/project_loader/extensions/test_gnome_3_38.py +++ b/tests/legacy/unit/project_loader/extensions/test_gnome_3_38.py @@ -77,6 +77,9 @@ def test_extension(self): "/usr/share/xml/iso-codes": { "bind": "$SNAP/gnome-platform/usr/share/xml/iso-codes" }, + "/usr/share/libdrm": { + "bind": "$SNAP/gnome-platform/usr/share/libdrm" + }, }, } ), From cfb8ec3f58e2937fd45530ac5eb44c7415ca1301 Mon Sep 17 00:00:00 2001 From: Sergio Schvezov Date: Wed, 12 Jan 2022 12:38:41 -0300 Subject: [PATCH 141/167] Merge pull request #3620 from sergiusens/revert Revert "lifecycle: do not stage deps for pull on core20" --- .../internal/lifecycle/_runner.py | 16 +--- .../unit/lifecycle/test_order_core20.py | 81 ------------------- 2 files changed, 2 insertions(+), 95 deletions(-) delete mode 100644 tests/legacy/unit/lifecycle/test_order_core20.py diff --git a/snapcraft_legacy/internal/lifecycle/_runner.py b/snapcraft_legacy/internal/lifecycle/_runner.py index f7196d6d5d..8643a101bf 100644 --- a/snapcraft_legacy/internal/lifecycle/_runner.py +++ b/snapcraft_legacy/internal/lifecycle/_runner.py @@ -290,20 +290,13 @@ def _run_prime(self, part): def _reprime(self, part, hint=""): self._rerun_step(step=steps.PRIME, part=part, progress="Re-priming", hint=hint) - def _handle_part_dependencies( - self, *, step: steps.Step, part: pluginhandler.PluginHandler - ) -> None: - # core20 uses Plugins V2 which does not require staging parts for pull - # like V1 Plugins do. - if self.project._get_build_base() == "core20" and step == steps.PULL: - return - + def _prepare_step(self, *, step: steps.Step, part: pluginhandler.PluginHandler): + common.reset_env() all_dependencies = self.parts_config.get_dependencies(part.name) # Filter dependencies down to only those that need to run the # prerequisite step prerequisite_step = steps.get_dependency_prerequisite_step(step) - dependencies = { p for p in all_dependencies @@ -321,11 +314,6 @@ def _handle_part_dependencies( ) self.run(prerequisite_step, dependency_names) - def _prepare_step(self, *, step: steps.Step, part: pluginhandler.PluginHandler): - common.reset_env() - - self._handle_part_dependencies(step=step, part=part) - # Run the preparation function for this step (if implemented) preparation_function = getattr(part, "prepare_{}".format(step.name), None) if preparation_function: diff --git a/tests/legacy/unit/lifecycle/test_order_core20.py b/tests/legacy/unit/lifecycle/test_order_core20.py deleted file mode 100644 index 7877086086..0000000000 --- a/tests/legacy/unit/lifecycle/test_order_core20.py +++ /dev/null @@ -1,81 +0,0 @@ -# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- -# -# Copyright (C) 2022 Canonical Ltd -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License version 3 as -# published by the Free Software Foundation. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -from unittest.mock import call, patch - -import pytest - -from snapcraft_legacy.internal import steps -from snapcraft_legacy.internal.lifecycle._runner import _Executor as Executor -from snapcraft_legacy.internal.meta.snap import Snap -from snapcraft_legacy.project import Project - - -class FakePart: - def __init__(self, name: str = "part1") -> None: - self.name = name - - def should_step_run(self, prerequisite_step): - return True - - -@pytest.fixture -def project_config(): - class Parts: - def get_dependencies(self, part_name: str): - return [FakePart("dep")] - - class Config: - def __init__(self): - self.project = Project() - self.project._snap_meta = Snap( - name="project-name", base="core20", version="1.0", confinement="strict" - ) - self.parts = Parts() - - return Config() - - -@pytest.fixture -def mock_executor_run(): - patcher = patch.object(Executor, "run") - yield patcher.start() - patcher.stop() - - -def test_pull(project_config, mock_executor_run): - executor = Executor(project_config) - - executor._handle_part_dependencies(step=steps.PULL, part=FakePart()) - - assert mock_executor_run.mock_calls == [] - - -@pytest.mark.parametrize("step", [steps.BUILD, steps.STAGE]) -def test_build_stage(step, project_config, mock_executor_run): - executor = Executor(project_config) - - executor._handle_part_dependencies(step=step, part=FakePart()) - - assert mock_executor_run.mock_calls == [call(steps.STAGE, {"dep"})] - - -def test_prime(project_config, mock_executor_run): - executor = Executor(project_config) - - executor._handle_part_dependencies(step=steps.PRIME, part=FakePart()) - - assert mock_executor_run.mock_calls == [call(steps.PRIME, {"dep"})] From cef07f41b4fdd9562f7feec729bf124c73840af4 Mon Sep 17 00:00:00 2001 From: Sergio Schvezov Date: Thu, 13 Jan 2022 12:19:51 -0300 Subject: [PATCH 142/167] Merge pull request #3622 from snapcore/core20-pull-build-opt-in lifecycle: core22 lifecycle conditional on build-attributes entry (CRAFT-725) --- schema/snapcraft.json | 1 + .../internal/lifecycle/_runner.py | 27 +++++- .../pluginhandler/_build_attributes.py | 3 + .../unit/lifecycle/test_order_core20.py | 97 +++++++++++++++++++ .../core22-step-dependencies/snapcraft.yaml | 20 ++++ .../core22-step-dependencies/task.yaml | 15 +++ 6 files changed, 161 insertions(+), 2 deletions(-) create mode 100644 tests/legacy/unit/lifecycle/test_order_core20.py create mode 100644 tests/spread/general/core22-step-dependencies/snapcraft.yaml create mode 100644 tests/spread/general/core22-step-dependencies/task.yaml diff --git a/schema/snapcraft.json b/schema/snapcraft.json index 501f06cb55..0886116031 100644 --- a/schema/snapcraft.json +++ b/schema/snapcraft.json @@ -1063,6 +1063,7 @@ "items": { "type": "string", "enum": [ + "core22-step-dependencies", "enable-patchelf", "no-patchelf", "no-install", diff --git a/snapcraft_legacy/internal/lifecycle/_runner.py b/snapcraft_legacy/internal/lifecycle/_runner.py index 8643a101bf..f7ac3f7de5 100644 --- a/snapcraft_legacy/internal/lifecycle/_runner.py +++ b/snapcraft_legacy/internal/lifecycle/_runner.py @@ -290,13 +290,31 @@ def _run_prime(self, part): def _reprime(self, part, hint=""): self._rerun_step(step=steps.PRIME, part=part, progress="Re-priming", hint=hint) - def _prepare_step(self, *, step: steps.Step, part: pluginhandler.PluginHandler): - common.reset_env() + def _handle_part_dependencies( + self, *, step: steps.Step, part: pluginhandler.PluginHandler + ) -> None: + # core20 uses Plugins V2 which does not require staging parts for pull + # like V1 Plugins do. + if ( + part._build_attributes.core22_step_dependencies() + and self.project._get_build_base() == "core20" + and step == steps.PULL + ): + return + elif ( + part._build_attributes.core22_step_dependencies() + and self.project._get_build_base() != "core20" + ): + logger.warning( + f"Ignoring core22 lifecycle request for {part.name!r} as it is only supported for core20." + ) + all_dependencies = self.parts_config.get_dependencies(part.name) # Filter dependencies down to only those that need to run the # prerequisite step prerequisite_step = steps.get_dependency_prerequisite_step(step) + dependencies = { p for p in all_dependencies @@ -314,6 +332,11 @@ def _prepare_step(self, *, step: steps.Step, part: pluginhandler.PluginHandler): ) self.run(prerequisite_step, dependency_names) + def _prepare_step(self, *, step: steps.Step, part: pluginhandler.PluginHandler): + common.reset_env() + + self._handle_part_dependencies(step=step, part=part) + # Run the preparation function for this step (if implemented) preparation_function = getattr(part, "prepare_{}".format(step.name), None) if preparation_function: diff --git a/snapcraft_legacy/internal/pluginhandler/_build_attributes.py b/snapcraft_legacy/internal/pluginhandler/_build_attributes.py index d365bbdc0d..05aecab0df 100644 --- a/snapcraft_legacy/internal/pluginhandler/_build_attributes.py +++ b/snapcraft_legacy/internal/pluginhandler/_build_attributes.py @@ -19,6 +19,9 @@ class BuildAttributes: def __init__(self, build_attributes): self._attributes = build_attributes + def core22_step_dependencies(self): + return "core22-step-dependencies" in self._attributes + def enable_patchelf(self): return "enable-patchelf" in self._attributes diff --git a/tests/legacy/unit/lifecycle/test_order_core20.py b/tests/legacy/unit/lifecycle/test_order_core20.py new file mode 100644 index 0000000000..dcda9ee3a2 --- /dev/null +++ b/tests/legacy/unit/lifecycle/test_order_core20.py @@ -0,0 +1,97 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright (C) 2022 Canonical Ltd +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from typing import Sequence +from unittest.mock import call, patch + +import pytest + +from snapcraft_legacy.internal import steps +from snapcraft_legacy.internal.lifecycle._runner import _Executor as Executor +from snapcraft_legacy.internal.meta.snap import Snap +from snapcraft_legacy.internal.pluginhandler._build_attributes import BuildAttributes +from snapcraft_legacy.project import Project + + +class FakePart: + def __init__(self, name: str = "part1", build_attributes: Sequence = None) -> None: + self.name = name + + if build_attributes is None: + build_attributes = ["core22-step-dependencies"] + self._build_attributes = BuildAttributes(build_attributes) + + def should_step_run(self, prerequisite_step): + return True + + +@pytest.fixture +def project_config(): + class Parts: + def get_dependencies(self, part_name: str): + return [FakePart("dep")] + + class Config: + def __init__(self): + self.project = Project() + self.project._snap_meta = Snap( + name="project-name", base="core20", version="1.0", confinement="strict" + ) + self.parts = Parts() + + return Config() + + +@pytest.fixture +def mock_executor_run(): + patcher = patch.object(Executor, "run") + yield patcher.start() + patcher.stop() + + +def test_pull(project_config, mock_executor_run): + executor = Executor(project_config) + + executor._handle_part_dependencies(step=steps.PULL, part=FakePart()) + + assert mock_executor_run.mock_calls == [] + + +def test_pull_no_build_attribute(project_config, mock_executor_run): + executor = Executor(project_config) + + executor._handle_part_dependencies( + step=steps.PULL, part=FakePart(name="part1", build_attributes=[]) + ) + + assert mock_executor_run.mock_calls == [call(steps.STAGE, {"dep"})] + + +@pytest.mark.parametrize("step", [steps.BUILD, steps.STAGE]) +def test_build_stage(step, project_config, mock_executor_run): + executor = Executor(project_config) + + executor._handle_part_dependencies(step=step, part=FakePart()) + + assert mock_executor_run.mock_calls == [call(steps.STAGE, {"dep"})] + + +def test_prime(project_config, mock_executor_run): + executor = Executor(project_config) + + executor._handle_part_dependencies(step=steps.PRIME, part=FakePart()) + + assert mock_executor_run.mock_calls == [call(steps.PRIME, {"dep"})] diff --git a/tests/spread/general/core22-step-dependencies/snapcraft.yaml b/tests/spread/general/core22-step-dependencies/snapcraft.yaml new file mode 100644 index 0000000000..06c7b3d1d9 --- /dev/null +++ b/tests/spread/general/core22-step-dependencies/snapcraft.yaml @@ -0,0 +1,20 @@ +name: core22-step-dependencies +version: "1.0" +summary: Quick snap to ensure only pull happens when deps are used +description: | + Only pull should happen when using after and the core22-step-depdendencies + build-attributes entry on core20. + +base: core20 +grade: devel +confinement: strict + +parts: + part1: + plugin: nil + override-build: | + touch $SNAPCRAFT_PART_INSTALL/part1 + part2: + build-attributes: [core22-step-dependencies] + plugin: nil + after: [part1] diff --git a/tests/spread/general/core22-step-dependencies/task.yaml b/tests/spread/general/core22-step-dependencies/task.yaml new file mode 100644 index 0000000000..5844be0b98 --- /dev/null +++ b/tests/spread/general/core22-step-dependencies/task.yaml @@ -0,0 +1,15 @@ +summary: Pull a snap that with the core22 step dependencies + +systems: [ubuntu-20*] + +restore: | + snapcraft clean + rm -f ./*.snap + +execute: | + snapcraft pull + + if [ -f stage/part1 ]; then + echo "FAIL: staged files from part1 found" + exit 1 + fi From 08dfda7e97625e5e6056d8948b72959a4b8ecb73 Mon Sep 17 00:00:00 2001 From: Sergio Schvezov Date: Mon, 17 Jan 2022 11:37:05 -0300 Subject: [PATCH 143/167] Merge pull request #3624 from om26er/fix-npm-root-install npm plugin: allow running as root --- snapcraft_legacy/plugins/v2/npm.py | 1 + tests/legacy/unit/plugins/v2/test_npm.py | 1 + 2 files changed, 2 insertions(+) diff --git a/snapcraft_legacy/plugins/v2/npm.py b/snapcraft_legacy/plugins/v2/npm.py index 51fb5ff220..825eb4e0e8 100644 --- a/snapcraft_legacy/plugins/v2/npm.py +++ b/snapcraft_legacy/plugins/v2/npm.py @@ -102,5 +102,6 @@ def get_build_environment(self) -> Dict[str, str]: def get_build_commands(self) -> List[str]: return [ self._get_node_command(), + "npm config set unsafe-perm true", 'npm install -g --prefix "${SNAPCRAFT_PART_INSTALL}" $(npm pack . | tail -1)', ] diff --git a/tests/legacy/unit/plugins/v2/test_npm.py b/tests/legacy/unit/plugins/v2/test_npm.py index ecada6cd45..9d1dae9bcd 100644 --- a/tests/legacy/unit/plugins/v2/test_npm.py +++ b/tests/legacy/unit/plugins/v2/test_npm.py @@ -96,6 +96,7 @@ class Options: fi """ ), + "npm config set unsafe-perm true", 'npm install -g --prefix "${SNAPCRAFT_PART_INSTALL}" $(npm pack . | tail -1)', ] ), From f5330843bfe83dd8cdf3ff58841ac2c6b394db8a Mon Sep 17 00:00:00 2001 From: Sergio Schvezov Date: Mon, 24 Jan 2022 08:54:55 -0300 Subject: [PATCH 144/167] Merge pull request #3625 from om26er/npm-no-preserve npm plugin: extract node archive without preserving ownership --- snapcraft_legacy/plugins/v2/npm.py | 2 +- tests/legacy/unit/plugins/v2/test_npm.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/snapcraft_legacy/plugins/v2/npm.py b/snapcraft_legacy/plugins/v2/npm.py index 825eb4e0e8..d6ce2d2ed3 100644 --- a/snapcraft_legacy/plugins/v2/npm.py +++ b/snapcraft_legacy/plugins/v2/npm.py @@ -91,7 +91,7 @@ def _get_node_command(self) -> str: return dedent( f"""\ if [ ! -f "${{SNAPCRAFT_PART_INSTALL}}/bin/node" ]; then - curl -s "{node_uri}" | tar xzf - -C "${{SNAPCRAFT_PART_INSTALL}}/" --strip-components=1 + curl -s "{node_uri}" | tar xzf - -C "${{SNAPCRAFT_PART_INSTALL}}/" --no-same-owner --strip-components=1 fi """ ) diff --git a/tests/legacy/unit/plugins/v2/test_npm.py b/tests/legacy/unit/plugins/v2/test_npm.py index 9d1dae9bcd..062c2d1b78 100644 --- a/tests/legacy/unit/plugins/v2/test_npm.py +++ b/tests/legacy/unit/plugins/v2/test_npm.py @@ -92,7 +92,7 @@ class Options: dedent( """\ if [ ! -f "${SNAPCRAFT_PART_INSTALL}/bin/node" ]; then - curl -s "https://nodejs.org/dist/v6.0.0/node-v6.0.0-linux-x64.tar.gz" | tar xzf - -C "${SNAPCRAFT_PART_INSTALL}/" --strip-components=1 + curl -s "https://nodejs.org/dist/v6.0.0/node-v6.0.0-linux-x64.tar.gz" | tar xzf - -C "${SNAPCRAFT_PART_INSTALL}/" --no-same-owner --strip-components=1 fi """ ), From 8e2cf1a2571ce38060f0dc8ae345a223a92c974f Mon Sep 17 00:00:00 2001 From: Sergio Schvezov Date: Mon, 7 Feb 2022 11:52:45 -0300 Subject: [PATCH 145/167] Merge pull request #3628 from diddledani/fix-autogensh-crash Autotools Plugin (v1): Fix fatal crash when running autogen.sh or bootstrap --- snapcraft_legacy/internal/common.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/snapcraft_legacy/internal/common.py b/snapcraft_legacy/internal/common.py index ce74c3e1ec..923d4b6ff5 100644 --- a/snapcraft_legacy/internal/common.py +++ b/snapcraft_legacy/internal/common.py @@ -99,7 +99,7 @@ def _run(cmd: List[str], runner: Callable, **kwargs): # Finally, execute desired command. lines.append("#############################") lines.append("# Execute command:") - cmd_string = " ".join([shlex.quote(c) for c in cmd]) + cmd_string = " ".join([shlex.quote(str(c)) for c in cmd]) lines.append(f"exec {cmd_string}") # Save script executed by snapcraft. From 03c62b467053a908385438dfda95987d9e890b15 Mon Sep 17 00:00:00 2001 From: Sergio Schvezov Date: Tue, 8 Feb 2022 14:10:48 -0300 Subject: [PATCH 146/167] Merge pull request #3634 from mr-cal/dependencies-library-resolution dependencies: missing library resolution (CRAFT-422) LP: #1934403 --- .../internal/pluginhandler/_dependencies.py | 5 +---- snapcraft_legacy/internal/repo/_deb.py | 19 +++++++++++++------ snapcraft_legacy/internal/repo/errors.py | 3 ++- .../pluginhandler/test_missing_dependency.py | 3 ++- tests/legacy/unit/repo/test_deb.py | 19 +++++++++++++++---- 5 files changed, 33 insertions(+), 16 deletions(-) diff --git a/snapcraft_legacy/internal/pluginhandler/_dependencies.py b/snapcraft_legacy/internal/pluginhandler/_dependencies.py index d333ed7ff9..aed4a28cf6 100644 --- a/snapcraft_legacy/internal/pluginhandler/_dependencies.py +++ b/snapcraft_legacy/internal/pluginhandler/_dependencies.py @@ -14,7 +14,6 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -import os from typing import Sequence, Set from snapcraft_legacy.internal import repo @@ -53,9 +52,7 @@ def __init__(self, elf_files: Sequence[str]) -> None: def _process(self, elf_files: Sequence[str]) -> None: for elf_file in elf_files: try: - stage_package = repo.Repo.get_package_for_file( - file_path=os.path.join(os.path.sep, elf_file) - ) + stage_package = repo.Repo.get_package_for_file(file_path=elf_file) self._stage_packages_dependencies.add(stage_package) except repo.errors.FileProviderNotFound: self._unhandled_dependencies.add(elf_file) diff --git a/snapcraft_legacy/internal/repo/_deb.py b/snapcraft_legacy/internal/repo/_deb.py index 4ad513627f..4e2e9c1241 100644 --- a/snapcraft_legacy/internal/repo/_deb.py +++ b/snapcraft_legacy/internal/repo/_deb.py @@ -208,11 +208,11 @@ @functools.lru_cache(maxsize=256) -def _run_dpkg_query_search(file_path: str) -> str: +def _run_dpkg_query_search(file_path: pathlib.Path) -> str: try: output = ( subprocess.check_output( - ["dpkg-query", "-S", os.path.join(os.path.sep, file_path)], + ["dpkg-query", "-S", file_path], stderr=subprocess.STDOUT, env=dict(LANG="C.UTF-8"), ) @@ -220,9 +220,7 @@ def _run_dpkg_query_search(file_path: str) -> str: .strip() ) except subprocess.CalledProcessError as call_error: - logger.debug( - "Error finding package for {}: {}".format(file_path, str(call_error)) - ) + logger.debug(f"Error finding package for {file_path}: {call_error}") raise errors.FileProviderNotFound(file_path=file_path) from call_error # Remove diversions @@ -289,7 +287,16 @@ def get_package_libraries(cls, package_name: str) -> Set[str]: @classmethod def get_package_for_file(cls, file_path: str) -> str: - return _run_dpkg_query_search(file_path) + try: + absolute_file_path = pathlib.Path(os.path.sep, file_path) + logger.debug(f"searching for {absolute_file_path}") + return _run_dpkg_query_search(absolute_file_path) + except errors.FileProviderNotFound: + # follow symlinks to custom library paths + # or to libraries moved by usrmerge + real_file_path = pathlib.Path(os.path.sep, file_path).resolve() + logger.debug(f"searching for {real_file_path}") + return _run_dpkg_query_search(real_file_path) @classmethod def get_packages_for_source_type(cls, source_type): diff --git a/snapcraft_legacy/internal/repo/errors.py b/snapcraft_legacy/internal/repo/errors.py index a9c0cda007..b06d3943ce 100644 --- a/snapcraft_legacy/internal/repo/errors.py +++ b/snapcraft_legacy/internal/repo/errors.py @@ -14,6 +14,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +from pathlib import Path from typing import List, Optional, Sequence from snapcraft_legacy import formatting_utils @@ -60,7 +61,7 @@ class FileProviderNotFound(RepoError): fmt = "{file_path} is not provided by any package." - def __init__(self, *, file_path: str) -> None: + def __init__(self, *, file_path: Path) -> None: super().__init__(file_path=file_path) diff --git a/tests/legacy/unit/pluginhandler/test_missing_dependency.py b/tests/legacy/unit/pluginhandler/test_missing_dependency.py index 72c3a16af3..adeadf18ca 100644 --- a/tests/legacy/unit/pluginhandler/test_missing_dependency.py +++ b/tests/legacy/unit/pluginhandler/test_missing_dependency.py @@ -14,6 +14,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +from pathlib import Path from unittest import mock import fixtures @@ -37,7 +38,7 @@ def fake_repo_query(*args, **kwargs): try: return packages[file_path] except KeyError: - raise repo.errors.FileProviderNotFound(file_path=file_path) + raise repo.errors.FileProviderNotFound(file_path=Path(file_path)) self.useFixture( fixtures.MockPatch( diff --git a/tests/legacy/unit/repo/test_deb.py b/tests/legacy/unit/repo/test_deb.py index c75aeaad00..cb64f6b0f3 100644 --- a/tests/legacy/unit/repo/test_deb.py +++ b/tests/legacy/unit/repo/test_deb.py @@ -506,18 +506,23 @@ def setUp(self): def fake_dpkg_query(*args, **kwargs): # dpkg-query -S file_path - if args[0][2] == "/bin/bash": + if args[0][2].as_posix() == "/bin/bash": return "bash: /bin/bash\n".encode() - elif args[0][2] == "/bin/sh": + elif args[0][2].as_posix() == "/bin/sh": return ( "diversion by dash from: /bin/sh\n" "diversion by dash to: /bin/sh.distrib\n" "dash: /bin/sh\n" ).encode() + elif "symlink" in args[0][2].as_posix(): + raise CalledProcessError( + 1, f"dpkg-query: no path found matching pattern {args[0][2]}", + ) + elif "target" in args[0][2].as_posix(): + return "coreutils: /usr/bin/dirname\n".encode() else: raise CalledProcessError( - 1, - "dpkg-query: no path found matching pattern {}".format(args[0][2]), + 1, f"dpkg-query: no path found matching pattern {args[0][2]}", ) self.useFixture( @@ -540,6 +545,12 @@ def test_get_package_for_file_not_found(self): "/bin/not-found", ) + def test_get_package_with_symlink(self): + symlink = Path.cwd() / "symlink" + target = Path.cwd() / "target" + symlink.symlink_to(target) + self.assertThat(repo.Ubuntu.get_package_for_file(symlink), Equals("coreutils")) + class TestGetPackagesInBase(testtools.TestCase): def test_hardcoded_core18(self): From b4e9dc83a45f0ee535fdb991830f0f0bf8bff3ba Mon Sep 17 00:00:00 2001 From: Sergio Schvezov Date: Tue, 15 Feb 2022 11:51:54 -0300 Subject: [PATCH 147/167] Merge pull request #3640 from snapcore/sergiusens-patch-1 spread: update error when local snap is missing --- spread.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spread.yaml b/spread.yaml index a6efb149d3..6b1280d407 100644 --- a/spread.yaml +++ b/spread.yaml @@ -234,7 +234,7 @@ prepare: | if stat /snapcraft/tests/*.snap 2>/dev/null; then snap install --classic --dangerous /snapcraft/tests/*.snap else - echo "Expected a snap to exist in /snapcraft/. If your intention"\ + echo "Expected a snap to exist in /snapcraft/tests/. If your intention"\ "was to install from the store, set \$SNAPCRAFT_CHANNEL." exit 1 fi From 5969c32bbdff1999ad9dd086a9fd110628dfcd34 Mon Sep 17 00:00:00 2001 From: Sergio Schvezov Date: Fri, 18 Feb 2022 15:14:05 -0300 Subject: [PATCH 148/167] Merge pull request #3641 from snapcore/simplify-dep-prereq lifecycle: fix behavior for core22-step-dependencies (CRAFT-733) --- .../internal/lifecycle/_status_cache.py | 11 ++++++++++- .../core22-step-dependencies/snapcraft.yaml | 4 +++- .../general/core22-step-dependencies/task.yaml | 12 ++++++++++++ .../core22-step-dependencies/testfile.zip | Bin 0 -> 1166 bytes 4 files changed, 25 insertions(+), 2 deletions(-) create mode 100644 tests/spread/general/core22-step-dependencies/testfile.zip diff --git a/snapcraft_legacy/internal/lifecycle/_status_cache.py b/snapcraft_legacy/internal/lifecycle/_status_cache.py index 361950dd07..ceb1ce132a 100644 --- a/snapcraft_legacy/internal/lifecycle/_status_cache.py +++ b/snapcraft_legacy/internal/lifecycle/_status_cache.py @@ -156,9 +156,18 @@ def _ensure_dirty_report( # properties specific to that part. If it's not dirty because of those, # we need to expand it here to also take its dependencies (if any) into # account - prerequisite_step = steps.get_dependency_prerequisite_step(step) dependencies = self.config.parts.get_dependencies(part.name, recursive=True) + # core20 uses Plugins V2 which does not require staging parts for pull + # like V1 Plugins do. + if ( + self.config.project._get_build_base() == "core20" + and part._build_attributes.core22_step_dependencies() + ): + prerequisite_step = step + else: + prerequisite_step = steps.get_dependency_prerequisite_step(step) + changed_dependencies: List[pluginhandler.Dependency] = [] with contextlib.suppress(errors.StepHasNotRunError): timestamp = part.step_timestamp(step) diff --git a/tests/spread/general/core22-step-dependencies/snapcraft.yaml b/tests/spread/general/core22-step-dependencies/snapcraft.yaml index 06c7b3d1d9..04f5664c6c 100644 --- a/tests/spread/general/core22-step-dependencies/snapcraft.yaml +++ b/tests/spread/general/core22-step-dependencies/snapcraft.yaml @@ -16,5 +16,7 @@ parts: touch $SNAPCRAFT_PART_INSTALL/part1 part2: build-attributes: [core22-step-dependencies] - plugin: nil + source: ./testfile.zip + source-type: zip + plugin: dump after: [part1] diff --git a/tests/spread/general/core22-step-dependencies/task.yaml b/tests/spread/general/core22-step-dependencies/task.yaml index 5844be0b98..89837a80b6 100644 --- a/tests/spread/general/core22-step-dependencies/task.yaml +++ b/tests/spread/general/core22-step-dependencies/task.yaml @@ -13,3 +13,15 @@ execute: | echo "FAIL: staged files from part1 found" exit 1 fi + + # save the time of file left in first pulling + TIMESTAMP1=$(stat parts/part2/src/testfile --format=%y) + + # now stage (which should NOT pull again, as part2 was already pulled) + snapcraft stage + + TIMESTAMP2=$(stat parts/part2/src/testfile --format=%y) + if [ "$TIMESTAMP2" != "$TIMESTAMP1" ]; then + echo "FAIL: file timestamp is modified (part2 did a re-pull)" + exit 1 + fi diff --git a/tests/spread/general/core22-step-dependencies/testfile.zip b/tests/spread/general/core22-step-dependencies/testfile.zip new file mode 100644 index 0000000000000000000000000000000000000000..3b4bca6b9bfc8b662defea177d73acf1d11691d3 GIT binary patch literal 1166 zcmWIWW@h1H0D*E({}BJy3-K?Q85lsAgF%L&B(=CCEi)%IG=!6Zxp4+}5(t-8a5FHn zfRuoVS?Sl)+Z}}Xg5ynY6qmiLyJBpRVWl3`x2q%N!kIS=hKuEWgFZC}m~YSjCTd=^ zl0_$I6<=Hb-Pa7ieE3v6>UYY-H>>1-O|n|vT)A!HH(+Mz492WyNznIbQ#Aatuo7IsLz6LC}=4M?d-$k7t+UeM!D!=bbk{>wA6Z_eruByPR8aeH)P?L7~hc=4>-n|`JlGqvA47rfrnzJ+O5ZQtxY7Y+0UAS@U1yVu;V{A1VTz|Ed+wGpO*~m}HajVt?UV4cjB!TcQG|CoGWIdRm)>$NpSl zbW&q><23GxR>#zqNX7J8Uy8Dv%DQv@Ho;d6#~Q?u zV0}@!Rl(_k?LKW@@o+Uqg`IgB`)Yl*F|c<^7P2huTWaboVj%CaYR#{k2D3KqNZcqh z^KPm16Ok)x^!KLTUX^`+qOq~cL}!uRoYTuBek{Hpl*>?aU8JJyE$7p;+lC*l-QU<* zy=Y5_I?u^t>&klc)xo>lgFMe?h|Ju=*MG%9Xm@I7QBwHwJLlN6I#$%C+EvAGU)?Xk z>%zzXdt&|UB{Pa%-u@LDqI>(~DL;*~H}6ba@ZtHpC!r?uT}wXdmoG^$JnOML?v?PP zYX?sl{fhZMziGkZ+X+7fwp+@dUBDhv!>d@AuvW)wox9ih#rK2HYFf{b&;DrI+H^i* zZ|Ij#L3-C)-mG-KEiQA!F#7uXNy@>RGI@F85i@55DeN|IE!-Ni*IBumSzL03=x)B% z51+sJe=n-*%)dJt6>lC)P`?;`_pvP>$IF|wOBMCMS68wu`h6$-^4k1!(M)q!Fk3#o zxN>uFhiZtUdEQ@+)cZ?wK3!8?mf3#Nt}6Sf z=T+;b+zpz$OIk*$H?R6ufHxzP95b%6LIPZdfQT)PAQDk>utG`>v{EC$o0Scuju8m` KfV371hz9`6;|CJ} literal 0 HcmV?d00001 From a414a2ea2cf01641f6453fabebee6d8b07421f73 Mon Sep 17 00:00:00 2001 From: Sergio Schvezov Date: Mon, 21 Feb 2022 09:44:55 -0300 Subject: [PATCH 149/167] Merge pull request #3643 from snapcore/kde-compose-dead extension: compose and deadkeys for neon --- .../internal/project_loader/_extensions/kde_neon.py | 2 +- tests/legacy/unit/project_loader/extensions/test_kde_neon.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/snapcraft_legacy/internal/project_loader/_extensions/kde_neon.py b/snapcraft_legacy/internal/project_loader/_extensions/kde_neon.py index a25f89493f..f19e16b0e4 100644 --- a/snapcraft_legacy/internal/project_loader/_extensions/kde_neon.py +++ b/snapcraft_legacy/internal/project_loader/_extensions/kde_neon.py @@ -21,7 +21,6 @@ from ._extension import Extension - _ExtensionInfo = namedtuple("ExtensionInfo", "cmake_args content provider build_snaps") _Info = dict( @@ -104,6 +103,7 @@ def __init__(self, *, extension_name: str, yaml_data: Dict[str, Any]) -> None: "command-chain": ["snap/command-chain/hooks-configure-desktop"], } }, + "layout": {"/usr/share/X11": {"symlink": "$SNAP/kf5/usr/share/X11"}}, } if info.cmake_args is not None: diff --git a/tests/legacy/unit/project_loader/extensions/test_kde_neon.py b/tests/legacy/unit/project_loader/extensions/test_kde_neon.py index 3fa0bbed62..3ab2cbb939 100644 --- a/tests/legacy/unit/project_loader/extensions/test_kde_neon.py +++ b/tests/legacy/unit/project_loader/extensions/test_kde_neon.py @@ -50,6 +50,7 @@ def test_extension_core18(): "command-chain": ["snap/command-chain/hooks-configure-desktop"], } }, + "layout": {"/usr/share/X11": {"symlink": "$SNAP/kf5/usr/share/X11"}}, } assert kde_neon_extension.app_snippet == { "command-chain": ["snap/command-chain/desktop-launch"], @@ -82,6 +83,7 @@ def test_extension_core20(): "plugs": ["desktop"], } }, + "layout": {"/usr/share/X11": {"symlink": "$SNAP/kf5/usr/share/X11"}}, "plugs": { "desktop": {"mount-host-font-cache": False}, "icon-themes": { From f3a031aeac1e49b7abc912d19bad64b29317465b Mon Sep 17 00:00:00 2001 From: Sergio Schvezov Date: Mon, 21 Feb 2022 11:51:53 -0300 Subject: [PATCH 150/167] Merge pull request #3638 from artivis/fix/colcon-cmake-args colcon v2: forward cmake args --- snapcraft_legacy/plugins/v2/colcon.py | 3 +++ tests/legacy/unit/plugins/v2/test_colcon.py | 3 ++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/snapcraft_legacy/plugins/v2/colcon.py b/snapcraft_legacy/plugins/v2/colcon.py index 0845d40e0a..909ddb060c 100644 --- a/snapcraft_legacy/plugins/v2/colcon.py +++ b/snapcraft_legacy/plugins/v2/colcon.py @@ -165,6 +165,9 @@ def _get_build_commands(self) -> List[str]: if self.options.colcon_packages: cmd.extend(["--packages-select", *self.options.colcon_packages]) + if self.options.colcon_cmake_args: + cmd.extend(["--cmake-args", *self.options.colcon_cmake_args]) + if self.options.colcon_ament_cmake_args: cmd.extend(["--ament-cmake-args", *self.options.colcon_ament_cmake_args]) diff --git a/tests/legacy/unit/plugins/v2/test_colcon.py b/tests/legacy/unit/plugins/v2/test_colcon.py index 605f186f2b..f2f7327efa 100644 --- a/tests/legacy/unit/plugins/v2/test_colcon.py +++ b/tests/legacy/unit/plugins/v2/test_colcon.py @@ -174,7 +174,8 @@ class Options: '--base-paths "${SNAPCRAFT_PART_SRC}" --build-base "${SNAPCRAFT_PART_BUILD}" ' '--merge-install --install-base "${SNAPCRAFT_PART_INSTALL}"/opt/ros/snap ' "--packages-ignore ipackage1 ipackage2... --packages-select package1 " - "package2... --ament-cmake-args ament args... --catkin-cmake-args catkin " + "package2... --cmake-args cmake args... " + "--ament-cmake-args ament args... --catkin-cmake-args catkin " 'args... --parallel-workers "${SNAPCRAFT_PARALLEL_BUILD_COUNT}"', 'if [ -f "${SNAPCRAFT_PART_INSTALL}"/opt/ros/snap/COLCON_IGNORE ]; then', 'rm "${SNAPCRAFT_PART_INSTALL}"/opt/ros/snap/COLCON_IGNORE', From 2b6d45b013cf530f3e9e63e99ae0f8e7c2346ca5 Mon Sep 17 00:00:00 2001 From: Sergio Schvezov Date: Mon, 21 Feb 2022 15:37:05 -0300 Subject: [PATCH 151/167] Merge pull request #3648 from aritra24/reintroduce-help-options cli: reintroduce remote-build and promote to snapcraft help --- snapcraft_legacy/cli/_command_group.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/snapcraft_legacy/cli/_command_group.py b/snapcraft_legacy/cli/_command_group.py index 9ec0188140..67423d1a18 100644 --- a/snapcraft_legacy/cli/_command_group.py +++ b/snapcraft_legacy/cli/_command_group.py @@ -68,8 +68,4 @@ def get_command(self, ctx, cmd_name): def list_commands(self, ctx): commands = super().list_commands(ctx) - # Hide commands with unstable cli - commands.pop(commands.index("promote")) - commands.pop(commands.index("remote-build")) - return commands From f2bcfb9a48faa7af84d79a5dbbd07b165c24ee48 Mon Sep 17 00:00:00 2001 From: Sergio Schvezov Date: Thu, 3 Mar 2022 11:54:45 +0100 Subject: [PATCH 152/167] Merge pull request #3656 from nessita/update-staging-store-urls tools: update staging store URL for uploading blobs --- tools/staging_env.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/staging_env.sh b/tools/staging_env.sh index 7a0cc6a4e2..165801fa59 100755 --- a/tools/staging_env.sh +++ b/tools/staging_env.sh @@ -13,7 +13,7 @@ deactivate() { export STORE_DASHBOARD_URL="https://dashboard.staging.snapcraft.io/" export STORE_API_URL="https://api.staging.snapcraft.io/" -export STORE_UPLOAD_URL="https://upload.apps.staging.ubuntu.com/" +export STORE_UPLOAD_URL="https://storage.staging.snapcraftcontent.com/" export UBUNTU_ONE_SSO_URL="https://login.staging.ubuntu.com/" export TEST_STORE="staging" From 6c72c8ee0152e4da8c5868fc99a5090e87c7752e Mon Sep 17 00:00:00 2001 From: Sergio Schvezov Date: Thu, 17 Mar 2022 23:22:01 -0300 Subject: [PATCH 153/167] Merge pull request #3658 from jriddell/master switch to new kde-framework content snap --- .../internal/project_loader/_extensions/kde_neon.py | 8 ++++---- .../unit/project_loader/extensions/test_kde_neon.py | 12 ++++++------ 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/snapcraft_legacy/internal/project_loader/_extensions/kde_neon.py b/snapcraft_legacy/internal/project_loader/_extensions/kde_neon.py index f19e16b0e4..65f665868e 100644 --- a/snapcraft_legacy/internal/project_loader/_extensions/kde_neon.py +++ b/snapcraft_legacy/internal/project_loader/_extensions/kde_neon.py @@ -31,10 +31,10 @@ build_snaps=["kde-frameworks-5-core18-sdk/latest/stable"], ), core20=_ExtensionInfo( - cmake_args="-DCMAKE_FIND_ROOT_PATH=/snap/kde-frameworks-5-qt-5-15-3-core20-sdk/current", - content="kde-frameworks-5-qt-5-15-3-core20-all", - provider="kde-frameworks-5-qt-5-15-3-core20", - build_snaps=["kde-frameworks-5-qt-5-15-3-core20-sdk/latest/stable"], + cmake_args="-DCMAKE_FIND_ROOT_PATH=/snap/kde-frameworks-5-91-qt-5-15-3-core20-sdk/current", + content="kde-frameworks-5-91-qt-5-15-3-core20-all", + provider="kde-frameworks-5-91-qt-5-15-3-core20", + build_snaps=["kde-frameworks-5-91-qt-5-15-3-core20-sdk/latest/stable"], ), ) diff --git a/tests/legacy/unit/project_loader/extensions/test_kde_neon.py b/tests/legacy/unit/project_loader/extensions/test_kde_neon.py index 3ab2cbb939..c3ec1c2372 100644 --- a/tests/legacy/unit/project_loader/extensions/test_kde_neon.py +++ b/tests/legacy/unit/project_loader/extensions/test_kde_neon.py @@ -91,9 +91,9 @@ def test_extension_core20(): "interface": "content", "target": "$SNAP/data-dir/icons", }, - "kde-frameworks-5-qt-5-15-3-core20": { - "content": "kde-frameworks-5-qt-5-15-3-core20-all", - "default-provider": "kde-frameworks-5-qt-5-15-3-core20", + "kde-frameworks-5-91-qt-5-15-3-core20": { + "content": "kde-frameworks-5-91-qt-5-15-3-core20-all", + "default-provider": "kde-frameworks-5-91-qt-5-15-3-core20", "interface": "content", "target": "$SNAP/kf5", }, @@ -111,15 +111,15 @@ def test_extension_core20(): assert kde_neon_extension.part_snippet == { "build-environment": [ { - "SNAPCRAFT_CMAKE_ARGS": "-DCMAKE_FIND_ROOT_PATH=/snap/kde-frameworks-5-qt-5-15-3-core20-sdk/current" + "SNAPCRAFT_CMAKE_ARGS": "-DCMAKE_FIND_ROOT_PATH=/snap/kde-frameworks-5-91-qt-5-15-3-core20-sdk/current" } ] } assert kde_neon_extension.parts == { "kde-neon-extension": { "build-packages": ["g++"], - "build-snaps": ["kde-frameworks-5-qt-5-15-3-core20-sdk/latest/stable"], - "make-parameters": ["PLATFORM_PLUG=kde-frameworks-5-qt-5-15-3-core20"], + "build-snaps": ["kde-frameworks-5-91-qt-5-15-3-core20-sdk/latest/stable"], + "make-parameters": ["PLATFORM_PLUG=kde-frameworks-5-91-qt-5-15-3-core20"], "plugin": "make", "source": "$SNAPCRAFT_EXTENSIONS_DIR/desktop", "source-subdir": "kde-neon", From 6f09243454240cee8a126f883505ad35d61737f1 Mon Sep 17 00:00:00 2001 From: Sergio Schvezov Date: Fri, 18 Mar 2022 01:28:48 -0300 Subject: [PATCH 154/167] Merge pull request #3595 from jriddell/work/kde-neon-compression-lzo kde extension: use lzo compression --- .../internal/project_loader/_extensions/kde_neon.py | 1 + tests/legacy/unit/project_loader/extensions/test_kde_neon.py | 2 ++ 2 files changed, 3 insertions(+) diff --git a/snapcraft_legacy/internal/project_loader/_extensions/kde_neon.py b/snapcraft_legacy/internal/project_loader/_extensions/kde_neon.py index 65f665868e..d8529f5c47 100644 --- a/snapcraft_legacy/internal/project_loader/_extensions/kde_neon.py +++ b/snapcraft_legacy/internal/project_loader/_extensions/kde_neon.py @@ -77,6 +77,7 @@ def __init__(self, *, extension_name: str, yaml_data: Dict[str, Any]) -> None: info = _Info[yaml_data["base"]] self.root_snippet = { "assumes": ["snapd2.43"], # for 'snapctl is-connected' + "compression": "lzo", "plugs": { "desktop": {"mount-host-font-cache": False}, "icon-themes": { diff --git a/tests/legacy/unit/project_loader/extensions/test_kde_neon.py b/tests/legacy/unit/project_loader/extensions/test_kde_neon.py index c3ec1c2372..3501c3b274 100644 --- a/tests/legacy/unit/project_loader/extensions/test_kde_neon.py +++ b/tests/legacy/unit/project_loader/extensions/test_kde_neon.py @@ -24,6 +24,7 @@ def test_extension_core18(): assert kde_neon_extension.root_snippet == { "assumes": ["snapd2.43"], + "compression": "lzo", "plugs": { "desktop": {"mount-host-font-cache": False}, "icon-themes": { @@ -76,6 +77,7 @@ def test_extension_core20(): assert kde_neon_extension.root_snippet == { "assumes": ["snapd2.43"], + "compression": "lzo", "environment": {"SNAP_DESKTOP_RUNTIME": "$SNAP/kf5"}, "hooks": { "configure": { From 25eb75d65a052bebddd82684c9dfc624092546bd Mon Sep 17 00:00:00 2001 From: Sergio Schvezov Date: Fri, 18 Mar 2022 12:33:02 -0300 Subject: [PATCH 155/167] Merge pull request #3661 from lupino3/patch-2 gradle v1 plugin: add support for JDK 17 --- snapcraft_legacy/plugins/v1/gradle.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/snapcraft_legacy/plugins/v1/gradle.py b/snapcraft_legacy/plugins/v1/gradle.py index 47752e1f29..59ccbb5617 100644 --- a/snapcraft_legacy/plugins/v1/gradle.py +++ b/snapcraft_legacy/plugins/v1/gradle.py @@ -49,8 +49,8 @@ - gradle-openjdk-version: (string) - openjdk version available to the base to use. If not set the latest - version available to the base will be used. + openjdk version available to the base to use. If not set, version 11 + will be used. """ import logging @@ -149,11 +149,11 @@ def __init__(self, name, options, project): self._setup_base_tools(project._get_build_base()) def _setup_base_tools(self, base): - valid_versions = ["8", "11"] + valid_versions = ["8", "11", "17"] version = self.options.gradle_openjdk_version if not version: - version = valid_versions[-1] + version = "11" elif version not in valid_versions: raise UnsupportedJDKVersionError( version=version, base=base, valid_versions=valid_versions From fe185e8df56dbada2ff0691a544f67b4f1f9466b Mon Sep 17 00:00:00 2001 From: Sergio Schvezov Date: Fri, 18 Mar 2022 15:24:18 -0300 Subject: [PATCH 156/167] Merge pull request #3607 from mhoeher/launchpad-bug-1956216 docker: fix python installation --- docker/Dockerfile | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index f552d82973..bffb2192c4 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -26,17 +26,29 @@ RUN curl -L $(curl -H 'X-Ubuntu-Series: 16' 'https://api.snapcraft.io/api/v1/sna RUN mkdir -p /snap/core18 RUN unsquashfs -d /snap/core18/current core18.snap +# Grab the core20 snap (which snapcraft uses as a base) from the stable channel +# and unpack it in the proper place. +RUN curl -L $(curl -H 'X-Ubuntu-Series: 16' 'https://api.snapcraft.io/api/v1/snaps/details/core20' | jq '.download_url' -r) --output core20.snap +RUN mkdir -p /snap/core20 +RUN unsquashfs -d /snap/core20/current core20.snap + # Grab the snapcraft snap from the $RISK channel and unpack it in the proper # place. RUN curl -L $(curl -H 'X-Ubuntu-Series: 16' 'https://api.snapcraft.io/api/v1/snaps/details/snapcraft?channel='$RISK | jq '.download_url' -r) --output snapcraft.snap RUN mkdir -p /snap/snapcraft RUN unsquashfs -d /snap/snapcraft/current snapcraft.snap +# Fix Python3 installation: Make sure we use the interpreter from +# the snapcraft snap: +RUN unlink /snap/snapcraft/current/usr/bin/python3 +RUN ln -s /snap/snapcraft/current/usr/bin/python3.* /snap/snapcraft/current/usr/bin/python3 +RUN echo /snap/snapcraft/current/lib/python3.*/site-packages >> /snap/snapcraft/current/usr/lib/python3/dist-packages/site-packages.pth + # Create a snapcraft runner (TODO: move version detection to the core of # snapcraft). RUN mkdir -p /snap/bin RUN echo "#!/bin/sh" > /snap/bin/snapcraft -RUN snap_version="$(awk '/^version:/{print $2}' /snap/snapcraft/current/meta/snap.yaml)" && echo "export SNAP_VERSION=\"$snap_version\"" >> /snap/bin/snapcraft +RUN snap_version="$(awk '/^version:/{print $2}' /snap/snapcraft/current/meta/snap.yaml | tr -d \')" && echo "export SNAP_VERSION=\"$snap_version\"" >> /snap/bin/snapcraft RUN echo 'exec "$SNAP/usr/bin/python3" "$SNAP/bin/snapcraft" "$@"' >> /snap/bin/snapcraft RUN chmod +x /snap/bin/snapcraft @@ -45,6 +57,7 @@ RUN chmod +x /snap/bin/snapcraft FROM ubuntu:$UBUNTU COPY --from=builder /snap/core /snap/core COPY --from=builder /snap/core18 /snap/core18 +COPY --from=builder /snap/core20 /snap/core20 COPY --from=builder /snap/snapcraft /snap/snapcraft COPY --from=builder /snap/bin/snapcraft /snap/bin/snapcraft @@ -55,7 +68,7 @@ RUN apt-get update && apt-get dist-upgrade --yes && apt-get install --yes snapd ENV LANG="en_US.UTF-8" ENV LANGUAGE="en_US:en" ENV LC_ALL="en_US.UTF-8" -ENV PATH="/snap/bin:$PATH" +ENV PATH="/snap/bin:/snap/snapcraft/current/usr/bin:$PATH" ENV SNAP="/snap/snapcraft/current" ENV SNAP_NAME="snapcraft" ENV SNAP_ARCH="amd64" From bfc7fec527fa4b1587be3e22c3e4875233f29c71 Mon Sep 17 00:00:00 2001 From: Sergio Schvezov Date: Fri, 18 Mar 2022 16:10:06 -0300 Subject: [PATCH 157/167] Merge pull request #3664 from Guillaumebeuzeboc/ROBENG-147/source-subdir-not-used-by-ros-plugin-v2 ROS plugins v2: respect source-subdir key --- snapcraft_legacy/plugins/v2/_plugin.py | 2 +- snapcraft_legacy/plugins/v2/_ros.py | 6 ++-- snapcraft_legacy/plugins/v2/catkin.py | 2 +- snapcraft_legacy/plugins/v2/catkin_tools.py | 6 +--- snapcraft_legacy/plugins/v2/colcon.py | 2 +- tests/legacy/unit/plugins/v2/test_catkin.py | 12 +++---- .../unit/plugins/v2/test_catkin_tools.py | 13 ++++--- tests/legacy/unit/plugins/v2/test_colcon.py | 12 +++---- .../plugins/v2/colcon-ros2-hello/task.yaml | 13 +++++-- tests/spread/plugins/v2/ros1-hello/task.yaml | 14 ++++++-- .../catkin-noetic-subdir/snap/snapcraft.yaml | 22 ++++++++++++ .../subdir/src/snapcraft_hello/CMakeLists.txt | 22 ++++++++++++ .../subdir/src/snapcraft_hello/package.xml | 13 +++++++ .../subdir/src/snapcraft_hello/src/hello.cpp | 24 +++++++++++++ .../snap/snapcraft.yaml | 21 ++++++++++++ .../subdir/src/snapcraft_hello/CMakeLists.txt | 22 ++++++++++++ .../subdir/src/snapcraft_hello/package.xml | 13 +++++++ .../subdir/src/snapcraft_hello/src/hello.cpp | 24 +++++++++++++ .../CMakeLists.txt | 34 +++++++++++++++++++ .../colcon-ros2-foxy-rlcpp-hello/hello.cpp | 28 +++++++++++++++ .../colcon-ros2-foxy-rlcpp-hello/package.xml | 24 +++++++++++++ .../snaps/colcon-subdir/snap/snapcraft.yaml | 23 +++++++++++++ .../CMakeLists.txt | 13 +++++++ .../package.xml | 19 +++++++++++ 24 files changed, 350 insertions(+), 34 deletions(-) create mode 100644 tests/spread/plugins/v2/snaps/catkin-noetic-subdir/snap/snapcraft.yaml create mode 100644 tests/spread/plugins/v2/snaps/catkin-noetic-subdir/subdir/src/snapcraft_hello/CMakeLists.txt create mode 100644 tests/spread/plugins/v2/snaps/catkin-noetic-subdir/subdir/src/snapcraft_hello/package.xml create mode 100644 tests/spread/plugins/v2/snaps/catkin-noetic-subdir/subdir/src/snapcraft_hello/src/hello.cpp create mode 100644 tests/spread/plugins/v2/snaps/catkin-tools-noetic-subdir/snap/snapcraft.yaml create mode 100644 tests/spread/plugins/v2/snaps/catkin-tools-noetic-subdir/subdir/src/snapcraft_hello/CMakeLists.txt create mode 100644 tests/spread/plugins/v2/snaps/catkin-tools-noetic-subdir/subdir/src/snapcraft_hello/package.xml create mode 100644 tests/spread/plugins/v2/snaps/catkin-tools-noetic-subdir/subdir/src/snapcraft_hello/src/hello.cpp create mode 100644 tests/spread/plugins/v2/snaps/colcon-subdir/colcon-ros2-foxy-rlcpp-hello/CMakeLists.txt create mode 100644 tests/spread/plugins/v2/snaps/colcon-subdir/colcon-ros2-foxy-rlcpp-hello/hello.cpp create mode 100644 tests/spread/plugins/v2/snaps/colcon-subdir/colcon-ros2-foxy-rlcpp-hello/package.xml create mode 100644 tests/spread/plugins/v2/snaps/colcon-subdir/snap/snapcraft.yaml create mode 100644 tests/spread/plugins/v2/snaps/colcon-subdir/this-package-should-not-be-compiled/CMakeLists.txt create mode 100644 tests/spread/plugins/v2/snaps/colcon-subdir/this-package-should-not-be-compiled/package.xml diff --git a/snapcraft_legacy/plugins/v2/_plugin.py b/snapcraft_legacy/plugins/v2/_plugin.py index 8fdbe82e62..e78bddca07 100644 --- a/snapcraft_legacy/plugins/v2/_plugin.py +++ b/snapcraft_legacy/plugins/v2/_plugin.py @@ -35,7 +35,7 @@ def __init__(self, *, part_name: str, options) -> None: @abc.abstractmethod def get_build_snaps(self) -> Set[str]: """ - Return a set of required packages to install in the build environment. + Return a set of required snaps to install in the build environment. """ @abc.abstractmethod diff --git a/snapcraft_legacy/plugins/v2/_ros.py b/snapcraft_legacy/plugins/v2/_ros.py index 139e53f9be..ed32e173a4 100644 --- a/snapcraft_legacy/plugins/v2/_ros.py +++ b/snapcraft_legacy/plugins/v2/_ros.py @@ -103,7 +103,7 @@ def _get_stage_runtime_dependencies_commands(self) -> List[str]: os.path.abspath(__file__), "stage-runtime-dependencies", "--part-src", - '"${SNAPCRAFT_PART_SRC}"', + '"${SNAPCRAFT_PART_SRC_WORK}"', "--part-install", '"${SNAPCRAFT_PART_INSTALL}"', "--ros-version", @@ -122,7 +122,7 @@ def get_build_commands(self) -> List[str]: + [ "if [ ! -f /etc/ros/rosdep/sources.list.d/20-default.list ]; then sudo rosdep init; fi", 'rosdep update --include-eol-distros --rosdistro "${ROS_DISTRO}"', - 'rosdep install --default-yes --ignore-packages-from-source --from-paths "${SNAPCRAFT_PART_SRC}"', + 'rosdep install --default-yes --ignore-packages-from-source --from-paths "${SNAPCRAFT_PART_SRC_WORK}"', ] + self._get_build_commands() + self._get_stage_runtime_dependencies_commands() @@ -135,7 +135,7 @@ def plugin_cli(): @plugin_cli.command() -@click.option("--part-src", envvar="SNAPCRAFT_PART_SRC", required=True) +@click.option("--part-src", envvar="SNAPCRAFT_PART_SRC_WORK", required=True) @click.option("--part-install", envvar="SNAPCRAFT_PART_INSTALL", required=True) @click.option("--ros-version", envvar="ROS_VERSION", required=True) @click.option("--ros-distro", envvar="ROS_DISTRO", required=True) diff --git a/snapcraft_legacy/plugins/v2/catkin.py b/snapcraft_legacy/plugins/v2/catkin.py index cda2e9fa49..c8d4038e77 100644 --- a/snapcraft_legacy/plugins/v2/catkin.py +++ b/snapcraft_legacy/plugins/v2/catkin.py @@ -110,7 +110,7 @@ def _get_build_commands(self) -> List[str]: "--install", "--merge", "--source-space", - '"${SNAPCRAFT_PART_SRC}"', + '"${SNAPCRAFT_PART_SRC_WORK}"', "--build-space", '"${SNAPCRAFT_PART_BUILD}"', "--install-space", diff --git a/snapcraft_legacy/plugins/v2/catkin_tools.py b/snapcraft_legacy/plugins/v2/catkin_tools.py index f3c1183c3b..23c2e9bb1f 100644 --- a/snapcraft_legacy/plugins/v2/catkin_tools.py +++ b/snapcraft_legacy/plugins/v2/catkin_tools.py @@ -62,10 +62,6 @@ def get_schema(cls) -> Dict[str, Any]: def get_build_packages(self) -> Set[str]: return super().get_build_packages() | { "python3-catkin-tools", - # FIXME: Only needed because of a botched release: - # https://github.com/catkin/catkin_tools/issues/594#issuecomment-688149976 - # Once fixed, remove this. - "python3-osrf-pycommon", } def _get_workspace_activation_commands(self) -> List[str]: @@ -116,7 +112,7 @@ def _get_build_commands(self) -> List[str]: "default", "--install", "--source-space", - '"${SNAPCRAFT_PART_SRC}"', + '"${SNAPCRAFT_PART_SRC_WORK}"', "--build-space", '"${SNAPCRAFT_PART_BUILD}"', "--install-space", diff --git a/snapcraft_legacy/plugins/v2/colcon.py b/snapcraft_legacy/plugins/v2/colcon.py index 909ddb060c..5e7df88594 100644 --- a/snapcraft_legacy/plugins/v2/colcon.py +++ b/snapcraft_legacy/plugins/v2/colcon.py @@ -151,7 +151,7 @@ def _get_build_commands(self) -> List[str]: "colcon", "build", "--base-paths", - '"${SNAPCRAFT_PART_SRC}"', + '"${SNAPCRAFT_PART_SRC_WORK}"', "--build-base", '"${SNAPCRAFT_PART_BUILD}"', "--merge-install", diff --git a/tests/legacy/unit/plugins/v2/test_catkin.py b/tests/legacy/unit/plugins/v2/test_catkin.py index c0d229867e..7dccfa94c3 100644 --- a/tests/legacy/unit/plugins/v2/test_catkin.py +++ b/tests/legacy/unit/plugins/v2/test_catkin.py @@ -98,14 +98,14 @@ class Options: "if [ ! -f /etc/ros/rosdep/sources.list.d/20-default.list ]; then sudo rosdep " "init; fi", 'rosdep update --include-eol-distros --rosdistro "${ROS_DISTRO}"', - 'rosdep install --default-yes --ignore-packages-from-source --from-paths "${SNAPCRAFT_PART_SRC}"', + 'rosdep install --default-yes --ignore-packages-from-source --from-paths "${SNAPCRAFT_PART_SRC_WORK}"', "catkin_make_isolated --install --merge " - '--source-space "${SNAPCRAFT_PART_SRC}" --build-space "${SNAPCRAFT_PART_BUILD}" ' + '--source-space "${SNAPCRAFT_PART_SRC_WORK}" --build-space "${SNAPCRAFT_PART_BUILD}" ' '--install-space "${SNAPCRAFT_PART_INSTALL}/opt/ros/${ROS_DISTRO}" ' '-j "${SNAPCRAFT_PARALLEL_BUILD_COUNT}"', "env -i LANG=C.UTF-8 LC_ALL=C.UTF-8 /test/python3 -I " "/test/_ros.py " - 'stage-runtime-dependencies --part-src "${SNAPCRAFT_PART_SRC}" --part-install "${SNAPCRAFT_PART_INSTALL}" ' + 'stage-runtime-dependencies --part-src "${SNAPCRAFT_PART_SRC_WORK}" --part-install "${SNAPCRAFT_PART_INSTALL}" ' '--ros-version "${ROS_VERSION}" --ros-distro "${ROS_DISTRO}" --target-arch "${SNAPCRAFT_TARGET_ARCH}"', ] @@ -151,9 +151,9 @@ class Options: "if [ ! -f /etc/ros/rosdep/sources.list.d/20-default.list ]; then sudo rosdep " "init; fi", 'rosdep update --include-eol-distros --rosdistro "${ROS_DISTRO}"', - 'rosdep install --default-yes --ignore-packages-from-source --from-paths "${SNAPCRAFT_PART_SRC}"', + 'rosdep install --default-yes --ignore-packages-from-source --from-paths "${SNAPCRAFT_PART_SRC_WORK}"', "catkin_make_isolated --install --merge " - '--source-space "${SNAPCRAFT_PART_SRC}" --build-space "${SNAPCRAFT_PART_BUILD}" ' + '--source-space "${SNAPCRAFT_PART_SRC_WORK}" --build-space "${SNAPCRAFT_PART_BUILD}" ' '--install-space "${SNAPCRAFT_PART_INSTALL}/opt/ros/${ROS_DISTRO}" ' '-j "${SNAPCRAFT_PARALLEL_BUILD_COUNT}" --pkg package1 package2... ' "--ignore-pkg ipackage1 ipackage2... --cmake-args cmake args...", @@ -162,6 +162,6 @@ class Options: "http_proxy=http://foo https_proxy=https://bar " "/test/python3 -I " "/test/_ros.py " - 'stage-runtime-dependencies --part-src "${SNAPCRAFT_PART_SRC}" --part-install "${SNAPCRAFT_PART_INSTALL}" ' + 'stage-runtime-dependencies --part-src "${SNAPCRAFT_PART_SRC_WORK}" --part-install "${SNAPCRAFT_PART_INSTALL}" ' '--ros-version "${ROS_VERSION}" --ros-distro "${ROS_DISTRO}" --target-arch "${SNAPCRAFT_TARGET_ARCH}"', ] diff --git a/tests/legacy/unit/plugins/v2/test_catkin_tools.py b/tests/legacy/unit/plugins/v2/test_catkin_tools.py index f6ef5769f9..9ba0984302 100644 --- a/tests/legacy/unit/plugins/v2/test_catkin_tools.py +++ b/tests/legacy/unit/plugins/v2/test_catkin_tools.py @@ -49,7 +49,6 @@ def test_get_build_packages(): assert plugin.get_build_packages() == { "python3-rosdep", "python3-catkin-tools", - "python3-osrf-pycommon", } @@ -94,16 +93,16 @@ class Options: "if [ ! -f /etc/ros/rosdep/sources.list.d/20-default.list ]; then sudo rosdep " "init; fi", 'rosdep update --include-eol-distros --rosdistro "${ROS_DISTRO}"', - 'rosdep install --default-yes --ignore-packages-from-source --from-paths "${SNAPCRAFT_PART_SRC}"', + 'rosdep install --default-yes --ignore-packages-from-source --from-paths "${SNAPCRAFT_PART_SRC_WORK}"', "catkin init", "catkin profile add -f default", "catkin config --profile default --install " - '--source-space "${SNAPCRAFT_PART_SRC}" --build-space "${SNAPCRAFT_PART_BUILD}" ' + '--source-space "${SNAPCRAFT_PART_SRC_WORK}" --build-space "${SNAPCRAFT_PART_BUILD}" ' '--install-space "${SNAPCRAFT_PART_INSTALL}/opt/ros/${ROS_DISTRO}"', 'catkin build --no-notify --profile default -j "${SNAPCRAFT_PARALLEL_BUILD_COUNT}"', "env -i LANG=C.UTF-8 LC_ALL=C.UTF-8 /test/python3 -I " "/test/_ros.py " - 'stage-runtime-dependencies --part-src "${SNAPCRAFT_PART_SRC}" --part-install "${SNAPCRAFT_PART_INSTALL}" ' + 'stage-runtime-dependencies --part-src "${SNAPCRAFT_PART_SRC_WORK}" --part-install "${SNAPCRAFT_PART_INSTALL}" ' '--ros-version "${ROS_VERSION}" --ros-distro "${ROS_DISTRO}" --target-arch "${SNAPCRAFT_TARGET_ARCH}"', ] @@ -148,11 +147,11 @@ class Options: "if [ ! -f /etc/ros/rosdep/sources.list.d/20-default.list ]; then sudo rosdep " "init; fi", 'rosdep update --include-eol-distros --rosdistro "${ROS_DISTRO}"', - 'rosdep install --default-yes --ignore-packages-from-source --from-paths "${SNAPCRAFT_PART_SRC}"', + 'rosdep install --default-yes --ignore-packages-from-source --from-paths "${SNAPCRAFT_PART_SRC_WORK}"', "catkin init", "catkin profile add -f default", "catkin config --profile default --install " - '--source-space "${SNAPCRAFT_PART_SRC}" --build-space "${SNAPCRAFT_PART_BUILD}" ' + '--source-space "${SNAPCRAFT_PART_SRC_WORK}" --build-space "${SNAPCRAFT_PART_BUILD}" ' '--install-space "${SNAPCRAFT_PART_INSTALL}/opt/ros/${ROS_DISTRO}" --cmake-args cmake args...', 'catkin build --no-notify --profile default -j "${SNAPCRAFT_PARALLEL_BUILD_COUNT}" package1 package2...', "env -i LANG=C.UTF-8 LC_ALL=C.UTF-8 PATH=/bin:/test SNAP=TESTSNAP " @@ -160,6 +159,6 @@ class Options: "http_proxy=http://foo https_proxy=https://bar " "/test/python3 -I " "/test/_ros.py " - 'stage-runtime-dependencies --part-src "${SNAPCRAFT_PART_SRC}" --part-install "${SNAPCRAFT_PART_INSTALL}" ' + 'stage-runtime-dependencies --part-src "${SNAPCRAFT_PART_SRC_WORK}" --part-install "${SNAPCRAFT_PART_INSTALL}" ' '--ros-version "${ROS_VERSION}" --ros-distro "${ROS_DISTRO}" --target-arch "${SNAPCRAFT_TARGET_ARCH}"', ] diff --git a/tests/legacy/unit/plugins/v2/test_colcon.py b/tests/legacy/unit/plugins/v2/test_colcon.py index f2f7327efa..e4a67ac238 100644 --- a/tests/legacy/unit/plugins/v2/test_colcon.py +++ b/tests/legacy/unit/plugins/v2/test_colcon.py @@ -115,9 +115,9 @@ class Options: "if [ ! -f /etc/ros/rosdep/sources.list.d/20-default.list ]; then sudo rosdep " "init; fi", 'rosdep update --include-eol-distros --rosdistro "${ROS_DISTRO}"', - 'rosdep install --default-yes --ignore-packages-from-source --from-paths "${SNAPCRAFT_PART_SRC}"', + 'rosdep install --default-yes --ignore-packages-from-source --from-paths "${SNAPCRAFT_PART_SRC_WORK}"', "colcon build " - '--base-paths "${SNAPCRAFT_PART_SRC}" --build-base "${SNAPCRAFT_PART_BUILD}" ' + '--base-paths "${SNAPCRAFT_PART_SRC_WORK}" --build-base "${SNAPCRAFT_PART_BUILD}" ' '--merge-install --install-base "${SNAPCRAFT_PART_INSTALL}"/opt/ros/snap ' '--parallel-workers "${SNAPCRAFT_PARALLEL_BUILD_COUNT}"', 'if [ -f "${SNAPCRAFT_PART_INSTALL}"/opt/ros/snap/COLCON_IGNORE ]; then', @@ -125,7 +125,7 @@ class Options: "fi", "env -i LANG=C.UTF-8 LC_ALL=C.UTF-8 /test/python3 -I " "/test/_ros.py " - 'stage-runtime-dependencies --part-src "${SNAPCRAFT_PART_SRC}" --part-install "${SNAPCRAFT_PART_INSTALL}" ' + 'stage-runtime-dependencies --part-src "${SNAPCRAFT_PART_SRC_WORK}" --part-install "${SNAPCRAFT_PART_INSTALL}" ' '--ros-version "${ROS_VERSION}" --ros-distro "${ROS_DISTRO}" --target-arch "${SNAPCRAFT_TARGET_ARCH}"', ] @@ -169,9 +169,9 @@ class Options: "if [ ! -f /etc/ros/rosdep/sources.list.d/20-default.list ]; then sudo rosdep " "init; fi", 'rosdep update --include-eol-distros --rosdistro "${ROS_DISTRO}"', - 'rosdep install --default-yes --ignore-packages-from-source --from-paths "${SNAPCRAFT_PART_SRC}"', + 'rosdep install --default-yes --ignore-packages-from-source --from-paths "${SNAPCRAFT_PART_SRC_WORK}"', "colcon build " - '--base-paths "${SNAPCRAFT_PART_SRC}" --build-base "${SNAPCRAFT_PART_BUILD}" ' + '--base-paths "${SNAPCRAFT_PART_SRC_WORK}" --build-base "${SNAPCRAFT_PART_BUILD}" ' '--merge-install --install-base "${SNAPCRAFT_PART_INSTALL}"/opt/ros/snap ' "--packages-ignore ipackage1 ipackage2... --packages-select package1 " "package2... --cmake-args cmake args... " @@ -184,6 +184,6 @@ class Options: "SNAP_ARCH=TESTARCH SNAP_NAME=TESTSNAPNAME SNAP_VERSION=TESTV1 " "http_proxy=http://foo https_proxy=https://bar " "/test/python3 -I /test/_ros.py " - 'stage-runtime-dependencies --part-src "${SNAPCRAFT_PART_SRC}" --part-install "${SNAPCRAFT_PART_INSTALL}" ' + 'stage-runtime-dependencies --part-src "${SNAPCRAFT_PART_SRC_WORK}" --part-install "${SNAPCRAFT_PART_INSTALL}" ' '--ros-version "${ROS_VERSION}" --ros-distro "${ROS_DISTRO}" --target-arch "${SNAPCRAFT_TARGET_ARCH}"', ] diff --git a/tests/spread/plugins/v2/colcon-ros2-hello/task.yaml b/tests/spread/plugins/v2/colcon-ros2-hello/task.yaml index d6c928ef4c..126357d475 100644 --- a/tests/spread/plugins/v2/colcon-ros2-hello/task.yaml +++ b/tests/spread/plugins/v2/colcon-ros2-hello/task.yaml @@ -6,6 +6,7 @@ kill-timeout: 180m environment: SNAP/colcon_ros2_foxy_rlcpp_hello: colcon-ros2-foxy-rlcpp-hello + SNAP/colcon_subdir: colcon-subdir SNAPCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS: "1" systems: @@ -24,7 +25,8 @@ restore: | snapcraft clean rm -f ./*.snap - git checkout src/hello.cpp + [ -f src/hello ] && git checkout src/hello.cpp + [ -f colcon-ros2-foxy-rlcpp-hello/hello.cpp ] && git checkout colcon-ros2-foxy-rlcpp-hello/hello.cpp #shellcheck source=tests/spread/tools/snapcraft-yaml.sh . "$TOOLS_DIR/snapcraft-yaml.sh" @@ -45,7 +47,14 @@ execute: | [ "$($SNAP)" = "hello world" ] # Make sure that what we built runs with the changes applied. - modified_file=src/hello.cpp + if [ -f src/hello.cpp ]; then + modified_file=src/hello.cpp + elif [ -f colcon-ros2-foxy-rlcpp-hello/hello.cpp ]; then + modified_file=colcon-ros2-foxy-rlcpp-hello/hello.cpp + else + FATAL "Cannot setup ${SNAP} for rebuilding" + fi + sed -i "${modified_file}" -e 's/hello world/hello rebuilt world/' snapcraft diff --git a/tests/spread/plugins/v2/ros1-hello/task.yaml b/tests/spread/plugins/v2/ros1-hello/task.yaml index 420a342c35..c8bbff6c66 100644 --- a/tests/spread/plugins/v2/ros1-hello/task.yaml +++ b/tests/spread/plugins/v2/ros1-hello/task.yaml @@ -6,7 +6,9 @@ kill-timeout: 180m environment: SNAP/catkin_noetic_hello: catkin-noetic-hello + SNAP/catkin_noetic_subdir: catkin-noetic-subdir SNAP/catkin_tools_noetic_hello: catkin-tools-noetic-hello + SNAP/catkin_tools_noetic_subdir: catkin-tools-noetic-subdir SNAPCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS: "1" systems: @@ -25,7 +27,8 @@ restore: | snapcraft clean rm -f ./*.snap - git checkout src/snapcraft_hello/src/hello.cpp + [ -f src/snapcraft_hello/src/hello.cpp ] && git checkout src/snapcraft_hello/src/hello.cpp + [ -f subdir/src/snapcraft_hello/src/hello.cpp ] && git checkout subdir/src/snapcraft_hello/src/hello.cpp #shellcheck source=tests/spread/tools/snapcraft-yaml.sh . "$TOOLS_DIR/snapcraft-yaml.sh" @@ -46,7 +49,14 @@ execute: | [ "$($SNAP)" = "hello world" ] # Make sure that what we built runs with the changes applied. - modified_file=src/snapcraft_hello/src/hello.cpp + if [ -f src/snapcraft_hello/src/hello.cpp ]; then + modified_file=src/snapcraft_hello/src/hello.cpp + elif [ -f subdir/src/snapcraft_hello/src/hello.cpp ]; then + modified_file=subdir/src/snapcraft_hello/src/hello.cpp + else + FATAL "Cannot setup ${SNAP} for rebuilding" + fi + sed -i "${modified_file}" -e 's/hello world/hello rebuilt world/' snapcraft diff --git a/tests/spread/plugins/v2/snaps/catkin-noetic-subdir/snap/snapcraft.yaml b/tests/spread/plugins/v2/snaps/catkin-noetic-subdir/snap/snapcraft.yaml new file mode 100644 index 0000000000..fef7bc321d --- /dev/null +++ b/tests/spread/plugins/v2/snaps/catkin-noetic-subdir/snap/snapcraft.yaml @@ -0,0 +1,22 @@ +name: catkin-noetic-subdir +version: "1.0" +summary: hello world +description: | + A ROS 1 roscpp-based workspace. + +grade: stable +confinement: strict +base: core20 + +apps: + catkin-noetic-subdir: + command: opt/ros/noetic/lib/snapcraft_hello/snapcraft_hello + plugs: [network, network-bind] + extensions: [ros1-noetic] + +parts: + hello: + plugin: catkin + source: . + source-subdir: subdir + build-packages: [g++, make] diff --git a/tests/spread/plugins/v2/snaps/catkin-noetic-subdir/subdir/src/snapcraft_hello/CMakeLists.txt b/tests/spread/plugins/v2/snaps/catkin-noetic-subdir/subdir/src/snapcraft_hello/CMakeLists.txt new file mode 100644 index 0000000000..af5cba06c3 --- /dev/null +++ b/tests/spread/plugins/v2/snaps/catkin-noetic-subdir/subdir/src/snapcraft_hello/CMakeLists.txt @@ -0,0 +1,22 @@ +cmake_minimum_required(VERSION 3.0.2) +project(snapcraft_hello) + +find_package(catkin REQUIRED COMPONENTS + roscpp +) + +catkin_package() + +include_directories( + ${catkin_INCLUDE_DIRS} +) + +add_executable(${PROJECT_NAME} src/hello.cpp) + +target_link_libraries(${PROJECT_NAME} + ${catkin_LIBRARIES} +) + +install(TARGETS ${PROJECT_NAME} + RUNTIME DESTINATION ${CATKIN_PACKAGE_BIN_DESTINATION} +) diff --git a/tests/spread/plugins/v2/snaps/catkin-noetic-subdir/subdir/src/snapcraft_hello/package.xml b/tests/spread/plugins/v2/snaps/catkin-noetic-subdir/subdir/src/snapcraft_hello/package.xml new file mode 100644 index 0000000000..3e92ea7b10 --- /dev/null +++ b/tests/spread/plugins/v2/snaps/catkin-noetic-subdir/subdir/src/snapcraft_hello/package.xml @@ -0,0 +1,13 @@ + + + snapcraft_hello + 0.0.1 + snapcraft test for roscpp + me + GPLv3 + catkin + roscpp + fake_package_that_does_not_exists + roscpp + roscpp + diff --git a/tests/spread/plugins/v2/snaps/catkin-noetic-subdir/subdir/src/snapcraft_hello/src/hello.cpp b/tests/spread/plugins/v2/snaps/catkin-noetic-subdir/subdir/src/snapcraft_hello/src/hello.cpp new file mode 100644 index 0000000000..fd1bf18e1a --- /dev/null +++ b/tests/spread/plugins/v2/snaps/catkin-noetic-subdir/subdir/src/snapcraft_hello/src/hello.cpp @@ -0,0 +1,24 @@ +// Copyright (C) 2022 Canonical Ltd +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License version 3 as +// published by the Free Software Foundation. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#include +#include + +int main(int argc, char * argv[]) +{ + ros::init(argc, argv, "snapcraft_hello"); + std::cout << "hello world" << std::endl; + ros::shutdown(); + return 0; +} diff --git a/tests/spread/plugins/v2/snaps/catkin-tools-noetic-subdir/snap/snapcraft.yaml b/tests/spread/plugins/v2/snaps/catkin-tools-noetic-subdir/snap/snapcraft.yaml new file mode 100644 index 0000000000..6329ce177e --- /dev/null +++ b/tests/spread/plugins/v2/snaps/catkin-tools-noetic-subdir/snap/snapcraft.yaml @@ -0,0 +1,21 @@ +name: catkin-tools-noetic-subdir +version: "1.0" +summary: hello world +description: | + A ROS 1 roscpp-based workspace. + +grade: stable +confinement: strict +base: core20 + +apps: + catkin-tools-noetic-subdir: + command: opt/ros/noetic/lib/snapcraft_hello/snapcraft_hello + plugs: [network, network-bind] + extensions: [ros1-noetic] + +parts: + hello: + plugin: catkin-tools + source: . + build-packages: [g++, make] diff --git a/tests/spread/plugins/v2/snaps/catkin-tools-noetic-subdir/subdir/src/snapcraft_hello/CMakeLists.txt b/tests/spread/plugins/v2/snaps/catkin-tools-noetic-subdir/subdir/src/snapcraft_hello/CMakeLists.txt new file mode 100644 index 0000000000..af5cba06c3 --- /dev/null +++ b/tests/spread/plugins/v2/snaps/catkin-tools-noetic-subdir/subdir/src/snapcraft_hello/CMakeLists.txt @@ -0,0 +1,22 @@ +cmake_minimum_required(VERSION 3.0.2) +project(snapcraft_hello) + +find_package(catkin REQUIRED COMPONENTS + roscpp +) + +catkin_package() + +include_directories( + ${catkin_INCLUDE_DIRS} +) + +add_executable(${PROJECT_NAME} src/hello.cpp) + +target_link_libraries(${PROJECT_NAME} + ${catkin_LIBRARIES} +) + +install(TARGETS ${PROJECT_NAME} + RUNTIME DESTINATION ${CATKIN_PACKAGE_BIN_DESTINATION} +) diff --git a/tests/spread/plugins/v2/snaps/catkin-tools-noetic-subdir/subdir/src/snapcraft_hello/package.xml b/tests/spread/plugins/v2/snaps/catkin-tools-noetic-subdir/subdir/src/snapcraft_hello/package.xml new file mode 100644 index 0000000000..3e92ea7b10 --- /dev/null +++ b/tests/spread/plugins/v2/snaps/catkin-tools-noetic-subdir/subdir/src/snapcraft_hello/package.xml @@ -0,0 +1,13 @@ + + + snapcraft_hello + 0.0.1 + snapcraft test for roscpp + me + GPLv3 + catkin + roscpp + fake_package_that_does_not_exists + roscpp + roscpp + diff --git a/tests/spread/plugins/v2/snaps/catkin-tools-noetic-subdir/subdir/src/snapcraft_hello/src/hello.cpp b/tests/spread/plugins/v2/snaps/catkin-tools-noetic-subdir/subdir/src/snapcraft_hello/src/hello.cpp new file mode 100644 index 0000000000..fd1bf18e1a --- /dev/null +++ b/tests/spread/plugins/v2/snaps/catkin-tools-noetic-subdir/subdir/src/snapcraft_hello/src/hello.cpp @@ -0,0 +1,24 @@ +// Copyright (C) 2022 Canonical Ltd +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License version 3 as +// published by the Free Software Foundation. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#include +#include + +int main(int argc, char * argv[]) +{ + ros::init(argc, argv, "snapcraft_hello"); + std::cout << "hello world" << std::endl; + ros::shutdown(); + return 0; +} diff --git a/tests/spread/plugins/v2/snaps/colcon-subdir/colcon-ros2-foxy-rlcpp-hello/CMakeLists.txt b/tests/spread/plugins/v2/snaps/colcon-subdir/colcon-ros2-foxy-rlcpp-hello/CMakeLists.txt new file mode 100644 index 0000000000..cdc4b78dae --- /dev/null +++ b/tests/spread/plugins/v2/snaps/colcon-subdir/colcon-ros2-foxy-rlcpp-hello/CMakeLists.txt @@ -0,0 +1,34 @@ +cmake_minimum_required(VERSION 3.5) +project(colcon_ros2_rlcpp_hello) + +# Default to C++14 +if(NOT CMAKE_CXX_STANDARD) + set(CMAKE_CXX_STANDARD 14) +endif() + +if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang") + add_compile_options(-Wall -Wextra -Wpedantic) +endif() + +find_package(ament_cmake REQUIRED) +find_package(rclcpp REQUIRED) +find_package(rclcpp_components REQUIRED) +find_package(std_msgs REQUIRED) + +# This package installs libraries without exporting them. +# Export the library path to ensure that the installed libraries are available. +if(NOT WIN32) + ament_environment_hooks( + "${ament_cmake_package_templates_ENVIRONMENT_HOOK_LIBRARY_PATH}" + ) +endif() + +add_executable(colcon_ros2_rlcpp_hello hello.cpp) +target_link_libraries(colcon_ros2_rlcpp_hello) +ament_target_dependencies(colcon_ros2_rlcpp_hello rclcpp class_loader) + +install(TARGETS + colcon_ros2_rlcpp_hello + DESTINATION lib/${PROJECT_NAME}) + +ament_package() diff --git a/tests/spread/plugins/v2/snaps/colcon-subdir/colcon-ros2-foxy-rlcpp-hello/hello.cpp b/tests/spread/plugins/v2/snaps/colcon-subdir/colcon-ros2-foxy-rlcpp-hello/hello.cpp new file mode 100644 index 0000000000..775e37769e --- /dev/null +++ b/tests/spread/plugins/v2/snaps/colcon-subdir/colcon-ros2-foxy-rlcpp-hello/hello.cpp @@ -0,0 +1,28 @@ +// Copyright (C) 2022 Canonical Ltd +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License version 3 as +// published by the Free Software Foundation. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#include +#include "rclcpp/rclcpp.hpp" + +int main(int argc, char * argv[]) +{ + rclcpp::init(argc, argv); + rclcpp::executors::SingleThreadedExecutor exec; + rclcpp::NodeOptions options; + + printf("hello world"); + + rclcpp::shutdown(); + return 0; +} diff --git a/tests/spread/plugins/v2/snaps/colcon-subdir/colcon-ros2-foxy-rlcpp-hello/package.xml b/tests/spread/plugins/v2/snaps/colcon-subdir/colcon-ros2-foxy-rlcpp-hello/package.xml new file mode 100644 index 0000000000..c79c2138b5 --- /dev/null +++ b/tests/spread/plugins/v2/snaps/colcon-subdir/colcon-ros2-foxy-rlcpp-hello/package.xml @@ -0,0 +1,24 @@ + + + + colcon_ros2_rlcpp_hello + 0.0.1 + snapcraft test for rlcpp + me + GPLv3 + + ament_cmake + + rclcpp + rclcpp_components + std_msgs + fake_package_that_does_not_exists + + rclcpp + rclcpp_components + std_msgs + + + ament_cmake + + diff --git a/tests/spread/plugins/v2/snaps/colcon-subdir/snap/snapcraft.yaml b/tests/spread/plugins/v2/snaps/colcon-subdir/snap/snapcraft.yaml new file mode 100644 index 0000000000..e28f8cddc5 --- /dev/null +++ b/tests/spread/plugins/v2/snaps/colcon-subdir/snap/snapcraft.yaml @@ -0,0 +1,23 @@ +name: colcon-subdir +version: "1.0" +summary: hello world +description: | + A ROS2 rlcpp-based workspace. + +grade: stable +confinement: strict +base: core20 + +apps: + colcon-subdir: + command: opt/ros/foxy/bin/ros2 run colcon_ros2_rlcpp_hello colcon_ros2_rlcpp_hello + plugs: [network, network-bind] + extensions: [ros2-foxy] + +parts: + hello: + plugin: colcon + source: . + source-subdir: colcon-ros2-foxy-rlcpp-hello + build-packages: [g++, make] + stage-packages: [ros-foxy-ros2run] diff --git a/tests/spread/plugins/v2/snaps/colcon-subdir/this-package-should-not-be-compiled/CMakeLists.txt b/tests/spread/plugins/v2/snaps/colcon-subdir/this-package-should-not-be-compiled/CMakeLists.txt new file mode 100644 index 0000000000..10cc730be1 --- /dev/null +++ b/tests/spread/plugins/v2/snaps/colcon-subdir/this-package-should-not-be-compiled/CMakeLists.txt @@ -0,0 +1,13 @@ +cmake_minimum_required(VERSION 3.5) +project(this_package_should_not_be_compiled) + +find_package(ament_cmake REQUIRED) +find_package(this_dependency_does_not_exist REQUIRED) + +add_executable(NonCompiledTarget this-file-does-not-exist.cpp) + +install(TARGETS + NonCompiledTarget + DESTINATION lib/${PROJECT_NAME}) + +ament_package() diff --git a/tests/spread/plugins/v2/snaps/colcon-subdir/this-package-should-not-be-compiled/package.xml b/tests/spread/plugins/v2/snaps/colcon-subdir/this-package-should-not-be-compiled/package.xml new file mode 100644 index 0000000000..be6761d20f --- /dev/null +++ b/tests/spread/plugins/v2/snaps/colcon-subdir/this-package-should-not-be-compiled/package.xml @@ -0,0 +1,19 @@ + + + + this_package_should_not_be_compiled + 0.0.1 + this package should not be compiled + me + GPLv3 + + ament_cmake + + this_dependency_does_not_exist + + this_dependency_does_not_exist + + + ament_cmake + + From 4efd2e0b2894a435ba5a82c0523097744fe4e123 Mon Sep 17 00:00:00 2001 From: Sergio Schvezov Date: Fri, 18 Mar 2022 18:37:58 -0300 Subject: [PATCH 158/167] Merge pull request #3425 from jhenstridge/schema-dbus-activation schema: add support for activates-on app property to schema --- schema/snapcraft.json | 14 ++++++++++++- tests/legacy/unit/project/test_schema.py | 25 ++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/schema/snapcraft.json b/schema/snapcraft.json index 0886116031..2c0fe0d0db 100644 --- a/schema/snapcraft.json +++ b/schema/snapcraft.json @@ -616,6 +616,9 @@ "bus-name": [ "daemon" ], + "activates-on": [ + "daemon" + ], "refresh-mode": [ "daemon" ], @@ -673,10 +676,19 @@ }, "bus-name": { "type": "string", - "description": "D-Bus name this service is reachable as (mandatory if daemon=dbus)", + "description": "D-Bus name this service is reachable as", "pattern": "^[A-Za-z0-9/. _#:$-]*$", "validation-failure": "{.instance!r} is not a valid bus name." }, + "activates-on": { + "type": "array", + "description": "dbus interface slots this service activates on", + "minitems": 1, + "uniqueItems": true, + "items": { + "type": "string" + } + }, "desktop": { "type": "string", "description": "path to a desktop file representing the app, relative to the prime directory" diff --git a/tests/legacy/unit/project/test_schema.py b/tests/legacy/unit/project/test_schema.py index 22338d3934..7e6b24f423 100644 --- a/tests/legacy/unit/project/test_schema.py +++ b/tests/legacy/unit/project/test_schema.py @@ -170,6 +170,11 @@ def test_valid_app_daemons(self): "daemon": "simple", "install-mode": "disable", }, + "service17": { + "command": "binary17", + "daemon": "simple", + "activates-on": ["slot1", "slot2"], + }, } Validator(self.data).validate() @@ -198,6 +203,25 @@ def test_invalid_restart_condition(self): ), ) + def test_invalid_activates_on(self): + self.data["apps"] = { + "service1": { + "command": "binary1", + "daemon": "simple", + "activates-on": ["slot1", "slot1"], + }, + } + raised = self.assertRaises( + snapcraft_legacy.yaml_utils.errors.YamlValidationError, + Validator(self.data).validate, + ) + self.assertThat( + str(raised), + Contains( + "The 'apps/service1/activates-on' property does not match the required schema: ['slot1', 'slot1'] has non-unique elements" + ), + ) + def test_missing_required_property_and_missing_adopt_info(self): del self.data["summary"] del self.data["adopt-info"] @@ -240,6 +264,7 @@ def test_invalid_install_mode(self): ("post-stop-command", "binary1 --post-stop"), ("before", ["service1"]), ("after", ["service2"]), + ("activates-on", ["slot1"]), ], ) def test_daemon_dependency(data, option, value): From 2e626fc1516697e36d4f1c8428a0082408ce2d98 Mon Sep 17 00:00:00 2001 From: Sergio Schvezov Date: Fri, 25 Mar 2022 19:51:22 -0300 Subject: [PATCH 159/167] Merge pull request #3542 from Blacksmoke16/crystal-v2-plugin plugins: add crystal v2 plugin --- .../pluginhandler/_part_environment.py | 9 + snapcraft_legacy/plugins/_plugin_finder.py | 1 + snapcraft_legacy/plugins/v2/__init__.py | 1 + snapcraft_legacy/plugins/v2/crystal.py | 187 ++++++++++++++++++ tests/legacy/unit/plugins/v2/test_crystal.py | 140 +++++++++++++ .../unit/project_loader/test_environment.py | 15 ++ .../plugins/v2/build-and-run-hello/task.yaml | 4 + .../plugins/v2/snaps/crystal-hello/hello.cr | 1 + .../plugins/v2/snaps/crystal-hello/shard.lock | 2 + .../plugins/v2/snaps/crystal-hello/shard.yml | 6 + .../snaps/crystal-hello/snap/snapcraft.yaml | 20 ++ 11 files changed, 386 insertions(+) create mode 100644 snapcraft_legacy/plugins/v2/crystal.py create mode 100644 tests/legacy/unit/plugins/v2/test_crystal.py create mode 100644 tests/spread/plugins/v2/snaps/crystal-hello/hello.cr create mode 100644 tests/spread/plugins/v2/snaps/crystal-hello/shard.lock create mode 100644 tests/spread/plugins/v2/snaps/crystal-hello/shard.yml create mode 100644 tests/spread/plugins/v2/snaps/crystal-hello/snap/snapcraft.yaml diff --git a/snapcraft_legacy/internal/pluginhandler/_part_environment.py b/snapcraft_legacy/internal/pluginhandler/_part_environment.py index ea9f1a620c..1c5823605c 100644 --- a/snapcraft_legacy/internal/pluginhandler/_part_environment.py +++ b/snapcraft_legacy/internal/pluginhandler/_part_environment.py @@ -63,6 +63,14 @@ def get_snapcraft_global_environment( else: grade = "" + content_dirs = project._get_provider_content_dirs() + if content_dirs: + content_dirs_envvar = formatting_utils.combine_paths( + content_dirs, prepend="", separator=":" + ) + else: + content_dirs_envvar = "" + return { "SNAPCRAFT_ARCH_TRIPLET": project.arch_triplet, "SNAPCRAFT_EXTENSIONS_DIR": common.get_extensionsdir(), @@ -74,6 +82,7 @@ def get_snapcraft_global_environment( "SNAPCRAFT_PROJECT_GRADE": grade, "SNAPCRAFT_STAGE": project.stage_dir, "SNAPCRAFT_TARGET_ARCH": project.target_arch, + "SNAPCRAFT_CONTENT_DIRS": content_dirs_envvar, } diff --git a/snapcraft_legacy/plugins/_plugin_finder.py b/snapcraft_legacy/plugins/_plugin_finder.py index f111a99fe7..921a26d9d7 100644 --- a/snapcraft_legacy/plugins/_plugin_finder.py +++ b/snapcraft_legacy/plugins/_plugin_finder.py @@ -63,6 +63,7 @@ "cmake": v2.CMakePlugin, "colcon": v2.ColconPlugin, "conda": v2.CondaPlugin, + "crystal": v2.CrystalPlugin, "dump": v2.DumpPlugin, "go": v2.GoPlugin, "make": v2.MakePlugin, diff --git a/snapcraft_legacy/plugins/v2/__init__.py b/snapcraft_legacy/plugins/v2/__init__.py index 864849f2f1..fc15d52e77 100644 --- a/snapcraft_legacy/plugins/v2/__init__.py +++ b/snapcraft_legacy/plugins/v2/__init__.py @@ -26,6 +26,7 @@ from .cmake import CMakePlugin # noqa: F401 from .colcon import ColconPlugin # noqa: F401 from .conda import CondaPlugin # noqa: F401 + from .crystal import CrystalPlugin # noqa: F401 from .dump import DumpPlugin # noqa: F401 from .go import GoPlugin # noqa: F401 from .make import MakePlugin # noqa: F401 diff --git a/snapcraft_legacy/plugins/v2/crystal.py b/snapcraft_legacy/plugins/v2/crystal.py new file mode 100644 index 0000000000..60074e92e9 --- /dev/null +++ b/snapcraft_legacy/plugins/v2/crystal.py @@ -0,0 +1,187 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright (C) 2019 Manas.Tech +# License granted by Canonical Limited +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +"""The Crystal plugin can be used for Crystal projects using `shards`. + +This plugin uses the common plugin keywords as well as those for "sources". +For more information check the 'plugins' topic for the former and the +'sources' topic for the latter. + +Additionally, this plugin uses the following plugin-specific keywords: + + - crystal-channel: + (string, default: latest/stable) + The Snap Store channel to install Crystal from. + + - crystal-build-options + (list of strings, default: '[]') + These options are passed to `shards build`. +""" +import os +import shlex +import shutil +import sys +from typing import Any, Dict, List, Set + +import click + +from snapcraft_legacy import file_utils +from snapcraft_legacy.internal import common, elf, errors + +from snapcraft_legacy.plugins.v2 import PluginV2 + +_CRYSTAL_CHANNEL = "latest/stable" + + +class CrystalPlugin(PluginV2): + @classmethod + def get_schema(cls) -> Dict[str, Any]: + return { + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "object", + "additionalProperties": False, + "properties": { + "crystal-channel": {"type": "string", "default": _CRYSTAL_CHANNEL}, + "crystal-build-options": { + "type": "array", + "uniqueItems": True, + "items": {"type": "string"}, + "default": [], + }, + }, + "required": ["source"], + } + + def get_build_snaps(self) -> Set[str]: + return {f"crystal/{self.options.crystal_channel}"} + + def get_build_packages(self) -> Set[str]: + # See https://github.com/crystal-lang/distribution-scripts/blob/8bc01e26291dc518390129e15df8f757d687871c/docker/ubuntu.Dockerfile#L9 + return { + "git", + "make", + "gcc", + "pkg-config", + "libssl-dev", + "libxml2-dev", + "libyaml-dev", + "libgmp-dev", + "libpcre3-dev", + "libevent-dev", + "libz-dev", + } + + def get_build_environment(self) -> Dict[str, str]: + return dict() + + def get_build_commands(self) -> List[str]: + if self.options.crystal_build_options: + build_options = " ".join( + [shlex.quote(option) for option in self.options.crystal_build_options] + ) + else: + build_options = "" + + env = dict(LANG="C.UTF-8", LC_ALL="C.UTF-8") + env_flags = [f"{key}={value}" for key, value in env.items()] + + return [ + f"shards build --without-development {build_options}", + 'cp -r ./bin "${SNAPCRAFT_PART_INSTALL}"/bin', + " ".join( + [ + "env", + "-i", + *env_flags, + sys.executable, + "-I", + os.path.abspath(__file__), + "stage-runtime-dependencies", + "--part-src", + '"${SNAPCRAFT_PART_SRC}"', + "--part-install", + '"${SNAPCRAFT_PART_INSTALL}"', + "--part-build", + '"${SNAPCRAFT_PART_BUILD}"', + "--arch-triplet", + '"${SNAPCRAFT_ARCH_TRIPLET}"', + "--content-dirs", + '"${SNAPCRAFT_CONTENT_DIRS}"', + ] + ), + ] + + +@click.group() +def plugin_cli(): + pass + + +@plugin_cli.command() +@click.option("--part-src", envvar="SNAPCRAFT_PART_SRC", required=True) +@click.option("--part-install", envvar="SNAPCRAFT_PART_INSTALL", required=True) +@click.option("--part-build", envvar="SNAPCRAFT_PART_BUILD", required=True) +@click.option("--arch-triplet", envvar="SNAPCRAFT_ARCH_TRIPLET", required=True) +@click.option("--content-dirs", envvar="SNAPCRAFT_CONTENT_DIRS", required=True) +def stage_runtime_dependencies( + part_src: str, + part_install: str, + part_build: str, + arch_triplet: str, + content_dirs: str, +): + build_path = os.path.join(part_build, "bin") + install_path = os.path.join(part_install, "bin") + + if not os.path.exists(build_path): + raise errors.SnapcraftEnvironmentError( + "No binaries were built. Ensure the shards.yaml contains valid targets." + ) + + bin_paths = (os.path.join(build_path, b) for b in os.listdir(build_path)) + elf_files = (elf.ElfFile(path=b) for b in bin_paths if elf.ElfFile.is_elf(b)) + os.makedirs(install_path, exist_ok=True) + + # convert colon-delimited paths into a set + if content_dirs == "": + content_dirs_set = set() + else: + content_dirs_set = set(content_dirs.split(":")) + + for elf_file in elf_files: + shutil.copy2( + elf_file.path, os.path.join(install_path, os.path.basename(elf_file.path)), + ) + + elf_dependencies_path = elf_file.load_dependencies( + root_path=part_install, + core_base_path=common.get_installed_snap_path("core20"), + arch_triplet=arch_triplet, + content_dirs=content_dirs_set, + ) + + for elf_dependency_path in elf_dependencies_path: + lib_install_path = os.path.join(part_install, elf_dependency_path[1:]) + os.makedirs(os.path.dirname(lib_install_path), exist_ok=True) + if not os.path.exists(lib_install_path): + file_utils.link_or_copy( + elf_dependency_path, lib_install_path, follow_symlinks=True + ) + + +if __name__ == "__main__": + plugin_cli() diff --git a/tests/legacy/unit/plugins/v2/test_crystal.py b/tests/legacy/unit/plugins/v2/test_crystal.py new file mode 100644 index 0000000000..c419ed91a8 --- /dev/null +++ b/tests/legacy/unit/plugins/v2/test_crystal.py @@ -0,0 +1,140 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright (C) 2020 Canonical Ltd +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import os +import sys + +from testtools import TestCase +from testtools.matchers import Equals + +import snapcraft_legacy.plugins.v2.crystal as crystal +from snapcraft_legacy.plugins.v2.crystal import CrystalPlugin + + +class CrystalPluginTest(TestCase): + def test_schema(self): + self.assertThat( + CrystalPlugin.get_schema(), + Equals( + { + "$schema": "http://json-schema.org/draft-04/schema#", + "additionalProperties": False, + "type": "object", + "properties": { + "crystal-channel": { + "type": "string", + "default": "latest/stable", + }, + "crystal-build-options": { + "type": "array", + "uniqueItems": True, + "items": {"type": "string"}, + "default": [], + }, + }, + "required": ["source"], + } + ), + ) + + def test_get_build_snaps(self): + class Options: + crystal_channel = "latest/edge" + + self.assertThat( + CrystalPlugin(part_name="my-part", options=Options()).get_build_snaps(), + Equals({"crystal/latest/edge"}), + ) + + def test_get_build_packages(self): + self.assertThat( + CrystalPlugin( + part_name="my-part", options=lambda: None + ).get_build_packages(), + Equals( + { + "git", + "make", + "gcc", + "pkg-config", + "libssl-dev", + "libxml2-dev", + "libyaml-dev", + "libgmp-dev", + "libpcre3-dev", + "libevent-dev", + "libz-dev", + } + ), + ) + + def test_get_build_environment(self): + self.assertThat( + CrystalPlugin( + part_name="my-part", options=lambda: None + ).get_build_environment(), + Equals(dict()), + ) + + +def test_get_build_commands(monkeypatch): + class Options: + crystal_channel = "latest/stable" + crystal_build_options = [] + + monkeypatch.setattr(sys, "path", ["", "/test"]) + monkeypatch.setattr(sys, "executable", "/test/python3") + monkeypatch.setattr(crystal, "__file__", "/test/crystal.py") + monkeypatch.setattr(os, "environ", dict()) + + plugin = CrystalPlugin(part_name="my-part", options=Options()) + + assert plugin.get_build_commands() == [ + "shards build --without-development ", + 'cp -r ./bin "${SNAPCRAFT_PART_INSTALL}"/bin', + "env -i LANG=C.UTF-8 LC_ALL=C.UTF-8 /test/python3 -I /test/crystal.py " + 'stage-runtime-dependencies --part-src "${SNAPCRAFT_PART_SRC}" --part-install ' + '"${SNAPCRAFT_PART_INSTALL}" --part-build "${SNAPCRAFT_PART_BUILD}" ' + '--arch-triplet "${SNAPCRAFT_ARCH_TRIPLET}" --content-dirs ' + '"${SNAPCRAFT_CONTENT_DIRS}"', + ] + + +def test_get_build_commands_with_build_options(monkeypatch): + class Options: + crystal_channel = "latest/stable" + crystal_build_options = [ + "--release", + "--static", + "--link-flags=-s -wl,-z,relro,-z,now", + ] + + monkeypatch.setattr(sys, "path", ["", "/test"]) + monkeypatch.setattr(sys, "executable", "/test/python3") + monkeypatch.setattr(crystal, "__file__", "/test/crystal.py") + monkeypatch.setattr(os, "environ", dict()) + + plugin = CrystalPlugin(part_name="my-part", options=Options()) + + assert plugin.get_build_commands() == [ + "shards build --without-development --release --static '--link-flags=-s -wl,-z,relro,-z,now'", + 'cp -r ./bin "${SNAPCRAFT_PART_INSTALL}"/bin', + "env -i LANG=C.UTF-8 LC_ALL=C.UTF-8 /test/python3 -I /test/crystal.py " + 'stage-runtime-dependencies --part-src "${SNAPCRAFT_PART_SRC}" --part-install ' + '"${SNAPCRAFT_PART_INSTALL}" --part-build "${SNAPCRAFT_PART_BUILD}" ' + '--arch-triplet "${SNAPCRAFT_ARCH_TRIPLET}" --content-dirs ' + '"${SNAPCRAFT_CONTENT_DIRS}"', + ] diff --git a/tests/legacy/unit/project_loader/test_environment.py b/tests/legacy/unit/project_loader/test_environment.py index e2aefbff83..13e330614a 100644 --- a/tests/legacy/unit/project_loader/test_environment.py +++ b/tests/legacy/unit/project_loader/test_environment.py @@ -227,6 +227,7 @@ def test_config_env_dedup(self): 'SNAPCRAFT_ARCH_TRIPLET="{}"'.format( project_config.project.arch_triplet ), + 'SNAPCRAFT_CONTENT_DIRS=""', 'SNAPCRAFT_EXTENSIONS_DIR="{}"'.format(common.get_extensionsdir()), 'SNAPCRAFT_PARALLEL_BUILD_COUNT="2"', 'SNAPCRAFT_PART_BUILD="{}/parts/main/build"'.format(self.path), @@ -440,6 +441,20 @@ def test_project_dir(self): env = project_config.parts.build_env_for_part(project_config.parts.all_parts[0]) self.assertThat(env, Contains('SNAPCRAFT_PROJECT_DIR="{}"'.format(self.path))) + def test_content_dirs_default(self): + project_config = self.make_snapcraft_project(self.snapcraft_yaml) + env = project_config.parts.build_env_for_part(project_config.parts.all_parts[0]) + self.assertThat(env, Contains('SNAPCRAFT_CONTENT_DIRS=""')) + + @mock.patch( + "snapcraft_legacy.project._project.Project._get_provider_content_dirs", + return_value=sorted({"/tmp/test1", "/tmp/test2"}), + ) + def test_content_dirs(self, mock_get_content_dirs): + project_config = self.make_snapcraft_project(self.snapcraft_yaml) + env = project_config.parts.build_env_for_part(project_config.parts.all_parts[0]) + self.assertThat(env, Contains('SNAPCRAFT_CONTENT_DIRS="/tmp/test1:/tmp/test2"')) + def test_build_environment(self): self.useFixture(FakeOsRelease()) diff --git a/tests/spread/plugins/v2/build-and-run-hello/task.yaml b/tests/spread/plugins/v2/build-and-run-hello/task.yaml index 09fc0173e3..083e9315e4 100644 --- a/tests/spread/plugins/v2/build-and-run-hello/task.yaml +++ b/tests/spread/plugins/v2/build-and-run-hello/task.yaml @@ -9,6 +9,7 @@ environment: SNAP/cmake_ninja: cmake-hello-ninja SNAP/cmake_subdir: cmake-hello-subdir SNAP/conda: conda-hello + SNAP/crystal: crystal-hello SNAP/make: make-hello SNAP/local_plugin_from_base: local-plugin-from-base-hello SNAP/local_plugin_from_nil: local-plugin-from-nil-hello @@ -51,6 +52,7 @@ restore: | [ -f src/hello.cpp ] && git checkout src/hello.cpp [ -f src/main.rs ] && git checkout src/main.rs [ -f lib/src/lib.rs ] && git checkout lib/src/lib.rs + [ -f hello.cr ] && git checkout hello.cr #shellcheck source=tests/spread/tools/snapcraft-yaml.sh . "$TOOLS_DIR/snapcraft-yaml.sh" @@ -87,6 +89,8 @@ execute: | modified_file=src/main.rs elif [ -f say/src/lib.rs ]; then modified_file=say/src/lib.rs + elif [ -f hello.cr ]; then + modified_file=hello.cr else FATAL "Cannot setup ${SNAP} for rebuilding" fi diff --git a/tests/spread/plugins/v2/snaps/crystal-hello/hello.cr b/tests/spread/plugins/v2/snaps/crystal-hello/hello.cr new file mode 100644 index 0000000000..15aaec76ce --- /dev/null +++ b/tests/spread/plugins/v2/snaps/crystal-hello/hello.cr @@ -0,0 +1 @@ +puts "hello world" diff --git a/tests/spread/plugins/v2/snaps/crystal-hello/shard.lock b/tests/spread/plugins/v2/snaps/crystal-hello/shard.lock new file mode 100644 index 0000000000..4f3e149cca --- /dev/null +++ b/tests/spread/plugins/v2/snaps/crystal-hello/shard.lock @@ -0,0 +1,2 @@ +version: 2.0 +shards: {} diff --git a/tests/spread/plugins/v2/snaps/crystal-hello/shard.yml b/tests/spread/plugins/v2/snaps/crystal-hello/shard.yml new file mode 100644 index 0000000000..9e8a6669c8 --- /dev/null +++ b/tests/spread/plugins/v2/snaps/crystal-hello/shard.yml @@ -0,0 +1,6 @@ +name: hello +version: 0.1.0 + +targets: + hello: + main: hello.cr diff --git a/tests/spread/plugins/v2/snaps/crystal-hello/snap/snapcraft.yaml b/tests/spread/plugins/v2/snaps/crystal-hello/snap/snapcraft.yaml new file mode 100644 index 0000000000..8ebe46dafc --- /dev/null +++ b/tests/spread/plugins/v2/snaps/crystal-hello/snap/snapcraft.yaml @@ -0,0 +1,20 @@ +name: crystal-hello +version: "1.0" +summary: test the crystal plugin +description: | + This is a basic crystal snap. It just prints a hello world. + If you want to add other functionalities to this snap, please don't. + Make a new one. + +grade: devel +base: core20 +confinement: strict + +apps: + crystal-hello: + command: bin/hello + +parts: + hello: + plugin: crystal + source: . From 75bd46c3168690a91d6639162c8603832f42ba32 Mon Sep 17 00:00:00 2001 From: Sergio Schvezov Date: Sat, 30 Apr 2022 16:21:08 -0300 Subject: [PATCH 160/167] commands: upload This removes functionality for delta uploads, to be reintroduced in a future revision. Signed-off-by: Sergio Schvezov --- snapcraft/cli.py | 3 +- snapcraft/commands/__init__.py | 2 + snapcraft/commands/store/client.py | 90 +++ snapcraft/commands/upload.py | 116 ++++ snapcraft_legacy/__init__.py | 1 - snapcraft_legacy/_store.py | 195 +----- snapcraft_legacy/cli/store.py | 50 +- snapcraft_legacy/storeapi/_dashboard_api.py | 47 +- snapcraft_legacy/storeapi/_store_client.py | 28 +- snapcraft_legacy/storeapi/_up_down_client.py | 29 - snapcraft_legacy/storeapi/_upload.py | 81 --- tests/legacy/unit/__init__.py | 2 +- tests/legacy/unit/commands/__init__.py | 19 - .../commands/test_edit_validation_sets.py | 4 +- tests/legacy/unit/commands/test_list_keys.py | 1 - .../commands/test_list_validation_sets.py | 2 +- .../unit/commands/test_set_default_track.py | 1 + tests/legacy/unit/commands/test_sign_build.py | 18 +- tests/legacy/unit/commands/test_upload.py | 570 ------------------ tests/legacy/unit/plugins/v2/test_conda.py | 2 +- tests/legacy/unit/store/test_store_client.py | 145 ----- .../legacy/unit/yaml_utils/test_yaml_utils.py | 2 +- tests/unit/commands/store/test_client.py | 211 +++++++ tests/unit/commands/test_upload.py | 132 ++++ 24 files changed, 576 insertions(+), 1175 deletions(-) create mode 100644 snapcraft/commands/upload.py delete mode 100644 snapcraft_legacy/storeapi/_up_down_client.py delete mode 100644 snapcraft_legacy/storeapi/_upload.py delete mode 100644 tests/legacy/unit/commands/test_upload.py create mode 100644 tests/unit/commands/test_upload.py diff --git a/snapcraft/cli.py b/snapcraft/cli.py index f2cd5bc0fb..5be813a138 100644 --- a/snapcraft/cli.py +++ b/snapcraft/cli.py @@ -67,6 +67,7 @@ commands.StoreReleaseCommand, commands.StoreCloseCommand, commands.StoreStatusCommand, + commands.StoreUploadCommand, ], ), craft_cli.CommandGroup( @@ -104,7 +105,7 @@ def get_dispatcher() -> craft_cli.Dispatcher: legacy.legacy_run() # set lib loggers to debug level so that all messages are sent to Emitter - for lib_name in ("craft_parts", "craft_providers"): + for lib_name in ("craft_parts", "craft_providers", "craft_store"): logger = logging.getLogger(lib_name) logger.setLevel(logging.DEBUG) diff --git a/snapcraft/commands/__init__.py b/snapcraft/commands/__init__.py index 5cd6740798..f4c78a7a40 100644 --- a/snapcraft/commands/__init__.py +++ b/snapcraft/commands/__init__.py @@ -44,6 +44,7 @@ StoreRegisterCommand, ) from .status import StoreListTracksCommand, StoreStatusCommand, StoreTracksCommand +from .upload import StoreUploadCommand from .version import VersionCommand __all__ = [ @@ -67,6 +68,7 @@ "StoreReleaseCommand", "StoreStatusCommand", "StoreTracksCommand", + "StoreUploadCommand", "StoreWhoAmICommand", "ExtensionsCommand", "ListExtensionsCommand", diff --git a/snapcraft/commands/store/client.py b/snapcraft/commands/store/client.py index fb87a1196c..4e2054626c 100644 --- a/snapcraft/commands/store/client.py +++ b/snapcraft/commands/store/client.py @@ -18,6 +18,7 @@ import os import platform +import time from datetime import timedelta from typing import Any, Dict, Optional, Sequence, Tuple @@ -31,6 +32,15 @@ _TESTING_ENV_PREFIXES = ["TRAVIS", "AUTOPKGTEST_TMP"] +_POLL_DELAY = 1 +_HUMAN_STATUS = { + "being_processed": "processing", + "ready_to_release": "ready to release!", + "need_manual_review": "will need manual review", + "processing_upload_delta_error": "error while processing delta", + "processing_error": "error while processing", +} + def build_user_agent( version=__version__, os_platform: utils.OSPlatform = utils.get_os_platform() @@ -307,3 +317,83 @@ def close(self, snap_id: str, channel: str) -> None: self._base_url + f"/dev/api/snaps/{snap_id}/close", json={"channels": [channel]}, ) + + def verify_upload( + self, + *, + snap_name: str, + ) -> None: + """Verify if this account can perform an upload for this snap_name.""" + data = { + "name": snap_name, + "dry_run": True, + } + self.request( + "POST", + self._base_url + "/dev/api/snap-push/", + json=data, + headers={ + "Accept": "application/json", + }, + ) + + def notify_upload( + self, + *, + snap_name: str, + upload_id: str, + snap_file_size: int, + built_at: Optional[str], + channels: Optional[Sequence[str]], + ) -> int: + """Notify an upload to the Snap Store. + + :param snap_name: name of the snap + :param upload_id: the upload_id to register with the Snap Store + :param snap_file_size: the file size of the uploaded snap + :param built_at: the build timestamp for this build + :param channels: the channels to release to after being accepted into the Snap Store + :returns: the snap's processed revision + """ + data = { + "name": snap_name, + "series": constants.DEFAULT_SERIES, + "updown_id": upload_id, + "binary_filesize": snap_file_size, + "source_uploaded": False, + } + if built_at is not None: + data["built_at"] = built_at + if channels is not None: + data["channels"] = channels + + response = self.request( + "POST", + self._base_url + "/dev/api/snap-push/", + json=data, + headers={ + "Accept": "application/json", + }, + ) + + status_url = response.json()["status_details_url"] + while True: + response = self.request("GET", status_url) + status = response.json() + human_status = _HUMAN_STATUS.get(status["code"], status["code"]) + emit.progress(f"Status: {human_status}") + + if status.get("processed", False): + if status.get("errors"): + error_messages = [ + e["message"] for e in status["errors"] if "message" in e + ] + error_string = "\n".join([f"- {e}" for e in error_messages]) + raise errors.SnapcraftError( + f"Issues while processing snap:\n{error_string}" + ) + break + + time.sleep(_POLL_DELAY) + + return status["revision"] diff --git a/snapcraft/commands/upload.py b/snapcraft/commands/upload.py new file mode 100644 index 0000000000..1559e9bf5c --- /dev/null +++ b/snapcraft/commands/upload.py @@ -0,0 +1,116 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright 2022 Canonical Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +"""Snapcraft Store uploading related commands.""" + +import pathlib +import textwrap +from typing import TYPE_CHECKING, List, Optional + +from craft_cli import BaseCommand, emit +from craft_cli.errors import ArgumentParsingError +from overrides import overrides +from requests_toolbelt import MultipartEncoder, MultipartEncoderMonitor + +from snapcraft import utils +from snapcraft_legacy._store import get_data_from_snap_file + +from . import store + +if TYPE_CHECKING: + import argparse + + +class StoreUploadCommand(BaseCommand): + """Command to upload a snap to the Snap Store.""" + + name = "upload" + help_msg = "Login to the Snap Store" + overview = textwrap.dedent( + """ + By passing --release with a comma separated list of channels the snap would + be released to the selected channels if the store review passes for this + . + + This operation will block until the store finishes processing this . + + If --release is used, the channel map will be displayed after the operation + takes place. + """ + ) + + @overrides + def fill_parser(self, parser: "argparse.ArgumentParser") -> None: + parser.add_argument( + "snap_file", + metavar="", + type=str, + help="Snap to upload", + ) + parser.add_argument( + "--release", + metavar="", + dest="channels", + type=str, + default=None, + help="Optional comma separated list of channels to release to", + ) + + @overrides + def run(self, parsed_args): + snap_file = pathlib.Path(parsed_args.snap_file) + if not snap_file.exists() or not snap_file.is_file(): + raise ArgumentParsingError(f"{str(snap_file)!r} is not a valid file") + + channels: Optional[List[str]] = None + if parsed_args.channels: + channels = parsed_args.channels.split(",") + + client = store.StoreClientCLI() + + snap_yaml = get_data_from_snap_file(snap_file) + snap_name = snap_yaml["name"] + built_at = snap_yaml.get("snapcraft-started-at") + + client.verify_upload(snap_name=snap_name) + + upload_id = client.store_client.upload_file( + filepath=snap_file, monitor_callback=create_callback + ) + + revision = client.notify_upload( + snap_name=snap_name, + upload_id=upload_id, + built_at=built_at, + channels=channels, + snap_file_size=snap_file.stat().st_size, + ) + + message = f"Revision {revision!r} created for {snap_name!r}" + if channels: + message += f" and released to {utils.humanize_list(channels, 'and')}" + emit.message(message) + + +def create_callback(encoder: MultipartEncoder): + """Create a callback suitable for upload_file.""" + with emit.progress_bar("Uploading...", encoder.len, delta=False) as progress: + + def progress_callback(monitor: MultipartEncoderMonitor): + progress.advance(monitor.bytes_read) + + return progress_callback diff --git a/snapcraft_legacy/__init__.py b/snapcraft_legacy/__init__.py index aec5daf4a7..60eaf11c4a 100644 --- a/snapcraft_legacy/__init__.py +++ b/snapcraft_legacy/__init__.py @@ -362,7 +362,6 @@ def _get_version(): register_key, sign_build, status, - upload, upload_metadata, validate, ) diff --git a/snapcraft_legacy/_store.py b/snapcraft_legacy/_store.py index 9920ef369d..7c435c8d1f 100644 --- a/snapcraft_legacy/_store.py +++ b/snapcraft_legacy/_store.py @@ -15,7 +15,6 @@ # along with this program. If not, see . import contextlib -import hashlib import json import logging import operator @@ -34,35 +33,28 @@ from tabulate import tabulate from snapcraft_legacy import storeapi, yaml_utils + # Ideally we would move stuff into more logical components from snapcraft_legacy.cli import echo from snapcraft_legacy.file_utils import ( - calculate_sha3_384, get_host_tool_path, get_snap_tool_path, ) -from snapcraft_legacy.internal import cache, deltas -from snapcraft_legacy.internal.deltas.errors import ( - DeltaGenerationError, - DeltaGenerationTooBigError, -) from snapcraft_legacy.internal.errors import ( SnapDataExtractionError, SnapcraftEnvironmentError, - ToolMissingError, ) from snapcraft_legacy.storeapi.constants import DEFAULT_SERIES from snapcraft_legacy.storeapi.metrics import MetricsFilter, MetricsResults if TYPE_CHECKING: - from snapcraft_legacy.storeapi._status_tracker import StatusTracker from snapcraft_legacy.storeapi.v2.releases import Releases logger = logging.getLogger(__name__) -def _get_data_from_snap_file(snap_path): +def get_data_from_snap_file(snap_path): with tempfile.TemporaryDirectory() as temp_dir: unsquashfs_path = get_snap_tool_path("unsquashfs") try: @@ -373,31 +365,6 @@ def register( ) -> None: super().register(snap_name=snap_name, is_private=is_private, store_id=store_id) - @_login_wrapper - @_register_wrapper - def upload( - self, - *, - snap_name: str, - snap_filename: str, - built_at: Optional[str] = None, - channels: Optional[List[str]] = None, - delta_format: Optional[str] = None, - source_hash: Optional[str] = None, - target_hash: Optional[str] = None, - delta_hash: Optional[str] = None, - ) -> "StatusTracker": - return super().upload( - snap_name=snap_name, - snap_filename=snap_filename, - built_at=built_at, - channels=channels, - delta_format=delta_format, - source_hash=source_hash, - target_hash=target_hash, - delta_hash=delta_hash, - ) - def list_registered(): account_info = StoreClientCLI().get_account_information() @@ -592,7 +559,7 @@ def sign_build(snap_filename, key_name=None, local=False): if not os.path.exists(snap_filename): raise FileNotFoundError("The file {!r} does not exist.".format(snap_filename)) - snap_yaml = _get_data_from_snap_file(snap_filename) + snap_yaml = get_data_from_snap_file(snap_filename) snap_name = snap_yaml["name"] grade = snap_yaml.get("grade", "stable") @@ -643,7 +610,7 @@ def upload_metadata(snap_filename, force): logger.debug("Uploading metadata to the Store (force=%s)", force) # get the metadata from the snap - snap_yaml = _get_data_from_snap_file(snap_filename) + snap_yaml = get_data_from_snap_file(snap_filename) metadata = { "summary": snap_yaml["summary"], "description": snap_yaml["description"], @@ -670,160 +637,6 @@ def upload_metadata(snap_filename, force): logger.info("The metadata has been uploaded") -def upload(snap_filename, release_channels=None) -> Tuple[str, int]: - """Upload a snap_filename to the store. - - If a cached snap is available, a delta will be generated from - the cached snap to the new target snap and uploaded instead. In the - case of a delta processing or upload failure, upload will fall back to - uploading the full snap. - - If release_channels is defined it also releases it to those channels if the - store deems the uploaded snap as ready to release. - """ - snap_yaml = _get_data_from_snap_file(snap_filename) - snap_name = snap_yaml["name"] - built_at = snap_yaml.get("snapcraft-started-at") - - logger.debug( - "Run upload precheck and verify cached data for {!r}.".format(snap_filename) - ) - store_client = StoreClientCLI() - store_client.upload_precheck(snap_name=snap_name) - - snap_cache = cache.SnapCache(project_name=snap_name) - - try: - deb_arch = snap_yaml["architectures"][0] - except KeyError: - deb_arch = "all" - - source_snap = snap_cache.get(deb_arch=deb_arch) - sha3_384_available = hasattr(hashlib, "sha3_384") - - result: Optional[Dict[str, Any]] = None - if sha3_384_available and source_snap: - try: - result = _upload_delta( - store_client, - snap_name=snap_name, - snap_filename=snap_filename, - source_snap=source_snap, - built_at=built_at, - channels=release_channels, - ) - except storeapi.errors.StoreDeltaApplicationError as e: - logger.warning( - "Error generating delta: {}\n" - "Falling back to uploading full snap...".format(str(e)) - ) - except storeapi.errors.StoreUploadError as upload_error: - logger.warning( - "Unable to upload delta to store: {}\n" - "Falling back to uploading full snap...".format(upload_error.error_list) - ) - - if result is None: - result = _upload_snap( - store_client, - snap_name=snap_name, - snap_filename=snap_filename, - built_at=built_at, - channels=release_channels, - ) - - snap_cache.cache(snap_filename=snap_filename) - snap_cache.prune(deb_arch=deb_arch, keep_hash=calculate_sha3_384(snap_filename)) - - return snap_name, result["revision"] - - -def _upload_snap( - store_client, - *, - snap_name: str, - snap_filename: str, - built_at: str, - channels: Optional[List[str]], -) -> Dict[str, Any]: - tracker = store_client.upload( - snap_name=snap_name, - snap_filename=snap_filename, - built_at=built_at, - channels=channels, - ) - result = tracker.track() - tracker.raise_for_code() - return result - - -def _upload_delta( - store_client, - *, - snap_name: str, - snap_filename: str, - source_snap: str, - built_at: str, - channels: Optional[List[str]] = None, -) -> Dict[str, Any]: - delta_format = "xdelta3" - logger.debug("Found cached source snap {}.".format(source_snap)) - target_snap = os.path.join(os.getcwd(), snap_filename) - - try: - xdelta_generator = deltas.XDelta3Generator( - source_path=source_snap, target_path=target_snap - ) - delta_filename = xdelta_generator.make_delta() - except (DeltaGenerationError, DeltaGenerationTooBigError, ToolMissingError) as e: - raise storeapi.errors.StoreDeltaApplicationError(str(e)) - - snap_hashes = { - "source_hash": calculate_sha3_384(source_snap), - "target_hash": calculate_sha3_384(target_snap), - "delta_hash": calculate_sha3_384(delta_filename), - } - - try: - logger.debug("Uploading delta {!r}.".format(delta_filename)) - delta_tracker = store_client.upload( - snap_name=snap_name, - snap_filename=delta_filename, - built_at=built_at, - channels=channels, - delta_format=delta_format, - source_hash=snap_hashes["source_hash"], - target_hash=snap_hashes["target_hash"], - delta_hash=snap_hashes["delta_hash"], - ) - result = delta_tracker.track() - delta_tracker.raise_for_code() - except storeapi.errors.StoreReviewError as e: - if e.code == "processing_upload_delta_error": - raise storeapi.errors.StoreDeltaApplicationError(str(e)) - else: - raise - except craft_store.errors.StoreServerError as store_error: - raise storeapi.errors.StoreUploadError(snap_name, store_error.response) - finally: - if os.path.isfile(delta_filename): - try: - os.remove(delta_filename) - except OSError: - logger.warning("Unable to remove delta {}.".format(delta_filename)) - return result - - -def _get_text_for_opened_channels(opened_channels): - if len(opened_channels) == 1: - return "The {!r} channel is now open.".format(opened_channels[0]) - else: - channels = ("{!r}".format(channel) for channel in opened_channels[:-1]) - return "The {} and {!r} channels are now open.".format( - ", ".join(channels), opened_channels[-1] - ) - - def _get_text_for_channel(channel): if "progressive" in channel: notes = "progressive ({}%)".format(channel["progressive"]["percentage"]) diff --git a/snapcraft_legacy/cli/store.py b/snapcraft_legacy/cli/store.py index b125bb7d08..dbcae5c6ae 100644 --- a/snapcraft_legacy/cli/store.py +++ b/snapcraft_legacy/cli/store.py @@ -25,12 +25,11 @@ from tabulate import tabulate import snapcraft_legacy -from snapcraft_legacy import formatting_utils, storeapi +from snapcraft_legacy import storeapi from snapcraft_legacy._store import StoreClientCLI from snapcraft_legacy.storeapi import metrics as metrics_module from . import echo from ._metrics import convert_metrics_to_table -from ._review import review_snap @click.group() @@ -74,53 +73,6 @@ def _human_readable_acls(store_client: storeapi.StoreClient) -> str: ) -@storecli.command() -@click.option( - "--release", - metavar="", - help="Optional comma separated list of channels to release ", -) -@click.argument( - "snap-file", - metavar="", - type=click.Path(exists=True, readable=True, resolve_path=True, dir_okay=False), -) -def upload(snap_file, release): - """Upload to the store. - - By passing --release with a comma separated list of channels the snap would - be released to the selected channels if the store review passes for this - . - - This operation will block until the store finishes processing this - . - - If --release is used, the channel map will be displayed after the - operation takes place. - - \b - Examples: - snapcraft upload my-snap_0.1_amd64.snap - snapcraft upload my-snap_0.2_amd64.snap --release edge - snapcraft upload my-snap_0.3_amd64.snap --release candidate,beta - """ - click.echo("Preparing to upload {!r}.".format(os.path.basename(snap_file))) - if release: - channel_list = release.split(",") - click.echo( - "After uploading, the resulting snap revision will be released to " - "{} when it passes the Snap Store review." - "".format(formatting_utils.humanize_list(channel_list, "and")) - ) - else: - channel_list = None - - review_snap(snap_file=snap_file) - snap_name, snap_revision = snapcraft_legacy.upload(snap_file, channel_list) - - echo.info("Revision {!r} of {!r} created.".format(snap_revision, snap_name)) - - @storecli.command("upload-metadata") @click.option( "--force", diff --git a/snapcraft_legacy/storeapi/_dashboard_api.py b/snapcraft_legacy/storeapi/_dashboard_api.py index cf66abece7..ad544e31e1 100644 --- a/snapcraft_legacy/storeapi/_dashboard_api.py +++ b/snapcraft_legacy/storeapi/_dashboard_api.py @@ -23,9 +23,8 @@ import requests from simplejson.scanner import JSONDecodeError -from . import _metadata, constants, errors, metrics +from . import _metadata, errors, metrics from ._requests import Requests -from ._status_tracker import StatusTracker from .v2 import releases, validation_sets, whoami logger = logging.getLogger(__name__) @@ -131,50 +130,6 @@ def snap_upload_precheck(self, snap_name) -> None: snap_name, store_error.response ) from store_error - def snap_upload_metadata( - self, - snap_name, - updown_data, - delta_format=None, - delta_hash=None, - source_hash=None, - target_hash=None, - built_at=None, - channels: Optional[List[str]] = None, - ) -> StatusTracker: - data = { - "name": snap_name, - "series": constants.DEFAULT_SERIES, - "updown_id": updown_data["upload_id"], - "binary_filesize": updown_data["binary_filesize"], - "source_uploaded": updown_data["source_uploaded"], - } - - if delta_format: - data["delta_format"] = delta_format - data["delta_hash"] = delta_hash - data["source_hash"] = source_hash - data["target_hash"] = target_hash - if built_at is not None: - data["built_at"] = built_at - if channels is not None: - data["channels"] = channels - try: - response = self.post( - "/dev/api/snap-push/", - json=data, - headers={ - "Content-Type": "application/json", - "Accept": "application/json", - }, - ) - except craft_store.errors.StoreServerError as store_error: - raise errors.StoreUploadError( - data["name"], store_error.response - ) from store_error - - return StatusTracker(response.json()["status_details_url"]) - def upload_metadata(self, snap_id, snap_name, metadata, force): """Upload the metadata to SCA.""" metadata_handler = _metadata.StoreMetadataHandler( diff --git a/snapcraft_legacy/storeapi/_store_client.py b/snapcraft_legacy/storeapi/_store_client.py index 9d8178b584..fe365177a3 100644 --- a/snapcraft_legacy/storeapi/_store_client.py +++ b/snapcraft_legacy/storeapi/_store_client.py @@ -24,10 +24,9 @@ import requests from snapcraft_legacy.internal.indicators import download_requests_stream -from . import _upload, agent, constants, errors, metrics +from . import agent, constants, errors, metrics from ._dashboard_api import DashboardAPI from ._snap_api import SnapAPI -from ._up_down_client import UpDownClient from .constants import DEFAULT_SERIES from .v2 import releases, validation_sets, whoami @@ -77,7 +76,6 @@ def __init__(self, ephemeral=False) -> None: self.snap = SnapAPI(self.client) self.dashboard = DashboardAPI(self.auth_client) - self._updown = UpDownClient(self.client) @staticmethod def use_candid() -> bool: @@ -163,30 +161,6 @@ def upload_precheck(self, snap_name): def push_snap_build(self, snap_id, snap_build): return self.dashboard.push_snap_build(snap_id, snap_build) - def upload( - self, - snap_name, - snap_filename, - delta_format=None, - source_hash=None, - target_hash=None, - delta_hash=None, - built_at=None, - channels: Optional[List[str]] = None, - ): - updown_data = _upload.upload_files(snap_filename, self._updown) - - return self.dashboard.snap_upload_metadata( - snap_name, - updown_data, - delta_format=delta_format, - source_hash=source_hash, - target_hash=target_hash, - delta_hash=delta_hash, - built_at=built_at, - channels=channels, - ) - def release( self, snap_name, diff --git a/snapcraft_legacy/storeapi/_up_down_client.py b/snapcraft_legacy/storeapi/_up_down_client.py deleted file mode 100644 index 35b37e2b9c..0000000000 --- a/snapcraft_legacy/storeapi/_up_down_client.py +++ /dev/null @@ -1,29 +0,0 @@ -import os -from urllib.parse import urljoin - -import requests - -from ._requests import Requests -from . import constants - - -class UpDownClient(Requests): - """The Up/Down server provide upload/download snap capabilities.""" - - def __init__(self, client) -> None: - self._client = client - self._root_url = os.getenv("STORE_UPLOAD_URL", constants.STORE_UPLOAD_URL) - - def _request(self, method, urlpath, **kwargs) -> requests.Response: - url = urljoin(self._root_url, urlpath) - return self._client.request(method, url, **kwargs) - - def upload(self, monitor): - return self.post( - "/unscanned-upload/", - data=monitor, - headers={ - "Content-Type": monitor.content_type, - "Accept": "application/json", - }, - ) diff --git a/snapcraft_legacy/storeapi/_upload.py b/snapcraft_legacy/storeapi/_upload.py deleted file mode 100644 index 24dc3e60f7..0000000000 --- a/snapcraft_legacy/storeapi/_upload.py +++ /dev/null @@ -1,81 +0,0 @@ -# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- -# -# Copyright (C) 2016 Canonical Ltd -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License version 3 as -# published by the Free Software Foundation. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -import functools -import logging -import os - -from progressbar import Bar, Percentage, ProgressBar -from requests_toolbelt import MultipartEncoder, MultipartEncoderMonitor - -from snapcraft_legacy.storeapi.errors import StoreUpDownError - -logger = logging.getLogger(__name__) - - -def _update_progress_bar(progress_bar, maximum_value, monitor): - if monitor.bytes_read <= maximum_value: - progress_bar.update(monitor.bytes_read) - - -def upload_files(binary_filename, updown_client): - """Upload a binary file to the Store. - - Submit a file to the Store upload service and return the - corresponding upload_id. - """ - try: - binary_file_size = os.path.getsize(binary_filename) - binary_file = open(binary_filename, "rb") - encoder = MultipartEncoder( - fields={"binary": ("filename", binary_file, "application/octet-stream")} - ) - - # Create a progress bar that looks like: Uploading foo [== ] 50% - progress_bar = ProgressBar( - widgets=[ - "Pushing {!r} ".format(os.path.basename(binary_filename)), - Bar(marker="=", left="[", right="]"), - " ", - Percentage(), - ], - maxval=os.path.getsize(binary_filename), - ) - progress_bar.start() - # Create a monitor for this upload, so that progress can be displayed - monitor = MultipartEncoderMonitor( - encoder, - functools.partial(_update_progress_bar, progress_bar, binary_file_size), - ) - - # Begin upload - response = updown_client.upload(monitor) - - # Make sure progress bar shows 100% complete - progress_bar.finish() - finally: - # Close the open file - binary_file.close() - - if not response.ok: - raise StoreUpDownError(response) - - response_data = response.json() - return { - "upload_id": response_data["upload_id"], - "binary_filesize": binary_file_size, - "source_uploaded": False, - } diff --git a/tests/legacy/unit/__init__.py b/tests/legacy/unit/__init__.py index 81192da000..96fac5e843 100644 --- a/tests/legacy/unit/__init__.py +++ b/tests/legacy/unit/__init__.py @@ -28,8 +28,8 @@ import testtools from snapcraft_legacy.internal import common, steps -from tests.legacy import fake_servers, fixture_setup from tests.file_utils import get_snapcraft_path +from tests.legacy import fake_servers, fixture_setup from tests.legacy.unit.part_loader import load_part diff --git a/tests/legacy/unit/commands/__init__.py b/tests/legacy/unit/commands/__init__.py index 32d89ad0bb..2eaa69ec9c 100644 --- a/tests/legacy/unit/commands/__init__.py +++ b/tests/legacy/unit/commands/__init__.py @@ -335,25 +335,6 @@ def setUp(self): ) self.useFixture(self.fake_store_get_releases) - # Uploading - self.mock_tracker = mock.Mock(storeapi._status_tracker.StatusTracker) - self.mock_tracker.track.return_value = { - "code": "ready_to_release", - "processed": True, - "can_release": True, - "url": "/fake/url", - "revision": 19, - } - self.fake_store_upload_precheck = fixtures.MockPatchObject( - storeapi.StoreClient, "upload_precheck" - ) - self.useFixture(self.fake_store_upload_precheck) - - self.fake_store_upload = fixtures.MockPatchObject( - storeapi.StoreClient, "upload", return_value=self.mock_tracker - ) - self.useFixture(self.fake_store_upload) - # Mock the snap command, pass through a select few. self.fake_check_output = fixtures.MockPatch( "subprocess.check_output", side_effect=mock_check_output diff --git a/tests/legacy/unit/commands/test_edit_validation_sets.py b/tests/legacy/unit/commands/test_edit_validation_sets.py index 1692e8e264..76384d4c23 100644 --- a/tests/legacy/unit/commands/test_edit_validation_sets.py +++ b/tests/legacy/unit/commands/test_edit_validation_sets.py @@ -15,13 +15,13 @@ # along with this program. If not, see . import json -from typing import Dict, Any +from typing import Any, Dict from unittest import mock import pytest -from snapcraft_legacy.storeapi.v2 import validation_sets from snapcraft_legacy.storeapi import StoreClient +from snapcraft_legacy.storeapi.v2 import validation_sets @pytest.fixture diff --git a/tests/legacy/unit/commands/test_list_keys.py b/tests/legacy/unit/commands/test_list_keys.py index 045e481bdd..cb808b5641 100644 --- a/tests/legacy/unit/commands/test_list_keys.py +++ b/tests/legacy/unit/commands/test_list_keys.py @@ -19,7 +19,6 @@ import fixtures from testtools.matchers import Contains, Equals - from . import FAKE_UNAUTHORIZED_ERROR, FakeStoreCommandsBaseTestCase, get_sample_key diff --git a/tests/legacy/unit/commands/test_list_validation_sets.py b/tests/legacy/unit/commands/test_list_validation_sets.py index 55537faf8b..8901139971 100644 --- a/tests/legacy/unit/commands/test_list_validation_sets.py +++ b/tests/legacy/unit/commands/test_list_validation_sets.py @@ -19,8 +19,8 @@ import pytest -from snapcraft_legacy.storeapi.v2 import validation_sets from snapcraft_legacy.storeapi import StoreClient +from snapcraft_legacy.storeapi.v2 import validation_sets @pytest.fixture diff --git a/tests/legacy/unit/commands/test_set_default_track.py b/tests/legacy/unit/commands/test_set_default_track.py index 91966e15f0..6a04e177e4 100644 --- a/tests/legacy/unit/commands/test_set_default_track.py +++ b/tests/legacy/unit/commands/test_set_default_track.py @@ -18,6 +18,7 @@ from testtools.matchers import Contains, Equals from snapcraft_legacy import storeapi + from . import FAKE_UNAUTHORIZED_ERROR, FakeStoreCommandsBaseTestCase diff --git a/tests/legacy/unit/commands/test_sign_build.py b/tests/legacy/unit/commands/test_sign_build.py index 507250c168..c02078d901 100644 --- a/tests/legacy/unit/commands/test_sign_build.py +++ b/tests/legacy/unit/commands/test_sign_build.py @@ -23,7 +23,7 @@ import tests.legacy from snapcraft_legacy import internal, storeapi -from . import FakeStoreCommandsBaseTestCase, get_sample_key, mock_check_output +from . import FakeStoreCommandsBaseTestCase, mock_check_output class SnapTest(fixtures.TempDir): @@ -76,7 +76,7 @@ def test_sign_build_invalid_snap(self): self.assertThat(str(raised), Contains("Cannot read data from snap")) - @mock.patch("snapcraft_legacy._store._get_data_from_snap_file") + @mock.patch("snapcraft_legacy._store.get_data_from_snap_file") def test_sign_build_missing_account_info( self, mock_get_snap_data, @@ -99,7 +99,7 @@ def test_sign_build_missing_account_info( ), ) - @mock.patch("snapcraft_legacy._store._get_data_from_snap_file") + @mock.patch("snapcraft_legacy._store.get_data_from_snap_file") def test_sign_build_no_usable_keys( self, mock_get_snap_data, @@ -128,7 +128,7 @@ def test_sign_build_no_usable_keys( self.assertThat(snap_build_path, Not(FileExists())) @mock.patch.object(storeapi._dashboard_api.DashboardAPI, "get_account_information") - @mock.patch("snapcraft_legacy._store._get_data_from_snap_file") + @mock.patch("snapcraft_legacy._store.get_data_from_snap_file") def test_sign_build_no_usable_named_key( self, mock_get_snap_data, @@ -161,7 +161,7 @@ def test_sign_build_no_usable_named_key( self.assertThat(snap_build_path, Not(FileExists())) @mock.patch.object(storeapi._dashboard_api.DashboardAPI, "get_account_information") - @mock.patch("snapcraft_legacy._store._get_data_from_snap_file") + @mock.patch("snapcraft_legacy._store.get_data_from_snap_file") def test_sign_build_unregistered_key( self, mock_get_snap_data, @@ -200,7 +200,7 @@ def test_sign_build_unregistered_key( self.assertThat(snap_build_path, Not(FileExists())) @mock.patch.object(storeapi._dashboard_api.DashboardAPI, "get_account_information") - @mock.patch("snapcraft_legacy._store._get_data_from_snap_file") + @mock.patch("snapcraft_legacy._store.get_data_from_snap_file") def test_sign_build_locally_successfully( self, mock_get_snap_data, @@ -242,7 +242,7 @@ def test_sign_build_locally_successfully( ) @mock.patch.object(storeapi._dashboard_api.DashboardAPI, "get_account_information") - @mock.patch("snapcraft_legacy._store._get_data_from_snap_file") + @mock.patch("snapcraft_legacy._store.get_data_from_snap_file") def test_sign_build_missing_grade( self, mock_get_snap_data, @@ -285,7 +285,7 @@ def test_sign_build_missing_grade( @mock.patch.object(storeapi._dashboard_api.DashboardAPI, "push_snap_build") @mock.patch.object(storeapi._dashboard_api.DashboardAPI, "get_account_information") - @mock.patch("snapcraft_legacy._store._get_data_from_snap_file") + @mock.patch("snapcraft_legacy._store.get_data_from_snap_file") def test_sign_build_upload_successfully( self, mock_get_snap_data, @@ -339,7 +339,7 @@ def test_sign_build_upload_successfully( @mock.patch.object(storeapi._dashboard_api.DashboardAPI, "push_snap_build") @mock.patch.object(storeapi._dashboard_api.DashboardAPI, "get_account_information") - @mock.patch("snapcraft_legacy._store._get_data_from_snap_file") + @mock.patch("snapcraft_legacy._store.get_data_from_snap_file") def test_sign_build_upload_existing( self, mock_get_snap_data, diff --git a/tests/legacy/unit/commands/test_upload.py b/tests/legacy/unit/commands/test_upload.py deleted file mode 100644 index ed718de1c6..0000000000 --- a/tests/legacy/unit/commands/test_upload.py +++ /dev/null @@ -1,570 +0,0 @@ -# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- -# -# Copyright (C) 2016-2020 Canonical Ltd -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License version 3 as -# published by the Free Software Foundation. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -import json -import logging -import os -from unittest import mock - -import craft_store -import fixtures -import requests -from testtools.matchers import Contains, Equals, FileExists, Not -from xdg import BaseDirectory - -import tests.legacy -from snapcraft_legacy import file_utils, internal, storeapi -from snapcraft_legacy.internal import review_tools -from snapcraft_legacy.storeapi.errors import ( - StoreDeltaApplicationError, - StoreUpDownError, - StoreUploadError, -) - -from . import FAKE_UNAUTHORIZED_ERROR, FakeResponse, FakeStoreCommandsBaseTestCase - - -class UploadCommandBaseTestCase(FakeStoreCommandsBaseTestCase): - def setUp(self): - super().setUp() - - self.snap_file = os.path.join( - os.path.dirname(tests.legacy.__file__), "data", "test-snap.snap" - ) - - self.fake_review_tools_run = fixtures.MockPatch( - "snapcraft_legacy.internal.review_tools.run" - ) - self.useFixture(self.fake_review_tools_run) - - self.fake_review_tools_is_available = fixtures.MockPatch( - "snapcraft_legacy.internal.review_tools.is_available", return_value=False - ) - self.useFixture(self.fake_review_tools_is_available) - - -class UploadCommandTestCase(UploadCommandBaseTestCase): - def test_upload_without_snap_must_raise_exception(self): - result = self.run_command(["upload"]) - - self.assertThat(result.exit_code, Equals(2)) - self.assertThat(result.output, Contains("Usage:")) - - def test_upload_a_snap(self): - # Upload - result = self.run_command(["upload", self.snap_file]) - - self.assertThat(result.exit_code, Equals(0)) - self.assertThat(result.output, Contains("Revision 19 of 'basic' created.")) - self.fake_store_upload.mock.assert_called_once_with( - snap_name="basic", - snap_filename=self.snap_file, - built_at=None, - channels=None, - delta_format=None, - delta_hash=None, - source_hash=None, - target_hash=None, - ) - - def test_review_tools_not_available(self): - result = self.run_command(["upload", self.snap_file]) - - self.assertThat(result.exit_code, Equals(0)) - self.assertThat( - result.output, - Contains( - "Install the review-tools from the Snap Store for enhanced " - "checks before uploading this snap" - ), - ) - self.fake_review_tools_run.mock.assert_not_called() - - def test_upload_a_snap_review_tools_run_success(self): - self.fake_review_tools_is_available.mock.return_value = True - - result = self.run_command(["upload", self.snap_file]) - - self.assertThat(result.exit_code, Equals(0)) - self.fake_review_tools_run.mock.assert_called_once_with( - snap_filename=self.snap_file - ) - - def test_upload_a_snap_review_tools_run_fail(self): - self.fake_review_tools_is_available.mock.return_value = True - self.fake_review_tools_run.mock.side_effect = review_tools.errors.ReviewError( - { - "snap.v2_functional": {"error": {}, "warn": {}}, - "snap.v2_security": { - "error": { - "security-snap-v2:security_issue": { - "text": "(NEEDS REVIEW) security message." - } - }, - "warn": {}, - }, - "snap.v2_lint": {"error": {}, "warn": {}}, - } - ) - - result = self.run_command(["upload", self.snap_file]) - - self.assertThat(result.exit_code, Equals(0)) - self.assertThat( - result.output, - Contains( - "Review Tools did not fully pass for this snap.\n" - "Specific measures might need to be taken on the Snap Store before " - "this snap can be fully accepted.\n" - "Security Issues:\n" - "- (NEEDS REVIEW) security message" - ), - ) - self.fake_review_tools_run.mock.assert_called_once_with( - snap_filename=self.snap_file - ) - - def test_upload_with_started_at(self): - snap_file = os.path.join( - os.path.dirname(tests.legacy.__file__), - "data", - "test-snap-with-started-at.snap", - ) - - # Upload - result = self.run_command(["upload", snap_file]) - - self.assertThat(result.exit_code, Equals(0)) - self.assertThat(result.output, Contains("Revision 19 of 'basic' created.")) - self.fake_store_upload.mock.assert_called_once_with( - snap_name="basic", - snap_filename=snap_file, - built_at="2019-05-07T19:25:53.939041Z", - channels=None, - delta_format=None, - delta_hash=None, - source_hash=None, - target_hash=None, - ) - - def test_upload_without_login_must_ask(self): - self.fake_store_upload_precheck.mock.side_effect = [ - FAKE_UNAUTHORIZED_ERROR, - None, - ] - - result = self.run_command( - ["upload", self.snap_file], input="\n\n\n\nuser@example.com\nsecret\n" - ) - - self.assertThat( - result.output, Contains("You are required to login before continuing.") - ) - - def test_upload_nonexisting_snap_must_raise_exception(self): - result = self.run_command(["upload", "test-unexisting-snap"]) - - self.assertThat(result.exit_code, Equals(2)) - - def test_upload_invalid_snap_must_raise_exception(self): - snap_path = os.path.join( - os.path.dirname(tests.legacy.__file__), "data", "invalid.snap" - ) - - raised = self.assertRaises( - internal.errors.SnapDataExtractionError, - self.run_command, - ["upload", snap_path], - ) - - self.assertThat(str(raised), Contains("Cannot read data from snap")) - - def test_upload_unregistered_snap_must_ask(self): - class MockResponse: - status_code = 404 - - def json(self): - return dict( - error_list=[ - { - "code": "resource-not-found", - "message": "Snap not found for name=basic", - } - ] - ) - - self.fake_store_upload_precheck.mock.side_effect = [ - StoreUploadError("basic", MockResponse()), - None, - ] - - result = self.run_command(["upload", self.snap_file], input="y\n") - - self.assertThat(result.exit_code, Equals(0)) - self.assertThat( - result.output, - Contains("You are required to register this snap before continuing. "), - ) - self.fake_store_register.mock.assert_called_once_with( - "basic", is_private=False, series="16", store_id=None - ) - - def test_upload_unregistered_snap_must_raise_exception_if_not_registering(self): - class MockResponse: - status_code = 404 - - def json(self): - return dict( - error_list=[ - { - "code": "resource-not-found", - "message": "Snap not found for name=basic", - } - ] - ) - - self.fake_store_upload_precheck.mock.side_effect = [ - StoreUploadError("basic", MockResponse()), - None, - ] - - raised = self.assertRaises( - storeapi.errors.StoreUploadError, - self.run_command, - ["upload", self.snap_file], - ) - - self.assertThat( - str(raised), - Contains("This snap is not registered. Register the snap and try again."), - ) - self.fake_store_register.mock.assert_not_called() - - def test_upload_with_updown_error(self): - # We really don't know of a reason why this would fail - # aside from a 5xx style error on the server. - class MockResponse: - text = "stub error" - reason = "stub reason" - - self.fake_store_upload.mock.side_effect = StoreUpDownError(MockResponse()) - - self.assertRaises( - storeapi.errors.StoreUpDownError, - self.run_command, - ["upload", self.snap_file], - ) - - def test_upload_raises_deprecation_warning(self): - fake_logger = fixtures.FakeLogger(level=logging.INFO) - self.useFixture(fake_logger) - - # Push - result = self.run_command(["push", self.snap_file]) - - self.assertThat(result.exit_code, Equals(0)) - self.assertThat(result.output, Contains("Revision 19 of 'basic' created.")) - self.assertThat( - fake_logger.output, - Contains( - "DEPRECATED: The 'push' set of commands have been replaced with 'upload'." - ), - ) - self.fake_store_upload.mock.assert_called_once_with( - snap_name="basic", - snap_filename=self.snap_file, - built_at=None, - channels=None, - delta_format=None, - delta_hash=None, - source_hash=None, - target_hash=None, - ) - - def test_upload_and_release_a_snap(self): - self.useFixture - # Upload - result = self.run_command(["upload", self.snap_file, "--release", "beta"]) - - self.assertThat(result.exit_code, Equals(0)) - self.assertThat(result.output, Contains("Revision 19 of 'basic' created")) - self.fake_store_upload.mock.assert_called_once_with( - snap_name="basic", - snap_filename=self.snap_file, - built_at=None, - channels=["beta"], - delta_format=None, - delta_hash=None, - source_hash=None, - target_hash=None, - ) - - def test_upload_and_release_a_snap_to_N_channels(self): - # Upload - result = self.run_command( - ["upload", self.snap_file, "--release", "edge,beta,candidate"] - ) - - self.assertThat(result.exit_code, Equals(0)) - self.assertThat(result.output, Contains("Revision 19 of 'basic' created")) - self.fake_store_upload.mock.assert_called_once_with( - snap_name="basic", - snap_filename=self.snap_file, - built_at=None, - channels=["edge", "beta", "candidate"], - delta_format=None, - delta_hash=None, - source_hash=None, - target_hash=None, - ) - - def test_upload_displays_humanized_message(self): - result = self.run_command( - ["upload", self.snap_file, "--release", "edge,beta,candidate"] - ) - - self.assertThat( - result.output, - Contains( - "After uploading, the resulting snap revision will be released to " - "'beta', 'candidate', and 'edge' when it passes the Snap Store review." - ), - ) - - -class UploadCommandDeltasTestCase(UploadCommandBaseTestCase): - def setUp(self): - super().setUp() - - self.latest_snap_revision = 8 - self.new_snap_revision = self.latest_snap_revision + 1 - - self.mock_tracker.track.return_value = { - "code": "ready_to_release", - "processed": True, - "can_release": True, - "url": "/fake/url", - "revision": self.new_snap_revision, - } - - def test_upload_revision_cached_with_experimental_deltas(self): - # Upload - result = self.run_command(["upload", self.snap_file]) - - self.assertThat(result.exit_code, Equals(0)) - snap_cache = os.path.join( - BaseDirectory.xdg_cache_home, - "snapcraft", - "projects", - "basic", - "snap_hashes", - "amd64", - ) - cached_snap = os.path.join( - snap_cache, file_utils.calculate_sha3_384(self.snap_file) - ) - - self.assertThat(cached_snap, FileExists()) - - def test_upload_revision_uses_available_delta(self): - # Upload - result = self.run_command(["upload", self.snap_file]) - - self.assertThat(result.exit_code, Equals(0)) - - # Upload again - result = self.run_command(["upload", self.snap_file]) - - self.assertThat(result.exit_code, Equals(0)) - _, kwargs = self.fake_store_upload.mock.call_args - self.assertThat(kwargs.get("delta_format"), Equals("xdelta3")) - - def test_upload_with_delta_generation_failure_falls_back(self): - # Upload and ensure fallback is called - with mock.patch( - "snapcraft_legacy._store._upload_delta", - side_effect=StoreDeltaApplicationError("error"), - ): - result = self.run_command(["upload", self.snap_file]) - - self.assertThat(result.exit_code, Equals(0)) - self.fake_store_upload.mock.assert_called_once_with( - snap_name="basic", - snap_filename=self.snap_file, - built_at=None, - channels=None, - delta_format=None, - delta_hash=None, - source_hash=None, - target_hash=None, - ) - - def test_upload_with_delta_upload_failure_falls_back(self): - # Upload - result = self.run_command(["upload", self.snap_file]) - - self.assertThat(result.exit_code, Equals(0)) - - result = { - "code": "processing_upload_delta_error", - "errors": [{"message": "Delta service failed to apply delta within 60s"}], - } - self.mock_tracker.raise_for_code.side_effect = [ - storeapi.errors.StoreReviewError(result=result), - None, - ] - - # Upload and ensure fallback is called - result = self.run_command(["upload", self.snap_file]) - - self.assertThat(result.exit_code, Equals(0)) - self.fake_store_upload.mock.assert_has_calls( - [ - mock.call( - snap_name="basic", - snap_filename=mock.ANY, - built_at=None, - channels=None, - delta_format="xdelta3", - delta_hash=mock.ANY, - source_hash=mock.ANY, - target_hash=mock.ANY, - ), - mock.call( - snap_name="basic", - snap_filename=self.snap_file, - built_at=None, - channels=None, - delta_format=None, - delta_hash=None, - source_hash=None, - target_hash=None, - ), - ] - ) - - def test_upload_with_disabled_delta_falls_back(self): - # Upload - result = self.run_command(["upload", self.snap_file]) - - self.assertThat(result.exit_code, Equals(0)) - - self.fake_store_upload.mock.side_effect = [ - craft_store.errors.StoreServerError( - FakeResponse( - status_code=requests.codes.not_implemented, - content=json.dumps( - { - "error_list": [ - { - "code": "feature-disabled", - "message": "The delta upload support is currently disabled.", - } - ] - } - ), - ) - ), - self.mock_tracker, - ] - - # Upload and ensure fallback is called - with mock.patch("snapcraft_legacy.storeapi._status_tracker.StatusTracker"): - result = self.run_command(["upload", self.snap_file]) - self.assertThat(result.exit_code, Equals(0)) - self.fake_store_upload.mock.assert_has_calls( - [ - mock.call( - snap_name="basic", - snap_filename=mock.ANY, - built_at=None, - channels=None, - delta_format="xdelta3", - delta_hash=mock.ANY, - source_hash=mock.ANY, - target_hash=mock.ANY, - ), - mock.call( - snap_name="basic", - snap_filename=self.snap_file, - built_at=None, - channels=None, - delta_format=None, - delta_hash=None, - source_hash=None, - target_hash=None, - ), - ] - ) - - -class UploadCommandDeltasWithPruneTestCase(UploadCommandBaseTestCase): - def run_test(self, cached_snaps): - snap_revision = 19 - - self.mock_tracker.track.return_value = { - "code": "ready_to_release", - "processed": True, - "can_release": True, - "url": "/fake/url", - "revision": snap_revision, - } - - deb_arch = "amd64" - - snap_cache = os.path.join( - BaseDirectory.xdg_cache_home, - "snapcraft", - "projects", - "basic", - "snap_hashes", - deb_arch, - ) - os.makedirs(snap_cache) - - for cached_snap in cached_snaps: - cached_snap = cached_snap.format(deb_arch) - open(os.path.join(snap_cache, cached_snap), "a").close() - - # Upload - result = self.run_command(["upload", self.snap_file]) - - self.assertThat(result.exit_code, Equals(0)) - - real_cached_snap = os.path.join( - snap_cache, file_utils.calculate_sha3_384(self.snap_file) - ) - - self.assertThat(os.path.join(snap_cache, real_cached_snap), FileExists()) - - for snap in cached_snaps: - snap = snap.format(deb_arch) - self.assertThat(os.path.join(snap_cache, snap), Not(FileExists())) - self.assertThat(len(os.listdir(snap_cache)), Equals(1)) - - def test_delete_other_cache_files_with_valid_name(self): - self.run_test( - ["a-cached-snap_0.3_{}_8.snap", "another-cached-snap_1.0_fakearch_6.snap"] - ) - - def test_delete_other_cache_file_with_invalid_name(self): - self.run_test( - [ - "a-cached-snap_0.3_{}.snap", - "cached-snap-without-revision_1.0_fakearch.snap", - "another-cached-snap-without-version_fakearch.snap", - ] - ) diff --git a/tests/legacy/unit/plugins/v2/test_conda.py b/tests/legacy/unit/plugins/v2/test_conda.py index 7b4108feee..b1046d6cac 100644 --- a/tests/legacy/unit/plugins/v2/test_conda.py +++ b/tests/legacy/unit/plugins/v2/test_conda.py @@ -19,8 +19,8 @@ import pytest from snapcraft_legacy.plugins.v2.conda import ( - CondaPlugin, ArchitectureMissing, + CondaPlugin, _get_miniconda_source, ) diff --git a/tests/legacy/unit/store/test_store_client.py b/tests/legacy/unit/store/test_store_client.py index 42355fffd0..e175a5ab63 100644 --- a/tests/legacy/unit/store/test_store_client.py +++ b/tests/legacy/unit/store/test_store_client.py @@ -19,7 +19,6 @@ import os import tempfile from textwrap import dedent -from unittest import mock import fixtures import pytest @@ -33,7 +32,6 @@ Not, ) -import tests.legacy from snapcraft_legacy import storeapi from snapcraft_legacy.storeapi import errors, metrics from snapcraft_legacy.storeapi.v2 import releases, validation_sets, whoami @@ -631,134 +629,6 @@ def test_push_bad_response(self): self.assertIn("Invalid response from the server", self.fake_logger.output) -class UploadTestCase(StoreTestCase): - def setUp(self): - super().setUp() - self.snap_path = os.path.join( - os.path.dirname(tests.legacy.__file__), "data", "test-snap.snap" - ) - # These should eventually converge to the same module - pbars = ( - "snapcraft_legacy.storeapi._upload.ProgressBar", - "snapcraft_legacy.storeapi._status_tracker.ProgressBar", - ) - for pbar in pbars: - patcher = mock.patch(pbar, new=unit.SilentProgressBar) - patcher.start() - self.addCleanup(patcher.stop) - - def test_upload_snap(self): - self.client.register("test-snap") - tracker = self.client.upload("test-snap", self.snap_path) - self.assertTrue(isinstance(tracker, storeapi._status_tracker.StatusTracker)) - result = tracker.track() - expected_result = { - "code": "ready_to_release", - "revision": "1", - "url": "/dev/click-apps/5349/rev/1", - "can_release": True, - "processed": True, - } - self.assertThat(result, Equals(expected_result)) - - # This should not raise - tracker.raise_for_code() - - def test_upload_snap_requires_review(self): - self.client.register("test-review-snap") - tracker = self.client.upload("test-review-snap", self.snap_path) - self.assertTrue(isinstance(tracker, storeapi._status_tracker.StatusTracker)) - result = tracker.track() - expected_result = { - "code": "need_manual_review", - "revision": "1", - "url": "/dev/click-apps/5349/rev/1", - "can_release": False, - "processed": True, - } - self.assertThat(result, Equals(expected_result)) - - self.assertRaises(errors.StoreReviewError, tracker.raise_for_code) - - def test_upload_duplicate_snap(self): - self.client.register("test-duplicate-snap") - tracker = self.client.upload("test-duplicate-snap", self.snap_path) - self.assertTrue(isinstance(tracker, storeapi._status_tracker.StatusTracker)) - result = tracker.track() - expected_result = { - "code": "processing_error", - "revision": "1", - "url": "/dev/click-apps/5349/rev/1", - "can_release": False, - "processed": True, - "errors": [{"message": "Duplicate snap already uploaded"}], - } - self.assertThat(result, Equals(expected_result)) - - raised = self.assertRaises(errors.StoreReviewError, tracker.raise_for_code) - - self.assertThat( - str(raised), - Equals( - "The store was unable to accept this snap.\n" - " - Duplicate snap already uploaded" - ), - ) - - def test_braces_in_error_messages_are_literals(self): - self.client.register("test-scan-error-with-braces") - tracker = self.client.upload("test-scan-error-with-braces", self.snap_path) - self.assertTrue(isinstance(tracker, storeapi._status_tracker.StatusTracker)) - result = tracker.track() - expected_result = { - "code": "processing_error", - "revision": "1", - "url": "/dev/click-apps/5349/rev/1", - "can_release": False, - "processed": True, - "errors": [{"message": "Error message with {braces}"}], - } - self.assertThat(result, Equals(expected_result)) - - raised = self.assertRaises(errors.StoreReviewError, tracker.raise_for_code) - - self.assertThat( - str(raised), - Equals( - "The store was unable to accept this snap.\n" - " - Error message with {braces}" - ), - ) - - def test_upload_unregistered_snap(self): - raised = self.assertRaises( - errors.StoreUploadError, - self.client.upload, - "test-snap-unregistered", - self.snap_path, - ) - self.assertThat( - str(raised), - Equals("This snap is not registered. Register the snap and try again."), - ) - - def test_upload_forbidden_snap(self): - raised = self.assertRaises( - errors.StoreUploadError, - self.client.upload, - "test-snap-forbidden", - self.snap_path, - ) - self.assertThat( - str(raised), - Equals( - "You are not the publisher or allowed to upload revisions for " - "this snap. Ensure you are logged in with the proper account " - "and try again." - ), - ) - - class ReleaseTest(StoreTestCase): def test_release_snap(self): channel_map = self.client.release("test-snap", "19", ["beta"]) @@ -1066,11 +936,6 @@ def _setup_snap(self): These are all the previous steps needed to upload metadata. """ self.client.register("basic") - path = os.path.join( - os.path.dirname(tests.legacy.__file__), "data", "test-snap.snap" - ) - tracker = self.client.upload("basic", path) - tracker.track() def test_invalid_data(self): self._setup_snap() @@ -1170,11 +1035,6 @@ def _setup_snap(self): These are all the previous steps needed to upload binary metadata. """ self.client.register("basic") - path = os.path.join( - os.path.dirname(tests.legacy.__file__), "data", "test-snap.snap" - ) - tracker = self.client.upload("basic", path) - tracker.track() def test_invalid_data(self): self._setup_snap() @@ -1258,11 +1118,6 @@ def _setup_snap(self): These are all the previous steps needed to upload binary metadata. """ self.client.register("basic") - path = os.path.join( - os.path.dirname(tests.legacy.__file__), "data", "test-snap.snap" - ) - tracker = self.client.upload("basic", path) - tracker.track() def assert_raises(self, method): self._setup_snap() diff --git a/tests/legacy/unit/yaml_utils/test_yaml_utils.py b/tests/legacy/unit/yaml_utils/test_yaml_utils.py index 0815007a03..c26628c13a 100644 --- a/tests/legacy/unit/yaml_utils/test_yaml_utils.py +++ b/tests/legacy/unit/yaml_utils/test_yaml_utils.py @@ -15,8 +15,8 @@ # along with this program. If not, see . import io -import pytest +import pytest from snapcraft_legacy import yaml_utils from snapcraft_legacy.yaml_utils import YamlValidationError diff --git a/tests/unit/commands/store/test_client.py b/tests/unit/commands/store/test_client.py index f405a2d85e..eada9c74e5 100644 --- a/tests/unit/commands/store/test_client.py +++ b/tests/unit/commands/store/test_client.py @@ -15,6 +15,8 @@ # along with this program. If not, see . import json +import textwrap +import time from unittest.mock import call import craft_store @@ -31,6 +33,12 @@ ############# # Fixtures # +############# + + +@pytest.fixture +def no_wait(monkeypatch): + monkeypatch.setattr(time, "sleep", lambda x: None) @pytest.fixture @@ -581,3 +589,206 @@ def test_get_channel_map(fake_client, channel_map_payload): headers={"Accept": "application/json"}, ) ] + + +################# +# Verify Upload # +################# + + +def test_verify_upload(fake_client): + client.StoreClientCLI().verify_upload(snap_name="foo") + + assert fake_client.request.mock_calls == [ + call( + "POST", + "https://dashboard.snapcraft.io/dev/api/snap-push/", + json={"name": "foo", "dry_run": True}, + headers={"Accept": "application/json"}, + ) + ] + + +################# +# Notify Upload # +################# + + +@pytest.mark.usefixtures("no_wait") +def test_notify_upload(fake_client): + fake_client.request.side_effect = [ + FakeResponse( + status_code=200, content=json.dumps({"status_details_url": "https://track"}) + ), + FakeResponse( + status_code=200, + content=json.dumps({"code": "processing", "processed": False}), + ), + FakeResponse( + status_code=200, + content=json.dumps({"code": "done", "processed": True, "revision": 42}), + ), + ] + + client.StoreClientCLI().notify_upload( + snap_name="foo", + upload_id="some-id", + channels=None, + built_at=None, + snap_file_size=999, + ) + + assert fake_client.request.mock_calls == [ + call( + "POST", + "https://dashboard.snapcraft.io/dev/api/snap-push/", + json={ + "name": "foo", + "series": "16", + "updown_id": "some-id", + "binary_filesize": 999, + "source_uploaded": False, + }, + headers={"Accept": "application/json"}, + ), + call("GET", "https://track"), + call("GET", "https://track"), + ] + + +@pytest.mark.usefixtures("no_wait") +def test_notify_upload_built_at(fake_client): + fake_client.request.side_effect = [ + FakeResponse( + status_code=200, content=json.dumps({"status_details_url": "https://track"}) + ), + FakeResponse( + status_code=200, + content=json.dumps({"code": "processing", "processed": False}), + ), + FakeResponse( + status_code=200, + content=json.dumps({"code": "done", "processed": True, "revision": 42}), + ), + ] + + client.StoreClientCLI().notify_upload( + snap_name="foo", + upload_id="some-id", + channels=None, + built_at="some-date", + snap_file_size=999, + ) + + assert fake_client.request.mock_calls == [ + call( + "POST", + "https://dashboard.snapcraft.io/dev/api/snap-push/", + json={ + "name": "foo", + "series": "16", + "updown_id": "some-id", + "binary_filesize": 999, + "source_uploaded": False, + "built_at": "some-date", + }, + headers={"Accept": "application/json"}, + ), + call("GET", "https://track"), + call("GET", "https://track"), + ] + + +@pytest.mark.usefixtures("no_wait") +def test_notify_upload_channels(fake_client): + fake_client.request.side_effect = [ + FakeResponse( + status_code=200, content=json.dumps({"status_details_url": "https://track"}) + ), + FakeResponse( + status_code=200, + content=json.dumps({"code": "processing", "processed": False}), + ), + FakeResponse( + status_code=200, + content=json.dumps({"code": "done", "processed": True, "revision": 42}), + ), + ] + + client.StoreClientCLI().notify_upload( + snap_name="foo", + upload_id="some-id", + channels=["stable"], + built_at=None, + snap_file_size=999, + ) + + assert fake_client.request.mock_calls == [ + call( + "POST", + "https://dashboard.snapcraft.io/dev/api/snap-push/", + json={ + "name": "foo", + "series": "16", + "updown_id": "some-id", + "binary_filesize": 999, + "channels": ["stable"], + "source_uploaded": False, + }, + headers={"Accept": "application/json"}, + ), + call("GET", "https://track"), + call("GET", "https://track"), + ] + + +@pytest.mark.usefixtures("no_wait") +def test_notify_upload_error(fake_client): + fake_client.request.side_effect = [ + FakeResponse( + status_code=200, content=json.dumps({"status_details_url": "https://track"}) + ), + FakeResponse( + status_code=200, + content=json.dumps({"code": "processing", "processed": False}), + ), + FakeResponse( + status_code=200, + content=json.dumps( + {"code": "done", "processed": True, "errors": [{"message": "bad-snap"}]} + ), + ), + ] + + with pytest.raises(errors.SnapcraftError) as raised: + client.StoreClientCLI().notify_upload( + snap_name="foo", + upload_id="some-id", + channels=["stable"], + built_at=None, + snap_file_size=999, + ) + + assert str(raised.value) == textwrap.dedent( + """\ + Issues while processing snap: + - bad-snap""" + ) + + assert fake_client.request.mock_calls == [ + call( + "POST", + "https://dashboard.snapcraft.io/dev/api/snap-push/", + json={ + "name": "foo", + "series": "16", + "updown_id": "some-id", + "binary_filesize": 999, + "channels": ["stable"], + "source_uploaded": False, + }, + headers={"Accept": "application/json"}, + ), + call("GET", "https://track"), + call("GET", "https://track"), + ] diff --git a/tests/unit/commands/test_upload.py b/tests/unit/commands/test_upload.py new file mode 100644 index 0000000000..5e71ad4f38 --- /dev/null +++ b/tests/unit/commands/test_upload.py @@ -0,0 +1,132 @@ +import argparse +import pathlib +from unittest.mock import ANY, call + +import craft_cli.errors +import pytest + +from snapcraft import commands +from tests import unit + +############ +# Fixtures # +############ + + +@pytest.fixture(autouse=True) +def fake_store_client_upload_file(mocker): + fake_client = mocker.patch( + "craft_store.BaseClient.upload_file", + autospec=True, + return_value="2ecbfac1-3448-4e7d-85a4-7919b999f120", + ) + return fake_client + + +@pytest.fixture +def fake_store_notify_upload(mocker): + fake_client = mocker.patch( + "snapcraft.commands.store.StoreClientCLI.notify_upload", + autospec=True, + return_value=10, + ) + return fake_client + + +@pytest.fixture +def fake_store_verify_upload(mocker): + fake_client = mocker.patch( + "snapcraft.commands.store.StoreClientCLI.verify_upload", + autospec=True, + return_value=None, + ) + return fake_client + + +@pytest.fixture +def snap_file(): + return str( + ( + pathlib.Path(unit.__file__) + / ".." + / ".." + / "legacy" + / "data" + / "test-snap.snap" + ).resolve() + ) + + +################## +# Upload Command # +################## + + +@pytest.mark.usefixtures("memory_keyring") +def test_default( + emitter, fake_store_notify_upload, fake_store_verify_upload, snap_file +): + cmd = commands.StoreUploadCommand(None) + + cmd.run( + argparse.Namespace( + snap_file=snap_file, + channels=None, + ) + ) + + assert fake_store_verify_upload.mock_calls == [call(ANY, snap_name="basic")] + assert fake_store_notify_upload.mock_calls == [ + call( + ANY, + snap_name="basic", + upload_id="2ecbfac1-3448-4e7d-85a4-7919b999f120", + built_at=None, + channels=None, + snap_file_size=4096, + ) + ] + emitter.assert_message("Revision 10 created for 'basic'") + + +@pytest.mark.usefixtures("memory_keyring") +def test_default_channels( + emitter, fake_store_notify_upload, fake_store_verify_upload, snap_file +): + cmd = commands.StoreUploadCommand(None) + + cmd.run( + argparse.Namespace( + snap_file=snap_file, + channels="stable,edge", + ) + ) + + assert fake_store_verify_upload.mock_calls == [call(ANY, snap_name="basic")] + assert fake_store_notify_upload.mock_calls == [ + call( + ANY, + snap_name="basic", + upload_id="2ecbfac1-3448-4e7d-85a4-7919b999f120", + built_at=None, + channels=["stable", "edge"], + snap_file_size=4096, + ) + ] + emitter.assert_message( + "Revision 10 created for 'basic' and released to 'edge' and 'stable'" + ) + + +def test_invalid_file(): + cmd = commands.StoreUploadCommand(None) + + with pytest.raises(craft_cli.errors.ArgumentParsingError) as raised: + cmd.run( + argparse.Namespace( + snap_file="invalid.snap", + channels=None, + ) + ) + + assert str(raised.value) == "'invalid.snap' is not a valid file" From 7fbaf82d9fdfd9b5380af18093e604ebff8b1753 Mon Sep 17 00:00:00 2001 From: Claudio Matsuoka Date: Sat, 30 Apr 2022 14:56:15 -0300 Subject: [PATCH 161/167] tests: fix content provider snap not found for core22 Snapcraft for core22 has a different error message compared to legacy, adjust expected result in spread test accordingly. Signed-off-by: Claudio Matsuoka --- .../general/content-interface-provider-not-found/task.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/spread/general/content-interface-provider-not-found/task.yaml b/tests/spread/general/content-interface-provider-not-found/task.yaml index 5f3c9e6e6d..c758c91c3c 100644 --- a/tests/spread/general/content-interface-provider-not-found/task.yaml +++ b/tests/spread/general/content-interface-provider-not-found/task.yaml @@ -19,6 +19,6 @@ restore: | execute: | cd "$SNAP_DIR" - output=$(snapcraft prime 2>&1 >/dev/null) + output=$(snapcraft prime 2>&1 >/dev/null || true) - echo "$output" | MATCH "Could not install snap defined in plug" + echo "$output" | grep -q -e "Could not install snap defined in plug" -e "Failed to install or refresh snap 'unknown-content-snap'" From 83e8991e2798b8e3a582628b7598c0294db20d66 Mon Sep 17 00:00:00 2001 From: Claudio Matsuoka Date: Mon, 2 May 2022 15:21:19 +0200 Subject: [PATCH 162/167] requirements: update dependencies Signed-off-by: Claudio Matsuoka --- requirements-devel.txt | 8 ++++---- requirements.txt | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/requirements-devel.txt b/requirements-devel.txt index 8f12b65338..87aae6d01f 100644 --- a/requirements-devel.txt +++ b/requirements-devel.txt @@ -11,7 +11,7 @@ codespell==2.1.0 coverage==6.3.2 craft-cli==0.5.0 craft-grammar==1.1.1 -craft-parts==1.6.0 +craft-parts==1.6.1 craft-providers==1.2.0 craft-store==2.1.1 cryptography==3.4 @@ -101,17 +101,17 @@ tomli==2.0.1 translationstring==1.4 types-Deprecated==1.2.7 types-PyYAML==6.0.7 -types-requests==2.27.24 +types-requests==2.27.25 types-setuptools==57.4.14 types-tabulate==0.8.8 -types-urllib3==1.26.13 +types-urllib3==1.26.14 typing-utils==0.1.0 typing_extensions==4.2.0 urllib3==1.26.9 venusian==3.0.0 wadllib==1.3.6 WebOb==1.8.7 -wrapt==1.14.0 +wrapt==1.14.1 ws4py==0.5.1 zipp==3.8.0 zope.deprecation==4.4.0 diff --git a/requirements.txt b/requirements.txt index 85f9cd0580..4d95e8c35e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,7 +7,7 @@ charset-normalizer==2.0.12 click==8.1.3 craft-cli==0.5.0 craft-grammar==1.1.1 -craft-parts==1.6.0 +craft-parts==1.6.1 craft-providers==1.2.0 craft-store==2.1.1 cryptography==3.4 @@ -63,7 +63,7 @@ typing-utils==0.1.0 typing_extensions==4.2.0 urllib3==1.26.9 wadllib==1.3.6 -wrapt==1.14.0 +wrapt==1.14.1 ws4py==0.5.1 zipp==3.8.0 python-apt @ https://launchpad.net/ubuntu/+archive/primary/+sourcefiles/python-apt/2.0.0ubuntu0.20.04.6/python-apt_2.0.0ubuntu0.20.04.6.tar.xz; sys.platform == "linux" From e2aaf34647cdce25f456642033ce46a33c0a01a7 Mon Sep 17 00:00:00 2001 From: Sergio Schvezov Date: Tue, 3 May 2022 08:24:54 +0200 Subject: [PATCH 163/167] providers: user core22 buildd Signed-off-by: Sergio Schvezov --- snapcraft/providers/_buildd.py | 1 + snapcraft/providers/_lxd.py | 21 +++++++-------------- 2 files changed, 8 insertions(+), 14 deletions(-) diff --git a/snapcraft/providers/_buildd.py b/snapcraft/providers/_buildd.py index a58c69e525..035dd54e6a 100644 --- a/snapcraft/providers/_buildd.py +++ b/snapcraft/providers/_buildd.py @@ -25,6 +25,7 @@ from snapcraft import utils +# TODO fix this overengineered configuration BASE_TO_BUILDD_IMAGE_ALIAS = { "core22": bases.BuilddBaseAlias.JAMMY, } diff --git a/snapcraft/providers/_lxd.py b/snapcraft/providers/_lxd.py index 24d78329ab..cc2a7797fe 100644 --- a/snapcraft/providers/_lxd.py +++ b/snapcraft/providers/_lxd.py @@ -31,8 +31,6 @@ logger = logging.getLogger(__name__) -_BASE_IMAGE = {"core22": "ubuntu:22.04"} - class LXDProvider(Provider): """LXD build environment provider. @@ -156,21 +154,16 @@ def launched_environment( project_name=project_name, project_path=project_path, ) - - base_image = _BASE_IMAGE[base] - if ":" in base_image: - image_remote, image_name = base_image.split(":", 1) - else: - try: - image_remote = lxd.configure_buildd_image_remote() - image_name = base_image - except lxd.LXDError as error: - raise ProviderError(str(error)) from error + alias = BASE_TO_BUILDD_IMAGE_ALIAS[base] + try: + image_remote = lxd.configure_buildd_image_remote() + except lxd.LXDError as error: + raise ProviderError(str(error)) from error environment = self.get_command_environment() base_configuration = SnapcraftBuilddBaseConfiguration( - alias=alias, # type: ignore + alias=alias, environment=environment, hostname=instance_name, ) @@ -179,7 +172,7 @@ def launched_environment( instance = lxd.launch( name=instance_name, base_configuration=base_configuration, - image_name=image_name, + image_name=base, image_remote=image_remote, auto_clean=True, auto_create_project=True, From b6df2713e331fc7977d89a93ce892bf8b356f76b Mon Sep 17 00:00:00 2001 From: Claudio Matsuoka Date: Tue, 3 May 2022 16:27:47 +0200 Subject: [PATCH 164/167] meta: add layout to snap.yaml Improve project layout validation and add layout definition to snap.yaml. Also change project definition to not allow extra fields, and fix toplevel and part environment field type. Signed-off-by: Claudio Matsuoka --- snapcraft/meta/snap_yaml.py | 2 ++ snapcraft/parts/setup_assets.py | 2 +- snapcraft/projects.py | 12 +++++++----- tests/unit/meta/test_snap_yaml.py | 15 +++++++++++++++ tests/unit/test_projects.py | 18 +++++++++++++++++- 5 files changed, 42 insertions(+), 7 deletions(-) diff --git a/snapcraft/meta/snap_yaml.py b/snapcraft/meta/snap_yaml.py index 4c818d491f..417a15d9d8 100644 --- a/snapcraft/meta/snap_yaml.py +++ b/snapcraft/meta/snap_yaml.py @@ -111,6 +111,7 @@ class SnapMetadata(YamlModel): environment: Optional[Dict[str, Any]] plugs: Optional[Dict[str, Any]] hooks: Optional[Dict[str, Any]] + layout: Optional[Dict[str, Dict[str, str]]] def write(project: Project, prime_dir: Path, *, arch: str): @@ -177,6 +178,7 @@ def write(project: Project, prime_dir: Path, *, arch: str): environment=project.environment, plugs=project.plugs, hooks=project.hooks, + layout=project.layout, ) yaml.add_representer(str, _repr_str, Dumper=yaml.SafeDumper) diff --git a/snapcraft/parts/setup_assets.py b/snapcraft/parts/setup_assets.py index 15d22c5fbb..02c1d7f453 100644 --- a/snapcraft/parts/setup_assets.py +++ b/snapcraft/parts/setup_assets.py @@ -169,7 +169,7 @@ def _write_snapcraft_runner(*, prime_dir: Path): content = textwrap.dedent( """#!/bin/sh export PATH="$SNAP/usr/sbin:$SNAP/usr/bin:$SNAP/sbin:$SNAP/bin:$PATH" - export LD_LIBRARY_PATH=$SNAP_LIBRARY_PATH:$LD_LIBRARY_PATH + export LD_LIBRARY_PATH="$SNAP_LIBRARY_PATH:$LD_LIBRARY_PATH" exec "$@" """ ) diff --git a/snapcraft/projects.py b/snapcraft/projects.py index 5f9570fb45..1fe926df58 100644 --- a/snapcraft/projects.py +++ b/snapcraft/projects.py @@ -35,7 +35,7 @@ class Config: # pylint: disable=too-few-public-methods """Pydantic model configuration.""" validate_assignment = True - extra = "allow" # FIXME: change to 'forbid' after model complete + extra = "forbid" allow_mutation = True # project is updated with adopted metadata allow_population_by_field_name = True alias_generator = lambda s: s.replace("_", "-") # noqa: E731 @@ -135,7 +135,7 @@ class App(ProjectModel): slots: Optional[UniqueStrList] plugs: Optional[UniqueStrList] aliases: Optional[UniqueStrList] - environment: Optional[Dict[str, Any]] + environment: Optional[Dict[str, str]] adapter: Optional[Literal["none", "full"]] command_chain: List[str] = [] sockets: Optional[Dict[str, Socket]] @@ -192,7 +192,7 @@ class Hook(ProjectModel): """Snapcraft project hook definition.""" command_chain: Optional[List[str]] - environment: Optional[Dict[str, Any]] + environment: Optional[Dict[str, str]] plugs: Optional[UniqueStrList] passthrough: Optional[Dict[str, Any]] @@ -253,7 +253,9 @@ class Project(ProjectModel): type: Optional[Literal["app", "base", "gadget", "kernel", "snapd"]] icon: Optional[str] confinement: Literal["classic", "devmode", "strict"] - layout: Optional[Dict[str, Dict[str, Any]]] + layout: Optional[ + Dict[str, Dict[Literal["symlink", "bind", "bind-file", "type"], str]] + ] license: Optional[str] grade: Optional[Literal["stable", "devel"]] architectures: List[Architecture] = [] @@ -267,7 +269,7 @@ class Project(ProjectModel): parts: Dict[str, Any] # parts are handled by craft-parts epoch: Optional[str] adopt_info: Optional[str] - environment: Optional[Dict[str, Any]] + environment: Optional[Dict[str, str]] @pydantic.validator("plugs") @classmethod diff --git a/tests/unit/meta/test_snap_yaml.py b/tests/unit/meta/test_snap_yaml.py index 37b84a3660..ce2411dcc8 100644 --- a/tests/unit/meta/test_snap_yaml.py +++ b/tests/unit/meta/test_snap_yaml.py @@ -174,6 +174,14 @@ def complex_project(): install: environment: environment-var-1: "test" + + layout: + /usr/share/libdrm: + bind: $SNAP/gnome-platform/usr/share/libdrm + /usr/lib/x86_64-linux-gnu/webkit2gtk-4.0: + bind: $SNAP/gnome-platform/usr/lib/x86_64-linux-gnu/webkit2gtk-4.0 + /usr/share/xml/iso-codes: + bind: $SNAP/gnome-platform/usr/share/xml/iso-codes """ ) data = yaml.safe_load(snapcraft_yaml) @@ -275,5 +283,12 @@ def test_complex_snap_yaml(complex_project, new_dir): install: environment: environment-var-1: test + layout: + /usr/share/libdrm: + bind: $SNAP/gnome-platform/usr/share/libdrm + /usr/lib/x86_64-linux-gnu/webkit2gtk-4.0: + bind: $SNAP/gnome-platform/usr/lib/x86_64-linux-gnu/webkit2gtk-4.0 + /usr/share/xml/iso-codes: + bind: $SNAP/gnome-platform/usr/share/xml/iso-codes """ ) diff --git a/tests/unit/test_projects.py b/tests/unit/test_projects.py index 1834807cd3..9eadff80a1 100644 --- a/tests/unit/test_projects.py +++ b/tests/unit/test_projects.py @@ -493,6 +493,22 @@ def test_project_get_content_snaps(self, project_yaml_data): project = Project.unmarshal(project_yaml_data(plugs=content_plug_data)) assert project.get_content_snaps() == ["test-provider"] + @pytest.mark.parametrize("decl_type", ["symlink", "bind", "bind-file", "type"]) + def test_project_layout(self, decl_type, project_yaml_data): + project = Project.unmarshal( + project_yaml_data(layout={"foo": {decl_type: "bar"}}) + ) + assert project.layout is not None + assert project.layout["foo"][decl_type] == "bar" + + def test_project_layout_invalid(self, project_yaml_data): + error = ( + "Bad snapcraft.yaml content:\n" + "- unexpected value; permitted: 'symlink', 'bind', 'bind-file', 'type'" + ) + with pytest.raises(errors.ProjectValidationError, match=error): + Project.unmarshal(project_yaml_data(layout={"foo": {"invalid": "bar"}})) + class TestHookValidation: """Validate hooks.""" @@ -504,7 +520,7 @@ class TestHookValidation: { "configure": { "command-chain": ["test-1", "test-2"], - "build-environment": { + "environment": { "FIRST_VARIABLE": "test-3", "SECOND_VARIABLE": "test-4", }, From 656dfdbb6464f543411bfae536ba8becbb686a0c Mon Sep 17 00:00:00 2001 From: Sergio Schvezov Date: Wed, 4 May 2022 07:23:46 +0200 Subject: [PATCH 165/167] providers: setup dirmngr Setup dirmngr for apt-key to work. Signed-off-by: Sergio Schvezov --- snapcraft/providers/_buildd.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/snapcraft/providers/_buildd.py b/snapcraft/providers/_buildd.py index 035dd54e6a..848498025f 100644 --- a/snapcraft/providers/_buildd.py +++ b/snapcraft/providers/_buildd.py @@ -62,6 +62,13 @@ def _setup_snapcraft(*, executor: Executor) -> None: :raises BaseConfigurationError: on error. """ + # Requirement for apt gpg + executor.execute_run( + ["apt-get", "install", "-y", "dirmngr"], + capture_output=True, + check=True, + ) + snap_channel = utils.get_managed_environment_snap_channel() if snap_channel is None and sys.platform != "linux": snap_channel = "stable" From a5c52a5baa3b90bd991bfe1a5e035e5099975c9e Mon Sep 17 00:00:00 2001 From: Sergio Schvezov Date: Wed, 4 May 2022 08:56:58 +0200 Subject: [PATCH 166/167] commands: help for all legacy commands Passthrough to legacy implementation. Signed-off-by: Sergio Schvezov --- snapcraft/cli.py | 29 ++- snapcraft/commands/__init__.py | 42 +++- snapcraft/commands/legacy.py | 401 +++++++++++++++++++++++++++++++++ snapcraft_legacy/cli/store.py | 78 ------- 4 files changed, 461 insertions(+), 89 deletions(-) create mode 100644 snapcraft/commands/legacy.py diff --git a/snapcraft/cli.py b/snapcraft/cli.py index 5be813a138..b5142f60a9 100644 --- a/snapcraft/cli.py +++ b/snapcraft/cli.py @@ -41,6 +41,15 @@ commands.PrimeCommand, commands.PackCommand, commands.SnapCommand, # hidden (legacy compatibility) + commands.StoreLegacyRemoteBuildCommand, + ], + ), + craft_cli.CommandGroup( + "Extensions", + [ + commands.ListExtensionsCommand, + commands.ExtensionsCommand, # hidden (alias to list-extensions) + commands.ExpandExtensionsCommand, ], ), craft_cli.CommandGroup( @@ -59,6 +68,8 @@ commands.StoreNamesCommand, commands.StoreLegacyListRegisteredCommand, commands.StoreLegacyListCommand, + commands.StoreLegacyMetricsCommand, + commands.StoreLegacyUploadMetadataCommand, ], ), craft_cli.CommandGroup( @@ -68,6 +79,8 @@ commands.StoreCloseCommand, commands.StoreStatusCommand, commands.StoreUploadCommand, + commands.StoreLegacyPromoteCommand, + commands.StoreLegacyListRevisionsCommand, ], ), craft_cli.CommandGroup( @@ -75,14 +88,20 @@ [ commands.StoreListTracksCommand, commands.StoreTracksCommand, # hidden (alias to list-tracks) + commands.StoreLegacySetDefaultTrackCommand, ], ), craft_cli.CommandGroup( - "Extensions", + "Store Assertions", [ - commands.ListExtensionsCommand, - commands.ExtensionsCommand, # hidden (alias to list-extensions) - commands.ExpandExtensionsCommand, + commands.StoreLegacyCreateKeyCommand, + commands.StoreLegacyEditValidationSetsCommand, + commands.StoreLegacyGatedCommand, + commands.StoreLegacyListValidationSetsCommand, + commands.StoreLegacyRegisterKeyCommand, + commands.StoreLegacySignBuildCommand, + commands.StoreLegacyValidateCommand, + commands.StoreLegacyListKeysCommand, ], ), craft_cli.CommandGroup("Other", [commands.VersionCommand]), @@ -105,7 +124,7 @@ def get_dispatcher() -> craft_cli.Dispatcher: legacy.legacy_run() # set lib loggers to debug level so that all messages are sent to Emitter - for lib_name in ("craft_parts", "craft_providers", "craft_store"): + for lib_name in ("craft_parts", "craft_providers"): logger = logging.getLogger(lib_name) logger.setLevel(logging.DEBUG) diff --git a/snapcraft/commands/__init__.py b/snapcraft/commands/__init__.py index f4c78a7a40..7f01129761 100644 --- a/snapcraft/commands/__init__.py +++ b/snapcraft/commands/__init__.py @@ -27,6 +27,22 @@ ExtensionsCommand, ListExtensionsCommand, ) +from .legacy import ( + StoreLegacyCreateKeyCommand, + StoreLegacyEditValidationSetsCommand, + StoreLegacyGatedCommand, + StoreLegacyListKeysCommand, + StoreLegacyListRevisionsCommand, + StoreLegacyListValidationSetsCommand, + StoreLegacyMetricsCommand, + StoreLegacyPromoteCommand, + StoreLegacyRegisterKeyCommand, + StoreLegacyRemoteBuildCommand, + StoreLegacySetDefaultTrackCommand, + StoreLegacySignBuildCommand, + StoreLegacyUploadMetadataCommand, + StoreLegacyValidateCommand, +) from .lifecycle import ( BuildCommand, CleanCommand, @@ -51,26 +67,40 @@ "BuildCommand", "CleanCommand", "ExpandExtensionsCommand", + "ExtensionsCommand", + "ListExtensionsCommand", "PackCommand", "PrimeCommand", "PullCommand", "SnapCommand", "StageCommand", "StoreCloseCommand", - "StoreLoginCommand", - "StoreNamesCommand", "StoreExportLoginCommand", + "StoreLegacyCreateKeyCommand", + "StoreLegacyEditValidationSetsCommand", + "StoreLegacyGatedCommand", + "StoreLegacyListCommand", + "StoreLegacyListRegisteredCommand", + "StoreLegacyListRevisionsCommand", + "StoreLegacyListValidationSetsCommand", + "StoreLegacyMetricsCommand", + "StoreLegacyPromoteCommand", + "StoreLegacyRegisterKeyCommand", + "StoreLegacyRemoteBuildCommand", + "StoreLegacySetDefaultTrackCommand", + "StoreLegacySignBuildCommand", + "StoreLegacyUploadMetadataCommand", + "StoreLegacyValidateCommand", + "StoreLegacyListKeysCommand", "StoreListTracksCommand", + "StoreLoginCommand", "StoreLogoutCommand", + "StoreNamesCommand", "StoreRegisterCommand", - "StoreLegacyListCommand", - "StoreLegacyListRegisteredCommand", "StoreReleaseCommand", "StoreStatusCommand", "StoreTracksCommand", "StoreUploadCommand", "StoreWhoAmICommand", - "ExtensionsCommand", - "ListExtensionsCommand", "VersionCommand", ] diff --git a/snapcraft/commands/legacy.py b/snapcraft/commands/legacy.py new file mode 100644 index 0000000000..da522b808b --- /dev/null +++ b/snapcraft/commands/legacy.py @@ -0,0 +1,401 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright 2022 Canonical Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +"""Snapcraft Commands that call to the legacy implementation.""" + +import textwrap +from typing import TYPE_CHECKING + +from craft_cli import BaseCommand +from overrides import overrides + +from snapcraft_legacy.cli import legacy + +if TYPE_CHECKING: + import argparse + + +class LegacyBaseCommand(BaseCommand): + """Legacy command runner.""" + + @overrides + def run(self, parsed_args): + legacy.legacy_run() + + +######### +# Store # +######### + + +class StoreLegacyUploadMetadataCommand(LegacyBaseCommand): + """Command passthrough for the upload-metadata command.""" + + name = "upload-metadata" + help_msg = "Upload metadata from to the store" + overview = textwrap.dedent( + """ + The following information will be retrieved from and used to + update the store: + + - summary + - description + - icon + + If --force is given, it will force the local metadata into the Store, + ignoring any possible conflict. + + Examples: + snapcraft upload-metadata my-snap_0.1_amd64.snap + snapcraft upload-metadata my-snap_0.1_amd64.snap --force + """ + ) + + @overrides + def fill_parser(self, parser: "argparse.ArgumentParser") -> None: + parser.add_argument( + "--force", + action="store_true", + default=False, + help="Force metadata update to override any possible conflict", + ) + + +class StoreLegacyPromoteCommand(LegacyBaseCommand): + """Command passthrough for the promote command.""" + + name = "promote" + help_msg = "Promote a build set from a channel" + overview = textwrap.dedent( + """ + A build set is a set of commonly tagged revisions, the most simple + form of a build set is a set of revisions released to a channel. + + Currently, only channels are supported to release from () + + Prior to releasing, visual confirmation shall be required. + + The format for channels is `[/][/]` where + + - is used to have long term release channels. It is implicitly + set to the default. + - is mandatory and can be either `stable`, `candidate`, `beta` + or `edge`. + - is optional and dynamically creates a channel with a + specific expiration date. + """ + ) + + @overrides + def fill_parser(self, parser: "argparse.ArgumentParser") -> None: + parser.add_argument( + "--from-channel", + metavar="from-channel", + help="the channel to promote from", + required=True, + ) + parser.add_argument( + "--to-channel", + metavar="to-channel", + help="the channel to promote to", + required=True, + ) + parser.add_argument( + "--yes", action="store_true", help="do not prompt for confirmation" + ) + + +class StoreLegacyListRevisionsCommand(LegacyBaseCommand): + """Command passthrough for the list-revisions command.""" + + name = "list-revisions" + help_msg = "List published revisions for " + overview = textwrap.dedent( + """ + Examples: + snapcraft list-revisions my-snap + snapcraft list-revisions my-snap --arch armhf + snapcraft revisions my-snap + """ + ) + + @overrides + def fill_parser(self, parser: "argparse.ArgumentParser") -> None: + parser.add_argument( + "snap_name", + metavar="snap-name", + ) + parser.add_argument( + "--arch", + metavar="arch", + help="architecture filter", + ) + + +class StoreLegacySetDefaultTrackCommand(LegacyBaseCommand): + """Command passthrough for the set-default-track command.""" + + name = "set-default-track" + help_msg = "Set the default track for a snap" + overview = textwrap.dedent( + """ + Set the default track for to . must already exist.""" + ) + + @overrides + def fill_parser(self, parser: "argparse.ArgumentParser") -> None: + parser.add_argument( + "snap_name", + metavar="snap-name", + ) + parser.add_argument( + "track", + ) + + +class StoreLegacyMetricsCommand(LegacyBaseCommand): + """Command passthrough for the metrics command.""" + + name = "metrics" + help_msg = "Get metrics for a snap" + overview = textwrap.dedent( + """ + Get different metrics from the Snap Store for a given snap.""" + ) + + @overrides + def fill_parser(self, parser: "argparse.ArgumentParser") -> None: + parser.add_argument("snap_name", metavar="snap-name") + parser.add_argument("--name", metavar="name", required=True, help="metric name") + parser.add_argument( + "--start", + metavar="start-date", + help="date in format YYYY-MM-DD", + ) + parser.add_argument( + "--end", + metavar="end-date", + help="date in format YYYY-MM-DD", + ) + parser.add_argument( + "--format", + metavar="format", + help="format for output", + choices=["table", "json"], + required=True, + ) + + +######### +# Build # +######### + + +class StoreLegacyRemoteBuildCommand(LegacyBaseCommand): + """Command passthrough for the remote-build command.""" + + name = "remote-build" + help_msg = "Dispatch a snap for remote build" + overview = textwrap.dedent( + """ + Command remote-build sends the current project to be built remotely. After the build + is complete, packages for each architecture are retrieved and will be available in + the local filesystem. + + If not specified in the snapcraft.yaml file, the list of architectures to build + can be set using the --build-on option. If both are specified, an error will occur. + + Interrupted remote builds can be resumed using the --recover option, followed by + the build number informed when the remote build was originally dispatched. The + current state of the remote build for each architecture can be checked using the + --status option.""" + ) + + @overrides + def fill_parser(self, parser: "argparse.ArgumentParser") -> None: + parser.add_argument( + "--recover", action="store_true", help="recover an interrupted build" + ) + parser.add_argument( + "--status", action="store_true", help="display remote build status" + ) + parser.add_argument( + "--build-on", + metavar="arch", + nargs="+", + help="architecture to build on", + ) + parser.add_argument( + "--build-id", metavar="build-id", help="specific build id to retrieve" + ) + parser.add_argument( + "--launchpad-accept-public-upload", + action="store_true", + help="acknowledge that uploaded code will be publicly available.", + ) + + +############## +# Assertions # +############## + + +class StoreLegacyListKeysCommand(LegacyBaseCommand): + """Command passthrough for the list-keys command.""" + + name = "list-keys" + help_msg = "List the keys available to sign assertions" + overview = textwrap.dedent( + """ + List the available keys to sign assertions together with they + local availability.""" + ) + + +class StoreLegacyCreateKeyCommand(LegacyBaseCommand): + """Command passthrough for the create-key command.""" + + name = "create-key" + help_msg = "Create a key to sign assertions." + overview = textwrap.dedent( + """ + Create a key and store it locally. Use the register-key command to register + it on the store.""" + ) + + @overrides + def fill_parser(self, parser: "argparse.ArgumentParser") -> None: + parser.add_argument( + "key_name", metavar="key-name", help="Key used to sign the assertion" + ) + + +class StoreLegacyRegisterKeyCommand(LegacyBaseCommand): + """Command passthrough for the register-key command.""" + + name = "register-key" + help_msg = "Register a key to sign assertions with the Snap Store." + overview = textwrap.dedent( + """ + Register a a key with the Snap Store. Prior to registration, use register-key + to create one.""" + ) + + @overrides + def fill_parser(self, parser: "argparse.ArgumentParser") -> None: + parser.add_argument( + "key_name", metavar="key-name", help="Key used to sign the assertion" + ) + + +class StoreLegacySignBuildCommand(LegacyBaseCommand): + """Command passthrough for the sign-build command.""" + + name = "sign-build" + help_msg = "Sign a built snap file and assert it using the developer's key" + overview = textwrap.dedent( + """ + Sign a specific build of a snap with a given key and upload the assertion + to the Snap Store (unless --local).""" + ) + + @overrides + def fill_parser(self, parser: "argparse.ArgumentParser") -> None: + parser.add_argument( + "--key-name", metavar="key-name", help="key used to sign the assertion" + ) + parser.add_argument( + "--local", + "--local", + action="store_true", + help="do not aupload to the Snap Store", + ) + + +class StoreLegacyValidateCommand(LegacyBaseCommand): + """Command passthrough for the validate command.""" + + name = "validate" + help_msg = "Validate a gated snap" + overview = textwrap.dedent( + """ + Each validation can be presented with either syntax: + + - = + - =""" + ) + + @overrides + def fill_parser(self, parser: "argparse.ArgumentParser") -> None: + parser.add_argument( + "--key-name", metavar="key-name", help="key used to sign the assertion" + ) + parser.add_argument("--revoke", action="store_true", help="revoke validations") + parser.add_argument("snap_name", metavar="snap-name") + parser.add_argument("validations", nargs="+") + + +class StoreLegacyGatedCommand(LegacyBaseCommand): + """Command passthrough for the gated command.""" + + name = "gated" + help_msg = "List all gated snaps for " + overview = textwrap.dedent( + """ + Get the list of snaps and revisions gating a snaps""" + ) + + @overrides + def fill_parser(self, parser: "argparse.ArgumentParser") -> None: + parser.add_argument("snap_name", metavar="snap-name") + + +class StoreLegacyListValidationSetsCommand(LegacyBaseCommand): + """Command passthrough for the edit-validation-sets command.""" + + name = "list-validation-sets" + help_msg = "Get the list of validation sets" + overview = textwrap.dedent( + """ + List all list-validation-sets snaps. + """ + ) + + @overrides + def fill_parser(self, parser: "argparse.ArgumentParser") -> None: + parser.add_argument("snap_name", metavar="snap-name") + + +class StoreLegacyEditValidationSetsCommand(LegacyBaseCommand): + """Command passthrough for the edit-validation-sets command.""" + + name = "edit-validation-sets" + help_msg = "Edit the list of validations for " + overview = textwrap.dedent( + """ + Refer to https://snapcraft.io/docs/validation-sets for further information + on Validation Sets. + """ + ) + + @overrides + def fill_parser(self, parser: "argparse.ArgumentParser") -> None: + parser.add_argument( + "--key-name", metavar="key-name", help="Key used to sign the assertion" + ) + parser.add_argument("account_id", metavar="account-id") + parser.add_argument("set_name", metavar="set-name") + parser.add_argument("sequence", metavar="sequence") diff --git a/snapcraft_legacy/cli/store.py b/snapcraft_legacy/cli/store.py index dbcae5c6ae..4f454a4f96 100644 --- a/snapcraft_legacy/cli/store.py +++ b/snapcraft_legacy/cli/store.py @@ -204,84 +204,6 @@ def promote(snap_name, from_channel, to_channel, yes): echo.wrapped("Channel promotion cancelled") -@storecli.command() -@click.option( - "--experimental-progressive-releases", - is_flag=True, - help="*EXPERIMENTAL* Enables 'progressive releases'.", - envvar="SNAPCRAFT_EXPERIMENTAL_PROGRESSIVE_RELEASES", -) -@click.option( - "architectures", - "--arch", - metavar="", - multiple=True, - help="Limit status to these architectures (can specify multiple times)", -) -@click.option( - "tracks", - "--track", - multiple=True, - metavar="", - help="Limit status to these tracks (can specify multiple times)", -) -@click.argument("snap-name", metavar="") -def status(snap_name, architectures, tracks, experimental_progressive_releases): - """Get the status on the store for . - - \b - Examples: - snapcraft status my-snap - snapcraft status --track 20 my-snap - snapcraft status --arch amd64 my-snap - """ - if experimental_progressive_releases: - os.environ["SNAPCRAFT_EXPERIMENTAL_PROGRESSIVE_RELEASES"] = "Y" - echo.warning("*EXPERIMENTAL* progressive releases in use.") - - snap_channel_map = StoreClientCLI().get_snap_channel_map(snap_name=snap_name) - existing_architectures = snap_channel_map.get_existing_architectures() - - if not snap_channel_map.channel_map: - echo.warning("This snap has no released revisions.") - else: - if architectures: - architectures = set(architectures) - for architecture in architectures.copy(): - if architecture not in existing_architectures: - echo.warning(f"No revisions for architecture {architecture!r}.") - architectures.remove(architecture) - - # If we have no revisions for any of the architectures requested, there's - # nothing to do here. - if not architectures: - return - else: - architectures = existing_architectures - - if tracks: - tracks = set(tracks) - existing_tracks = { - s.track for s in snap_channel_map.snap.channels if s.track in tracks - } - for track in tracks - existing_tracks: - echo.warning(f"No revisions in track {track!r}.") - tracks = existing_tracks - - # If we have no revisions in any of the tracks requested, there's - # nothing to do here. - if not tracks: - return - else: - tracks = None - - click.echo( - get_tabulated_channel_map( - snap_channel_map, architectures=architectures, tracks=tracks - ) - ) - - @storecli.command("list-revisions") @click.option( "--arch", metavar="", help="The snap architecture to get the status for" From 1202a20d53e1504531bf6d449e2dfdd713508397 Mon Sep 17 00:00:00 2001 From: Claudio Matsuoka Date: Wed, 4 May 2022 16:35:41 +0200 Subject: [PATCH 167/167] requirements: update dependencies Signed-off-by: Claudio Matsuoka --- requirements-devel.txt | 6 +++--- requirements.txt | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/requirements-devel.txt b/requirements-devel.txt index 87aae6d01f..5a3d31084a 100644 --- a/requirements-devel.txt +++ b/requirements-devel.txt @@ -1,4 +1,4 @@ -astroid==2.11.3 +astroid==2.11.4 attrs==21.4.0 black==22.3.0 catkin-pkg==0.4.24 @@ -9,7 +9,7 @@ charset-normalizer==2.0.12 click==8.1.3 codespell==2.1.0 coverage==6.3.2 -craft-cli==0.5.0 +craft-cli==0.6.0 craft-grammar==1.1.1 craft-parts==1.6.1 craft-providers==1.2.0 @@ -65,7 +65,7 @@ pydocstyle==6.1.1 pyelftools==0.28 pyflakes==2.4.0 pyftpdlib==1.5.6 -pylint==2.13.7 +pylint==2.13.8 pylint-fixme-info==1.0.3 pylint-pytest==1.1.2 pylxd==2.3.1 diff --git a/requirements.txt b/requirements.txt index 4d95e8c35e..5d47c15395 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,7 @@ cffi==1.15.0 chardet==4.0.0 charset-normalizer==2.0.12 click==8.1.3 -craft-cli==0.5.0 +craft-cli==0.6.0 craft-grammar==1.1.1 craft-parts==1.6.1 craft-providers==1.2.0