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`?
-----
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 }}
diff --git a/.github/workflows/spread.yml b/.github/workflows/spread.yml
index b2fde6fa43..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
@@ -84,7 +85,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 +108,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/.github/workflows/tests.yaml b/.github/workflows/tests.yaml
index 430639925d..3ef064050d 100644
--- a/.github/workflows/tests.yaml
+++ b/.github/workflows/tests.yaml
@@ -1,38 +1,89 @@
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
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: |
- ./tools/environment-setup-local.sh
+ sudo apt update
+ 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
run: |
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: |
+ make test-isort
- name: Run mypy
run: |
- source ${HOME}/.venv/snapcraft/bin/activate
make test-mypy
+ - name: Run pydocstyle
+ run: |
+ make test-pydocstyle
+ - name: Run pyright
+ run: |
+ sudo snap install --classic node
+ sudo snap install --classic pyright
+ make test-pyright
+ - name: Run pylint
+ env:
+ SNAPCRAFT_IGNORE_YAML_BINDINGS: "1"
+ run: |
+ make test-pylint
- name: Run shellcheck
run: |
make test-shellcheck
+
+ tests:
+ strategy:
+ fail-fast: false
+ matrix:
+ python-version: ["3.8", "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
+ env:
+ SNAPCRAFT_IGNORE_YAML_BINDINGS: "1"
run: |
- source ${HOME}/.venv/snapcraft/bin/activate
make test-units
- name: Upload code coverage
uses: codecov/codecov-action@v1
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/Makefile b/Makefile
index f4d4920012..902ecfd184 100644
--- a/Makefile
+++ b/Makefile
@@ -1,6 +1,8 @@
+SOURCES=setup.py snapcraft tests/*.py tests/unit
+
.PHONY: autoformat-black
autoformat-black:
- black .
+ black $(SOURCES)
.PHONY: freeze-requirements
freeze-requirements:
@@ -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,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:
- 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,too-many-public-methods,too-many-arguments
+
+.PHONY: test-pyright
+test-pyright:
+ pyright $(SOURCES)
.PHONY: test-shellcheck
test-shellcheck:
@@ -28,12 +47,19 @@ 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:
+test-units: test-legacy-units
pytest --cov-report=xml --cov=snapcraft tests/unit
.PHONY: tests
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/appveyor.yml b/appveyor.yml
index eb0c1691e6..2dd633fc06 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'
@@ -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..."
@@ -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/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/pyproject.toml b/pyproject.toml
index 3f03696340..807102191f 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -29,3 +29,23 @@ force_grid_wrap = 0
use_parentheses = true
ensure_newline_before_comments = true
line_length = 88
+
+[tool.pylint.messages_control]
+# 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
+max-args = 6
+good-names = "id"
+
+[tool.pylint.MASTER]
+extension-pkg-allow-list = [
+ "lxml.etree",
+ "pydantic",
+ "pytest",
+]
+load-plugins = "pylint_fixme_info,pylint_pytest"
+
+[tool.pylint.SIMILARITIES]
+min-similarity-lines=10
diff --git a/requirements-devel.txt b/requirements-devel.txt
index 99c05b1a07..5a3d31084a 100644
--- a/requirements-devel.txt
+++ b/requirements-devel.txt
@@ -1,91 +1,119 @@
-attrs==21.2.0
-catkin-pkg==0.4.23
-certifi==2021.5.30
-cffi==1.14.6
+astroid==2.11.4
+attrs==21.4.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.2
-click==8.0.1
+charset-normalizer==2.0.12
+click==8.1.3
codespell==2.1.0
-coverage==5.5
+coverage==6.3.2
+craft-cli==0.6.0
+craft-grammar==1.1.1
+craft-parts==1.6.1
+craft-providers==1.2.0
+craft-store==2.1.1
cryptography==3.4
-distro==1.5.0
-docutils==0.17.1
-entrypoints==0.3
+Deprecated==1.2.13
+dill==0.3.4
+distro==1.7.0
+docutils==0.18.1
extras==1.0.0
-fixtures==3.0.0
-flake8==3.7.9
+fixtures==4.0.0
+flake8==4.0.1
gnupg==2.3.1
-httplib2==0.19.1
+httplib2==0.20.4
hupper==1.10.3
-idna==3.2
-importlib-metadata==4.6.1
+idna==3.3
+importlib-metadata==4.11.3
iniconfig==1.1.1
-isort==5.9.2
-jeepney==0.7.0
+isort==5.10.1
+jeepney==0.8.0
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.5
+keyring==23.5.0
+launchpadlib==1.10.16
+lazr.restfulclient==0.14.4
+lazr.uri==1.0.6
+lazy-object-proxy==1.7.1
+lxml==4.8.0
macaroonbakery==1.3.1
mccabe==0.6.1
-mypy==0.770
+mypy==0.950
mypy-extensions==0.4.3
-oauthlib==3.1.1
-packaging==21.0
+oauthlib==3.2.0
+overrides==6.1.0
+packaging==21.3
PasteDeploy==2.1.1
-pbr==5.6.0
+pathspec==0.9.0
+pbr==5.8.1
pexpect==4.8.0
plaster==1.0
plaster-pastedeploy==0.7
-pluggy==0.13.1
+platformdirs==2.5.2
+pluggy==1.0.0
progressbar==2.5
-protobuf==3.17.3
-psutil==5.8.0
+protobuf==3.20.1
+psutil==5.9.0
ptyprocess==0.7.0
-py==1.10.0
-pycodestyle==2.5.0
-pycparser==2.20
-pyelftools==0.27
-pyflakes==2.1.1
+py==1.11.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.4.0
pyftpdlib==1.5.6
-pylxd==2.3.0
+pylint==2.13.8
+pylint-fixme-info==1.0.3
+pylint-pytest==1.1.2
+pylxd==2.3.1
pymacaroons==0.13.0
-pyparsing==2.4.7
+pyparsing==3.0.8
pyramid==2.0
pyRFC3339==1.1
-pytest==6.2.4
-pytest-cov==2.12.1
-pytest-subprocess==1.1.1
+pytest==7.1.2
+pytest-cov==3.0.0
+pytest-mock==3.7.0
+pytest-subprocess==1.4.1
python-dateutil==2.8.2
-python-debian==0.1.40
-pytz==2021.1
+python-debian==0.1.43
+pytz==2022.1
pyxdg==0.27
-PyYAML==5.4
+PyYAML==6.0
raven==6.10.0
-requests==2.26.0
+requests==2.27.1
requests-toolbelt==0.9.1
-requests-unixsocket==0.2.0
-SecretStorage==3.3.1
-semantic-version==2.8.5
-simplejson==3.17.3
+requests-unixsocket==0.3.0
+SecretStorage==3.3.2
+semantic-version==2.9.0
+semver==2.13.0
+simplejson==3.17.6
six==1.16.0
+snowballstemmer==2.2.0
tabulate==0.8.9
-testresources==2.0.1
testscenarios==0.5.0
testtools==2.5.0
-tinydb==4.5.0
+tinydb==4.7.0
toml==0.10.2
+tomli==2.0.1
translationstring==1.4
-typed-ast==1.4.3
-typing-extensions==3.10.0.0
-urllib3==1.26.6
+types-Deprecated==1.2.7
+types-PyYAML==6.0.7
+types-requests==2.27.25
+types-setuptools==57.4.14
+types-tabulate==0.8.8
+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.5
+wadllib==1.3.6
WebOb==1.8.7
+wrapt==1.14.1
ws4py==0.5.1
-zipp==3.5.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 6b0690ef89..5d47c15395 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,60 +1,72 @@
-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.12
+click==8.1.3
+craft-cli==0.6.0
+craft-grammar==1.1.1
+craft-parts==1.6.1
+craft-providers==1.2.0
+craft-store==2.1.1
cryptography==3.4
-distro==1.5.0
-docutils==0.17.1
+Deprecated==1.2.13
+distro==1.7.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.4
+idna==3.3
+importlib-metadata==4.11.3
+jeepney==0.8.0
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.5
+keyring==23.5.0
+launchpadlib==1.10.16
+lazr.restfulclient==0.14.4
+lazr.uri==1.0.6
+lxml==4.8.0
macaroonbakery==1.3.1
mypy-extensions==0.4.3
-oauthlib==3.1.1
-pbr==5.6.0
+oauthlib==3.2.0
+overrides==6.1.0
+platformdirs==2.5.2
progressbar==2.5
-protobuf==3.17.3
-psutil==5.8.0
-pycparser==2.20
-pyelftools==0.27
-pylxd==2.3.0
+protobuf==3.20.1
+psutil==5.9.0
+pycparser==2.21
+pydantic==1.9.0
+pydantic-yaml==0.6.3
+pyelftools==0.28
+pylxd==2.3.1
pymacaroons==0.13.0
-pyparsing==2.4.7
+pyparsing==3.0.8
pyRFC3339==1.1
python-dateutil==2.8.2
-python-debian==0.1.40
-pytz==2021.1
+python-debian==0.1.43
+pytz==2022.1
pyxdg==0.27
-PyYAML==5.4
+PyYAML==6.0
raven==6.10.0
-requests==2.26.0
+requests==2.27.1
requests-toolbelt==0.9.1
-requests-unixsocket==0.2.0
-SecretStorage==3.3.1
-semantic-version==2.8.5
-simplejson==3.17.3
+requests-unixsocket==0.3.0
+SecretStorage==3.3.2
+semantic-version==2.9.0
+semver==2.13.0
+simplejson==3.17.6
six==1.16.0
tabulate==0.8.9
-testresources==2.0.1
-tinydb==4.5.0
+tinydb==4.7.0
toml==0.10.2
-typing-extensions==3.10.0.0
-urllib3==1.26.6
-wadllib==1.3.5
+types-Deprecated==1.2.7
+typing-utils==0.1.0
+typing_extensions==4.2.0
+urllib3==1.26.9
+wadllib==1.3.6
+wrapt==1.14.1
ws4py==0.5.1
-zipp==3.5.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"
-
+setuptools==49.6.0
diff --git a/schema/snapcraft.json b/schema/snapcraft.json
index 286b0a9f31..2c0fe0d0db 100644
--- a/schema/snapcraft.json
+++ b/schema/snapcraft.json
@@ -832,7 +832,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/setup.cfg b/setup.cfg
index 599afeeb6e..a80dbe21d5 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,
@@ -29,11 +26,17 @@ exclude =
prime
[mypy]
-python_version = 3.6
+python_version = 3.8
ignore_missing_imports = True
follow_imports = silent
[pycodestyle]
max-line-length = 88
-ignore = E501,W503,E203
+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)
+# 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
old mode 100644
new mode 100755
index 2886a0bccb..eb5c449222
--- 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
@@ -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",
]
@@ -59,23 +59,33 @@ def recursive_data_files(directory, install_directory):
scripts = []
dev_requires = [
+ "black",
"codespell",
"coverage",
- "flake8==3.7.9",
- "pyflakes==2.1.1",
+ "flake8",
+ "pyflakes",
"fixtures",
"isort",
"mccabe",
- "mypy==0.770",
+ "mypy",
"testscenarios",
"pexpect",
"pip",
- "pycodestyle==2.5.0",
+ "pycodestyle",
+ "pydocstyle",
"pyftpdlib",
+ "pylint",
+ "pylint-fixme-info",
+ "pylint-pytest",
"pyramid",
"pytest",
"pytest-cov",
+ "pytest-mock",
"pytest-subprocess",
+ "types-PyYAML",
+ "types-requests",
+ "types-setuptools",
+ "types-tabulate",
]
if sys.platform == "win32":
@@ -84,6 +94,11 @@ def recursive_data_files(directory, install_directory):
install_requires = [
"attrs",
"click",
+ "craft-cli",
+ "craft-grammar",
+ "craft-parts",
+ "craft-providers",
+ "craft-store",
"cryptography==3.4",
"gnupg",
"jsonschema==2.5.1",
@@ -92,17 +107,19 @@ def recursive_data_files(directory, install_directory):
"lxml",
"macaroonbakery",
"mypy-extensions",
+ "overrides",
"progressbar",
"pyelftools",
"pymacaroons",
"pyxdg",
- "pyyaml==5.4",
+ "pyyaml",
"raven",
"requests-toolbelt",
"requests-unixsocket",
"requests",
"simplejson",
"tabulate",
+ "toml",
"tinydb",
"typing-extensions",
]
@@ -139,7 +156,12 @@ 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_legacy = snapcraft_legacy.cli.__main__:run",
+ "snapcraft = snapcraft.cli:run",
+ ]
+ ),
data_files=(
recursive_data_files("schema", "share/snapcraft")
+ recursive_data_files("keyrings", "share/snapcraft")
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.spec b/snapcraft.spec
index b3c2a0bb76..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,9 +12,13 @@ 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\\cli\\__main__.py"],
+ ["snapcraft_legacy\\cli\\__main__.py"],
pathex=[],
binaries=[],
datas=data,
diff --git a/snapcraft/__init__.py b/snapcraft/__init__.py
index 8613351eaa..6c9a0a516e 100644
--- a/snapcraft/__init__.py
+++ b/snapcraft/__init__.py
@@ -1,6 +1,6 @@
# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*-
#
-# Copyright (C) 2015-2017, 2020 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
@@ -14,357 +14,20 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
-"""Snapcraft plugins drive different build systems
+"""Publish your app for Linux users for desktop, cloud, and IoT."""
-Each part has a build system . Most parts are built from source using one of
-a range of build systems such as CMake or Scons. Some parts are pre-built
-and just copied into place, for example parts that reuse existing binary
-packages.
+import os
-You tell snapcraft which build system it must drive by specifying the
-snapcraft plugin for that part. Every part must specify a plugin explicitly
-(when you see a part that does not specify a plugin, that's because the
-actual part definition is in the cloud, where the plugin is specified!)
-
-These plugins implement a lifecycle over the following steps:
-
- - pull: retrieve the source for the part from the specified location
- - build: drive the build system determined by the choice of plugin
- - stage: consolidate desirable files from all the parts in one tree
- - prime: distill down to only the files which will go into the snap
- - snap: compress the prime tree into the installable snap file
-
-These steps correspond to snapcraft commands. So when you initiate a
-'snapcraft pull' you will invoke the respective plugin for each part in
-the snap, in sequence, to handle the source pull. Each part will then have a
-fully populated parts//src/ directory. Similarly, if you then say
-'snapcraft build' you will invoke the plugin responsible for each part in
-turn, to build the part.
-
-# Snapcraft Lifecycle
-
-## Pull
-
-In this first step, source material is retrieved from the specified
-location, whether that is a URL for a tarball, a local path to a source tree
-inside the snap, a revision control reference to checkout, or something
-specific to the plugin such as PyPI. The plugin might also download
-necessary artifacts, such as the Java SDK, which are not specific to the
-particular part but which are needed by the plugin to handle its type of
-build system.
-
-All the downloaded content for each part goes into the
-`parts//src/` directory, which acts as a cache to prevent
-re-fetching content. You can clean that cache out with 'snapcraft clean'.
-
-## Build
-
-Snapcraft calculates an appropriate sequence to build the parts, based on
-explicit 'after' references and the order of the parts in the
-snapcraft.yaml. Each part is built in the `parts//build`
-directory and installed into `parts//install`.
-
-Note the install step - we might actually want to use built artifacts from
-one part in the build process of another, so the `parts//install`
-directory is useful as a 'working fresh install' of the part.
-
-Between the plugin, the part definition YAML, and the build system of the
-part, it is expected that the part can be built and installed in the right
-place.
-
-At this point you have a tree under `parts/` with a subdirectory for every
-part, and underneath those, separate src, build and install trees for each
-part.
-
-## Stage
-
-We now need to start consolidating the important pieces of each part into a
-single tree. We do this twice - once in a very sweeping way that will
-produce a lot of extraneous materials but is useful for debugging. This is
-the 'stage' step of the lifecycle, because we move a lot of the build output
-from each part into a consolidated tree under `stage/` which has the
-structure of a snap but has way too much extra information.
-
-The important thing about the staging area is that it lets you get all the
-shared libraries in one place and lets you find overlapping content in the
-parts. You can also try this directory as if it were a snap, and you'll have
-all the debugging information in the tree, which is useful for developers.
-
-Each part describes its own staging content - the files that should be
-staged. The part will often describe "chunks" of content, called filesets,
-so that they can be referred to as a useful set rather than having to call
-out individual files.
-
-## Prime
-
-It is useful to have a directory tree which exactly mirrors the structure of
-the final snap. This is the `prime/` directory, and the lifecycle includes a
-'prime' step which copies only that final, required content from the
-`stage/` directory into the `prime/` directory.
-
-So the `prime/` directory contains only the content that will be put into
-the final snap, unlike the staging area which may include debug and
-development files not destined for your snap.
-
-The snap metadata will also be placed in `./prime/meta` during the prime
-step, so this `./prime` directory is useful for inspecting exactly what is
-going into your snap or to conduct any final post-processing on snapcraft's
-output.
-
-## Snap
-
-The final step in the snapcraft lifecycle builds a snap out of the `prime/`
-directory. It will be in the top level directory, alongside snapcraft.yaml,
-called --.snap
-
-
-# Standard part definition keywords
-
-There are several builtin keywords which can be used in any part regardless
-of the choice of plugin.
-
- - after: [part, part, part...]
-
- Snapcraft will make sure that it builds all of the listed parts before
- it tries to build this part. Essentially these listed dependencies for
- this part, useful when the part needs a library or tool built by another
- part.
-
- If such a dependency part is not defined in this snapcraft.yaml, it must
- be defined in the cloud parts library, and snapcraft will retrieve the
- definition of the part from the cloud. In this way, a shared library of
- parts is available to every snap author - just say 'after' and list the
- parts you want that others have already defined.
-
- - build-packages: [pkg, pkg, pkg...]
-
- A list of packages to install on the build host before building
- the part. The files from these packages typically will not go into the
- final snap unless they contain libraries that are direct dependencies of
- binaries within the snap (in which case they'll be discovered via `ldd`),
- or they are explicitly described in stage-packages.
-
- - stage-packages: YAML list
-
- A set of packages to be downloaded and unpacked to join the part
- before it's built. Note that these packages are not installed on the host.
- Like the rest of the part, all files from these packages will make it into
- the final snap unless filtered out via the `snap` keyword.
-
- One may simply specify packages in a flat list, in which case the packages
- will be fetched and unpacked regardless of build environment. In addition,
- a specific grammar made up of sub-lists is supported here that allows one
- to filter stage packages depending on various selectors (e.g. the target
- arch), as well as specify optional packages. The grammar is made up of two
- nestable statements: 'on' and 'try'.
-
- Let's discuss `on`.
-
- - on [,...]:
- - ...
- - else[ fail]:
- - ...
-
- The body of the 'on' clause is taken into account if every (AND, not OR)
- selector is true for the target build environment. Currently the only
- selectors supported are target architectures (e.g. amd64).
-
- If the 'on' clause doesn't match and it's immediately followed by an 'else'
- clause, the 'else' clause must be satisfied. An 'on' clause without an
- 'else' clause is considered satisfied even if no selector matched. The
- 'else fail' form allows erroring out if an 'on' clause was not matched.
-
- For example, say you only wanted to stage `foo` if building for amd64 (and
- not stage `foo` if otherwise):
-
- - on amd64: [foo]
-
- Building on that, say you wanted to stage `bar` if building on an arch
- other than amd64:
-
- - on amd64: [foo]
- - else: [bar]
-
- You can nest these for more complex behaviors:
-
- - on amd64: [foo]
- - else:
- - on i386: [bar]
- - on armhf: [baz]
-
- If your project requires a package that is only available on amd64, you can
- fail if you're not building for amd64:
-
- - on amd64: [foo]
- - else fail
-
- Now let's discuss `try`:
-
- - try:
- - ...
- - else:
- - ...
-
- The body of the 'try' clause is taken into account only when all packages
- contained within it are valid. If not, if it's immediately followed by
- 'else' clauses they are tried in order, and one of them must be satisfied.
- A 'try' clause with no 'else' clause is considered satisfied even if it
- contains invalid packages.
-
- For example, say you wanted to stage `foo`, but it wasn't available for all
- architectures. Assuming your project builds without it, you can make it an
- optional stage package:
-
- - try: [foo]
-
- You can also add alternatives:
-
- - try: [foo]
- - else: [bar]
-
- Again, you can nest these for more complex behaviors:
-
- - on amd64: [foo]
- - else:
- - try: [bar]
-
- - organize: YAML
-
- Snapcraft will rename files according to this YAML sub-section. The
- content of the 'organize' section consists of old path keys, and their
- new values after the renaming.
-
- This can be used to avoid conflicts between parts that use the same
- name, or to map content from different parts into a common conventional
- file structure. For example:
-
- organize:
- usr/oldfilename: usr/newfilename
- usr/local/share/: usr/share/
-
- The key is the internal part filename, the value is the exposed filename
- that will be used during the staging process. You can rename whole
- subtrees of the part, or just specific files.
-
- Note that the path is relative (even though it is "usr/local") because
- it refers to content underneath parts//install which is going
- to be mapped into the stage and prime areas.
-
- - filesets: YAML
-
- When we map files into the stage and prime areas on the way to putting
- them into the snap, it is convenient to be able to refer to groups of
- files as well as individual files. Snapcraft lets you name a fileset
- and then use it later for inclusion or exclusion of those files from the
- resulting snap.
-
- For example, consider man pages of header files.. You might want them
- in, or you might want to leave them out, but you definitely don't want
- to repeatedly have to list all of them either way.
-
- This section is thus a YAML map of fileset names (the keys) to a list of
- filenames. The list is built up by adding individual files or whole
- subdirectory paths (and all the files under that path) and wildcard
- globs, and then pruning from those paths.
-
- The wildcard * globs all files in that path. Exclusions are denoted by
- an initial `-`.
-
- For example you could add usr/local/* then remove usr/local/man/*:
-
- filesets:
- allbutman: [ usr/local/*, -usr/local/man/* ]
- manpages: [ usr/local/man ]
-
- Filenames are relative to the part install directory in
- `parts//install`. If you have used 'organize' to rename files
- then the filesets will be built up from the names after organization.
-
- - stage: YAML file and fileset list
-
- A list of files from a part install directory to copy into `stage/`.
- Rules applying to the list here are the same as those of filesets.
- Referencing of fileset keys is done with a $ prefixing the fileset key,
- which will expand with the value of such key.
-
- For example:
-
- stage:
- - usr/lib/* # Everything under parts//install/usr/lib
- - -usr/lib/libtest.so # Excludng libtest.so
- - $manpages # Including the 'manpages' fileset
-
- - snap: YAML file and fileset list
-
- A list of files from a part install directory to copy into `prime/`.
- This section takes exactly the same form as the 'stage' section but the
- files identified here will go into the ultimate snap (because the
- `prime/` directory reflects the file structure of the snap with no
- extraneous content).
-
- - build-attributes: [attribute1, attribute2]
-
- A list of special attributes that affect the build of this specific part.
- Supported attributes:
-
- - no-install:
- Do not run the install target provided by the plugin's build system.
-
- Supported by: kbuild
-
- - debug:
- Plugins that support the concept of build types build in Release mode
- by default. Setting the 'debug' attribute requests that they instead
- build in Debug mode.
-"""
-
-from collections import OrderedDict # noqa
-
-import pkg_resources # noqa
+import pkg_resources
def _get_version():
- import os as _os
-
- if _os.environ.get("SNAP_NAME") == "snapcraft":
- return _os.environ["SNAP_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"
-# Set this early so that the circular imports aren't too painful
__version__ = _get_version()
-
-# Workaround for potential import loops.
-from snapcraft.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
-
-# FIXME LP: #1662658
-from snapcraft._store import ( # noqa
- create_key,
- download,
- gated,
- list_keys,
- list_registered,
- login,
- register,
- register_key,
- sign_build,
- status,
- upload,
- upload_metadata,
- validate,
-)
-
-from snapcraft.project._project_options import ProjectOptions # noqa isort:skip
diff --git a/snapcraft/__main__.py b/snapcraft/__main__.py
new file mode 100644
index 0000000000..8675237d10
--- /dev/null
+++ b/snapcraft/__main__.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 .
+
+"""Main entry point."""
+
+import sys
+
+from snapcraft import cli
+
+sys.exit(cli.run())
diff --git a/snapcraft/cli.py b/snapcraft/cli.py
new file mode 100644
index 0000000000..b5142f60a9
--- /dev/null
+++ b/snapcraft/cli.py
@@ -0,0 +1,192 @@
+# -*- 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 .
+
+"""Command-line application entry point."""
+
+import contextlib
+import logging
+import os
+import sys
+
+import craft_cli
+import craft_store
+from craft_cli import ArgumentParsingError, EmitterMode, ProvideHelpException, emit
+
+from snapcraft import __version__, errors, utils
+from snapcraft_legacy.cli import legacy
+
+from . import commands
+
+COMMAND_GROUPS = [
+ craft_cli.CommandGroup(
+ "Lifecycle",
+ [
+ commands.CleanCommand,
+ commands.PullCommand,
+ commands.BuildCommand,
+ commands.StageCommand,
+ 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(
+ "Store Account",
+ [
+ commands.StoreLoginCommand,
+ commands.StoreExportLoginCommand,
+ commands.StoreLogoutCommand,
+ commands.StoreWhoAmICommand,
+ ],
+ ),
+ craft_cli.CommandGroup(
+ "Store Snap Names",
+ [
+ commands.StoreRegisterCommand,
+ commands.StoreNamesCommand,
+ commands.StoreLegacyListRegisteredCommand,
+ commands.StoreLegacyListCommand,
+ commands.StoreLegacyMetricsCommand,
+ commands.StoreLegacyUploadMetadataCommand,
+ ],
+ ),
+ craft_cli.CommandGroup(
+ "Store Snap Release Management",
+ [
+ commands.StoreReleaseCommand,
+ commands.StoreCloseCommand,
+ commands.StoreStatusCommand,
+ commands.StoreUploadCommand,
+ commands.StoreLegacyPromoteCommand,
+ commands.StoreLegacyListRevisionsCommand,
+ ],
+ ),
+ craft_cli.CommandGroup(
+ "Store Snap Tracks",
+ [
+ commands.StoreListTracksCommand,
+ commands.StoreTracksCommand, # hidden (alias to list-tracks)
+ commands.StoreLegacySetDefaultTrackCommand,
+ ],
+ ),
+ craft_cli.CommandGroup(
+ "Store Assertions",
+ [
+ commands.StoreLegacyCreateKeyCommand,
+ commands.StoreLegacyEditValidationSetsCommand,
+ commands.StoreLegacyGatedCommand,
+ commands.StoreLegacyListValidationSetsCommand,
+ commands.StoreLegacyRegisterKeyCommand,
+ commands.StoreLegacySignBuildCommand,
+ commands.StoreLegacyValidateCommand,
+ commands.StoreLegacyListKeysCommand,
+ ],
+ ),
+ craft_cli.CommandGroup("Other", [commands.VersionCommand]),
+]
+
+GLOBAL_ARGS = [
+ craft_cli.GlobalArgument(
+ "version", "flag", "-V", "--version", "Show the application version and exit"
+ )
+]
+
+
+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()
+
+ # 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)
+
+ if utils.is_managed_mode():
+ 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,
+ )
+
+ return craft_cli.Dispatcher(
+ "snapcraft",
+ COMMAND_GROUPS,
+ summary="Package, distribute, and update snaps for Linux and IoT",
+ extra_global_args=GLOBAL_ARGS,
+ 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"):
+ emit.message(f"snapcraft {__version__}")
+ else:
+ dispatcher.load_command(None)
+ dispatcher.run()
+ emit.ended_ok()
+ 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 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
+
+ return retcode
diff --git a/snapcraft/cli/store.py b/snapcraft/cli/store.py
deleted file mode 100644
index efe2fbc354..0000000000
--- a/snapcraft/cli/store.py
+++ /dev/null
@@ -1,997 +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 .
-
-import functools
-import json
-import operator
-import os
-import stat
-import sys
-from datetime import date, timedelta
-from textwrap import dedent
-from typing import Dict, List, Optional, Set, Union
-
-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
-
-from . import echo
-from ._channel_map import get_tabulated_channel_map
-from ._metrics import convert_metrics_to_table
-from ._review import review_snap
-
-_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():
- """Store commands"""
-
-
-def _human_readable_acls(store_client: storeapi.StoreClient) -> str:
- acl = store_client.acl()
- snap_names = []
- snap_ids = acl["snap_ids"]
-
- if snap_ids is not None:
- try:
- for snap_id in snap_ids:
- snap_names.append(store_client.get_snap_name_for_id(snap_id))
- except TypeError:
- raise RuntimeError(f"invalid snap_ids: {snap_ids!r}")
- acl["snap_names"] = snap_names
- else:
- acl["snap_names"] = None
-
- human_readable_acl: Dict[str, Union[str, List[str], None]] = {
- "expires": str(acl["expires"])
- }
-
- for key in ("snap_names", "channels", "permissions"):
- human_readable_acl[key] = acl[key]
- if not acl[key]:
- human_readable_acl[key] = "No restriction"
-
- return dedent(
- """\
- snaps: {snap_names}
- channels: {channels}
- permissions: {permissions}
- expires: {expires}
- """.format(
- **human_readable_acl
- )
- )
-
-
-@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.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",
- 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.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")
-@click.option(
- "--force",
- is_flag=True,
- help="Force metadata update to override any possible conflict",
-)
-@click.argument(
- "snap-file",
- metavar="",
- type=click.Path(exists=True, readable=True, resolve_path=True, dir_okay=False),
-)
-def upload_metadata(snap_file, force):
- """Upload metadata from to the store.
-
- The following information will be retrieved from and used
- to update the store:
-
- \b
- - summary
- - description
- - icon
-
- If --force is given, it will force the local metadata into the Store,
- ignoring any possible conflict.
-
- \b
- Examples:
- snapcraft upload-metadata my-snap_0.1_amd64.snap
- 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)
-
-
-@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(
- "--from-channel",
- metavar="",
- required=True,
- help="The channel to promote from.",
-)
-@click.option(
- "--to-channel",
- metavar="",
- required=True,
- help="The channel to promote to.",
-)
-@click.option("--yes", is_flag=True, help="Do not prompt for confirmation.")
-def promote(snap_name, from_channel, to_channel, yes):
- """Promote a build set from to a channel.
-
- 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
-
- \b
- - 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.
-
- \b
- Examples:
- snapcraft promote my-snap --from-channel candidate --to-channel stable
- snapcraft promote my-snap --from-channel lts/candidate --to-channel lts/stable
- snapcraft promote my-snap --from-channel stable/patch --to-channel stable
- snapcraft promote my-snap --from-channel experimental/stable --to-channel stable
- """
- echo.warning(
- "snapcraft promote does not have a stable CLI interface. Use with caution in scripts."
- )
- parsed_from_channel = storeapi.channels.Channel(from_channel)
- parsed_to_channel = storeapi.channels.Channel(to_channel)
-
- if parsed_from_channel == parsed_to_channel:
- raise click.BadOptionUsage(
- "--to-channel", "--from-channel and --to-channel cannot be the same."
- )
- elif (
- parsed_from_channel.risk == "edge"
- and parsed_from_channel.branch is None
- and yes
- ):
- raise click.BadOptionUsage(
- "--from-channel",
- "{!r} is not a valid set value for --from-channel when using --yes.".format(
- parsed_from_channel
- ),
- )
-
- store = storeapi.StoreClient()
- status_payload = store.get_snap_status(snap_name)
-
- snap_status = storeapi.status.SnapStatus(
- snap_name=snap_name, payload=status_payload
- )
- from_channel_set = snap_status.get_channel_set(parsed_from_channel)
- echo.info("Build set information for {!r}".format(parsed_from_channel))
- click.echo(
- tabulate(
- sorted(from_channel_set, key=operator.attrgetter("arch")),
- headers=["Arch", "Revision", "Version"],
- tablefmt="plain",
- )
- )
- if yes or echo.confirm(
- "Do you want to promote the current set to the {!r} channel?".format(
- parsed_to_channel
- )
- ):
- for c in from_channel_set:
- store.release(
- snap_name=snap_name,
- 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,
- )
- )
- else:
- 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"][DEFAULT_SERIES][snap_name]["snap-id"]
- except KeyError:
- raise storeapi.errors.StoreChannelClosingPermissionError(
- snap_name, 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",
- 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"
-)
-@click.argument("snap-name", metavar="")
-def list_revisions(snap_name, arch):
- """Get the history on the store for .
-
- This command has an alias of `revisions`.
-
- \b
- Examples:
- snapcraft list-revisions my-snap
- snapcraft list-revisions my-snap --arch armhf
- snapcraft revisions my-snap
- """
- releases = StoreClientCLI().get_snap_releases(snap_name=snap_name)
-
- def get_channels_for_revision(revision: int) -> List[str]:
- # channels: the set of channels revision was released to, active or not.
- channels: Set[str] = set()
- # seen_channel: applies to channels regardless of revision.
- # The first channel that shows up for each architecture is to
- # be marked as the active channel, all others are historic.
- seen_channel: Dict[str, Set[str]] = dict()
-
- for release in releases.releases:
- if release.architecture not in seen_channel:
- seen_channel[release.architecture] = set()
-
- # If the revision is in this release entry and was not seen
- # before it means that this channel is active and needs to
- # be represented with a *.
- if (
- release.revision == revision
- and release.channel not in seen_channel[release.architecture]
- ):
- channels.add(f"{release.channel}*")
- # All other releases found for a revision are inactive.
- elif (
- release.revision == revision
- and release.channel not in channels
- and f"{release.channel}*" not in channels
- ):
- channels.add(release.channel)
-
- seen_channel[release.architecture].add(release.channel)
-
- return sorted(list(channels))
-
- parsed_revisions = list()
- for rev in releases.revisions:
- if arch and arch not in rev.architectures:
- continue
- channels_for_revision = get_channels_for_revision(rev.revision)
- if channels_for_revision:
- channels = ",".join(channels_for_revision)
- else:
- channels = "-"
- parsed_revisions.append(
- (
- rev.revision,
- rev.created_at,
- ",".join(rev.architectures),
- rev.version,
- channels,
- )
- )
-
- tabulated_revisions = tabulate(
- parsed_revisions,
- numalign="left",
- headers=["Rev.", "Uploaded", "Arches", "Version", "Channels"],
- tablefmt="plain",
- )
-
- # 23 revisions + header should not need paging.
- if len(parsed_revisions) < 24:
- click.echo(tabulated_revisions)
- else:
- 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.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
- """
-
- snap_list = None
- channel_list = None
- acl_list = None
-
- if snaps:
- snap_list = []
- for package in snaps.split(","):
- snap_list.append({"name": package, "series": DEFAULT_SERIES})
-
- if channels:
- channel_list = channels.split(",")
-
- 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.login(
- store=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() == "-":
- echo.info("\nExported login starts on next line:")
- store_client.export_login(config_fd=sys.stdout, encode=True)
- print()
-
- preamble = "Login successfully exported and printed above"
- login_action = 'echo "" | snapcraft login --with -'
- 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)
-
- # 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)
-
- print()
- echo.info(
- dedent(
- """\
- {}. This can now be used with
-
- {}
-
- """.format(
- preamble, login_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
- """
- store_client = storeapi.StoreClient(use_candid=experimental_login)
- if store_client.use_candid:
- store_client.login(config_fd=login_file, save=True)
- else:
- snapcraft.login(store=store_client, config_fd=login_file)
-
- 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.")
-
-
-@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="")
-def set_default_track(snap_name: str, track_name: str):
- """Set the default track for to .
-
- The track must be a valid active track for this operation to be successful.
- """
- 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))
-
-
-@storecli.command()
-@click.argument("snap-name", metavar="", required=True)
-@click.option(
- "--name",
- metavar="",
- help="Metric name",
- type=click.Choice([x.value for x in metrics_module.MetricsNames]),
- required=True,
-)
-@click.option(
- "--start",
- metavar="",
- help="Date in format YYYY-MM-DD",
- required=True,
- default=_YESTERDAY,
-)
-@click.option(
- "--end",
- metavar="",
- help="Date in format YYYY-MM-DD",
- required=True,
- default=_YESTERDAY,
-)
-@click.option(
- "--format",
- metavar="",
- help="Format for output",
- type=click.Choice(["table", "json"]),
- required=True,
-)
-def metrics(snap_name: str, name: str, start: str, end: str, format: str):
- """Get metrics for ."""
- store = storeapi.StoreClient()
- account_info = store.get_account_information()
-
- try:
- snap_id = account_info["snaps"][DEFAULT_SERIES][snap_name]["snap-id"]
- except KeyError:
- echo.exit_error(
- brief="No permissions for snap.",
- resolution="Ensure the snap name and credentials are correct.is correct and that the correct credentials are used.",
- )
-
- mf = metrics_module.MetricsFilter(
- snap_id=snap_id, metric_name=name, start=start, end=end
- )
-
- results = store.get_metrics(filters=[mf], snap_name=snap_name)
-
- # Sanity check to ensure that only one result is found (as we currently only
- # support one query at a time).
- if len(results.metrics) != 1:
- raise RuntimeError(f"Unexpected metric results from store: {results!r}")
-
- metric_results = results.metrics[0]
-
- if format == "json":
- output = json.dumps(metric_results.marshal(), indent=2, sort_keys=True)
- click.echo(output)
- elif format == "table":
- rows = convert_metrics_to_table(metric_results, transpose=True)
- output = tabulate(rows, tablefmt="plain")
- echo.echo_with_pager_if_needed(output)
diff --git a/snapcraft/commands/__init__.py b/snapcraft/commands/__init__.py
new file mode 100644
index 0000000000..7f01129761
--- /dev/null
+++ b/snapcraft/commands/__init__.py
@@ -0,0 +1,106 @@
+# -*- 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 .account import (
+ StoreExportLoginCommand,
+ StoreLoginCommand,
+ StoreLogoutCommand,
+ StoreWhoAmICommand,
+)
+from .extensions import (
+ ExpandExtensionsCommand,
+ ExtensionsCommand,
+ ListExtensionsCommand,
+)
+from .legacy import (
+ StoreLegacyCreateKeyCommand,
+ StoreLegacyEditValidationSetsCommand,
+ StoreLegacyGatedCommand,
+ StoreLegacyListKeysCommand,
+ StoreLegacyListRevisionsCommand,
+ StoreLegacyListValidationSetsCommand,
+ StoreLegacyMetricsCommand,
+ StoreLegacyPromoteCommand,
+ StoreLegacyRegisterKeyCommand,
+ StoreLegacyRemoteBuildCommand,
+ StoreLegacySetDefaultTrackCommand,
+ StoreLegacySignBuildCommand,
+ StoreLegacyUploadMetadataCommand,
+ StoreLegacyValidateCommand,
+)
+from .lifecycle import (
+ BuildCommand,
+ CleanCommand,
+ PackCommand,
+ PrimeCommand,
+ PullCommand,
+ SnapCommand,
+ StageCommand,
+)
+from .manage import StoreCloseCommand, StoreReleaseCommand
+from .names import (
+ StoreLegacyListCommand,
+ StoreLegacyListRegisteredCommand,
+ StoreNamesCommand,
+ StoreRegisterCommand,
+)
+from .status import StoreListTracksCommand, StoreStatusCommand, StoreTracksCommand
+from .upload import StoreUploadCommand
+from .version import VersionCommand
+
+__all__ = [
+ "BuildCommand",
+ "CleanCommand",
+ "ExpandExtensionsCommand",
+ "ExtensionsCommand",
+ "ListExtensionsCommand",
+ "PackCommand",
+ "PrimeCommand",
+ "PullCommand",
+ "SnapCommand",
+ "StageCommand",
+ "StoreCloseCommand",
+ "StoreExportLoginCommand",
+ "StoreLegacyCreateKeyCommand",
+ "StoreLegacyEditValidationSetsCommand",
+ "StoreLegacyGatedCommand",
+ "StoreLegacyListCommand",
+ "StoreLegacyListRegisteredCommand",
+ "StoreLegacyListRevisionsCommand",
+ "StoreLegacyListValidationSetsCommand",
+ "StoreLegacyMetricsCommand",
+ "StoreLegacyPromoteCommand",
+ "StoreLegacyRegisterKeyCommand",
+ "StoreLegacyRemoteBuildCommand",
+ "StoreLegacySetDefaultTrackCommand",
+ "StoreLegacySignBuildCommand",
+ "StoreLegacyUploadMetadataCommand",
+ "StoreLegacyValidateCommand",
+ "StoreLegacyListKeysCommand",
+ "StoreListTracksCommand",
+ "StoreLoginCommand",
+ "StoreLogoutCommand",
+ "StoreNamesCommand",
+ "StoreRegisterCommand",
+ "StoreReleaseCommand",
+ "StoreStatusCommand",
+ "StoreTracksCommand",
+ "StoreUploadCommand",
+ "StoreWhoAmICommand",
+ "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/snapcraft/commands/extensions.py b/snapcraft/commands/extensions.py
new file mode 100644
index 0000000000..6c00e2cccb
--- /dev/null
+++ b/snapcraft/commands/extensions.py
@@ -0,0 +1,117 @@
+# -*- 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
+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,
+)
+
+
+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
+
+
+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/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/commands/lifecycle.py b/snapcraft/commands/lifecycle.py
new file mode 100644
index 0000000000..54de39e1f9
--- /dev/null
+++ b/snapcraft/commands/lifecycle.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 .
+
+"""Snapcraft lifecycle commands."""
+
+import abc
+import argparse
+import textwrap
+
+from craft_cli import BaseCommand, emit
+from overrides import overrides
+
+from snapcraft import pack
+from snapcraft.parts import lifecycle as parts_lifecycle
+
+
+class _LifecycleCommand(BaseCommand, abc.ABC):
+ """Run lifecycle-related commands."""
+
+ @overrides
+ def fill_parser(self, parser: "argparse.ArgumentParser") -> None:
+ group = parser.add_mutually_exclusive_group()
+ group.add_argument(
+ "--destructive-mode",
+ action="store_true",
+ help="Build in the current host",
+ )
+ group.add_argument(
+ "--use-lxd",
+ 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",
+ 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)
+
+ @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}")
+ parts_lifecycle.run(self.name, parsed_args)
+
+
+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="part-name",
+ type=str,
+ nargs="*",
+ help="Optional list of parts to process",
+ )
+
+ 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.",
+ )
+ group.add_argument(
+ "--shell-after",
+ action="store_true",
+ help="Shell into the environment after the step has run.",
+ )
+
+
+class PullCommand(_LifecycleStepCommand):
+ """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(_LifecycleStepCommand):
+ """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(_LifecycleStepCommand):
+ """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(_LifecycleStepCommand):
+ """Prepare the final payload for packing."""
+
+ name = "prime"
+ help_msg = "Prime artifacts defined for a part"
+ overview = textwrap.dedent(
+ """
+ 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.
+ """
+ )
+
+
+class PackCommand(_LifecycleCommand):
+ """Pack the final snap payload."""
+
+ name = "pack"
+ help_msg = "Create the snap package"
+ overview = textwrap.dedent(
+ """
+ Process parts and create a snap file containing the project payload
+ with the provided metadata. If a directory is specified, pack its
+ contents instead.
+ """
+ )
+
+ @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)
+
+
+class SnapCommand(_LifecycleCommand):
+ """Pack the final snap payload. This is a legacy compatibility command."""
+
+ name = "snap"
+ help_msg = "Create a snap"
+ hidden = True
+ overview = textwrap.dedent(
+ """
+ Process parts and create a snap file containing the project payload
+ with the provided metadata.
+ """
+ )
+
+ @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."""
+
+ 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/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/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/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
new file mode 100644
index 0000000000..f5c26953b6
--- /dev/null
+++ b/snapcraft/commands/store/__init__.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 .
+
+"""Snapcraft CLI interface for the Snap Store."""
+
+
+from . import constants
+from .channel_map import ChannelMap
+from .client import StoreClientCLI
+
+__all__ = [
+ "ChannelMap",
+ "StoreClientCLI",
+ "constants",
+]
diff --git a/snapcraft/storeapi/v2/channel_map.py b/snapcraft/commands/store/channel_map.py
similarity index 55%
rename from snapcraft/storeapi/v2/channel_map.py
rename to snapcraft/commands/store/channel_map.py
index 971b6d8b7b..f4e37fa0f5 100644
--- a/snapcraft/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
new file mode 100644
index 0000000000..4e2054626c
--- /dev/null
+++ b/snapcraft/commands/store/client.py
@@ -0,0 +1,399 @@
+# -*- 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
+import time
+from datetime import timedelta
+from typing import Any, Dict, Optional, Sequence, Tuple
+
+import craft_store
+import requests
+from craft_cli import emit
+
+from snapcraft import __version__, errors, utils
+
+from . import channel_map, constants
+
+_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()
+):
+ """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)
+ self._base_url = get_store_url()
+
+ 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
+
+ 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_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]:
+ """Return account information."""
+ return self.request(
+ "GET",
+ 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]},
+ )
+
+ 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/store/constants.py b/snapcraft/commands/store/constants.py
new file mode 100644
index 0000000000..ea74dd58a4
--- /dev/null
+++ b/snapcraft/commands/store/constants.py
@@ -0,0 +1,41 @@
+# -*- 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."""
+
+DEFAULT_SERIES = "16"
+"""Legacy value for older generation Snap Store APIs."""
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/commands/version.py b/snapcraft/commands/version.py
new file mode 100644
index 0000000000..74e7ac04f7
--- /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 = "Show the application version and exit"
+ common = True
+
+ def run(self, parsed_args):
+ """Run the command."""
+ emit.message(f"snapcraft {__version__}")
diff --git a/snapcraft/errors.py b/snapcraft/errors.py
new file mode 100644
index 0000000000..0aff817069
--- /dev/null
+++ b/snapcraft/errors.py
@@ -0,0 +1,60 @@
+# -*- 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 use an unimplemented feature."""
+
+ 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."""
+
+
+class ExtensionError(SnapcraftError):
+ """Error during parts processing."""
+
+
+class MetadataExtractionError(SnapcraftError):
+ """Attempt to extract metadata from file was unsuccessful."""
+
+ 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/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..2a34670814
--- /dev/null
+++ b/snapcraft/extensions/_extension.py
@@ -0,0 +1,124 @@
+# -*- 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 base class definition."""
+
+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",
+ )
+
+ if 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..8022963c78
--- /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 .
+
+"""Extension application helpers."""
+
+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", {}).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
+
+ 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
+ )
+ 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] = []
+
+ 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/internal/project_loader/grammar/_compound.py b/snapcraft/internal/project_loader/grammar/_compound.py
deleted file mode 100644
index aef0fac68a..0000000000
--- a/snapcraft/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/internal/project_loader/grammar/_on.py b/snapcraft/internal/project_loader/grammar/_on.py
deleted file mode 100644
index 7e6269b9c7..0000000000
--- a/snapcraft/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
-
-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 import ProjectOptions
- >>> from snapcraft.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.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/internal/project_loader/grammar/_processor.py b/snapcraft/internal/project_loader/grammar/_processor.py
deleted file mode 100644
index b22dd8b65e..0000000000
--- a/snapcraft/internal/project_loader/grammar/_processor.py
+++ /dev/null
@@ -1,287 +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 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.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/internal/project_loader/grammar/_statement.py b/snapcraft/internal/project_loader/grammar/_statement.py
deleted file mode 100644
index 783c28efec..0000000000
--- a/snapcraft/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/internal/project_loader/grammar/_to.py b/snapcraft/internal/project_loader/grammar/_to.py
deleted file mode 100644
index 8166e11fbe..0000000000
--- a/snapcraft/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 import ProjectOptions
- >>> from snapcraft.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/internal/project_loader/grammar/_try.py b/snapcraft/internal/project_loader/grammar/_try.py
deleted file mode 100644
index 9521e599a3..0000000000
--- a/snapcraft/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 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/internal/project_loader/grammar/errors.py b/snapcraft/internal/project_loader/grammar/errors.py
deleted file mode 100644
index 50768e9f76..0000000000
--- a/snapcraft/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.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/internal/project_loader/grammar/typing.py b/snapcraft/internal/project_loader/grammar/typing.py
deleted file mode 100644
index 95b98c537a..0000000000
--- a/snapcraft/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/internal/project_loader/grammar/__init__.py b/snapcraft/meta/__init__.py
similarity index 74%
rename from snapcraft/internal/project_loader/grammar/__init__.py
rename to snapcraft/meta/__init__.py
index c1b7095226..70c7932f24 100644
--- a/snapcraft/internal/project_loader/grammar/__init__.py
+++ b/snapcraft/meta/__init__.py
@@ -1,6 +1,6 @@
# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*-
#
-# Copyright (C) 2017, 2018 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
@@ -14,7 +14,7 @@
# 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
+"""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..40862d9ebe
--- /dev/null
+++ b/snapcraft/meta/appstream.py
@@ -0,0 +1,278 @@
+# -*- 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 OSError as err:
+ raise errors.SnapcraftError(str(err)) from err
+ except lxml.etree.ParseError as err:
+ raise errors.MetadataExtractionError(path, str(err)) 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/snapcraft/meta/snap_yaml.py b/snapcraft/meta/snap_yaml.py
new file mode 100644
index 0000000000..417a15d9d8
--- /dev/null
+++ b/snapcraft/meta/snap_yaml.py
@@ -0,0 +1,201 @@
+# -*- 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."""
+
+from pathlib import Path
+from typing import Any, Dict, List, Optional, Union, cast
+
+import yaml
+from pydantic_yaml import YamlModel
+
+from snapcraft.projects import Project
+
+
+class Socket(YamlModel):
+ """snap.yaml app socket entry."""
+
+ 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.
+
+ This is currently a partial implementation, see
+ https://snapcraft.io/docs/snap-format for details.
+
+ TODO: implement desktop (CRAFT-804)
+ TODO: implement extensions (CRAFT-805)
+ TODO: implement passthrough (CRAFT-854)
+ TODO: implement slots (CRAFT-816)
+ """
+
+ command: str
+ 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."""
+
+ 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: implement adopt-info (CRAFT-803)
+ """
+
+ name: str
+ title: Optional[str]
+ version: str
+ summary: str
+ description: str
+ license: Optional[str]
+ type: Optional[str]
+ architectures: List[str]
+ base: Optional[str]
+ build_base: Optional[str]
+ assumes: Optional[List[str]]
+ epoch: Optional[str]
+ apps: Optional[Dict[str, SnapApp]]
+ confinement: str
+ grade: str
+ 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):
+ """Create a snap.yaml file."""
+ meta_dir = prime_dir / "meta"
+ meta_dir.mkdir(parents=True, exist_ok=True)
+
+ 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,
+ 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,
+ )
+
+ snap_metadata = SnapMetadata(
+ name=project.name,
+ title=project.title,
+ version=project.version,
+ summary=project.summary,
+ description=project.description, # type: ignore
+ license=project.license,
+ type=project.type,
+ architectures=[arch],
+ base=cast(str, project.base),
+ assumes=["command-chain"] if snap_apps else None,
+ epoch=project.epoch,
+ apps=snap_apps or None,
+ confinement=project.confinement,
+ grade=project.grade or "stable",
+ environment=project.environment,
+ plugs=project.plugs,
+ hooks=project.hooks,
+ layout=project.layout,
+ )
+
+ yaml.add_representer(str, _repr_str, Dumper=yaml.SafeDumper)
+ yaml_data = snap_metadata.yaml(
+ by_alias=True,
+ exclude_none=True,
+ allow_unicode=True,
+ sort_keys=False,
+ width=1000,
+ )
+
+ snap_yaml = meta_dir / "snap.yaml"
+ snap_yaml.write_text(yaml_data)
+
+
+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/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/snapcraft/pack.py b/snapcraft/pack.py
new file mode 100644
index 0000000000..bc8807a1da
--- /dev/null
+++ b/snapcraft/pack.py
@@ -0,0 +1,77 @@
+# -*- 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.
+ """
+ emit.trace(f"pack_snap: output={output!r}, compression={compression!r}")
+
+ 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...")
+ emit.trace(f"Pack command: {command}")
+ try:
+ subprocess.run(
+ command, capture_output=True, check=True, universal_newlines=True
+ ) # type: ignore
+ except subprocess.CalledProcessError as err:
+ 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/__init__.py b/snapcraft/parts/__init__.py
new file mode 100644
index 0000000000..f7e2a9038d
--- /dev/null
+++ b/snapcraft/parts/__init__.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 .
+
+"""Parts lifecycle processing."""
+
+from .parts import PartsLifecycle
+
+__all__ = ["PartsLifecycle"]
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/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
new file mode 100644
index 0000000000..18694de309
--- /dev/null
+++ b/snapcraft/parts/lifecycle.py
@@ -0,0 +1,333 @@
+# -*- 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."""
+
+import os
+import subprocess
+from dataclasses import dataclass
+from pathlib import Path
+from typing import TYPE_CHECKING, Any, Dict, List, Optional
+
+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.projects import GrammarAwareProject, Project
+from snapcraft.providers import capture_logs_from_instance
+
+from . import PartsLifecycle, grammar, plugins, yaml_utils
+from .setup_assets import setup_assets
+from .update_metadata import update_project_metadata
+
+if TYPE_CHECKING:
+ import argparse
+
+
+@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 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
+
+ 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.
+ """
+ yaml_data = {}
+
+ 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)
+
+ # 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
+ )
+
+ 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.
+
+ :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)
+ parse_info = _extract_parse_info(yaml_data)
+
+ 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(
+ command_name,
+ project=project,
+ parse_info=parse_info,
+ assets_dir=snap_project.assets_dir,
+ parsed_args=parsed_args,
+ )
+
+
+def _run_command(
+ command_name: str,
+ *,
+ project: Project,
+ parse_info: Dict[str, List[str]],
+ assets_dir: Path,
+ parsed_args: "argparse.Namespace",
+) -> None:
+ 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 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
+ and not os.getenv("SNAPCRAFT_BUILD_ENVIRONMENT") == "host"
+ ):
+ 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:
+ work_dir = utils.get_managed_environment_home_path()
+ else:
+ work_dir = Path.cwd()
+
+ step_name = "prime" if command_name in ("pack", "snap") else command_name
+
+ 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_name=project.name,
+ parse_info=parse_info,
+ project_vars={
+ "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)
+ return
+
+ 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
+ if step_name == "prime" and not part_names:
+ emit.progress("Extracting and updating metadata...")
+ 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,
+ )
+
+ 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(
+ project,
+ lifecycle.prime_dir,
+ arch=lifecycle.target_arch,
+ )
+ emit.message("Generated snap metadata", intermediate=True)
+
+ if command_name in ("pack", "snap"):
+ pack.pack_snap(
+ lifecycle.prime_dir,
+ output=parsed_args.output,
+ compression=project.compression,
+ )
+
+
+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
+ provider = providers.get_provider(provider_name)
+ provider.ensure_provider_is_available()
+
+ cmd = ["snapcraft", command_name]
+
+ 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:
+ cmd.append("--quiet")
+ 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...")
+ with provider.launched_environment(
+ project_name=project.name,
+ project_path=Path().absolute(),
+ base=project.get_effective_base(),
+ ) as instance:
+ try:
+ 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(
+ f"Failed to execute {command_name} in instance."
+ ) 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
+
+
+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
new file mode 100644
index 0000000000..2344fcc74d
--- /dev/null
+++ b/snapcraft/parts/parts.py
@@ -0,0 +1,280 @@
+# -*- 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
+import subprocess
+from typing import Any, Dict, List, Optional
+
+import craft_parts
+from craft_cli import emit
+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,
+ "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.
+ :param assets_dir: The directory containing project assets.
+ :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.
+ """
+
+ def __init__(
+ self,
+ all_parts: Dict[str, Any],
+ *,
+ work_dir: pathlib.Path,
+ assets_dir: pathlib.Path,
+ 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
+ self._part_names = part_names
+ self._adopt_info = adopt_info
+ self._parse_info = parse_info
+
+ 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},
+ application_name="snapcraft",
+ work_dir=work_dir,
+ cache_dir=cache_dir,
+ ignore_local_sources=["*.snap"],
+ extra_build_packages=extra_build_packages,
+ extra_build_snaps=extra_build_snaps,
+ project_name=project_name,
+ project_vars_part_name=adopt_info,
+ project_vars=project_vars,
+ )
+ except craft_parts.PartsError as err:
+ raise errors.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
+
+ @property
+ 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,
+ *,
+ debug: bool = False,
+ shell: bool = False,
+ shell_after: bool = False,
+ ) -> 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}")
+
+ 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
+
+ try:
+ if target_step:
+ actions = self._lcm.plan(target_step, part_names=self._part_names)
+ else:
+ actions = []
+
+ self._install_package_repositories()
+
+ emit.progress("Executing parts lifecycle...")
+
+ with self._lcm.action_executor() as aex:
+ for action in actions:
+ message = _action_message(action)
+ 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)
+
+ 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.
+
+ :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 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.lstrip("/")).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 _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: {
+ 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/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/snapcraft/parts/setup_assets.py b/snapcraft/parts/setup_assets.py
new file mode 100644
index 0000000000..02c1d7f453
--- /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
new file mode 100644
index 0000000000..ef781ee8d9
--- /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.lstrip("/")).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.lstrip("/")).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/parts/validation.py b/snapcraft/parts/validation.py
new file mode 100644
index 0000000000..f3a6921ae6
--- /dev/null
+++ b/snapcraft/parts/validation.py
@@ -0,0 +1,47 @@
+# -*- 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
+
+from craft_parts import plugins
+from craft_parts.parts import PartSpec
+
+
+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/snapcraft/parts/yaml_utils.py b/snapcraft/parts/yaml_utils.py
new file mode 100644
index 0000000000..ebc8dde67f
--- /dev/null
+++ b/snapcraft/parts/yaml_utils.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 .
+
+"""YAML utilities for Snapcraft."""
+
+from typing import Any, Dict, TextIO
+
+import yaml
+import yaml.error
+
+from snapcraft import errors, utils
+
+
+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.
+ :raises LegacyFallback: if the project's base is not core22.
+ """
+ try:
+ 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 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:
+ raise errors.SnapcraftError(f"snapcraft.yaml parsing error: {err!s}") from err
diff --git a/snapcraft/projects.py b/snapcraft/projects.py
new file mode 100644
index 0000000000..1fe926df58
--- /dev/null
+++ b/snapcraft/projects.py
@@ -0,0 +1,573 @@
+# -*- 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
+from typing import TYPE_CHECKING, Any, Dict, List, Literal, Optional, Tuple, Union
+
+import pydantic
+from craft_grammar.models import GrammarSingleEntryDictList, GrammarStr, GrammarStrList
+from pydantic import conlist, constr
+
+from snapcraft import repo, utils
+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 = True # project is updated with adopted metadata
+ 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:
+ UniqueStrList = List[str]
+else:
+ UniqueStrList = conlist(str, unique_items=True)
+# 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."""
+
+ 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."""
+
+ 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: Optional[UniqueStrList]
+ plugs: Optional[UniqueStrList]
+ aliases: Optional[UniqueStrList]
+ environment: Optional[Dict[str, str]]
+ 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_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)"
+ )
+
+ 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
+
+ @pydantic.validator("command_chain")
+ @classmethod
+ def _validate_command_chain(cls, command_chains):
+ return _validate_command_chain(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: Optional[List[str]]
+ environment: Optional[Dict[str, str]]
+ 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."""
+
+ build_on: Union[str, UniqueStrList]
+ 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]
+
+
+MANDATORY_ADOPTABLE_FIELDS = ("version", "summary", "description")
+
+
+class Project(ProjectModel):
+ """Snapcraft project definition.
+
+ See https://snapcraft.io/docs/snapcraft-yaml-reference
+
+ XXX: Not implemented in this version
+ - system-usernames
+ """
+
+ 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]
+ website: Optional[str]
+ summary: Optional[constr(max_length=78)] # type: ignore
+ description: Optional[str]
+ type: Optional[Literal["app", "base", "gadget", "kernel", "snapd"]]
+ icon: Optional[str]
+ confinement: Literal["classic", "devmode", "strict"]
+ layout: Optional[
+ Dict[str, Dict[Literal["symlink", "bind", "bind-file", "type"], str]]
+ ]
+ license: Optional[str]
+ grade: Optional[Literal["stable", "devel"]]
+ architectures: List[Architecture] = []
+ assumes: UniqueStrList = []
+ package_repositories: List[Dict[str, Any]] = [] # handled by repo
+ hooks: Optional[Dict[str, Hook]]
+ passthrough: Optional[Dict[str, Any]]
+ apps: Optional[Dict[str, App]]
+ 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]
+ adopt_info: Optional[str]
+ environment: Optional[Dict[str, str]]
+
+ @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_adoptable_fields(cls, values):
+ 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
+
+ @pydantic.root_validator(pre=True)
+ @classmethod
+ def _validate_mandatory_base(cls, values):
+ snap_type = values.get("type")
+ 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"
+ )
+ 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, values):
+ if not version and "adopt_info" not in values:
+ raise ValueError("Version must be declared if not adopting metadata")
+
+ 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 "
+ "begin with a period, colon, plus sign, tilde, or hyphen. They cannot end "
+ "with a period, colon, or hyphen"
+ )
+
+ 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):
+ """Build-base defaults to the base value if not specified."""
+ if not build_base:
+ 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):
+ """Verify each part (craft-parts will re-validate this)."""
+ 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.
+
+ 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("Project 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 _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
+
+ 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:
+ """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]
+ parse_info: Optional[List[str]]
+
+
+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."""
+ 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"):
+ """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_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:
+ 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/snapcraft/providers/__init__.py b/snapcraft/providers/__init__.py
new file mode 100644
index 0000000000..1172c31ac1
--- /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_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
+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..848498025f
--- /dev/null
+++ b/snapcraft/providers/_buildd.py
@@ -0,0 +1,143 @@
+# -*- 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 sys
+from typing import Optional
+
+from craft_providers import Executor, bases
+from craft_providers.actions import snap_installer
+from overrides import overrides
+
+from snapcraft import utils
+
+# TODO fix this overengineered configuration
+BASE_TO_BUILDD_IMAGE_ALIAS = {
+ "core22": bases.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.
+ """
+ # 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"
+
+ # Snaps that are already installed won't be reinstalled.
+ # See https://github.com/canonical/craft-providers/issues/91
+
+ 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
+
+ @overrides
+ 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)
+
+ @overrides
+ 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)
diff --git a/snapcraft/providers/_get_provider.py b/snapcraft/providers/_get_provider.py
new file mode 100644
index 0000000000..1f8e8a4338
--- /dev/null
+++ b/snapcraft/providers/_get_provider.py
@@ -0,0 +1,67 @@
+# -*- 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 os
+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) get the provider from the environment if valid,
+ (4) default to platform default (LXD on Linux).
+
+ :return: Provider instance.
+ """
+ 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":
+ return LXDProvider()
+
+ if provider == "multipass":
+ return MultipassProvider()
+
+ raise RuntimeError(f"Unsupported provider specified: {provider!r}.")
+
+
+def get_platform_default_provider() -> str:
+ """Obtain the default provider for the host platform.
+
+ :return: Default provider name.
+ """
+ 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..cc2a7797fe
--- /dev/null
+++ b/snapcraft/providers/_lxd.py
@@ -0,0 +1,201 @@
+# -*- 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__)
+
+
+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
+
+ instance_name = self.get_instance_name(
+ project_name=project_name,
+ project_path=project_path,
+ )
+
+ 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 == instance_name:
+ 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,
+ )
+ 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,
+ environment=environment,
+ hostname=instance_name,
+ )
+
+ try:
+ instance = lxd.launch(
+ name=instance_name,
+ base_configuration=base_configuration,
+ image_name=base,
+ 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..470f2063da
--- /dev/null
+++ b/snapcraft/providers/_multipass.py
@@ -0,0 +1,185 @@
+# -*- 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_cli import emit
+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():
+ 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:
+ 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/repo/__init__.py b/snapcraft/repo/__init__.py
new file mode 100644
index 0000000000..326519fa5b
--- /dev/null
+++ b/snapcraft/repo/__init__.py
@@ -0,0 +1,25 @@
+# -*- 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 .installer import install
+from .projects import validate_repository
+
+__all__ = [
+ "install",
+ "validate_repository",
+]
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..0139a62f6b
--- /dev/null
+++ b/snapcraft/repo/apt_sources_manager.py
@@ -0,0 +1,211 @@
+# -*- 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, utils
+
+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 = utils.get_host_architecture()
+
+ print(f"Architectures: {arch_text}", file=deb822)
+
+ return deb822.getvalue()
+
+
+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..d452b5c971
--- /dev/null
+++ b/snapcraft/repo/errors.py
@@ -0,0 +1,105 @@
+# -*- 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 PackageRepositoryError(SnapcraftError):
+ """Package repository error base."""
+
+
+class PackageRepositoryValidationError(PackageRepositoryError):
+ """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(PackageRepositoryError):
+ """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(PackageRepositoryError):
+ """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/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/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/snapcraft/repo/projects.py b/snapcraft/repo/projects.py
new file mode 100644
index 0000000000..8ee59b9d65
--- /dev/null
+++ b/snapcraft/repo/projects.py
@@ -0,0 +1,95 @@
+# -*- 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 TYPE_CHECKING, Any, Dict, List, Literal, 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"^[0-9A-F]{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"
+
+
+# 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."""
+
+ 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/snapcraft/storeapi/_dashboard_api.py b/snapcraft/storeapi/_dashboard_api.py
deleted file mode 100644
index 223a8423c9..0000000000
--- a/snapcraft/storeapi/_dashboard_api.py
+++ /dev/null
@@ -1,445 +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 .
-
-import json
-import logging
-import os
-from typing import Any, Dict, Iterable, List, Optional
-from urllib.parse import urlencode, urljoin
-
-import requests
-from simplejson.scanner import JSONDecodeError
-
-from . import _metadata, constants, errors, http_clients, metrics
-from ._requests import Requests
-from ._status_tracker import StatusTracker
-from .v2 import channel_map, releases, validation_sets, whoami
-
-logger = logging.getLogger(__name__)
-
-
-class DashboardAPI(Requests):
- """The Dashboard API is used to publish and manage snaps.
-
- This is an interface to query that API which is documented
- at https://dashboard.snapcraft.io/docs/.
- """
-
- def __init__(self, auth_client: http_clients.AuthClient) -> None:
- 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)
- 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):
- 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)
-
- 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)
-
- 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)
-
- def register(
- self, snap_name: str, *, is_private: bool, series: str, store_id: Optional[str]
- ) -> None:
- 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)
-
- def snap_upload_precheck(self, snap_name):
- 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)
-
- 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
- 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)
-
- 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,
- )
- 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,
- )
- metadata_handler.upload_binary(metadata, force)
-
- def snap_release(
- self,
- snap_name,
- revision,
- channels,
- delta_format=None,
- progressive_percentage: Optional[int] = None,
- ):
- data = {"name": snap_name, "revision": str(revision), "channels": channels}
- if delta_format:
- data["delta_format"] = delta_format
- if progressive_percentage is not None:
- data["progressive"] = {
- "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)
-
- response_json = response.json()
-
- return response_json
-
- def push_assertion(self, snap_id, assertion, endpoint, force):
- if endpoint == "validations":
- data = {"assertion": assertion.decode("utf-8")}
- elif endpoint == "developers":
- data = {"snap_developer": assertion.decode("utf-8")}
- else:
- raise RuntimeError("No valid endpoint")
-
- url = "/dev/api/snaps/{}/{}".format(snap_id, endpoint)
-
- # For `snap-developer`, revoking developers will require their uploads
- # to be invalidated.
- if force:
- url = url + "?ignore_revoked_uploads"
-
- response = self.put(
- url,
- json=data,
- headers={"Content-Type": "application/json", "Accept": "application/json"},
- )
-
- if not response.ok:
- raise errors.StoreValidationError(snap_id, response)
- try:
- response_json = response.json()
- except JSONDecodeError:
- message = (
- "Invalid response from the server when pushing validations: {} {}"
- ).format(response.status_code, response)
- logger.debug(message)
- raise errors.StoreValidationError(
- snap_id, response, message="Invalid response from the server"
- )
-
- 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_json = response.json()
- except JSONDecodeError:
- message = "Invalid response from the server when getting {}: {} {}".format(
- endpoint, response.status_code, response
- )
- logger.debug(message)
- raise errors.StoreValidationError(
- snap_id, response, message="Invalid response from the server"
- )
-
- return response_json
-
- def push_snap_build(self, snap_id, snap_build):
- url = f"/dev/api/snaps/{snap_id}/builds"
- data = json.dumps({"assertion": snap_build})
- headers = {
- "Content-Type": "application/json",
- }
- response = self.post(url, data=data, headers=headers)
- if not response.ok:
- raise errors.StoreSnapBuildError(response)
-
- def snap_status(self, snap_id, series, arch):
- qs = {}
- if series:
- qs["series"] = series
- if arch:
- qs["architecture"] = 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)
-
- response_json = response.json()
-
- 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"}
-
- response = self.post(url, data=json.dumps(data), headers=headers)
- if not response.ok:
- raise errors.StoreChannelClosingError(response)
-
- 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
- )
- )
- raise errors.StoreChannelClosingError(response)
-
- 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"},
- )
-
- 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)
-
- return channel_map.ChannelMap.unmarshal(response.json())
-
- def get_metrics(
- self, filters: List[metrics.MetricsFilter], snap_name: str
- ) -> metrics.MetricsResults:
- url = "/dev/api/snaps/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:
- raise errors.StoreMetricsError(
- filters=filters, response=response, snap_name=snap_name
- )
-
- try:
- results = response.json()
- return metrics.MetricsResults.unmarshal(results)
- except ValueError as error:
- raise errors.StoreMetricsUnmarshalError(
- filters=filters, snap_name=snap_name, response=response
- ) 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)
-
- 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)
-
- 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)
-
- 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)
-
- return validation_sets.ValidationSets.unmarshal(response.json())
-
- def get_validation_sets(
- self, *, name: Optional[str], sequence: Optional[str]
- ) -> validation_sets.ValidationSets:
- url = "/api/v2/validation-sets"
- if name is not None:
- url += "/" + name
- 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)
-
- return validation_sets.ValidationSets.unmarshal(response.json())
diff --git a/snapcraft/storeapi/_up_down_client.py b/snapcraft/storeapi/_up_down_client.py
deleted file mode 100644
index 35b37e2b9c..0000000000
--- a/snapcraft/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/storeapi/_upload.py b/snapcraft/storeapi/_upload.py
deleted file mode 100644
index 025f611052..0000000000
--- a/snapcraft/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.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/snapcraft/storeapi/http_clients/_candid_client.py b/snapcraft/storeapi/http_clients/_candid_client.py
deleted file mode 100644
index 93de96b389..0000000000
--- a/snapcraft/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.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/storeapi/http_clients/_config.py b/snapcraft/storeapi/http_clients/_config.py
deleted file mode 100644
index 45ce96570d..0000000000
--- a/snapcraft/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/storeapi/http_clients/_http_client.py b/snapcraft/storeapi/http_clients/_http_client.py
deleted file mode 100644
index 0c9f10ff6f..0000000000
--- a/snapcraft/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/storeapi/http_clients/_ubuntu_sso_client.py b/snapcraft/storeapi/http_clients/_ubuntu_sso_client.py
deleted file mode 100644
index 604b80f84f..0000000000
--- a/snapcraft/storeapi/http_clients/_ubuntu_sso_client.py
+++ /dev/null
@@ -1,232 +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.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/storeapi/http_clients/errors.py b/snapcraft/storeapi/http_clients/errors.py
deleted file mode 100644
index 41d2029328..0000000000
--- a/snapcraft/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.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/snapcraft/utils.py b/snapcraft/utils.py
new file mode 100644
index 0000000000..a7d59f779c
--- /dev/null
+++ b/snapcraft/utils.py
@@ -0,0 +1,242 @@
+# -*- 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 os
+import pathlib
+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:
+ """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
+}
+
+_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."""
+ 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_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:
+ """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 strtobool(managed_flag)
+
+
+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 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.
+
+ 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_text + choices)).lower().strip()
+ if reply and reply[0] == "y":
+ return True
+
+ if reply and reply[0] == "n":
+ return False
+
+ 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:
+ """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/snapcraft_legacy/__init__.py b/snapcraft_legacy/__init__.py
new file mode 100644
index 0000000000..60eaf11c4a
--- /dev/null
+++ b/snapcraft_legacy/__init__.py
@@ -0,0 +1,369 @@
+# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*-
+#
+# Copyright (C) 2015-2017, 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 .
+
+"""Snapcraft plugins drive different build systems
+
+Each part has a build system . Most parts are built from source using one of
+a range of build systems such as CMake or Scons. Some parts are pre-built
+and just copied into place, for example parts that reuse existing binary
+packages.
+
+You tell snapcraft which build system it must drive by specifying the
+snapcraft plugin for that part. Every part must specify a plugin explicitly
+(when you see a part that does not specify a plugin, that's because the
+actual part definition is in the cloud, where the plugin is specified!)
+
+These plugins implement a lifecycle over the following steps:
+
+ - pull: retrieve the source for the part from the specified location
+ - build: drive the build system determined by the choice of plugin
+ - stage: consolidate desirable files from all the parts in one tree
+ - prime: distill down to only the files which will go into the snap
+ - snap: compress the prime tree into the installable snap file
+
+These steps correspond to snapcraft commands. So when you initiate a
+'snapcraft pull' you will invoke the respective plugin for each part in
+the snap, in sequence, to handle the source pull. Each part will then have a
+fully populated parts//src/ directory. Similarly, if you then say
+'snapcraft build' you will invoke the plugin responsible for each part in
+turn, to build the part.
+
+# Snapcraft Lifecycle
+
+## Pull
+
+In this first step, source material is retrieved from the specified
+location, whether that is a URL for a tarball, a local path to a source tree
+inside the snap, a revision control reference to checkout, or something
+specific to the plugin such as PyPI. The plugin might also download
+necessary artifacts, such as the Java SDK, which are not specific to the
+particular part but which are needed by the plugin to handle its type of
+build system.
+
+All the downloaded content for each part goes into the
+`parts//src/` directory, which acts as a cache to prevent
+re-fetching content. You can clean that cache out with 'snapcraft clean'.
+
+## Build
+
+Snapcraft calculates an appropriate sequence to build the parts, based on
+explicit 'after' references and the order of the parts in the
+snapcraft.yaml. Each part is built in the `parts//build`
+directory and installed into `parts//install`.
+
+Note the install step - we might actually want to use built artifacts from
+one part in the build process of another, so the `parts//install`
+directory is useful as a 'working fresh install' of the part.
+
+Between the plugin, the part definition YAML, and the build system of the
+part, it is expected that the part can be built and installed in the right
+place.
+
+At this point you have a tree under `parts/` with a subdirectory for every
+part, and underneath those, separate src, build and install trees for each
+part.
+
+## Stage
+
+We now need to start consolidating the important pieces of each part into a
+single tree. We do this twice - once in a very sweeping way that will
+produce a lot of extraneous materials but is useful for debugging. This is
+the 'stage' step of the lifecycle, because we move a lot of the build output
+from each part into a consolidated tree under `stage/` which has the
+structure of a snap but has way too much extra information.
+
+The important thing about the staging area is that it lets you get all the
+shared libraries in one place and lets you find overlapping content in the
+parts. You can also try this directory as if it were a snap, and you'll have
+all the debugging information in the tree, which is useful for developers.
+
+Each part describes its own staging content - the files that should be
+staged. The part will often describe "chunks" of content, called filesets,
+so that they can be referred to as a useful set rather than having to call
+out individual files.
+
+## Prime
+
+It is useful to have a directory tree which exactly mirrors the structure of
+the final snap. This is the `prime/` directory, and the lifecycle includes a
+'prime' step which copies only that final, required content from the
+`stage/` directory into the `prime/` directory.
+
+So the `prime/` directory contains only the content that will be put into
+the final snap, unlike the staging area which may include debug and
+development files not destined for your snap.
+
+The snap metadata will also be placed in `./prime/meta` during the prime
+step, so this `./prime` directory is useful for inspecting exactly what is
+going into your snap or to conduct any final post-processing on snapcraft's
+output.
+
+## Snap
+
+The final step in the snapcraft lifecycle builds a snap out of the `prime/`
+directory. It will be in the top level directory, alongside snapcraft.yaml,
+called --.snap
+
+
+# Standard part definition keywords
+
+There are several builtin keywords which can be used in any part regardless
+of the choice of plugin.
+
+ - after: [part, part, part...]
+
+ Snapcraft will make sure that it builds all of the listed parts before
+ it tries to build this part. Essentially these listed dependencies for
+ this part, useful when the part needs a library or tool built by another
+ part.
+
+ If such a dependency part is not defined in this snapcraft.yaml, it must
+ be defined in the cloud parts library, and snapcraft will retrieve the
+ definition of the part from the cloud. In this way, a shared library of
+ parts is available to every snap author - just say 'after' and list the
+ parts you want that others have already defined.
+
+ - build-packages: [pkg, pkg, pkg...]
+
+ A list of packages to install on the build host before building
+ the part. The files from these packages typically will not go into the
+ final snap unless they contain libraries that are direct dependencies of
+ binaries within the snap (in which case they'll be discovered via `ldd`),
+ or they are explicitly described in stage-packages.
+
+ - stage-packages: YAML list
+
+ A set of packages to be downloaded and unpacked to join the part
+ before it's built. Note that these packages are not installed on the host.
+ Like the rest of the part, all files from these packages will make it into
+ the final snap unless filtered out via the `snap` keyword.
+
+ One may simply specify packages in a flat list, in which case the packages
+ will be fetched and unpacked regardless of build environment. In addition,
+ a specific grammar made up of sub-lists is supported here that allows one
+ to filter stage packages depending on various selectors (e.g. the target
+ arch), as well as specify optional packages. The grammar is made up of two
+ nestable statements: 'on' and 'try'.
+
+ Let's discuss `on`.
+
+ - on [,...]:
+ - ...
+ - else[ fail]:
+ - ...
+
+ The body of the 'on' clause is taken into account if every (AND, not OR)
+ selector is true for the target build environment. Currently the only
+ selectors supported are target architectures (e.g. amd64).
+
+ If the 'on' clause doesn't match and it's immediately followed by an 'else'
+ clause, the 'else' clause must be satisfied. An 'on' clause without an
+ 'else' clause is considered satisfied even if no selector matched. The
+ 'else fail' form allows erroring out if an 'on' clause was not matched.
+
+ For example, say you only wanted to stage `foo` if building for amd64 (and
+ not stage `foo` if otherwise):
+
+ - on amd64: [foo]
+
+ Building on that, say you wanted to stage `bar` if building on an arch
+ other than amd64:
+
+ - on amd64: [foo]
+ - else: [bar]
+
+ You can nest these for more complex behaviors:
+
+ - on amd64: [foo]
+ - else:
+ - on i386: [bar]
+ - on armhf: [baz]
+
+ If your project requires a package that is only available on amd64, you can
+ fail if you're not building for amd64:
+
+ - on amd64: [foo]
+ - else fail
+
+ Now let's discuss `try`:
+
+ - try:
+ - ...
+ - else:
+ - ...
+
+ The body of the 'try' clause is taken into account only when all packages
+ contained within it are valid. If not, if it's immediately followed by
+ 'else' clauses they are tried in order, and one of them must be satisfied.
+ A 'try' clause with no 'else' clause is considered satisfied even if it
+ contains invalid packages.
+
+ For example, say you wanted to stage `foo`, but it wasn't available for all
+ architectures. Assuming your project builds without it, you can make it an
+ optional stage package:
+
+ - try: [foo]
+
+ You can also add alternatives:
+
+ - try: [foo]
+ - else: [bar]
+
+ Again, you can nest these for more complex behaviors:
+
+ - on amd64: [foo]
+ - else:
+ - try: [bar]
+
+ - organize: YAML
+
+ Snapcraft will rename files according to this YAML sub-section. The
+ content of the 'organize' section consists of old path keys, and their
+ new values after the renaming.
+
+ This can be used to avoid conflicts between parts that use the same
+ name, or to map content from different parts into a common conventional
+ file structure. For example:
+
+ organize:
+ usr/oldfilename: usr/newfilename
+ usr/local/share/: usr/share/
+
+ The key is the internal part filename, the value is the exposed filename
+ that will be used during the staging process. You can rename whole
+ subtrees of the part, or just specific files.
+
+ Note that the path is relative (even though it is "usr/local") because
+ it refers to content underneath parts//install which is going
+ to be mapped into the stage and prime areas.
+
+ - filesets: YAML
+
+ When we map files into the stage and prime areas on the way to putting
+ them into the snap, it is convenient to be able to refer to groups of
+ files as well as individual files. Snapcraft lets you name a fileset
+ and then use it later for inclusion or exclusion of those files from the
+ resulting snap.
+
+ For example, consider man pages of header files.. You might want them
+ in, or you might want to leave them out, but you definitely don't want
+ to repeatedly have to list all of them either way.
+
+ This section is thus a YAML map of fileset names (the keys) to a list of
+ filenames. The list is built up by adding individual files or whole
+ subdirectory paths (and all the files under that path) and wildcard
+ globs, and then pruning from those paths.
+
+ The wildcard * globs all files in that path. Exclusions are denoted by
+ an initial `-`.
+
+ For example you could add usr/local/* then remove usr/local/man/*:
+
+ filesets:
+ allbutman: [ usr/local/*, -usr/local/man/* ]
+ manpages: [ usr/local/man ]
+
+ Filenames are relative to the part install directory in
+ `parts//install`. If you have used 'organize' to rename files
+ then the filesets will be built up from the names after organization.
+
+ - stage: YAML file and fileset list
+
+ A list of files from a part install directory to copy into `stage/`.
+ Rules applying to the list here are the same as those of filesets.
+ Referencing of fileset keys is done with a $ prefixing the fileset key,
+ which will expand with the value of such key.
+
+ For example:
+
+ stage:
+ - usr/lib/* # Everything under parts//install/usr/lib
+ - -usr/lib/libtest.so # Excludng libtest.so
+ - $manpages # Including the 'manpages' fileset
+
+ - snap: YAML file and fileset list
+
+ A list of files from a part install directory to copy into `prime/`.
+ This section takes exactly the same form as the 'stage' section but the
+ files identified here will go into the ultimate snap (because the
+ `prime/` directory reflects the file structure of the snap with no
+ extraneous content).
+
+ - build-attributes: [attribute1, attribute2]
+
+ A list of special attributes that affect the build of this specific part.
+ Supported attributes:
+
+ - no-install:
+ Do not run the install target provided by the plugin's build system.
+
+ Supported by: kbuild
+
+ - debug:
+ Plugins that support the concept of build types build in Release mode
+ by default. Setting the 'debug' attribute requests that they instead
+ build in Debug mode.
+"""
+
+from collections import OrderedDict # noqa
+
+import pkg_resources # noqa
+
+
+def _get_version():
+ import os as _os
+
+ 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"
+
+
+# Set this early so that the circular imports aren't too painful
+__version__ = _get_version()
+
+# Workaround for potential import loops.
+from snapcraft_legacy.internal import repo # noqa isort:skip
+
+# For backwards compatibility with external plugins.
+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_legacy._store import ( # noqa
+ create_key,
+ download,
+ gated,
+ list_keys,
+ list_registered,
+ login,
+ register,
+ register_key,
+ sign_build,
+ status,
+ upload_metadata,
+ validate,
+)
+
+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 70%
rename from snapcraft/_store.py
rename to snapcraft_legacy/_store.py
index 93978fc845..7c435c8d1f 100644
--- a/snapcraft/_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
@@ -23,42 +22,39 @@
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 Any, Dict, List, Optional, Sequence, TYPE_CHECKING, Tuple
from urllib.parse import urljoin
+import craft_store
+import requests
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 (
- calculate_sha3_384,
+from snapcraft_legacy.cli import echo
+from snapcraft_legacy.file_utils import (
get_host_tool_path,
get_snap_tool_path,
)
-from snapcraft.internal import cache, deltas
-from snapcraft.internal.deltas.errors import (
- DeltaGenerationError,
- DeltaGenerationTooBigError,
+from snapcraft_legacy.internal.errors import (
+ SnapDataExtractionError,
+ SnapcraftEnvironmentError,
)
-from snapcraft.internal.errors import SnapDataExtractionError, ToolMissingError
-from snapcraft.storeapi.constants import DEFAULT_SERIES
-from snapcraft.storeapi.metrics import MetricsFilter, MetricsResults
+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.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:
@@ -169,100 +165,115 @@ def _try_login(
email: str,
password: str,
*,
- store: 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:
+ store_client: storeapi.StoreClient,
+ ttl: int,
+ acls: Optional[Sequence[str]] = None,
+ packages: Optional[Sequence[str]] = None,
+ channels: Optional[Sequence[str]] = None,
+) -> str:
try:
- store.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.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,
)
- # Continue if agreement and namespace conditions are met.
- _check_dev_agreement_and_namespace_statuses(store)
+ return credentials
+
+
+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,
- 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"
+ store_client: storeapi.StoreClient,
+ 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:
+ credentials = store_client.login(
+ ttl=ttl,
+ acls=acls,
+ channels=channels,
+ packages=packages,
)
- 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)
-
- _try_login(
- email,
- password,
- store=store,
- packages=packages,
- acls=acls,
- channels=channels,
- expires=expires,
- config_fd=config_fd,
- save=save,
- )
+ else:
+ email, password = _prompt_login()
- return True
+ credentials = _try_login(
+ email,
+ password,
+ store_client=store_client,
+ ttl=ttl,
+ packages=packages,
+ acls=acls,
+ channels=channels,
+ )
+
+ # 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=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
@@ -301,26 +312,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
-
- @_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)
+ # TODO use an instance of this class directly from snapcraft_legacy.cli.store
@_login_wrapper
def get_metrics(
@@ -332,10 +337,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()
@@ -364,47 +365,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(
- 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()
@@ -525,11 +485,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])
@@ -545,15 +508,15 @@ 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(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()
@@ -596,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")
@@ -647,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"],
@@ -674,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 storeapi.http_clients.errors.StoreServerError as e:
- raise storeapi.errors.StoreUploadError(snap_name, e.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"])
@@ -931,7 +740,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/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 66de50408c..67423d1a18 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 95%
rename from snapcraft/cli/_errors.py
rename to snapcraft_legacy/cli/_errors.py
index 98176bc3a7..7eb72c2ed5 100644
--- a/snapcraft/cli/_errors.py
+++ b/snapcraft_legacy/cli/_errors.py
@@ -23,12 +23,13 @@
from typing import Dict
import click
+import craft_store
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
@@ -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
@@ -110,7 +113,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 +125,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 +262,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 +364,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 92%
rename from snapcraft/cli/assertions.py
rename to snapcraft_legacy/cli/assertions.py
index 2cd3e9d114..5020480aba 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 storeapi, 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,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.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")
@@ -100,7 +104,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 +120,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_legacy/cli/legacy.py b/snapcraft_legacy/cli/legacy.py
new file mode 100644
index 0000000000..9fb533b6f2
--- /dev/null
+++ b/snapcraft_legacy/cli/legacy.py
@@ -0,0 +1,28 @@
+# -*- 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."""
+
+import sys
+
+from ._runner import run # noqa: F401
+
+
+def legacy_run():
+ run()
+
+ # ensure this call never returns
+ sys.exit()
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_legacy/cli/store.py b/snapcraft_legacy/cli/store.py
new file mode 100644
index 0000000000..4f454a4f96
--- /dev/null
+++ b/snapcraft_legacy/cli/store.py
@@ -0,0 +1,373 @@
+# -*- 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 .
+
+import json
+import operator
+import os
+from datetime import date, timedelta
+from textwrap import dedent
+from typing import Dict, List, Set, Union
+
+import click
+from tabulate import tabulate
+
+import snapcraft_legacy
+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
+
+
+@click.group()
+def storecli():
+ """Store commands"""
+
+
+def _human_readable_acls(store_client: storeapi.StoreClient) -> str:
+ acl = store_client.acl()
+ snap_names = []
+ snap_ids = acl["snap_ids"]
+
+ if snap_ids is not None:
+ try:
+ for snap_id in snap_ids:
+ snap_names.append(store_client.get_snap_name_for_id(snap_id))
+ except TypeError:
+ raise RuntimeError(f"invalid snap_ids: {snap_ids!r}")
+ acl["snap_names"] = snap_names
+ else:
+ acl["snap_names"] = None
+
+ human_readable_acl: Dict[str, Union[str, List[str], None]] = {
+ "expires": str(acl["expires"])
+ }
+
+ for key in ("snap_names", "channels", "permissions"):
+ human_readable_acl[key] = acl[key]
+ if not acl[key]:
+ human_readable_acl[key] = "No restriction"
+
+ return dedent(
+ """\
+ snaps: {snap_names}
+ channels: {channels}
+ permissions: {permissions}
+ expires: {expires}
+ """.format(
+ **human_readable_acl
+ )
+ )
+
+
+@storecli.command("upload-metadata")
+@click.option(
+ "--force",
+ is_flag=True,
+ help="Force metadata update to override any possible conflict",
+)
+@click.argument(
+ "snap-file",
+ metavar="",
+ type=click.Path(exists=True, readable=True, resolve_path=True, dir_okay=False),
+)
+def upload_metadata(snap_file, force):
+ """Upload metadata from to the store.
+
+ The following information will be retrieved from and used
+ to update the store:
+
+ \b
+ - summary
+ - description
+ - icon
+
+ If --force is given, it will force the local metadata into the Store,
+ ignoring any possible conflict.
+
+ \b
+ Examples:
+ snapcraft upload-metadata my-snap_0.1_amd64.snap
+ snapcraft upload-metadata my-snap_0.1_amd64.snap --force
+ """
+ click.echo("Uploading metadata from {!r}".format(os.path.basename(snap_file)))
+ snapcraft_legacy.upload_metadata(snap_file, force)
+
+
+@storecli.command()
+@click.argument("snap-name", metavar="")
+@click.option(
+ "--from-channel",
+ metavar="",
+ required=True,
+ help="The channel to promote from.",
+)
+@click.option(
+ "--to-channel",
+ metavar="",
+ required=True,
+ help="The channel to promote to.",
+)
+@click.option("--yes", is_flag=True, help="Do not prompt for confirmation.")
+def promote(snap_name, from_channel, to_channel, yes):
+ """Promote a build set from to a channel.
+
+ 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
+
+ \b
+ - 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.
+
+ \b
+ Examples:
+ snapcraft promote my-snap --from-channel candidate --to-channel stable
+ snapcraft promote my-snap --from-channel lts/candidate --to-channel lts/stable
+ snapcraft promote my-snap --from-channel stable/patch --to-channel stable
+ snapcraft promote my-snap --from-channel experimental/stable --to-channel stable
+ """
+ echo.warning(
+ "snapcraft promote does not have a stable CLI interface. Use with caution in scripts."
+ )
+ parsed_from_channel = storeapi.channels.Channel(from_channel)
+ parsed_to_channel = storeapi.channels.Channel(to_channel)
+
+ if parsed_from_channel == parsed_to_channel:
+ raise click.BadOptionUsage(
+ "--to-channel", "--from-channel and --to-channel cannot be the same."
+ )
+ elif (
+ parsed_from_channel.risk == "edge"
+ and parsed_from_channel.branch is None
+ and yes
+ ):
+ raise click.BadOptionUsage(
+ "--from-channel",
+ "{!r} is not a valid set value for --from-channel when using --yes.".format(
+ parsed_from_channel
+ ),
+ )
+
+ store = storeapi.StoreClient()
+ status_payload = store.get_snap_status(snap_name)
+
+ snap_status = storeapi.status.SnapStatus(
+ snap_name=snap_name, payload=status_payload
+ )
+ from_channel_set = snap_status.get_channel_set(parsed_from_channel)
+ echo.info("Build set information for {!r}".format(parsed_from_channel))
+ click.echo(
+ tabulate(
+ sorted(from_channel_set, key=operator.attrgetter("arch")),
+ headers=["Arch", "Revision", "Version"],
+ tablefmt="plain",
+ )
+ )
+ if yes or echo.confirm(
+ "Do you want to promote the current set to the {!r} channel?".format(
+ parsed_to_channel
+ )
+ ):
+ for c in from_channel_set:
+ store.release(
+ snap_name=snap_name,
+ revision=str(c.revision),
+ channels=[str(parsed_to_channel)],
+ )
+ echo.wrapped(
+ f"Promotion from {parsed_from_channel} to {parsed_to_channel} complete"
+ )
+ else:
+ echo.wrapped("Channel promotion cancelled")
+
+
+@storecli.command("list-revisions")
+@click.option(
+ "--arch", metavar="", help="The snap architecture to get the status for"
+)
+@click.argument("snap-name", metavar="")
+def list_revisions(snap_name, arch):
+ """Get the history on the store for .
+
+ This command has an alias of `revisions`.
+
+ \b
+ Examples:
+ snapcraft list-revisions my-snap
+ snapcraft list-revisions my-snap --arch armhf
+ snapcraft revisions my-snap
+ """
+ releases = StoreClientCLI().get_snap_releases(snap_name=snap_name)
+
+ def get_channels_for_revision(revision: int) -> List[str]:
+ # channels: the set of channels revision was released to, active or not.
+ channels: Set[str] = set()
+ # seen_channel: applies to channels regardless of revision.
+ # The first channel that shows up for each architecture is to
+ # be marked as the active channel, all others are historic.
+ seen_channel: Dict[str, Set[str]] = dict()
+
+ for release in releases.releases:
+ if release.architecture not in seen_channel:
+ seen_channel[release.architecture] = set()
+
+ # If the revision is in this release entry and was not seen
+ # before it means that this channel is active and needs to
+ # be represented with a *.
+ if (
+ release.revision == revision
+ and release.channel not in seen_channel[release.architecture]
+ ):
+ channels.add(f"{release.channel}*")
+ # All other releases found for a revision are inactive.
+ elif (
+ release.revision == revision
+ and release.channel not in channels
+ and f"{release.channel}*" not in channels
+ ):
+ channels.add(release.channel)
+
+ seen_channel[release.architecture].add(release.channel)
+
+ return sorted(list(channels))
+
+ parsed_revisions = list()
+ for rev in releases.revisions:
+ if arch and arch not in rev.architectures:
+ continue
+ channels_for_revision = get_channels_for_revision(rev.revision)
+ if channels_for_revision:
+ channels = ",".join(channels_for_revision)
+ else:
+ channels = "-"
+ parsed_revisions.append(
+ (
+ rev.revision,
+ rev.created_at,
+ ",".join(rev.architectures),
+ rev.version,
+ channels,
+ )
+ )
+
+ tabulated_revisions = tabulate(
+ parsed_revisions,
+ numalign="left",
+ headers=["Rev.", "Uploaded", "Arches", "Version", "Channels"],
+ tablefmt="plain",
+ )
+
+ # 23 revisions + header should not need paging.
+ if len(parsed_revisions) < 24:
+ click.echo(tabulated_revisions)
+ else:
+ click.echo_via_pager(tabulated_revisions)
+
+
+@storecli.command()
+@click.argument("snap-name", metavar="")
+@click.argument("track_name", metavar="")
+def set_default_track(snap_name: str, track_name: str):
+ """Set the default track for to .
+
+ The track must be a valid active track for this operation to be successful.
+ """
+ store_client_cli = StoreClientCLI()
+
+ 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}.")
+
+
+_YESTERDAY = str(date.today() - timedelta(days=1))
+
+
+@storecli.command()
+@click.argument("snap-name", metavar="", required=True)
+@click.option(
+ "--name",
+ metavar="",
+ help="Metric name",
+ type=click.Choice([x.value for x in metrics_module.MetricsNames]),
+ required=True,
+)
+@click.option(
+ "--start",
+ metavar="",
+ help="Date in format YYYY-MM-DD",
+ required=True,
+ default=_YESTERDAY,
+)
+@click.option(
+ "--end",
+ metavar="",
+ help="Date in format YYYY-MM-DD",
+ required=True,
+ default=_YESTERDAY,
+)
+@click.option(
+ "--format",
+ metavar="",
+ help="Format for output",
+ type=click.Choice(["table", "json"]),
+ required=True,
+)
+def metrics(snap_name: str, name: str, start: str, end: str, format: str):
+ """Get metrics for ."""
+ 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:
+ echo.exit_error(
+ brief="No permissions for snap.",
+ resolution="Ensure the snap name and credentials are correct.is correct and that the correct credentials are used.",
+ )
+
+ mf = metrics_module.MetricsFilter(
+ snap_id=snap_id, metric_name=name, start=start, end=end
+ )
+
+ results = store.get_metrics(filters=[mf], snap_name=snap_name)
+
+ # Sanity check to ensure that only one result is found (as we currently only
+ # support one query at a time).
+ if len(results.metrics) != 1:
+ raise RuntimeError(f"Unexpected metric results from store: {results!r}")
+
+ metric_results = results.metrics[0]
+
+ if format == "json":
+ output = json.dumps(metric_results.marshal(), indent=2, sort_keys=True)
+ click.echo(output)
+ elif format == "table":
+ rows = convert_metrics_to_table(metric_results, transpose=True)
+ output = tabulate(rows, tablefmt="plain")
+ echo.echo_with_pager_if_needed(output)
diff --git a/snapcraft/cli/version.py b/snapcraft_legacy/cli/version.py
similarity index 90%
rename from snapcraft/cli/version.py
rename to snapcraft_legacy/cli/version.py
index 613aa458e7..054445490d 100644
--- a/snapcraft/cli/version.py
+++ b/snapcraft_legacy/cli/version.py
@@ -16,9 +16,9 @@
import click
-import snapcraft
+import snapcraft_legacy
-SNAPCRAFT_VERSION_TEMPLATE = "snapcraft, version %(version)s"
+SNAPCRAFT_VERSION_TEMPLATE = "snapcraft %(version)s"
@click.group()
@@ -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 2b5a8fcef7..a0caca745f 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 acec9199c7..5fa4f898ba 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 98%
rename from snapcraft/internal/build_providers/_snap.py
rename to snapcraft_legacy/internal/build_providers/_snap.py
index fb35c65d0b..a75ded5fe2 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__)
@@ -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/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 1c7ccfffaa..923d4b6ff5 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 a65a30d578..f7ac3f7de5 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,
)
@@ -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/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 622497803a..ceb1ce132a 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 6a020854c4..9317e20457 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 af4a0fbf6f..ba9d6fe24f 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"
@@ -214,7 +223,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)
@@ -222,13 +231,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
@@ -249,7 +258,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)
@@ -527,7 +538,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):
@@ -723,7 +734,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):
@@ -1267,7 +1278,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 6cc63a58d7..aed4a28cf6 100644
--- a/snapcraft/internal/pluginhandler/_dependencies.py
+++ b/snapcraft_legacy/internal/pluginhandler/_dependencies.py
@@ -16,7 +16,7 @@
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 817df08099..1c5823605c 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 96%
rename from snapcraft/internal/project_loader/_config.py
rename to snapcraft_legacy/internal/project_loader/_config.py
index 2c39e90381..2f2253eb31 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
@@ -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.
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 96%
rename from snapcraft/internal/project_loader/_parts_config.py
rename to snapcraft_legacy/internal/project_loader/_parts_config.py
index a9ba01d330..d27ec843fa 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
@@ -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/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_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 63%
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..38b1cb5307 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,37 +16,43 @@
from typing import Any, Dict, Set
-from snapcraft import project
-from snapcraft.internal import repo
-from snapcraft.internal.project_loader import grammar
+from craft_grammar import GrammarProcessor
+
+from snapcraft_legacy import project
+from snapcraft_legacy.internal import repo
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'}
"""
- 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/internal/project_loader/grammar_processing/_package_transformer.py b/snapcraft_legacy/internal/project_loader/grammar_processing/_package_transformer.py
similarity index 82%
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..8b32d90ba4 100644
--- a/snapcraft/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 import project
-from snapcraft.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/internal/project_loader/grammar_processing/_part_grammar_processor.py b/snapcraft_legacy/internal/project_loader/grammar_processing/_part_grammar_processor.py
similarity index 61%
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 323e473f40..2ca6367d44 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,10 @@
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 craft_grammar import Grammar, GrammarProcessor
+
+from snapcraft_legacy import BasePlugin, project
+from snapcraft_legacy.internal import repo
from ._package_transformer import package_transformer
@@ -28,7 +29,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 +38,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 +54,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'
@@ -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,70 +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/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 0f1512f2b9..ec168c0857 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():
@@ -244,7 +244,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 28b31e692c..465e381a28 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(
@@ -170,9 +170,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 f507040c4e..4e2e9c1241 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
@@ -375,11 +375,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 dbd92054bd..8d979c37c6 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 98307acbc2..b06d3943ce 100644
--- a/snapcraft/internal/repo/errors.py
+++ b/snapcraft_legacy/internal/repo/errors.py
@@ -17,10 +17,10 @@
from pathlib import Path
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 94fda53b89..6096d5a2b4 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,
)
@@ -106,7 +106,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.
@@ -148,7 +148,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 7ed32b4df3..13ef300713 100644
--- a/snapcraft/internal/sources/_git.py
+++ b/snapcraft_legacy/internal/sources/_git.py
@@ -261,7 +261,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 b8705f111d..5144376876 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 fa3db8077b..d83b049d62 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 1852aa9f05..4d3468579f 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 a17bccce55..ab137b5c73 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 90ec8dfc02..610903074b 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():
@@ -63,10 +63,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 835afbe531..0adb20c6bd 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 3504132162..921a26d9d7 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 93%
rename from snapcraft/plugins/v1/_ros/wstool.py
rename to snapcraft_legacy/plugins/v1/_ros/wstool.py
index 160673fd63..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__)
@@ -162,14 +162,15 @@ def _run(self, arguments: List[str]) -> str:
if "LD_LIBRARY_PATH" in env:
env["LD_LIBRARY_PATH"] += ":"
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(
- self._wstool_install_path, self._project.arch_triplet
- ),
- prepend="",
- separator=":",
+ env["LD_LIBRARY_PATH"] = (
+ ld_library_path
+ + snapcraft_legacy.formatting_utils.combine_paths(
+ snapcraft_legacy.common.get_library_paths(
+ self._wstool_install_path, self._project.arch_triplet
+ ),
+ prepend="",
+ separator=":",
+ )
)
# Make sure git can be used out of the wstool install path instead of needing
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 7763a3220d..59ccbb5617 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 537b3db43e..21e0dd37ed 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 f27c7f1e4c..ed32e173a4 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 85a94abb6f..c8d4038e77 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 2565ec023a..23c2e9bb1f 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 b13cd35d5d..5e7df88594 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/crystal.py b/snapcraft_legacy/plugins/v2/crystal.py
similarity index 96%
rename from snapcraft/plugins/v2/crystal.py
rename to snapcraft_legacy/plugins/v2/crystal.py
index d721c987ff..60074e92e9 100644
--- a/snapcraft/plugins/v2/crystal.py
+++ b/snapcraft_legacy/plugins/v2/crystal.py
@@ -39,10 +39,10 @@
import click
-from snapcraft import file_utils
-from snapcraft.internal import common, elf, errors
+from snapcraft_legacy import file_utils
+from snapcraft_legacy.internal import common, elf, errors
-from snapcraft.plugins.v2 import PluginV2
+from snapcraft_legacy.plugins.v2 import PluginV2
_CRYSTAL_CHANNEL = "latest/stable"
@@ -164,8 +164,7 @@ def stage_runtime_dependencies(
for elf_file in elf_files:
shutil.copy2(
- elf_file.path,
- os.path.join(install_path, os.path.basename(elf_file.path)),
+ elf_file.path, os.path.join(install_path, os.path.basename(elf_file.path)),
)
elf_dependencies_path = elf_file.load_dependencies(
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 7766afbe84..d6ce2d2ed3 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 64%
rename from snapcraft/storeapi/__init__.py
rename to snapcraft_legacy/storeapi/__init__.py
index 19156d408b..9b99cfed50 100644
--- a/snapcraft/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 constants # isort:skip
+from . import status # 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",
+ "constants",
+ "status",
+ "SnapAPI",
+ "StoreClient",
+]
diff --git a/snapcraft_legacy/storeapi/_dashboard_api.py b/snapcraft_legacy/storeapi/_dashboard_api.py
new file mode 100644
index 0000000000..ad544e31e1
--- /dev/null
+++ b/snapcraft_legacy/storeapi/_dashboard_api.py
@@ -0,0 +1,413 @@
+# -*- 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 .
+
+import json
+import logging
+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, errors, metrics
+from ._requests import Requests
+from .v2 import releases, validation_sets, whoami
+
+logger = logging.getLogger(__name__)
+
+
+class DashboardAPI(Requests):
+ """The Dashboard API is used to publish and manage snaps.
+
+ This is an interface to query that API which is documented
+ at https://dashboard.snapcraft.io/docs/.
+ """
+
+ def __init__(self, auth_client: craft_store.BaseClient) -> None:
+ super().__init__()
+
+ self._auth_client = auth_client
+
+ def _request(self, method: str, urlpath: str, **kwargs) -> requests.Response:
+ 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 verify_acl(self):
+ if not isinstance(self._auth_client, craft_store.UbuntuOneStoreClient):
+ raise NotImplementedError("Only supports UbuntuOneAuthClient.")
+
+ 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]:
+ 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}
+ 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]
+ ) -> None:
+ data = dict(snap_name=snap_name, is_private=is_private, series=series)
+ if store_id is not None:
+ data["store"] = store_id
+ 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) -> None:
+ data = {"name": snap_name, "dry_run": True}
+ 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 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,
+ )
+ 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,
+ )
+ metadata_handler.upload_binary(metadata, force)
+
+ def snap_release(
+ self,
+ snap_name,
+ revision,
+ channels,
+ delta_format=None,
+ progressive_percentage: Optional[int] = None,
+ ):
+ data = {"name": snap_name, "revision": str(revision), "channels": channels}
+ if delta_format:
+ data["delta_format"] = delta_format
+ if progressive_percentage is not None:
+ data["progressive"] = {
+ "percentage": progressive_percentage,
+ "paused": False,
+ }
+ 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()
+
+ return response_json
+
+ def push_assertion(self, snap_id, assertion, endpoint, force):
+ if endpoint == "validations":
+ data = {"assertion": assertion.decode("utf-8")}
+ elif endpoint == "developers":
+ data = {"snap_developer": assertion.decode("utf-8")}
+ else:
+ raise RuntimeError("No valid endpoint")
+
+ url = "/dev/api/snaps/{}/{}".format(snap_id, endpoint)
+
+ # For `snap-developer`, revoking developers will require their uploads
+ # to be invalidated.
+ if force:
+ url = url + "?ignore_revoked_uploads"
+
+ 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
+
+ try:
+ response_json = response.json()
+ except JSONDecodeError:
+ message = (
+ "Invalid response from the server when pushing validations: {} {}"
+ ).format(response.status_code, response)
+ logger.debug(message)
+ raise errors.StoreValidationError(
+ snap_id, response, message="Invalid response from the server"
+ )
+
+ return response_json
+
+ def get_assertion(self, snap_id, endpoint, params=None):
+ 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:
+ message = "Invalid response from the server when getting {}: {} {}".format(
+ endpoint, response.status_code, response
+ )
+ logger.debug(message)
+ raise errors.StoreValidationError(
+ snap_id, response, message="Invalid response from the server"
+ )
+
+ return response_json
+
+ def push_snap_build(self, snap_id, snap_build):
+ url = f"/dev/api/snaps/{snap_id}/builds"
+ data = json.dumps({"assertion": snap_build})
+ headers = {
+ "Content-Type": "application/json",
+ }
+ 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 = {}
+ if series:
+ qs["series"] = series
+ if arch:
+ qs["architecture"] = arch
+ url = "/dev/api/snaps/" + snap_id + "/state"
+ if qs:
+ url += "?" + urlencode(qs)
+ 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()
+
+ return response_json
+
+ def sign_developer_agreement(self, latest_tos_accepted=False):
+ data = {"latest_tos_accepted": latest_tos_accepted}
+ 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
+
+ return response.json()
+
+ def get_metrics(
+ self, filters: List[metrics.MetricsFilter], snap_name: str
+ ) -> metrics.MetricsResults:
+ url = "/dev/api/snaps/metrics"
+ data = {"filters": [f.marshal() for f in filters]}
+ headers = {"Content-Type": "application/json", "Accept": "application/json"}
+
+ try:
+ response = self.post(url, json=data, headers=headers)
+
+ except craft_store.errors.StoreServerError as store_error:
+ raise errors.StoreMetricsError(
+ filters=filters, response=store_error.response, snap_name=snap_name
+ ) from store_error
+
+ try:
+ results = response.json()
+ return metrics.MetricsResults.unmarshal(results)
+ except ValueError as error:
+ raise errors.StoreMetricsUnmarshalError(
+ filters=filters, snap_name=snap_name, response=response
+ ) from error
+
+ def get_snap_releases(self, *, snap_name: str) -> releases.Releases:
+ 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:
+ 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:
+ 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:
+ 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())
+
+ def get_validation_sets(
+ self, *, name: Optional[str], sequence: Optional[str]
+ ) -> validation_sets.ValidationSets:
+ url = "/api/v2/validation-sets"
+ if name is not None:
+ url += "/" + name
+ params = dict()
+ if sequence is not None:
+ params["sequence"] = sequence
+ 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/storeapi/_metadata.py b/snapcraft_legacy/storeapi/_metadata.py
similarity index 85%
rename from snapcraft/storeapi/_metadata.py
rename to snapcraft_legacy/storeapi/_metadata.py
index 8bb1bd109a..a15b8ff2ae 100644
--- a/snapcraft/storeapi/_metadata.py
+++ b/snapcraft_legacy/storeapi/_metadata.py
@@ -18,7 +18,9 @@
import json
import os
-from snapcraft.storeapi.errors import StoreMetadataError
+import craft_store
+
+from snapcraft_legacy.storeapi.errors import StoreMetadataError
def _media_hash(media_file):
@@ -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/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 78%
rename from snapcraft/storeapi/_snap_api.py
rename to snapcraft_legacy/storeapi/_snap_api.py
index a405946b99..e3e9ac64ef 100644
--- a/snapcraft/storeapi/_snap_api.py
+++ b/snapcraft_legacy/storeapi/_snap_api.py
@@ -16,12 +16,13 @@
import logging
import os
-from typing import Dict
+from typing import Dict, Optional
from urllib.parse import urljoin
+import craft_store
import requests
-from . import constants, errors
+from . import agent, constants, errors
from ._requests import Requests
from .info import SnapInfo
@@ -36,7 +37,9 @@ class SnapAPI(Requests):
at http://api.snapcraft.io/docs/.
"""
- def __init__(self, client):
+ def __init__(self, client: Optional[craft_store.HTTPClient] = None):
+ if client is None:
+ 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)
@@ -90,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
@@ -111,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/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 75%
rename from snapcraft/storeapi/_store_client.py
rename to snapcraft_legacy/storeapi/_store_client.py
index 59f8e0f380..fe365177a3 100644
--- a/snapcraft/storeapi/_store_client.py
+++ b/snapcraft_legacy/storeapi/_store_client.py
@@ -16,61 +16,80 @@
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, List, Optional, Sequence, Union
+import craft_store
import requests
-from snapcraft.internal.indicators import download_requests_stream
-
-from . import _upload, errors, http_clients, metrics
+from snapcraft_legacy.internal.indicators import download_requests_stream
+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 channel_map, releases, validation_sets, whoami
+from .v2 import releases, validation_sets, whoami
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."""
- @property
- def use_candid(self) -> bool:
- return isinstance(self.auth_client, http_clients.CandidClient)
+ def __init__(self, ephemeral=False) -> None:
+ user_agent = agent.get_user_agent()
- def __init__(self, use_candid: bool = False) -> None:
- super().__init__()
+ 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 = http_clients.Client()
+ self.client = craft_store.HTTPClient(user_agent=user_agent)
- candid_has_credentials = http_clients.CandidClient.has_credentials()
- logger.debug(
- f"Candid forced: {use_candid}. Candid credentials: {candid_has_credentials}."
- )
- if use_candid or candid_has_credentials:
- self.auth_client: http_clients.AuthClient = http_clients.CandidClient()
+ if self.use_candid() is True:
+ 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)
- self._updown = UpDownClient(self.client)
+
+ @staticmethod
+ def use_candid() -> bool:
+ return os.getenv(constants.ENVIRONMENT_STORE_AUTH) == "candid"
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",
@@ -82,16 +101,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()
@@ -138,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,
@@ -193,9 +192,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,
*,
@@ -222,11 +218,9 @@ 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(
- self,
+ cls,
snap_name,
*,
risk: str,
@@ -235,7 +229,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
)
@@ -245,12 +239,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
@@ -271,7 +266,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/storeapi/http_clients/agent.py b/snapcraft_legacy/storeapi/agent.py
similarity index 84%
rename from snapcraft/storeapi/http_clients/agent.py
rename to snapcraft_legacy/storeapi/agent.py
index e540f6ae5f..04d516e75b 100644
--- a/snapcraft/storeapi/http_clients/agent.py
+++ b/snapcraft_legacy/storeapi/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/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 76%
rename from snapcraft/storeapi/constants.py
rename to snapcraft_legacy/storeapi/constants.py
index b032fead12..b03d4fc794 100644
--- a/snapcraft/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."
@@ -46,3 +48,13 @@
"We strongly recommend enabling multi-factor authentication: "
"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.
+
+The only setting that changes the behavior is `candid`, every
+other value uses Ubuntu SSO.
+"""
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 170a7ae0d0..ab0ba4ac44 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/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 59%
rename from snapcraft/storeapi/v2/_api_schema.py
rename to snapcraft_legacy/storeapi/v2/_api_schema.py
index 8dc478ef6f..34bf16f492 100644
--- a/snapcraft/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/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 91%
rename from snapcraft/yaml_utils/__init__.py
rename to snapcraft_legacy/yaml_utils/__init__.py
index 3fff3e7f5b..42e49f4a35 100644
--- a/snapcraft/yaml_utils/__init__.py
+++ b/snapcraft_legacy/yaml_utils/__init__.py
@@ -17,11 +17,12 @@
import codecs
import collections
import logging
+import os
from typing import Any, Dict, Optional, TextIO, Union
import yaml
-from snapcraft.yaml_utils.errors import YamlValidationError
+from snapcraft_legacy.yaml_utils.errors import YamlValidationError
logger = logging.getLogger(__name__)
@@ -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)
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/spread.yaml b/spread.yaml
index 89ee1b01be..6b1280d407 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,16 +207,21 @@ 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)"
- 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
@@ -219,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
@@ -383,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/conftest.py b/tests/conftest.py
new file mode 100644
index 0000000000..702897bb88
--- /dev/null
+++ b/tests/conftest.py
@@ -0,0 +1,61 @@
+# -*- 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 keyring
+import pytest
+import xdg
+from craft_store.auth import MemoryKeyring
+
+
+@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
+def new_dir(tmp_path):
+ """Change to a new temporary directory."""
+
+ cwd = os.getcwd()
+ os.chdir(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/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 99%
rename from tests/fake_servers/__init__.py
rename to tests/legacy/fake_servers/__init__.py
index 1c5f9c75d9..30debdbc4b 100644
--- a/tests/fake_servers/__init__.py
+++ b/tests/legacy/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/legacy/fake_servers/api.py
similarity index 99%
rename from tests/fake_servers/api.py
rename to tests/legacy/fake_servers/api.py
index c7de4b3641..1b02d5b3d0 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__)
@@ -941,7 +941,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/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 97%
rename from tests/fixture_setup/_fixtures.py
rename to tests/legacy/fixture_setup/_fixtures.py
index eb9067637a..50ede1576e 100644
--- a/tests/fixture_setup/_fixtures.py
+++ b/tests/legacy/fixture_setup/_fixtures.py
@@ -31,13 +31,13 @@
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
try:
- from snapcraft import yaml_utils
+ from snapcraft_legacy import yaml_utils
except ImportError:
import yaml as yaml_utils # type: ignore
@@ -283,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()
@@ -339,23 +339,23 @@ def setUp(self):
self.useFixture(
fixtures.EnvironmentVariable(
"STORE_DASHBOARD_URL",
- "https://dashboard.staging.snapcraft.io/",
+ "https://dashboard.staging.snapcraft.io",
)
)
self.useFixture(
fixtures.EnvironmentVariable(
"STORE_UPLOAD_URL",
- "https://upload.apps.staging.ubuntu.com/",
+ "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"
)
)
@@ -711,14 +711,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/legacy/fixture_setup/_unittests.py
similarity index 93%
rename from tests/fixture_setup/_unittests.py
rename to tests/legacy/fixture_setup/_unittests.py
index 308ad7b63c..7209784c9c 100644
--- a/tests/fixture_setup/_unittests.py
+++ b/tests/legacy/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/legacy/fixture_setup/_unix.py
similarity index 95%
rename from tests/fixture_setup/_unix.py
rename to tests/legacy/fixture_setup/_unix.py
index 39f53cc9b2..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):
@@ -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/legacy/fixture_setup/os_release.py
similarity index 94%
rename from tests/fixture_setup/os_release.py
rename to tests/legacy/fixture_setup/os_release.py
index 723cfe3dd4..a0ed4cfd8b 100644
--- a/tests/fixture_setup/os_release.py
+++ b/tests/legacy/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/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/legacy/unit/__init__.py b/tests/legacy/unit/__init__.py
new file mode 100644
index 0000000000..96fac5e843
--- /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.file_utils import get_snapcraft_path
+from tests.legacy import fake_servers, fixture_setup
+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 91%
rename from tests/unit/build_providers/__init__.py
rename to tests/legacy/unit/build_providers/__init__.py
index 092e836044..681fd541ec 100644
--- a/tests/unit/build_providers/__init__.py
+++ b/tests/legacy/unit/build_providers/__init__.py
@@ -18,10 +18,10 @@
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 tests import fixture_setup, unit
+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.legacy import fixture_setup, unit
class ProviderImpl(Provider):
@@ -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/legacy/unit/build_providers/conftest.py
similarity index 91%
rename from tests/unit/build_providers/conftest.py
rename to tests/legacy/unit/build_providers/conftest.py
index bc812338ee..f589bb2834 100644
--- a/tests/unit/build_providers/conftest.py
+++ b/tests/legacy/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/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 96%
rename from tests/unit/build_providers/lxd/test_lxd.py
rename to tests/legacy/unit/build_providers/lxd/test_lxd.py
index 27fc4cf28b..3ef99089c0 100644
--- a/tests/unit/build_providers/lxd/test_lxd.py
+++ b/tests/legacy/unit/build_providers/lxd/test_lxd.py
@@ -22,11 +22,11 @@
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 tests.unit.build_providers import BaseProviderBaseTest
+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.legacy.unit.build_providers import BaseProviderBaseTest
if sys.platform == "linux":
import pylxd
@@ -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,
)
@@ -485,7 +485,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()
@@ -496,7 +497,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)
@@ -521,7 +523,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/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 96%
rename from tests/unit/build_providers/multipass/test_instance_info.py
rename to tests/legacy/unit/build_providers/multipass/test_instance_info.py
index f532d71072..fd6df8d098 100644
--- a/tests/unit/build_providers/multipass/test_instance_info.py
+++ b/tests/legacy/unit/build_providers/multipass/test_instance_info.py
@@ -18,11 +18,11 @@
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
+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 96%
rename from tests/unit/build_providers/multipass/test_multipass.py
rename to tests/legacy/unit/build_providers/multipass/test_multipass.py
index f8a22b3e81..ce45206617 100644
--- a/tests/unit/build_providers/multipass/test_multipass.py
+++ b/tests/legacy/unit/build_providers/multipass/test_multipass.py
@@ -23,11 +23,14 @@
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 tests.unit.build_providers import BaseProviderBaseTest, get_project
+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.legacy.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/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 6b725d1445..e18faffd5f 100644
--- a/tests/unit/build_providers/multipass/test_multipass_command.py
+++ b/tests/legacy/unit/build_providers/multipass/test_multipass_command.py
@@ -23,9 +23,9 @@
import pytest
from testtools.matchers import Equals
-from snapcraft.internal.build_providers import errors
-from snapcraft.internal.build_providers._multipass import MultipassCommand
-from tests import unit
+from snapcraft_legacy.internal.build_providers import errors
+from snapcraft_legacy.internal.build_providers._multipass import MultipassCommand
+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 99%
rename from tests/unit/build_providers/test_base_provider.py
rename to tests/legacy/unit/build_providers/test_base_provider.py
index a73b9e4b8c..e30236269c 100644
--- a/tests/unit/build_providers/test_base_provider.py
+++ b/tests/legacy/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/legacy/unit/build_providers/test_errors.py
similarity index 99%
rename from tests/unit/build_providers/test_errors.py
rename to tests/legacy/unit/build_providers/test_errors.py
index 6a0b2a0fb4..28bf314394 100644
--- a/tests/unit/build_providers/test_errors.py
+++ b/tests/legacy/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/legacy/unit/build_providers/test_snap.py
similarity index 98%
rename from tests/unit/build_providers/test_snap.py
rename to tests/legacy/unit/build_providers/test_snap.py
index f85244c629..bd9a20e415 100644
--- a/tests/unit/build_providers/test_snap.py
+++ b/tests/legacy/unit/build_providers/test_snap.py
@@ -22,12 +22,12 @@
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,
)
-from tests import fixture_setup, unit
+from tests.legacy import fixture_setup, unit
from . import ProviderImpl, get_project
@@ -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/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 96%
rename from tests/unit/cache/conftest.py
rename to tests/legacy/unit/cache/conftest.py
index 6b730c7a88..88b40d0eae 100644
--- a/tests/unit/cache/conftest.py
+++ b/tests/legacy/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/legacy/unit/cache/test_file.py
similarity index 97%
rename from tests/unit/cache/test_file.py
rename to tests/legacy/unit/cache/test_file.py
index e3aa424d29..75df63d771 100644
--- a/tests/unit/cache/test_file.py
+++ b/tests/legacy/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/legacy/unit/cache/test_snap.py
similarity index 94%
rename from tests/unit/cache/test_snap.py
rename to tests/legacy/unit/cache/test_snap.py
index cad7dcb26c..1c12df1e07 100644
--- a/tests/unit/cache/test_snap.py
+++ b/tests/legacy/unit/cache/test_snap.py
@@ -22,20 +22,20 @@
import fixtures
from testtools.matchers import Contains, Equals, Not
-import snapcraft
-import tests
-from snapcraft import file_utils
-from snapcraft.internal import cache
-from tests.unit.commands import CommandBaseTestCase
+import snapcraft_legacy
+import tests.legacy
+from snapcraft_legacy import file_utils
+from snapcraft_legacy.internal import cache
+from tests.legacy.unit.commands import CommandBaseTestCase
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"
+ 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 88%
rename from tests/unit/cli/conftest.py
rename to tests/legacy/unit/cli/conftest.py
index 5ad8db1841..a183d9f5b6 100644
--- a/tests/unit/cli/conftest.py
+++ b/tests/legacy/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/legacy/unit/cli/test_echo.py
similarity index 89%
rename from tests/unit/cli/test_echo.py
rename to tests/legacy/unit/cli/test_echo.py
index 721daed5b2..157ae3d6ba 100644
--- a/tests/unit/cli/test_echo.py
+++ b/tests/legacy/unit/cli/test_echo.py
@@ -21,13 +21,13 @@
import fixtures
import pytest
-from snapcraft.cli import echo
-from tests import unit
+from snapcraft_legacy.cli import echo
+from tests.legacy 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/legacy/unit/cli/test_errors.py
similarity index 93%
rename from tests/unit/cli/test_errors.py
rename to tests/legacy/unit/cli/test_errors.py
index bd2830d2ad..782715789d 100644
--- a/tests/unit/cli/test_errors.py
+++ b/tests/legacy/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 tests import fixture_setup, unit
+from snapcraft_legacy.internal.build_providers.errors import ProviderExecError
+from tests.legacy 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/legacy/unit/cli/test_lifecycle.py b/tests/legacy/unit/cli/test_lifecycle.py
new file mode 100644
index 0000000000..cfc703f136
--- /dev/null
+++ b/tests/legacy/unit/cli/test_lifecycle.py
@@ -0,0 +1,57 @@
+# -*- 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 .
+
+from unittest import mock
+
+import pytest
+
+from snapcraft_legacy.cli import lifecycle
+
+
+@pytest.mark.parametrize(
+ "output,pack_name,pack_dir",
+ [
+ ("/tmp/output.snap", "output.snap", "/tmp"),
+ ("/tmp", None, "/tmp"),
+ ("output.snap", "output.snap", None),
+ ],
+)
+@pytest.mark.parametrize(
+ "compression", ["xz", "lzo", None],
+)
+@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)
+
+ assert mock_host_tool.mock_calls == [
+ mock.call(command_name="snap", package_name="snapd")
+ ]
+
+ pack_command = ["/bin/snap", "pack"]
+
+ if compression:
+ pack_command.extend(["--compression", compression])
+
+ if pack_name:
+ pack_command.extend(["--filename", pack_name])
+
+ pack_command.append("/my/snap")
+
+ if pack_dir:
+ pack_command.append(pack_dir)
+
+ assert mock_run_pack.mock_calls == [mock.call(pack_command)]
diff --git a/tests/unit/cli/test_metrics.py b/tests/legacy/unit/cli/test_metrics.py
similarity index 97%
rename from tests/unit/cli/test_metrics.py
rename to tests/legacy/unit/cli/test_metrics.py
index 1154ccce78..6378729fa8 100644
--- a/tests/unit/cli/test_metrics.py
+++ b/tests/legacy/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/legacy/unit/cli/test_options.py
similarity index 98%
rename from tests/unit/cli/test_options.py
rename to tests/legacy/unit/cli/test_options.py
index aeca9bf1e0..ceda82c854 100644
--- a/tests/unit/cli/test_options.py
+++ b/tests/legacy/unit/cli/test_options.py
@@ -20,8 +20,8 @@
import fixtures
from testtools.matchers import Equals
-import snapcraft.cli._options as options
-from tests import unit
+import snapcraft_legacy.cli._options as options
+from tests.legacy import unit
class TestProviderOptions:
@@ -324,7 +324,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/legacy/unit/commands/__init__.py b/tests/legacy/unit/commands/__init__.py
new file mode 100644
index 0000000000..2eaa69ec9c
--- /dev/null
+++ b/tests/legacy/unit/commands/__init__.py
@@ -0,0 +1,379 @@
+# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*-
+#
+# Copyright (C) 2015-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 json
+import subprocess
+from pathlib import PosixPath
+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
+from snapcraft_legacy.cli._runner import run
+from snapcraft_legacy.storeapi import metrics
+from snapcraft_legacy.storeapi.v2.releases import Releases
+from tests.legacy import fixture_setup, unit
+
+_sample_keys = [
+ {
+ "name": "default",
+ "sha3-384": "vdEeQvRxmZ26npJCFaGnl-VfGz0lU2jZZkWp_s7E-RxVCNtH2_mtjcxq2NkDKkIp",
+ },
+ {
+ "name": "another",
+ "sha3-384": "JsfToV5hO2eN9l89pYYCKXUioTERrZIIHUgQQd47jW8YNNBskupiIjWYd3KXLY_D",
+ },
+]
+
+
+def get_sample_key(name):
+ for key in _sample_keys:
+ if key["name"] == name:
+ return key
+ raise KeyError(name)
+
+
+original_check_output = subprocess.check_output
+
+
+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")
+ 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)
+ elif command[0].endswith("snap") and command[1] == "export-key":
+ if not command[2].startswith("--account="):
+ raise AssertionError("Unhandled command: {}".format(command))
+ account_id = command[2][len("--account=") :]
+ name = command[3]
+ # This isn't a full account-key-request assertion, but it's enough
+ # for testing.
+ return dedent(
+ """\
+ type: account-key-request
+ account-id: {account_id}
+ name: {name}
+ public-key-sha3-384: {sha3_384}
+ """
+ ).format(
+ account_id=account_id, name=name, sha3_384=get_sample_key(name)["sha3-384"]
+ )
+ elif command[0].endswith("snap") and command[1:] == [
+ "create-key",
+ "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()
+ self.runner = CliRunner()
+
+ 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_legacy.cli.echo.is_tty_connected", return_value=True
+ )
+ )
+
+ with mock.patch("sys.argv", args):
+ return self.runner.invoke(run, args, catch_exceptions=False, **kwargs)
+
+
+class LifecycleCommandsBaseTestCase(CommandBaseTestCase):
+ def setUp(self):
+ super().setUp()
+
+ self.useFixture(fixtures.EnvironmentVariable("SNAPCRAFT_BUILD_ENVIRONMENT"))
+
+ self.fake_lifecycle_clean = fixtures.MockPatch(
+ "snapcraft_legacy.internal.lifecycle.clean"
+ )
+ self.useFixture(self.fake_lifecycle_clean)
+
+ self.fake_lifecycle_execute = fixtures.MockPatch(
+ "snapcraft_legacy.internal.lifecycle.execute"
+ )
+ self.useFixture(self.fake_lifecycle_execute)
+
+ self.fake_pack = fixtures.MockPatch("snapcraft_legacy.cli.lifecycle._pack")
+ self.useFixture(self.fake_pack)
+
+ self.snapcraft_yaml = fixture_setup.SnapcraftYaml(
+ self.path,
+ parts={
+ "part0": {"plugin": "nil"},
+ "part1": {"plugin": "nil"},
+ "part2": {"plugin": "nil"},
+ },
+ )
+ self.useFixture(self.snapcraft_yaml)
+
+ self.provider_class_mock = mock.MagicMock()
+ self.provider_mock = mock.MagicMock()
+ self.provider_class_mock.return_value.__enter__.return_value = (
+ self.provider_mock
+ )
+
+ self.fake_get_provider_for = fixtures.MockPatch(
+ "snapcraft_legacy.internal.build_providers.get_provider_for",
+ return_value=self.provider_class_mock,
+ )
+ self.useFixture(self.fake_get_provider_for)
+
+ def assert_clean_not_called(self):
+ self.fake_lifecycle_clean.mock.assert_not_called()
+ self.provider_mock.clean.assert_not_called()
+ self.provider_mock.clean_project.assert_not_called()
+
+
+class StoreCommandsBaseTestCase(CommandBaseTestCase):
+ def setUp(self):
+ super().setUp()
+ self.fake_store = fixture_setup.FakeStore()
+ 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):
+ super().setUp()
+
+ # Our experimental environment variable is sticky
+ self.useFixture(
+ fixtures.EnvironmentVariable(
+ "SNAPCRAFT_EXPERIMENTAL_PROGRESSIVE_RELEASES", None
+ )
+ )
+
+ 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"
+ )
+ self.useFixture(self.fake_store_register)
+
+ self.fake_store_account_info_data = {
+ "account_id": "abcd",
+ "account_keys": list(),
+ "snaps": {
+ "16": {
+ "snap-test": {
+ "snap-id": "snap-test-snap-id",
+ "status": "Approved",
+ "private": False,
+ "since": "2016-12-12T01:01Z",
+ "price": "0",
+ },
+ "basic": {
+ "snap-id": "basic-snap-id",
+ "status": "Approved",
+ "private": False,
+ "since": "2016-12-12T01:01Z",
+ "price": "0",
+ },
+ }
+ },
+ }
+
+ self.fake_store_account_info = fixtures.MockPatchObject(
+ storeapi._dashboard_api.DashboardAPI,
+ "get_account_information",
+ return_value=self.fake_store_account_info_data,
+ )
+ self.useFixture(self.fake_store_account_info)
+
+ self.fake_store_status = fixtures.MockPatchObject(
+ storeapi._dashboard_api.DashboardAPI, "snap_status", return_value=dict()
+ )
+ self.useFixture(self.fake_store_status)
+
+ self.fake_store_release = fixtures.MockPatchObject(
+ storeapi.StoreClient, "release"
+ )
+ self.useFixture(self.fake_store_release)
+
+ self.fake_store_register_key = fixtures.MockPatchObject(
+ storeapi._dashboard_api.DashboardAPI, "register_key"
+ )
+ self.useFixture(self.fake_store_register_key)
+
+ self.metrics = metrics.MetricsResults(
+ metrics=[
+ metrics.MetricResults(
+ status=metrics.MetricsStatus["OK"],
+ snap_id="test-snap-id",
+ metric_name="daily_device_change",
+ buckets=["2021-01-01", "2021-01-02", "2021-01-03"],
+ series=[
+ metrics.Series(
+ name="continued",
+ values=[10, 11, 12],
+ currently_released=None,
+ ),
+ metrics.Series(
+ name="lost", values=[1, 2, 3], currently_released=None
+ ),
+ metrics.Series(
+ name="new", values=[2, 3, 4], currently_released=None
+ ),
+ ],
+ )
+ ]
+ )
+ self.fake_store_get_metrics = fixtures.MockPatchObject(
+ storeapi.StoreClient, "get_metrics", return_value=self.metrics
+ )
+ self.useFixture(self.fake_store_get_metrics)
+
+ self.releases = Releases.unmarshal(
+ {
+ "revisions": [
+ {
+ "architectures": ["i386"],
+ "base": "core20",
+ "build_url": None,
+ "confinement": "strict",
+ "created_at": " 2016-09-27T19:23:40Z",
+ "grade": "stable",
+ "revision": 2,
+ "sha3-384": "a9060ef4872ccacbfa440617a76fcd84967896b28d0d1eb7571f00a1098d766e7e93353b084ba6ad841d7b14b95ede48",
+ "size": 20,
+ "status": "Published",
+ "version": "2.0.1",
+ },
+ {
+ "architectures": ["amd64"],
+ "base": "core20",
+ "build_url": None,
+ "confinement": "strict",
+ "created_at": "2016-09-27T18:38:43Z",
+ "grade": "stable",
+ "revision": 1,
+ "sha3-384": "a9060ef4872ccacbfa440617a76fcd84967896b28d0d1eb7571f00a1098d766e7e93353b084ba6ad841d7b14b95ede48",
+ "size": 20,
+ "status": "Published",
+ "version": "2.0.2",
+ },
+ ],
+ "releases": [
+ {
+ "architecture": "amd64",
+ "branch": None,
+ "channel": "latest/stable",
+ "expiration-date": None,
+ "revision": 1,
+ "risk": "stable",
+ "track": "latest",
+ "when": "2020-02-12T17:51:40.891996Z",
+ },
+ {
+ "architecture": "i386",
+ "branch": None,
+ "channel": "latest/stable",
+ "expiration-date": None,
+ "revision": None,
+ "risk": "stable",
+ "track": "latest",
+ "when": "2020-02-11T17:51:40.891996Z",
+ },
+ {
+ "architecture": "amd64",
+ "branch": None,
+ "channel": "latest/edge",
+ "expiration-date": None,
+ "revision": 1,
+ "risk": "stable",
+ "track": "latest",
+ "when": "2020-01-12T17:51:40.891996Z",
+ },
+ ],
+ }
+ )
+ self.fake_store_get_releases = fixtures.MockPatchObject(
+ storeapi.StoreClient, "get_snap_releases", return_value=self.releases
+ )
+ self.useFixture(self.fake_store_get_releases)
+
+ # Mock the snap command, pass through a select few.
+ self.fake_check_output = fixtures.MockPatch(
+ "subprocess.check_output", side_effect=mock_check_output
+ )
+ self.useFixture(self.fake_check_output)
+
+ # Pretend that the snap command is available
+ self.fake_package_installed = fixtures.MockPatch(
+ "snapcraft_legacy.internal.repo.Repo.is_package_installed",
+ 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/snapcraft/storeapi/http_clients/__init__.py b/tests/legacy/unit/commands/conftest.py
similarity index 63%
rename from snapcraft/storeapi/http_clients/__init__.py
rename to tests/legacy/unit/commands/conftest.py
index 0318e38608..e5f552e309 100644
--- a/snapcraft/storeapi/http_clients/__init__.py
+++ b/tests/legacy/unit/commands/conftest.py
@@ -1,6 +1,6 @@
# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*-
#
-# Copyright 2021 Canonical Ltd
+# 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
@@ -14,12 +14,20 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
-from typing import Union
+from typing import List
-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
+import pytest
+from click.testing import CliRunner
+from snapcraft_legacy.cli._runner import run
-AuthClient = Union[CandidClient, UbuntuOneAuthClient]
+
+@pytest.fixture
+def click_run():
+ """Run commands using Click's testing backend."""
+ cli = CliRunner()
+
+ def runner(args: List[str]):
+ return cli.invoke(run, args)
+
+ return runner
diff --git a/tests/unit/commands/snapcraftctl/__init__.py b/tests/legacy/unit/commands/snapcraftctl/__init__.py
similarity index 94%
rename from tests/unit/commands/snapcraftctl/__init__.py
rename to tests/legacy/unit/commands/snapcraftctl/__init__.py
index 8e4c4cc247..358915bfd6 100644
--- a/tests/unit/commands/snapcraftctl/__init__.py
+++ b/tests/legacy/unit/commands/snapcraftctl/__init__.py
@@ -19,8 +19,8 @@
import fixtures
from click.testing import CliRunner
-from snapcraft.cli.snapcraftctl._runner import run
-from tests import unit
+from snapcraft_legacy.cli.snapcraftctl._runner import run
+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 97%
rename from tests/unit/commands/snapcraftctl/test_build.py
rename to tests/legacy/unit/commands/snapcraftctl/test_build.py
index 3f754f5186..a96089088d 100644
--- a/tests/unit/commands/snapcraftctl/test_build.py
+++ b/tests/legacy/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/legacy/unit/commands/snapcraftctl/test_set_grade.py
similarity index 98%
rename from tests/unit/commands/snapcraftctl/test_set_grade.py
rename to tests/legacy/unit/commands/snapcraftctl/test_set_grade.py
index 1563508f0b..46ca203f05 100644
--- a/tests/unit/commands/snapcraftctl/test_set_grade.py
+++ b/tests/legacy/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/legacy/unit/commands/snapcraftctl/test_set_version.py
similarity index 98%
rename from tests/unit/commands/snapcraftctl/test_set_version.py
rename to tests/legacy/unit/commands/snapcraftctl/test_set_version.py
index 0b30e27132..b264e76370 100644
--- a/tests/unit/commands/snapcraftctl/test_set_version.py
+++ b/tests/legacy/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/legacy/unit/commands/test_build_providers.py
similarity index 92%
rename from tests/unit/commands/test_build_providers.py
rename to tests/legacy/unit/commands/test_build_providers.py
index a01ec5dd25..15dbb90b0b 100644
--- a/tests/unit/commands/test_build_providers.py
+++ b/tests/legacy/unit/commands/test_build_providers.py
@@ -21,11 +21,11 @@
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
-from tests import fixture_setup
-from tests.unit.build_providers import ProviderImpl
+import snapcraft_legacy.yaml_utils.errors
+from snapcraft_legacy.internal import steps
+from snapcraft_legacy.internal.build_providers.errors import ProviderExecError
+from tests.legacy import fixture_setup
+from tests.legacy.unit.build_providers import ProviderImpl
from . import CommandBaseTestCase
@@ -75,9 +75,9 @@ 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.
+ # 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)
@@ -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/legacy/unit/commands/test_clean.py
similarity index 98%
rename from tests/unit/commands/test_clean.py
rename to tests/legacy/unit/commands/test_clean.py
index 08be3f633f..fe63441c7d 100644
--- a/tests/unit/commands/test_clean.py
+++ b/tests/legacy/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_create_key.py b/tests/legacy/unit/commands/test_create_key.py
similarity index 98%
rename from tests/unit/commands/test_create_key.py
rename to tests/legacy/unit/commands/test_create_key.py
index bed827ee3d..c9619e485f 100644
--- a/tests/unit/commands/test_create_key.py
+++ b/tests/legacy/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/legacy/unit/commands/test_edit_validation_sets.py
similarity index 95%
rename from tests/unit/commands/test_edit_validation_sets.py
rename to tests/legacy/unit/commands/test_edit_validation_sets.py
index 9ea4f44f13..76384d4c23 100644
--- a/tests/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.storeapi.v2 import validation_sets
-from snapcraft.storeapi import StoreClient
+from snapcraft_legacy.storeapi import StoreClient
+from snapcraft_legacy.storeapi.v2 import validation_sets
@pytest.fixture
@@ -93,12 +93,13 @@ 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()
+@pytest.mark.usefixtures("memory_keyring")
@pytest.mark.usefixtures("mock_subprocess_run")
def test_edit_validation_sets_with_no_changes_to_existing_set(
click_run,
@@ -129,12 +130,13 @@ 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()
+@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,
diff --git a/tests/unit/commands/test_extensions.py b/tests/legacy/unit/commands/test_extensions.py
similarity index 97%
rename from tests/unit/commands/test_extensions.py
rename to tests/legacy/unit/commands/test_extensions.py
index 299b6897f8..b8b1610ada 100644
--- a/tests/unit/commands/test_extensions.py
+++ b/tests/legacy/unit/commands/test_extensions.py
@@ -20,9 +20,9 @@
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 tests import fixture_setup
+from snapcraft_legacy.internal.project_loader import errors, supported_extension_names
+from snapcraft_legacy.internal.project_loader._extensions._extension import Extension
+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 86%
rename from tests/unit/commands/test_gated.py
rename to tests/legacy/unit/commands/test_gated.py
index 722c0da17d..a7a355cfce 100644
--- a/tests/unit/commands/test_gated.py
+++ b/tests/legacy/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
@@ -27,10 +27,8 @@
class GatedCommandTestCase(StoreCommandsBaseTestCase):
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"],
)
@@ -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/unit/commands/test_help.py b/tests/legacy/unit/commands/test_help.py
similarity index 95%
rename from tests/unit/commands/test_help.py
rename to tests/legacy/unit/commands/test_help.py
index 7d76fc8f43..9a2e532533 100644
--- a/tests/unit/commands/test_help.py
+++ b/tests/legacy/unit/commands/test_help.py
@@ -21,9 +21,9 @@
import fixtures
from testtools.matchers import Contains, Equals, StartsWith
-from snapcraft.cli._runner import run
-from snapcraft.cli.help import _TOPICS
-from tests import fixture_setup
+from snapcraft_legacy.cli._runner import run
+from snapcraft_legacy.cli.help import _TOPICS
+from tests.legacy 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/legacy/unit/commands/test_init.py
similarity index 96%
rename from tests/unit/commands/test_init.py
rename to tests/legacy/unit/commands/test_init.py
index 05bfe3f3fa..301dcd12f5 100644
--- a/tests/unit/commands/test_init.py
+++ b/tests/legacy/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_keys.py b/tests/legacy/unit/commands/test_list_keys.py
similarity index 96%
rename from tests/unit/commands/test_list_keys.py
rename to tests/legacy/unit/commands/test_list_keys.py
index 2c454a0352..cb808b5641 100644
--- a/tests/unit/commands/test_list_keys.py
+++ b/tests/legacy/unit/commands/test_list_keys.py
@@ -16,12 +16,10 @@
from textwrap import dedent
-from testtools.matchers import Contains, Equals
import fixtures
+from testtools.matchers import Contains, Equals
-from snapcraft import storeapi
-
-from . import FakeStoreCommandsBaseTestCase, get_sample_key
+from . import FAKE_UNAUTHORIZED_ERROR, FakeStoreCommandsBaseTestCase, get_sample_key
class ListKeysCommandTestCase(FakeStoreCommandsBaseTestCase):
@@ -29,9 +27,9 @@ 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"),
+ 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/unit/commands/test_list_plugins.py b/tests/legacy/unit/commands/test_list_plugins.py
similarity index 92%
rename from tests/unit/commands/test_list_plugins.py
rename to tests/legacy/unit/commands/test_list_plugins.py
index 62f1e90f78..6d0ebfad82 100644
--- a/tests/unit/commands/test_list_plugins.py
+++ b/tests/legacy/unit/commands/test_list_plugins.py
@@ -17,8 +17,8 @@
import fixtures
from testtools.matchers import Contains, Equals
-import snapcraft
-from tests import fixture_setup
+import snapcraft_legacy
+from tests.legacy 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/legacy/unit/commands/test_list_revisions.py
similarity index 95%
rename from tests/unit/commands/test_list_revisions.py
rename to tests/legacy/unit/commands/test_list_revisions.py
index ed095a0c7d..82ad631269 100644
--- a/tests/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 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/unit/commands/test_list_validation_sets.py b/tests/legacy/unit/commands/test_list_validation_sets.py
similarity index 96%
rename from tests/unit/commands/test_list_validation_sets.py
rename to tests/legacy/unit/commands/test_list_validation_sets.py
index 8464e16f52..8901139971 100644
--- a/tests/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.storeapi.v2 import validation_sets
-from snapcraft.storeapi import StoreClient
+from snapcraft_legacy.storeapi import StoreClient
+from snapcraft_legacy.storeapi.v2 import validation_sets
@pytest.fixture
@@ -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(
{
diff --git a/tests/unit/commands/test_metrics.py b/tests/legacy/unit/commands/test_metrics.py
similarity index 96%
rename from tests/unit/commands/test_metrics.py
rename to tests/legacy/unit/commands/test_metrics.py
index b4dffb436e..146d692794 100644
--- a/tests/unit/commands/test_metrics.py
+++ b/tests/legacy/unit/commands/test_metrics.py
@@ -18,9 +18,7 @@
import pytest
-from snapcraft 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/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 99%
rename from tests/unit/commands/test_pull_build_stage_prime.py
rename to tests/legacy/unit/commands/test_pull_build_stage_prime.py
index d127d4be7d..acb54ee65a 100644
--- a/tests/unit/commands/test_pull_build_stage_prime.py
+++ b/tests/legacy/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/legacy/unit/commands/test_refresh.py
similarity index 93%
rename from tests/unit/commands/test_refresh.py
rename to tests/legacy/unit/commands/test_refresh.py
index a1a24145b3..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
@@ -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_key.py b/tests/legacy/unit/commands/test_register_key.py
similarity index 88%
rename from tests/unit/commands/test_register_key.py
rename to tests/legacy/unit/commands/test_register_key.py
index b17457e371..4a4fe28404 100644
--- a/tests/unit/commands/test_register_key.py
+++ b/tests/legacy/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
@@ -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,22 +82,6 @@ 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)
@@ -109,7 +92,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_remote.py b/tests/legacy/unit/commands/test_remote.py
similarity index 90%
rename from tests/unit/commands/test_remote.py
rename to tests/legacy/unit/commands/test_remote.py
index bb8a15a6ad..022a27938f 100644
--- a/tests/unit/commands/test_remote.py
+++ b/tests/legacy/unit/commands/test_remote.py
@@ -19,9 +19,9 @@
import fixtures
from testtools.matchers import Contains, Equals
-import snapcraft.internal.remote_build.errors as errors
-import snapcraft.project
-from tests import fixture_setup
+import snapcraft_legacy.internal.remote_build.errors as errors
+import snapcraft_legacy.project
+from tests.legacy import fixture_setup
from . import CommandBaseTestCase
@@ -36,7 +36,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"])
@@ -45,13 +47,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"])
@@ -70,7 +72,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"])
@@ -80,7 +82,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(
@@ -137,7 +139,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))
@@ -151,7 +153,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"])
@@ -160,7 +162,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"])
@@ -169,7 +171,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"]
@@ -182,7 +184,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(
[
@@ -201,7 +203,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"]
@@ -214,7 +216,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/legacy/unit/commands/test_set_default_track.py
similarity index 68%
rename from tests/unit/commands/test_set_default_track.py
rename to tests/legacy/unit/commands/test_set_default_track.py
index 37f22173c6..6a04e177e4 100644
--- a/tests/unit/commands/test_set_default_track.py
+++ b/tests/legacy/unit/commands/test_set_default_track.py
@@ -17,10 +17,9 @@
import fixtures
from testtools.matchers import Contains, Equals
-import snapcraft
-from snapcraft import storeapi
+from snapcraft_legacy import storeapi
-from . import FakeStoreCommandsBaseTestCase
+from . import FAKE_UNAUTHORIZED_ERROR, FakeStoreCommandsBaseTestCase
class SetDefaultTrackCommandTestCase(FakeStoreCommandsBaseTestCase):
@@ -40,7 +39,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,
]
@@ -59,20 +58,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.cli.echo.exit_error", wraps=snapcraft.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/unit/commands/test_sign_build.py b/tests/legacy/unit/commands/test_sign_build.py
similarity index 78%
rename from tests/unit/commands/test_sign_build.py
rename to tests/legacy/unit/commands/test_sign_build.py
index 7d1c56787c..c02078d901 100644
--- a/tests/unit/commands/test_sign_build.py
+++ b/tests/legacy/unit/commands/test_sign_build.py
@@ -15,16 +15,15 @@
# along with this program. If not, see .
import os
import shutil
-import subprocess
from unittest import mock
import fixtures
from testtools.matchers import Contains, Equals, FileExists, Not
-import tests
-from snapcraft import internal, storeapi
+import tests.legacy
+from snapcraft_legacy import internal, storeapi
-from . import CommandBaseTestCase
+from . import FakeStoreCommandsBaseTestCase, mock_check_output
class SnapTest(fixtures.TempDir):
@@ -34,7 +33,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__()
@@ -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")
@@ -66,7 +65,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(
@@ -77,20 +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._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,
):
- 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(
@@ -102,18 +99,12 @@ 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,
):
- 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())
@@ -137,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._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,
@@ -170,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._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,
@@ -209,47 +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._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._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,
@@ -262,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"
@@ -289,7 +242,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,
@@ -302,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"
@@ -331,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._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,
@@ -340,20 +294,21 @@ def test_sign_build_upload_successfully(
):
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"
@@ -384,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._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/unit/commands/test_snap.py b/tests/legacy/unit/commands/test_snap.py
similarity index 99%
rename from tests/unit/commands/test_snap.py
rename to tests/legacy/unit/commands/test_snap.py
index 71a93a2435..365033c29f 100644
--- a/tests/unit/commands/test_snap.py
+++ b/tests/legacy/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_upload_metadata.py b/tests/legacy/unit/commands/test_upload_metadata.py
similarity index 88%
rename from tests/unit/commands/test_upload_metadata.py
rename to tests/legacy/unit/commands/test_upload_metadata.py
index 62e909b432..aabc4f09c5 100644
--- a/tests/unit/commands/test_upload_metadata.py
+++ b/tests/legacy/unit/commands/test_upload_metadata.py
@@ -21,11 +21,11 @@
import fixtures
from testtools.matchers import Contains, Equals, Not
-import tests
-from snapcraft import storeapi
-from snapcraft.storeapi.errors import StoreUploadError
+import tests.legacy
+from snapcraft_legacy import storeapi
+from snapcraft_legacy.storeapi.errors import StoreUploadError
-from . import CommandBaseTestCase
+from . import FAKE_UNAUTHORIZED_ERROR, CommandBaseTestCase
class UploadMetadataCommandTestCase(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)
@@ -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):
@@ -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))
@@ -102,13 +102,13 @@ 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",
)
# 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))
@@ -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,
]
@@ -219,11 +222,11 @@ 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
- 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 +239,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/legacy/unit/commands/test_validate.py
similarity index 91%
rename from tests/unit/commands/test_validate.py
rename to tests/legacy/unit/commands/test_validate.py
index 417a047d9a..a968cad1ed 100644
--- a/tests/unit/commands/test_validate.py
+++ b/tests/legacy/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
@@ -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"])
@@ -81,7 +79,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 +88,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 +97,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 +122,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 +147,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 +172,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 +198,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 +223,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_logout.py b/tests/legacy/unit/commands/test_version.py
similarity index 56%
rename from tests/unit/commands/test_logout.py
rename to tests/legacy/unit/commands/test_version.py
index 1fc57e5839..78d077ed79 100644
--- a/tests/unit/commands/test_logout.py
+++ b/tests/legacy/unit/commands/test_version.py
@@ -1,6 +1,6 @@
# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*-
#
-# Copyright (C) 2016-2017 Canonical Ltd
+# 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
@@ -13,22 +13,21 @@
#
# 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
-
-from snapcraft.storeapi import StoreClient
+from testtools.matchers import Equals
from . import CommandBaseTestCase
-class LogoutCommandTestCase(CommandBaseTestCase):
- @mock.patch.object(StoreClient, "logout")
- def test_logout_clears_config(self, mock_logout):
- result = self.run_command(["logout"])
+class VersionCommandTestCase(CommandBaseTestCase):
+ def test_has_version(self):
+ result = self.run_command(["--version"])
+ self.assertThat(result.exit_code, Equals(0))
+ def test_has_version_without_hyphens(self):
+ result = self.run_command(["version"])
self.assertThat(result.exit_code, Equals(0))
- self.assertThat(
- result.output, MatchesRegex(".*Credentials cleared.\n", flags=re.DOTALL)
- )
+
+ def test_method_return_same_value(self):
+ result1 = self.run_command(["version"])
+ result2 = self.run_command(["--version"])
+ self.assertEqual(result1.output, result2.output)
diff --git a/tests/legacy/unit/conftest.py b/tests/legacy/unit/conftest.py
new file mode 100644
index 0000000000..9e73452b65
--- /dev/null
+++ b/tests/legacy/unit/conftest.py
@@ -0,0 +1,108 @@
+# -*- 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 pathlib
+from typing import List
+from unittest import mock
+
+import pytest
+import xdg
+
+
+def pytest_generate_tests(metafunc):
+ idlist = []
+ argvalues = []
+ if metafunc.cls is None:
+ return
+
+ for scenario in metafunc.cls.scenarios:
+ idlist.append(scenario[0])
+ items = scenario[1].items()
+ argnames = [x[0] for x in items]
+ argvalues.append([x[1] for x in items])
+ metafunc.parametrize(argnames, argvalues, ids=idlist, scope="class")
+
+
+@pytest.fixture
+def mock_subprocess_run():
+ """A no-op subprocess.run mock."""
+ patcher = mock.patch("subprocess.run")
+ yield patcher.start()
+ patcher.stop()
+
+
+@pytest.fixture
+def tmp_work_path(tmp_path):
+ """Setup a temporary directory and chdir to it."""
+ os.chdir(tmp_path)
+ return tmp_path
+
+
+@pytest.fixture
+def xdg_dirs(tmp_path, monkeypatch):
+ """Setup XDG directories in a temporary directory."""
+ monkeypatch.setattr(
+ xdg.BaseDirectory, "xdg_config_home", (tmp_path / ".config").as_posix()
+ )
+ monkeypatch.setattr(
+ xdg.BaseDirectory, "xdg_data_home", (tmp_path / ".local").as_posix()
+ )
+ monkeypatch.setattr(
+ xdg.BaseDirectory, "xdg_cache_home", (tmp_path / ".cache").as_posix()
+ )
+ monkeypatch.setattr(
+ xdg.BaseDirectory,
+ "xdg_config_dirs",
+ lambda: [(tmp_path / ".config").as_posix()],
+ )
+ monkeypatch.setattr(
+ xdg.BaseDirectory, "xdg_data_dirs", lambda: [(tmp_path / ".config").as_posix()]
+ )
+
+ monkeypatch.setenv("XDG_CONFIG_HOME", (tmp_path / ".config").as_posix())
+ monkeypatch.setenv("XDG_DATA_HOME", (tmp_path / ".local").as_posix())
+ monkeypatch.setenv("XDG_CACHE_HOME", (tmp_path / ".cache").as_posix())
+
+ return tmp_path
+
+
+@pytest.fixture()
+def in_snap(monkeypatch):
+ """Simualte being run from within the context of the Snapcraft snap."""
+ monkeypatch.setenv("SNAP", "/snap/snapcraft/current")
+ monkeypatch.setenv("SNAP_NAME", "snapcraft")
+ monkeypatch.setenv("SNAP_VERSION", "4.0")
+
+
+@pytest.fixture()
+def fake_exists(monkeypatch):
+ """Fakely return True when checking for preconfigured paths."""
+
+ class FileCheck:
+ def __init__(self) -> None:
+ self._original_exists = os.path.exists
+ self.paths: List[str] = list()
+
+ def exists(self, path: str) -> bool:
+ if pathlib.Path(path) in self.paths:
+ return True
+ return self._original_exists(path)
+
+ file_checker = FileCheck()
+ monkeypatch.setattr(os.path, "exists", file_checker.exists)
+
+ return file_checker
diff --git a/tests/unit/db/test_datastore.py b/tests/legacy/unit/db/test_datastore.py
similarity index 97%
rename from tests/unit/db/test_datastore.py
rename to tests/legacy/unit/db/test_datastore.py
index ad66ecfaae..8fb258d79a 100644
--- a/tests/unit/db/test_datastore.py
+++ b/tests/legacy/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/legacy/unit/db/test_errors.py
similarity index 96%
rename from tests/unit/db/test_errors.py
rename to tests/legacy/unit/db/test_errors.py
index 7f3752669f..cd4b439b9f 100644
--- a/tests/unit/db/test_errors.py
+++ b/tests/legacy/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/legacy/unit/db/test_migration.py
similarity index 97%
rename from tests/unit/db/test_migration.py
rename to tests/legacy/unit/db/test_migration.py
index 5005cd0473..3dbc7d1d5d 100644
--- a/tests/unit/db/test_migration.py
+++ b/tests/legacy/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/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 97%
rename from tests/unit/deltas/test_deltas.py
rename to tests/legacy/unit/deltas/test_deltas.py
index 29dc77afc5..9c72578b69 100644
--- a/tests/unit/deltas/test_deltas.py
+++ b/tests/legacy/unit/deltas/test_deltas.py
@@ -21,8 +21,8 @@
from testtools import TestCase
from testtools import matchers as m
-from snapcraft.internal import deltas
-from tests import fixture_setup
+from snapcraft_legacy.internal import deltas
+from tests.legacy import fixture_setup
class BaseDeltaGenerationTestCase(TestCase):
@@ -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/legacy/unit/deltas/test_deltas_xdelta3.py
similarity index 98%
rename from tests/unit/deltas/test_deltas_xdelta3.py
rename to tests/legacy/unit/deltas/test_deltas_xdelta3.py
index b56ae80570..15834f62a1 100644
--- a/tests/unit/deltas/test_deltas_xdelta3.py
+++ b/tests/legacy/unit/deltas/test_deltas_xdelta3.py
@@ -23,8 +23,8 @@
from progressbar import AnimatedMarker, ProgressBar
from testtools import matchers as m
-from snapcraft.internal import deltas
-from tests import fixture_setup, unit
+from snapcraft_legacy.internal import deltas
+from tests.legacy import fixture_setup, unit
class XDelta3TestCase(unit.TestCase):
diff --git a/tests/unit/pluginhandler/__init__.py b/tests/legacy/unit/extractors/__init__.py
similarity index 100%
rename from tests/unit/pluginhandler/__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 5254b1ac6c..e45f178b07 100644
--- a/tests/unit/extractors/test_appstream.py
+++ b/tests/legacy/unit/extractors/test_appstream.py
@@ -20,8 +20,8 @@
import testscenarios
from testtools.matchers import Equals
-from snapcraft.extractors import ExtractedMetadata, _errors, appstream
-from tests import unit
+from snapcraft_legacy.extractors import ExtractedMetadata, _errors, appstream
+from tests.legacy import unit
def _create_desktop_file(desktop_file_path, icon: str = None) -> None:
@@ -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/legacy/unit/extractors/test_metadata.py
similarity index 97%
rename from tests/unit/extractors/test_metadata.py
rename to tests/legacy/unit/extractors/test_metadata.py
index ee77160c9c..d2bbd65170 100644
--- a/tests/unit/extractors/test_metadata.py
+++ b/tests/legacy/unit/extractors/test_metadata.py
@@ -16,8 +16,8 @@
from testtools.matchers import Equals, Not
-from snapcraft.extractors._metadata import ExtractedMetadata
-from tests import unit
+from snapcraft_legacy.extractors._metadata import ExtractedMetadata
+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 97%
rename from tests/unit/extractors/test_setuppy.py
rename to tests/legacy/unit/extractors/test_setuppy.py
index e4c2c0e336..ab362d5fd0 100644
--- a/tests/unit/extractors/test_setuppy.py
+++ b/tests/legacy/unit/extractors/test_setuppy.py
@@ -19,8 +19,8 @@
from testscenarios import multiply_scenarios
from testtools.matchers import Equals
-from snapcraft.extractors import ExtractedMetadata, _errors, setuppy
-from tests import unit
+from snapcraft_legacy.extractors import ExtractedMetadata, _errors, setuppy
+from tests.legacy import unit
class TestSetupPy:
diff --git a/tests/unit/lifecycle/__init__.py b/tests/legacy/unit/lifecycle/__init__.py
similarity index 78%
rename from tests/unit/lifecycle/__init__.py
rename to tests/legacy/unit/lifecycle/__init__.py
index 65b044de7e..2f4dfc53bc 100644
--- a/tests/unit/lifecycle/__init__.py
+++ b/tests/legacy/unit/lifecycle/__init__.py
@@ -19,9 +19,9 @@
import fixtures
-import snapcraft
-from snapcraft.internal import project_loader
-from tests import unit
+import snapcraft_legacy
+from snapcraft_legacy.internal import project_loader
+from tests.legacy import unit
class LifecycleTestBase(unit.TestCase):
@@ -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/legacy/unit/lifecycle/test_errors.py
similarity index 95%
rename from tests/unit/lifecycle/test_errors.py
rename to tests/legacy/unit/lifecycle/test_errors.py
index d2c8c47aec..318ffa47d4 100644
--- a/tests/unit/lifecycle/test_errors.py
+++ b/tests/legacy/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/legacy/unit/lifecycle/test_global_state.py
similarity index 89%
rename from tests/unit/lifecycle/test_global_state.py
rename to tests/legacy/unit/lifecycle/test_global_state.py
index 60165bb05d..1f91b2c3fb 100644
--- a/tests/unit/lifecycle/test_global_state.py
+++ b/tests/legacy/unit/lifecycle/test_global_state.py
@@ -18,11 +18,11 @@
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 tests import fixture_setup
+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.legacy import fixture_setup
class TestGlobalState(TestCase):
@@ -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/legacy/unit/lifecycle/test_lifecycle.py
similarity index 89%
rename from tests/unit/lifecycle/test_lifecycle.py
rename to tests/legacy/unit/lifecycle/test_lifecycle.py
index 82a5f641e3..1ae3978a8f 100644
--- a/tests/unit/lifecycle/test_lifecycle.py
+++ b/tests/legacy/unit/lifecycle/test_lifecycle.py
@@ -29,11 +29,17 @@
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
-from tests import fixture_setup, unit
+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.legacy 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/legacy/unit/lifecycle/test_order.py
similarity index 99%
rename from tests/unit/lifecycle/test_order.py
rename to tests/legacy/unit/lifecycle/test_order.py
index c54342d42e..71e720d0be 100644
--- a/tests/unit/lifecycle/test_order.py
+++ b/tests/legacy/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_order_core20.py b/tests/legacy/unit/lifecycle/test_order_core20.py
similarity index 89%
rename from tests/unit/lifecycle/test_order_core20.py
rename to tests/legacy/unit/lifecycle/test_order_core20.py
index 2c75319bf1..dcda9ee3a2 100644
--- a/tests/unit/lifecycle/test_order_core20.py
+++ b/tests/legacy/unit/lifecycle/test_order_core20.py
@@ -19,11 +19,11 @@
import pytest
-from snapcraft.internal import steps
-from snapcraft.internal.lifecycle._runner import _Executor as Executor
-from snapcraft.internal.meta.snap import Snap
-from snapcraft.internal.pluginhandler._build_attributes import BuildAttributes
-from snapcraft.project import Project
+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:
diff --git a/tests/unit/lifecycle/test_snap_installation.py b/tests/legacy/unit/lifecycle/test_snap_installation.py
similarity index 82%
rename from tests/unit/lifecycle/test_snap_installation.py
rename to tests/legacy/unit/lifecycle/test_snap_installation.py
index 570accfc21..ead9431725 100644
--- a/tests/unit/lifecycle/test_snap_installation.py
+++ b/tests/legacy/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/legacy/unit/lifecycle/test_status_cache.py
similarity index 97%
rename from tests/unit/lifecycle/test_status_cache.py
rename to tests/legacy/unit/lifecycle/test_status_cache.py
index bc96ee3f2d..09d76b1028 100644
--- a/tests/unit/lifecycle/test_status_cache.py
+++ b/tests/legacy/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/plugins/__init__.py b/tests/legacy/unit/meta/__init__.py
similarity index 100%
rename from tests/unit/plugins/__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 98%
rename from tests/unit/meta/test_application.py
rename to tests/legacy/unit/meta/test_application.py
index 810a1c61a1..58f30a571b 100644
--- a/tests/unit/meta/test_application.py
+++ b/tests/legacy/unit/meta/test_application.py
@@ -19,9 +19,9 @@
from testtools.matchers import Contains, Equals, FileExists, Not
-from snapcraft import yaml_utils
-from snapcraft.internal.meta import application, desktop, errors
-from tests import unit
+from snapcraft_legacy import yaml_utils
+from snapcraft_legacy.internal.meta import application, desktop, errors
+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 17503ae11a..19c869dbee 100644
--- a/tests/unit/meta/test_command.py
+++ b/tests/legacy/unit/meta/test_command.py
@@ -21,8 +21,8 @@
import fixtures
from testtools.matchers import Equals, FileContains, FileExists, Is
-from snapcraft.internal.meta import command, errors
-from tests import unit
+from snapcraft_legacy.internal.meta import command, errors
+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 99%
rename from tests/unit/meta/test_command_mangle.py
rename to tests/legacy/unit/meta/test_command_mangle.py
index 6dda4c58c9..c9d124cc7c 100644
--- a/tests/unit/meta/test_command_mangle.py
+++ b/tests/legacy/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/legacy/unit/meta/test_desktop.py
similarity index 98%
rename from tests/unit/meta/test_desktop.py
rename to tests/legacy/unit/meta/test_desktop.py
index 8cc90a1b94..95b70a50cb 100644
--- a/tests/unit/meta/test_desktop.py
+++ b/tests/legacy/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/legacy/unit/meta/test_errors.py
similarity index 99%
rename from tests/unit/meta/test_errors.py
rename to tests/legacy/unit/meta/test_errors.py
index d7c20282cb..ecbcffa7ad 100644
--- a/tests/unit/meta/test_errors.py
+++ b/tests/legacy/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/legacy/unit/meta/test_hook.py
similarity index 96%
rename from tests/unit/meta/test_hook.py
rename to tests/legacy/unit/meta/test_hook.py
index c836e94e13..279dec6763 100644
--- a/tests/unit/meta/test_hook.py
+++ b/tests/legacy/unit/meta/test_hook.py
@@ -18,9 +18,9 @@
from testtools.matchers import Equals
-from snapcraft.internal.meta import errors
-from snapcraft.internal.meta.hooks import Hook
-from tests import unit
+from snapcraft_legacy.internal.meta import errors
+from snapcraft_legacy.internal.meta.hooks import Hook
+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 2178aff75c..04c027139f 100644
--- a/tests/unit/meta/test_meta.py
+++ b/tests/legacy/unit/meta/test_meta.py
@@ -34,12 +34,12 @@
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 tests import fixture_setup, unit
+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.legacy import fixture_setup, unit
class CreateBaseTestCase(unit.TestCase):
@@ -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/legacy/unit/meta/test_package_repository.py
similarity index 99%
rename from tests/unit/meta/test_package_repository.py
rename to tests/legacy/unit/meta/test_package_repository.py
index 6ce9862ade..c81c6c5688 100644
--- a/tests/unit/meta/test_package_repository.py
+++ b/tests/legacy/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/legacy/unit/meta/test_plugs.py
similarity index 96%
rename from tests/unit/meta/test_plugs.py
rename to tests/legacy/unit/meta/test_plugs.py
index bdb483cfc7..5db7dda980 100644
--- a/tests/unit/meta/test_plugs.py
+++ b/tests/legacy/unit/meta/test_plugs.py
@@ -18,9 +18,9 @@
from testtools.matchers import Equals, Is
-from snapcraft.internal.meta import errors
-from snapcraft.internal.meta.plugs import ContentPlug, Plug
-from tests import unit
+from snapcraft_legacy.internal.meta import errors
+from snapcraft_legacy.internal.meta.plugs import ContentPlug, Plug
+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 98%
rename from tests/unit/meta/test_slots.py
rename to tests/legacy/unit/meta/test_slots.py
index eea7145bea..a44496075a 100644
--- a/tests/unit/meta/test_slots.py
+++ b/tests/legacy/unit/meta/test_slots.py
@@ -18,9 +18,9 @@
from testtools.matchers import Equals
-from snapcraft.internal.meta import errors
-from snapcraft.internal.meta.slots import ContentSlot, DbusSlot, Slot
-from tests import unit
+from snapcraft_legacy.internal.meta import errors
+from snapcraft_legacy.internal.meta.slots import ContentSlot, DbusSlot, Slot
+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 98%
rename from tests/unit/meta/test_snap.py
rename to tests/legacy/unit/meta/test_snap.py
index 6af13dac94..e957ec4a8a 100644
--- a/tests/unit/meta/test_snap.py
+++ b/tests/legacy/unit/meta/test_snap.py
@@ -21,10 +21,10 @@
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 tests import unit
+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.legacy import unit
class SnapTests(unit.TestCase):
@@ -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/legacy/unit/meta/test_snap_packaging.py
similarity index 96%
rename from tests/unit/meta/test_snap_packaging.py
rename to tests/legacy/unit/meta/test_snap_packaging.py
index 521842f456..c071752cb0 100644
--- a/tests/unit/meta/test_snap_packaging.py
+++ b/tests/legacy/unit/meta/test_snap_packaging.py
@@ -19,10 +19,10 @@
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 tests import fixture_setup, unit
+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.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 96%
rename from tests/unit/meta/test_system_user.py
rename to tests/legacy/unit/meta/test_system_user.py
index a19dd2cef4..ddca88c2cf 100644
--- a/tests/unit/meta/test_system_user.py
+++ b/tests/legacy/unit/meta/test_system_user.py
@@ -16,9 +16,9 @@
from testtools.matchers import Equals
-from snapcraft.internal.meta import errors
-from snapcraft.internal.meta.system_user import SystemUser, SystemUserScope
-from tests import unit
+from snapcraft_legacy.internal.meta import errors
+from snapcraft_legacy.internal.meta.system_user import SystemUser, SystemUserScope
+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 87%
rename from tests/unit/part_loader.py
rename to tests/legacy/unit/part_loader.py
index 459c658cb9..91d2103562 100644
--- a/tests/unit/part_loader.py
+++ b/tests/legacy/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(
@@ -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/unit/plugins/v1/python/__init__.py b/tests/legacy/unit/pluginhandler/__init__.py
similarity index 100%
rename from tests/unit/plugins/v1/python/__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 93%
rename from tests/unit/pluginhandler/mocks.py
rename to tests/legacy/unit/pluginhandler/mocks.py
index 431bdbb9af..d025c4f7ba 100644
--- a/tests/unit/pluginhandler/mocks.py
+++ b/tests/legacy/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/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 1da3c0eb70..f701cf2761 100644
--- a/tests/unit/pluginhandler/test_clean.py
+++ b/tests/legacy/unit/pluginhandler/test_clean.py
@@ -18,9 +18,9 @@
from testtools.matchers import Equals
-from snapcraft import file_utils
-from snapcraft.internal import errors, pluginhandler, steps
-from tests.unit import TestCase, load_part
+from snapcraft_legacy import file_utils
+from snapcraft_legacy.internal import errors, pluginhandler, steps
+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 97%
rename from tests/unit/pluginhandler/test_dirty_report.py
rename to tests/legacy/unit/pluginhandler/test_dirty_report.py
index 0afaf4371c..4d17923a85 100644
--- a/tests/unit/pluginhandler/test_dirty_report.py
+++ b/tests/legacy/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/legacy/unit/pluginhandler/test_metadata_extraction.py
similarity index 94%
rename from tests/unit/pluginhandler/test_metadata_extraction.py
rename to tests/legacy/unit/pluginhandler/test_metadata_extraction.py
index 6802c2af80..6b809bddd2 100644
--- a/tests/unit/pluginhandler/test_metadata_extraction.py
+++ b/tests/legacy/unit/pluginhandler/test_metadata_extraction.py
@@ -19,10 +19,10 @@
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 tests import fixture_setup, unit
+from snapcraft_legacy import extractors
+from snapcraft_legacy.internal import errors
+from snapcraft_legacy.internal.pluginhandler import extract_metadata
+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 94%
rename from tests/unit/pluginhandler/test_missing_dependency.py
rename to tests/legacy/unit/pluginhandler/test_missing_dependency.py
index a2d1adddbe..adeadf18ca 100644
--- a/tests/unit/pluginhandler/test_missing_dependency.py
+++ b/tests/legacy/unit/pluginhandler/test_missing_dependency.py
@@ -20,9 +20,11 @@
import fixtures
from testtools.matchers import Equals
-from snapcraft.internal import repo
-from snapcraft.internal.pluginhandler._dependencies import MissingDependencyResolver
-from tests import unit
+from snapcraft_legacy.internal import repo
+from snapcraft_legacy.internal.pluginhandler._dependencies import (
+ MissingDependencyResolver,
+)
+from tests.legacy import unit
class MissingDependencyTest(unit.TestCase):
@@ -40,7 +42,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/legacy/unit/pluginhandler/test_patcher.py
similarity index 92%
rename from tests/unit/pluginhandler/test_patcher.py
rename to tests/legacy/unit/pluginhandler/test_patcher.py
index ba5e7039f4..e22d3b0b64 100644
--- a/tests/unit/pluginhandler/test_patcher.py
+++ b/tests/legacy/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 tests.unit import load_part
+from snapcraft_legacy import file_utils
+from snapcraft_legacy.internal import errors
+from snapcraft_legacy.internal.pluginhandler import PartPatcher
+from tests.legacy.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/legacy/unit/pluginhandler/test_plugin_loader.py
similarity index 88%
rename from tests/unit/pluginhandler/test_plugin_loader.py
rename to tests/legacy/unit/pluginhandler/test_plugin_loader.py
index ba1690cbae..92cc460ac1 100644
--- a/tests/unit/pluginhandler/test_plugin_loader.py
+++ b/tests/legacy/unit/pluginhandler/test_plugin_loader.py
@@ -22,11 +22,11 @@
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 tests import unit
+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.legacy import unit
class NonLocalTest(unit.TestCase):
@@ -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/legacy/unit/pluginhandler/test_pluginhandler.py
similarity index 97%
rename from tests/unit/pluginhandler/test_pluginhandler.py
rename to tests/legacy/unit/pluginhandler/test_pluginhandler.py
index 05e46ef346..78ed9f02b6 100644
--- a/tests/unit/pluginhandler/test_pluginhandler.py
+++ b/tests/legacy/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,9 +37,9 @@
states,
steps,
)
-from snapcraft.internal.sources.errors import SnapcraftSourceUnhandledError
-from snapcraft.project import Project
-from tests import fixture_setup, unit
+from snapcraft_legacy.internal.sources.errors import SnapcraftSourceUnhandledError
+from snapcraft_legacy.project import Project
+from tests.legacy 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/legacy/unit/pluginhandler/test_runner.py
similarity index 98%
rename from tests/unit/pluginhandler/test_runner.py
rename to tests/legacy/unit/pluginhandler/test_runner.py
index 570b7c99da..c10751450f 100644
--- a/tests/unit/pluginhandler/test_runner.py
+++ b/tests/legacy/unit/pluginhandler/test_runner.py
@@ -22,9 +22,9 @@
from testtools.matchers import Contains, FileContains, FileExists
-from snapcraft.internal import errors
-from snapcraft.internal.pluginhandler import _runner
-from tests import fixture_setup, unit
+from snapcraft_legacy.internal import errors
+from snapcraft_legacy.internal.pluginhandler import _runner
+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 96%
rename from tests/unit/pluginhandler/test_scriptlets.py
rename to tests/legacy/unit/pluginhandler/test_scriptlets.py
index e612874e9b..d5ac61606e 100644
--- a/tests/unit/pluginhandler/test_scriptlets.py
+++ b/tests/legacy/unit/pluginhandler/test_scriptlets.py
@@ -25,10 +25,10 @@
from testscenarios.scenarios import multiply_scenarios
from testtools.matchers import Equals
-from snapcraft import yaml_utils
-from snapcraft.internal import errors
-from tests import unit
-from tests.unit.commands import CommandBaseTestCase
+from snapcraft_legacy import yaml_utils
+from snapcraft_legacy.internal import errors
+from tests.legacy import unit
+from tests.legacy.unit.commands import CommandBaseTestCase
class ScriptletCommandsTestCase(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/legacy/unit/pluginhandler/test_state.py
similarity index 96%
rename from tests/unit/pluginhandler/test_state.py
rename to tests/legacy/unit/pluginhandler/test_state.py
index 9ff997e193..3dbc406ab1 100644
--- a/tests/unit/pluginhandler/test_state.py
+++ b/tests/legacy/unit/pluginhandler/test_state.py
@@ -21,9 +21,9 @@
import fixtures
from testtools.matchers import Contains, Equals
-from snapcraft import extractors, plugins
-from snapcraft.internal import elf, errors, states, steps
-from tests import fixture_setup, unit
+from snapcraft_legacy import extractors, plugins
+from snapcraft_legacy.internal import elf, errors, states, steps
+from tests.legacy import fixture_setup, unit
class StateBaseTestCase(unit.TestCase):
@@ -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))
@@ -132,7 +135,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",
@@ -204,7 +207,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",
@@ -759,9 +762,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
):
@@ -830,9 +833,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
):
@@ -888,9 +891,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/ros/__init__.py b/tests/legacy/unit/plugins/__init__.py
similarity index 100%
rename from tests/unit/plugins/v1/ros/__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 87%
rename from tests/unit/plugins/v1/__init__.py
rename to tests/legacy/unit/plugins/v1/__init__.py
index 663dc28647..d33ccf5213 100644
--- a/tests/unit/plugins/v1/__init__.py
+++ b/tests/legacy/unit/plugins/v1/__init__.py
@@ -14,9 +14,9 @@
# 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 tests import unit
+from snapcraft_legacy.internal.meta.snap import Snap
+from snapcraft_legacy.project import Project
+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 78%
rename from tests/unit/plugins/v1/conftest.py
rename to tests/legacy/unit/plugins/v1/conftest.py
index 744a6794f4..15f9fa9069 100644
--- a/tests/unit/plugins/v1/conftest.py
+++ b/tests/legacy/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/project_loader/extensions/__init__.py b/tests/legacy/unit/plugins/v1/python/__init__.py
similarity index 100%
rename from tests/unit/project_loader/extensions/__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 96%
rename from tests/unit/plugins/v1/python/test_errors.py
rename to tests/legacy/unit/plugins/v1/python/test_errors.py
index 430a43482c..194e107ed2 100644
--- a/tests/unit/plugins/v1/python/test_errors.py
+++ b/tests/legacy/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/legacy/unit/plugins/v1/python/test_pip.py
similarity index 99%
rename from tests/unit/plugins/v1/python/test_pip.py
rename to tests/legacy/unit/plugins/v1/python/test_pip.py
index dc4a70858d..a524026720 100644
--- a/tests/unit/plugins/v1/python/test_pip.py
+++ b/tests/legacy/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/legacy/unit/plugins/v1/python/test_python_finder.py
similarity index 99%
rename from tests/unit/plugins/v1/python/test_python_finder.py
rename to tests/legacy/unit/plugins/v1/python/test_python_finder.py
index 0a7e52633f..a58a3f6b3e 100644
--- a/tests/unit/plugins/v1/python/test_python_finder.py
+++ b/tests/legacy/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/legacy/unit/plugins/v1/python/test_sitecustomize.py
similarity index 99%
rename from tests/unit/plugins/v1/python/test_sitecustomize.py
rename to tests/legacy/unit/plugins/v1/python/test_sitecustomize.py
index 6b6465b8b7..a75aaf7dde 100644
--- a/tests/unit/plugins/v1/python/test_sitecustomize.py
+++ b/tests/legacy/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/project_loader/grammar/__init__.py b/tests/legacy/unit/plugins/v1/ros/__init__.py
similarity index 100%
rename from tests/unit/project_loader/grammar/__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 97%
rename from tests/unit/plugins/v1/ros/test_rosdep.py
rename to tests/legacy/unit/plugins/v1/ros/test_rosdep.py
index a7932ccaca..4fbf3b1589 100644
--- a/tests/unit/plugins/v1/ros/test_rosdep.py
+++ b/tests/legacy/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
-from tests import unit
+import snapcraft_legacy
+from snapcraft_legacy.plugins.v1._ros import rosdep
+from tests.legacy 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/legacy/unit/plugins/v1/ros/test_wstool.py
similarity index 95%
rename from tests/unit/plugins/v1/ros/test_wstool.py
rename to tests/legacy/unit/plugins/v1/ros/test_wstool.py
index 0c6925178a..799dd91107 100644
--- a/tests/unit/plugins/v1/ros/test_wstool.py
+++ b/tests/legacy/unit/plugins/v1/ros/test_wstool.py
@@ -19,22 +19,23 @@
import subprocess
from unittest import mock
+import fixtures
from testtools.matchers import Contains, Equals
-import snapcraft
-from snapcraft.plugins.v1._ros import wstool
-from tests import unit
+import snapcraft_legacy
+from snapcraft_legacy.plugins.v1._ros import wstool
+from tests.legacy 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)
@@ -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:
diff --git a/tests/unit/plugins/v1/test_ant.py b/tests/legacy/unit/plugins/v1/test_ant.py
similarity index 96%
rename from tests/unit/plugins/v1/test_ant.py
rename to tests/legacy/unit/plugins/v1/test_ant.py
index 95ce5c3d08..26848997c8 100644
--- a/tests/unit/plugins/v1/test_ant.py
+++ b/tests/legacy/unit/plugins/v1/test_ant.py
@@ -22,11 +22,11 @@
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 tests import unit
+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.legacy 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/legacy/unit/plugins/v1/test_autotools.py
similarity index 98%
rename from tests/unit/plugins/v1/test_autotools.py
rename to tests/legacy/unit/plugins/v1/test_autotools.py
index 5d4f4ba191..d443a2b607 100644
--- a/tests/unit/plugins/v1/test_autotools.py
+++ b/tests/legacy/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
@@ -444,9 +444,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/legacy/unit/plugins/v1/test_base.py
similarity index 67%
rename from tests/unit/plugins/v1/test_base.py
rename to tests/legacy/unit/plugins/v1/test_base.py
index d22cc999fb..890d30670e 100644
--- a/tests/unit/plugins/v1/test_base.py
+++ b/tests/legacy/unit/plugins/v1/test_base.py
@@ -18,58 +18,64 @@
from testtools.matchers import Equals
-import snapcraft
-from snapcraft.internal import errors
-from tests import unit
+import snapcraft_legacy
+from snapcraft_legacy.internal import errors
+from tests.legacy 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/legacy/unit/plugins/v1/test_catkin.py
similarity index 98%
rename from tests/unit/plugins/v1/test_catkin.py
rename to tests/legacy/unit/plugins/v1/test_catkin.py
index 82ae4b8b6f..824e5d137e 100644
--- a/tests/unit/plugins/v1/test_catkin.py
+++ b/tests/legacy/unit/plugins/v1/test_catkin.py
@@ -36,11 +36,11 @@
Not,
)
-import snapcraft
-from snapcraft import repo
-from snapcraft.internal import errors
-from snapcraft.plugins.v1 import _ros, catkin
-from tests import unit
+import snapcraft_legacy
+from snapcraft_legacy import repo
+from snapcraft_legacy.internal import errors
+from snapcraft_legacy.plugins.v1 import _ros, catkin
+from tests.legacy 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/legacy/unit/plugins/v1/test_catkin_tools.py
similarity index 97%
rename from tests/unit/plugins/v1/test_catkin_tools.py
rename to tests/legacy/unit/plugins/v1/test_catkin_tools.py
index 005fdba877..b57a0e200e 100644
--- a/tests/unit/plugins/v1/test_catkin_tools.py
+++ b/tests/legacy/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/legacy/unit/plugins/v1/test_cmake.py
similarity index 97%
rename from tests/unit/plugins/v1/test_cmake.py
rename to tests/legacy/unit/plugins/v1/test_cmake.py
index 9a6f99c558..b026a07e3c 100644
--- a/tests/unit/plugins/v1/test_cmake.py
+++ b/tests/legacy/unit/plugins/v1/test_cmake.py
@@ -19,9 +19,9 @@
from testtools.matchers import Equals, HasLength
-from snapcraft.internal import errors
-from snapcraft.plugins.v1 import cmake
-from tests import fixture_setup, unit
+from snapcraft_legacy.internal import errors
+from snapcraft_legacy.plugins.v1 import cmake
+from tests.legacy 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/legacy/unit/plugins/v1/test_colcon.py
similarity index 98%
rename from tests/unit/plugins/v1/test_colcon.py
rename to tests/legacy/unit/plugins/v1/test_colcon.py
index e30d93a447..3eacba69a0 100644
--- a/tests/unit/plugins/v1/test_colcon.py
+++ b/tests/legacy/unit/plugins/v1/test_colcon.py
@@ -24,10 +24,10 @@
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 tests import unit
+from snapcraft_legacy import repo
+from snapcraft_legacy.internal import errors
+from snapcraft_legacy.plugins.v1 import _ros, colcon
+from tests.legacy 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/legacy/unit/plugins/v1/test_conda.py
similarity index 98%
rename from tests/unit/plugins/v1/test_conda.py
rename to tests/legacy/unit/plugins/v1/test_conda.py
index ba64cef4b6..870ecb2c36 100644
--- a/tests/unit/plugins/v1/test_conda.py
+++ b/tests/legacy/unit/plugins/v1/test_conda.py
@@ -21,9 +21,9 @@
import pytest
from testtools.matchers import DirExists, Equals, HasLength, Not
-from snapcraft.internal import errors
-from snapcraft.plugins.v1 import conda
-from tests import unit
+from snapcraft_legacy.internal import errors
+from snapcraft_legacy.plugins.v1 import conda
+from tests.legacy 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/legacy/unit/plugins/v1/test_crystal.py
similarity index 95%
rename from tests/unit/plugins/v1/test_crystal.py
rename to tests/legacy/unit/plugins/v1/test_crystal.py
index 211eb0a816..f60f188e97 100644
--- a/tests/unit/plugins/v1/test_crystal.py
+++ b/tests/legacy/unit/plugins/v1/test_crystal.py
@@ -20,9 +20,9 @@
import fixtures
from testtools.matchers import Equals, FileExists, HasLength
-from snapcraft.internal import errors
-from snapcraft.plugins.v1 import crystal
-from tests import unit
+from snapcraft_legacy.internal import errors
+from snapcraft_legacy.plugins.v1 import crystal
+from tests.legacy 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/legacy/unit/plugins/v1/test_dotnet.py
similarity index 95%
rename from tests/unit/plugins/v1/test_dotnet.py
rename to tests/legacy/unit/plugins/v1/test_dotnet.py
index 304f8922ce..f60407c775 100644
--- a/tests/unit/plugins/v1/test_dotnet.py
+++ b/tests/legacy/unit/plugins/v1/test_dotnet.py
@@ -21,11 +21,11 @@
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
-from tests import unit
+import snapcraft_legacy
+from snapcraft_legacy import file_utils
+from snapcraft_legacy.internal import sources
+from snapcraft_legacy.plugins.v1 import dotnet
+from tests.legacy 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/legacy/unit/plugins/v1/test_dump.py
similarity index 96%
rename from tests/unit/plugins/v1/test_dump.py
rename to tests/legacy/unit/plugins/v1/test_dump.py
index 3a92d1c0cb..4b9d8fbd28 100644
--- a/tests/unit/plugins/v1/test_dump.py
+++ b/tests/legacy/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
-from tests import unit
+import snapcraft_legacy
+from snapcraft_legacy.plugins.v1.dump import DumpInvalidSymlinkError, DumpPlugin
+from tests.legacy 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/legacy/unit/plugins/v1/test_flutter.py
similarity index 95%
rename from tests/unit/plugins/v1/test_flutter.py
rename to tests/legacy/unit/plugins/v1/test_flutter.py
index ad2bb9f629..187923bb5d 100644
--- a/tests/unit/plugins/v1/test_flutter.py
+++ b/tests/legacy/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/legacy/unit/plugins/v1/test_go.py
similarity index 98%
rename from tests/unit/plugins/v1/test_go.py
rename to tests/legacy/unit/plugins/v1/test_go.py
index 8e47f3badd..0786f654a4 100644
--- a/tests/unit/plugins/v1/test_go.py
+++ b/tests/legacy/unit/plugins/v1/test_go.py
@@ -23,10 +23,10 @@
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 tests import fixture_setup, unit
+from snapcraft_legacy.internal import errors, meta
+from snapcraft_legacy.plugins.v1 import go
+from snapcraft_legacy.project import Project
+from tests.legacy 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/legacy/unit/plugins/v1/test_godeps.py
similarity index 98%
rename from tests/unit/plugins/v1/test_godeps.py
rename to tests/legacy/unit/plugins/v1/test_godeps.py
index 31ce03a4e1..a443f0bf4a 100644
--- a/tests/unit/plugins/v1/test_godeps.py
+++ b/tests/legacy/unit/plugins/v1/test_godeps.py
@@ -19,9 +19,9 @@
from testtools.matchers import Contains, Equals, HasLength, Not
-from snapcraft.internal import errors
-from snapcraft.plugins.v1 import godeps
-from tests import unit
+from snapcraft_legacy.internal import errors
+from snapcraft_legacy.plugins.v1 import godeps
+from tests.legacy 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/legacy/unit/plugins/v1/test_gradle.py
similarity index 98%
rename from tests/unit/plugins/v1/test_gradle.py
rename to tests/legacy/unit/plugins/v1/test_gradle.py
index a9a51f3028..8f701b3e92 100644
--- a/tests/unit/plugins/v1/test_gradle.py
+++ b/tests/legacy/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/legacy/unit/plugins/v1/test_kbuild.py
similarity index 97%
rename from tests/unit/plugins/v1/test_kbuild.py
rename to tests/legacy/unit/plugins/v1/test_kbuild.py
index ef2e832149..bb22941405 100644
--- a/tests/unit/plugins/v1/test_kbuild.py
+++ b/tests/legacy/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/legacy/unit/plugins/v1/test_kernel.py
similarity index 95%
rename from tests/unit/plugins/v1/test_kernel.py
rename to tests/legacy/unit/plugins/v1/test_kernel.py
index 17835d16db..0a2ab8d69c 100644
--- a/tests/unit/plugins/v1/test_kernel.py
+++ b/tests/legacy/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/legacy/unit/plugins/v1/test_make.py
similarity index 97%
rename from tests/unit/plugins/v1/test_make.py
rename to tests/legacy/unit/plugins/v1/test_make.py
index a61036ff52..e600a5297e 100644
--- a/tests/unit/plugins/v1/test_make.py
+++ b/tests/legacy/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/legacy/unit/plugins/v1/test_maven.py
similarity index 98%
rename from tests/unit/plugins/v1/test_maven.py
rename to tests/legacy/unit/plugins/v1/test_maven.py
index 0c08feaf87..f126a15caf 100644
--- a/tests/unit/plugins/v1/test_maven.py
+++ b/tests/legacy/unit/plugins/v1/test_maven.py
@@ -26,11 +26,11 @@
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 tests import unit
+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.legacy 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/legacy/unit/plugins/v1/test_meson.py
similarity index 98%
rename from tests/unit/plugins/v1/test_meson.py
rename to tests/legacy/unit/plugins/v1/test_meson.py
index a2c670b468..7ddd9742fc 100644
--- a/tests/unit/plugins/v1/test_meson.py
+++ b/tests/legacy/unit/plugins/v1/test_meson.py
@@ -20,9 +20,9 @@
from testtools.matchers import Equals, HasLength
-from snapcraft.internal import errors
-from snapcraft.plugins.v1 import meson
-from tests import unit
+from snapcraft_legacy.internal import errors
+from snapcraft_legacy.plugins.v1 import meson
+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 92%
rename from tests/unit/plugins/v1/test_nil.py
rename to tests/legacy/unit/plugins/v1/test_nil.py
index f11288b0b6..1633d12e6b 100644
--- a/tests/unit/plugins/v1/test_nil.py
+++ b/tests/legacy/unit/plugins/v1/test_nil.py
@@ -16,8 +16,8 @@
from testtools.matchers import Equals
-from snapcraft.plugins.v1.nil import NilPlugin
-from tests import unit
+from snapcraft_legacy.plugins.v1.nil import NilPlugin
+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 98%
rename from tests/unit/plugins/v1/test_nodejs.py
rename to tests/legacy/unit/plugins/v1/test_nodejs.py
index 3fa83f7043..d14fb0fcde 100644
--- a/tests/unit/plugins/v1/test_nodejs.py
+++ b/tests/legacy/unit/plugins/v1/test_nodejs.py
@@ -25,9 +25,9 @@
import pytest
from testtools.matchers import Equals, FileExists, HasLength
-from snapcraft.internal import errors
-from snapcraft.plugins.v1 import nodejs
-from tests import fixture_setup, unit
+from snapcraft_legacy.internal import errors
+from snapcraft_legacy.plugins.v1 import nodejs
+from tests.legacy 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/legacy/unit/plugins/v1/test_plainbox_provider.py
similarity index 98%
rename from tests/unit/plugins/v1/test_plainbox_provider.py
rename to tests/legacy/unit/plugins/v1/test_plainbox_provider.py
index 948d61d881..09309705b1 100644
--- a/tests/unit/plugins/v1/test_plainbox_provider.py
+++ b/tests/legacy/unit/plugins/v1/test_plainbox_provider.py
@@ -19,9 +19,9 @@
from testtools.matchers import Equals, HasLength
-from snapcraft.internal import errors
-from snapcraft.plugins.v1 import plainbox_provider
-from tests import fixture_setup, unit
+from snapcraft_legacy.internal import errors
+from snapcraft_legacy.plugins.v1 import plainbox_provider
+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 d519518fd6..df3ffd2663 100644
--- a/tests/unit/plugins/v1/test_python.py
+++ b/tests/legacy/unit/plugins/v1/test_python.py
@@ -21,9 +21,9 @@
import jsonschema
from testtools.matchers import Equals, HasLength
-from snapcraft.internal import errors
-from snapcraft.plugins.v1 import python
-from tests import fixture_setup, unit
+from snapcraft_legacy.internal import errors
+from snapcraft_legacy.plugins.v1 import python
+from tests.legacy 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/legacy/unit/plugins/v1/test_qmake.py
similarity index 98%
rename from tests/unit/plugins/v1/test_qmake.py
rename to tests/legacy/unit/plugins/v1/test_qmake.py
index f8b0de522a..2f450c01a1 100644
--- a/tests/unit/plugins/v1/test_qmake.py
+++ b/tests/legacy/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/legacy/unit/plugins/v1/test_ruby.py
similarity index 94%
rename from tests/unit/plugins/v1/test_ruby.py
rename to tests/legacy/unit/plugins/v1/test_ruby.py
index 13ce7df700..82295aef01 100644
--- a/tests/unit/plugins/v1/test_ruby.py
+++ b/tests/legacy/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/legacy/unit/plugins/v1/test_rust.py
similarity index 97%
rename from tests/unit/plugins/v1/test_rust.py
rename to tests/legacy/unit/plugins/v1/test_rust.py
index c06da3734e..5ecab1b939 100644
--- a/tests/unit/plugins/v1/test_rust.py
+++ b/tests/legacy/unit/plugins/v1/test_rust.py
@@ -25,10 +25,10 @@
import toml
from testtools.matchers import Contains, Equals, FileExists, Not
-import snapcraft
-from snapcraft.internal import errors, meta
-from snapcraft.plugins.v1 import rust
-from tests import fixture_setup, unit
+import snapcraft_legacy
+from snapcraft_legacy.internal import errors, meta
+from snapcraft_legacy.plugins.v1 import rust
+from tests.legacy 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/legacy/unit/plugins/v1/test_scons.py
similarity index 97%
rename from tests/unit/plugins/v1/test_scons.py
rename to tests/legacy/unit/plugins/v1/test_scons.py
index 3c2fb8d231..30a513e373 100644
--- a/tests/unit/plugins/v1/test_scons.py
+++ b/tests/legacy/unit/plugins/v1/test_scons.py
@@ -19,9 +19,9 @@
from testtools.matchers import Equals, HasLength
-from snapcraft.internal import errors
-from snapcraft.plugins.v1 import scons
-from tests import unit
+from snapcraft_legacy.internal import errors
+from snapcraft_legacy.plugins.v1 import scons
+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 97%
rename from tests/unit/plugins/v1/test_waf.py
rename to tests/legacy/unit/plugins/v1/test_waf.py
index 4e8556f985..15ac64b4e5 100644
--- a/tests/unit/plugins/v1/test_waf.py
+++ b/tests/legacy/unit/plugins/v1/test_waf.py
@@ -20,10 +20,10 @@
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 tests import unit
+from snapcraft_legacy.internal import errors, meta
+from snapcraft_legacy.plugins.v1 import waf
+from snapcraft_legacy.project import Project
+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 97%
rename from tests/unit/plugins/v2/test_autotools.py
rename to tests/legacy/unit/plugins/v2/test_autotools.py
index d0412cd52e..21a1f89132 100644
--- a/tests/unit/plugins/v2/test_autotools.py
+++ b/tests/legacy/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/legacy/unit/plugins/v2/test_catkin.py
similarity index 98%
rename from tests/unit/plugins/v2/test_catkin.py
rename to tests/legacy/unit/plugins/v2/test_catkin.py
index 1702876521..7dccfa94c3 100644
--- a/tests/unit/plugins/v2/test_catkin.py
+++ b/tests/legacy/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/legacy/unit/plugins/v2/test_catkin_tools.py
similarity index 98%
rename from tests/unit/plugins/v2/test_catkin_tools.py
rename to tests/legacy/unit/plugins/v2/test_catkin_tools.py
index 28a62669dd..9ba0984302 100644
--- a/tests/unit/plugins/v2/test_catkin_tools.py
+++ b/tests/legacy/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/legacy/unit/plugins/v2/test_cmake.py
similarity index 98%
rename from tests/unit/plugins/v2/test_cmake.py
rename to tests/legacy/unit/plugins/v2/test_cmake.py
index 420be23386..16d35de844 100644
--- a/tests/unit/plugins/v2/test_cmake.py
+++ b/tests/legacy/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/legacy/unit/plugins/v2/test_colcon.py
similarity index 98%
rename from tests/unit/plugins/v2/test_colcon.py
rename to tests/legacy/unit/plugins/v2/test_colcon.py
index d4a78a132b..e4a67ac238 100644
--- a/tests/unit/plugins/v2/test_colcon.py
+++ b/tests/legacy/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/legacy/unit/plugins/v2/test_conda.py
similarity index 98%
rename from tests/unit/plugins/v2/test_conda.py
rename to tests/legacy/unit/plugins/v2/test_conda.py
index d127f465d6..b1046d6cac 100644
--- a/tests/unit/plugins/v2/test_conda.py
+++ b/tests/legacy/unit/plugins/v2/test_conda.py
@@ -18,9 +18,9 @@
import pytest
-from snapcraft.plugins.v2.conda import (
- CondaPlugin,
+from snapcraft_legacy.plugins.v2.conda import (
ArchitectureMissing,
+ CondaPlugin,
_get_miniconda_source,
)
diff --git a/tests/unit/plugins/v2/test_crystal.py b/tests/legacy/unit/plugins/v2/test_crystal.py
similarity index 97%
rename from tests/unit/plugins/v2/test_crystal.py
rename to tests/legacy/unit/plugins/v2/test_crystal.py
index 23845b05c9..c419ed91a8 100644
--- a/tests/unit/plugins/v2/test_crystal.py
+++ b/tests/legacy/unit/plugins/v2/test_crystal.py
@@ -20,8 +20,8 @@
from testtools import TestCase
from testtools.matchers import Equals
-import snapcraft.plugins.v2.crystal as crystal
-from snapcraft.plugins.v2.crystal import CrystalPlugin
+import snapcraft_legacy.plugins.v2.crystal as crystal
+from snapcraft_legacy.plugins.v2.crystal import CrystalPlugin
class CrystalPluginTest(TestCase):
diff --git a/tests/unit/plugins/v2/test_dump.py b/tests/legacy/unit/plugins/v2/test_dump.py
similarity index 96%
rename from tests/unit/plugins/v2/test_dump.py
rename to tests/legacy/unit/plugins/v2/test_dump.py
index db22a56bad..e991eae4c9 100644
--- a/tests/unit/plugins/v2/test_dump.py
+++ b/tests/legacy/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/legacy/unit/plugins/v2/test_go.py
similarity index 98%
rename from tests/unit/plugins/v2/test_go.py
rename to tests/legacy/unit/plugins/v2/test_go.py
index d804fe6809..8b8c3f151b 100644
--- a/tests/unit/plugins/v2/test_go.py
+++ b/tests/legacy/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/legacy/unit/plugins/v2/test_make.py
similarity index 98%
rename from tests/unit/plugins/v2/test_make.py
rename to tests/legacy/unit/plugins/v2/test_make.py
index b3fc261f94..58998a26ef 100644
--- a/tests/unit/plugins/v2/test_make.py
+++ b/tests/legacy/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/legacy/unit/plugins/v2/test_meson.py
similarity index 98%
rename from tests/unit/plugins/v2/test_meson.py
rename to tests/legacy/unit/plugins/v2/test_meson.py
index f4737948ff..2eab5717c5 100644
--- a/tests/unit/plugins/v2/test_meson.py
+++ b/tests/legacy/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/legacy/unit/plugins/v2/test_nil.py
similarity index 96%
rename from tests/unit/plugins/v2/test_nil.py
rename to tests/legacy/unit/plugins/v2/test_nil.py
index 8fe0b18526..fd90dd6986 100644
--- a/tests/unit/plugins/v2/test_nil.py
+++ b/tests/legacy/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/legacy/unit/plugins/v2/test_npm.py
similarity index 98%
rename from tests/unit/plugins/v2/test_npm.py
rename to tests/legacy/unit/plugins/v2/test_npm.py
index 565f0451ca..062c2d1b78 100644
--- a/tests/unit/plugins/v2/test_npm.py
+++ b/tests/legacy/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/legacy/unit/plugins/v2/test_python.py
similarity index 98%
rename from tests/unit/plugins/v2/test_python.py
rename to tests/legacy/unit/plugins/v2/test_python.py
index abe5f5f6e5..c56eae0987 100644
--- a/tests/unit/plugins/v2/test_python.py
+++ b/tests/legacy/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/legacy/unit/plugins/v2/test_qmake.py
similarity index 98%
rename from tests/unit/plugins/v2/test_qmake.py
rename to tests/legacy/unit/plugins/v2/test_qmake.py
index 5b13805abc..f7d298906a 100644
--- a/tests/unit/plugins/v2/test_qmake.py
+++ b/tests/legacy/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/legacy/unit/plugins/v2/test_rust.py
similarity index 98%
rename from tests/unit/plugins/v2/test_rust.py
rename to tests/legacy/unit/plugins/v2/test_rust.py
index cd06c9bfd1..d3ab8832b8 100644
--- a/tests/unit/plugins/v2/test_rust.py
+++ b/tests/legacy/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/legacy/unit/project/__init__.py
similarity index 88%
rename from tests/unit/project/__init__.py
rename to tests/legacy/unit/project/__init__.py
index 48b4a2fc2c..0381a942b5 100644
--- a/tests/unit/project/__init__.py
+++ b/tests/legacy/unit/project/__init__.py
@@ -13,9 +13,9 @@
#
# 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
-from tests import unit
+import snapcraft_legacy.yaml_utils.errors
+from snapcraft_legacy.project import Project as _Project
+from tests.legacy import unit
class ProjectBaseTest(unit.TestCase):
@@ -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/legacy/unit/project/test_errors.py
similarity index 98%
rename from tests/unit/project/test_errors.py
rename to tests/legacy/unit/project/test_errors.py
index 3066343d7d..0b664765b2 100644
--- a/tests/unit/project/test_errors.py
+++ b/tests/legacy/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/legacy/unit/project/test_get_snapcraft.py
similarity index 97%
rename from tests/unit/project/test_get_snapcraft.py
rename to tests/legacy/unit/project/test_get_snapcraft.py
index 5e1abbedfa..9d0ce30bb0 100644
--- a/tests/unit/project/test_get_snapcraft.py
+++ b/tests/legacy/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/legacy/unit/project/test_project.py
similarity index 98%
rename from tests/unit/project/test_project.py
rename to tests/legacy/unit/project/test_project.py
index 9d5ab54a09..28ca90b846 100644
--- a/tests/unit/project/test_project.py
+++ b/tests/legacy/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/legacy/unit/project/test_project_info.py
similarity index 92%
rename from tests/unit/project/test_project_info.py
rename to tests/legacy/unit/project/test_project_info.py
index 5fa478e993..8b1d5c9713 100644
--- a/tests/unit/project/test_project_info.py
+++ b/tests/legacy/unit/project/test_project_info.py
@@ -19,9 +19,9 @@
import pytest
from testtools.matchers import Equals, Is, MatchesRegex
-import snapcraft.yaml_utils.errors
-from snapcraft.project._project_info import ProjectInfo
-from tests import unit
+import snapcraft_legacy.yaml_utils.errors
+from snapcraft_legacy.project._project_info import ProjectInfo
+from tests.legacy import unit
class ProjectInfoTest(unit.TestCase):
@@ -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,
)
@@ -177,7 +177,8 @@ def test_tab_in_yaml(self):
self.assertThat(
raised.message,
MatchesRegex(
- "found a tab character that violates indentation " "on line 5, column 1"
+ "found a tab character that violates? (indentation|intendation)"
+ " on line 5, column 1"
),
)
@@ -194,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,
)
@@ -225,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,
)
@@ -246,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/legacy/unit/project/test_sanity_checks.py
similarity index 95%
rename from tests/unit/project/test_sanity_checks.py
rename to tests/legacy/unit/project/test_sanity_checks.py
index f0b2287543..b168510b56 100644
--- a/tests/unit/project/test_sanity_checks.py
+++ b/tests/legacy/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
@@ -105,7 +105,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/legacy/unit/project/test_schema.py
similarity index 92%
rename from tests/unit/project/test_schema.py
rename to tests/legacy/unit/project/test_schema.py
index e67fe7b498..7e6b24f423 100644
--- a/tests/unit/project/test_schema.py
+++ b/tests/legacy/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"
@@ -185,7 +189,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,
)
@@ -208,7 +212,7 @@ def test_invalid_activates_on(self):
},
}
raised = self.assertRaises(
- snapcraft.yaml_utils.errors.YamlValidationError,
+ snapcraft_legacy.yaml_utils.errors.YamlValidationError,
Validator(self.data).validate,
)
self.assertThat(
@@ -223,7 +227,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,
)
@@ -242,7 +246,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,
)
@@ -266,7 +270,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(
@@ -279,7 +283,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)
@@ -334,7 +338,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(
@@ -366,7 +372,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()
@@ -380,7 +386,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)
@@ -465,7 +471,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 = (
@@ -511,7 +517,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()
@@ -539,7 +545,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()
@@ -574,7 +580,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 = (
@@ -592,7 +598,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 = (
@@ -724,7 +730,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)
@@ -783,7 +791,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)
@@ -906,7 +914,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 = (
@@ -924,7 +932,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 = (
@@ -941,7 +949,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 = (
@@ -1043,7 +1051,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 = (
@@ -1064,7 +1072,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 = (
@@ -1085,7 +1093,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 = (
@@ -1106,7 +1114,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 = (
@@ -1144,7 +1152,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 = (
@@ -1163,7 +1171,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 = (
@@ -1184,7 +1192,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"
@@ -1198,7 +1206,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()
@@ -1208,7 +1216,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"
@@ -1229,7 +1237,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."
@@ -1238,7 +1246,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"
@@ -1247,7 +1255,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"
@@ -1444,7 +1452,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)
@@ -1493,16 +1503,11 @@ def test_valid_metadata_links(data, contact, donation, issues, source_code, webs
@pytest.mark.parametrize(
- "contact",
- (1, {"mailto:project@acme.com", "team@acme.com"}, None),
+ "contact", (1, {"mailto:project@acme.com", "team@acme.com"}, None),
)
@pytest.mark.parametrize(
"donation",
- (
- 1,
- {"https://paypal.com", "https://cafecito.app", "https://ko-fi.com"},
- None,
- ),
+ (1, {"https://paypal.com", "https://cafecito.app", "https://ko-fi.com"}, None,),
)
@pytest.mark.parametrize(
"issues",
@@ -1526,5 +1531,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/legacy/unit/project_loader/__init__.py
similarity index 85%
rename from tests/unit/project_loader/__init__.py
rename to tests/legacy/unit/project_loader/__init__.py
index b609c0f7db..7ddf3769b9 100644
--- a/tests/unit/project_loader/__init__.py
+++ b/tests/legacy/unit/project_loader/__init__.py
@@ -16,9 +16,9 @@
from unittest import mock
-from snapcraft.internal import project_loader
-from snapcraft.project import Project as _Project
-from tests import unit
+from snapcraft_legacy.internal import project_loader
+from snapcraft_legacy.project import Project as _Project
+from tests.legacy import unit
class ProjectLoaderBaseTest(unit.TestCase):
@@ -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/grammar_processing/__init__.py b/tests/legacy/unit/project_loader/extensions/__init__.py
similarity index 100%
rename from tests/unit/project_loader/grammar_processing/__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 96%
rename from tests/unit/project_loader/extensions/test_extensions.py
rename to tests/legacy/unit/project_loader/extensions/test_extensions.py
index 3f2e353a84..10de3b6055 100644
--- a/tests/unit/project_loader/extensions/test_extensions.py
+++ b/tests/legacy/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/legacy/unit/project_loader/extensions/test_flutter.py
similarity index 95%
rename from tests/unit/project_loader/extensions/test_flutter.py
rename to tests/legacy/unit/project_loader/extensions/test_flutter.py
index 8c07425be2..bbb7b7ecb1 100644
--- a/tests/unit/project_loader/extensions/test_flutter.py
+++ b/tests/legacy/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/legacy/unit/project_loader/extensions/test_gnome_3_28.py
similarity index 97%
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
index 717d1fe63f..b54f5aa2d2 100644
--- a/tests/unit/project_loader/extensions/test_gnome_3_28.py
+++ b/tests/legacy/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/legacy/unit/project_loader/extensions/test_gnome_3_34.py
similarity index 97%
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 f7c9d94292..b031f6393b 100644
--- a/tests/unit/project_loader/extensions/test_gnome_3_34.py
+++ b/tests/legacy/unit/project_loader/extensions/test_gnome_3_34.py
@@ -16,8 +16,10 @@
from testtools.matchers import Equals
-from snapcraft.internal.project_loader._extensions.gnome_3_34 import ExtensionImpl
-from tests.unit.commands import CommandBaseTestCase
+from snapcraft_legacy.internal.project_loader._extensions.gnome_3_34 import (
+ ExtensionImpl,
+)
+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 97%
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 2c287a040c..953ba897e3 100644
--- a/tests/unit/project_loader/extensions/test_gnome_3_38.py
+++ b/tests/legacy/unit/project_loader/extensions/test_gnome_3_38.py
@@ -16,8 +16,10 @@
from testtools.matchers import Equals
-from snapcraft.internal.project_loader._extensions.gnome_3_38 import ExtensionImpl
-from tests.unit.commands import CommandBaseTestCase
+from snapcraft_legacy.internal.project_loader._extensions.gnome_3_38 import (
+ ExtensionImpl,
+)
+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 98%
rename from tests/unit/project_loader/extensions/test_kde_neon.py
rename to tests/legacy/unit/project_loader/extensions/test_kde_neon.py
index 5460187244..3501c3b274 100644
--- a/tests/unit/project_loader/extensions/test_kde_neon.py
+++ b/tests/legacy/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/legacy/unit/project_loader/extensions/test_ros1_noetic.py
similarity index 96%
rename from tests/unit/project_loader/extensions/test_ros1_noetic.py
rename to tests/legacy/unit/project_loader/extensions/test_ros1_noetic.py
index ab5cd90649..1e1b3bc96d 100644
--- a/tests/unit/project_loader/extensions/test_ros1_noetic.py
+++ b/tests/legacy/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/legacy/unit/project_loader/extensions/test_ros2_foxy.py
similarity index 96%
rename from tests/unit/project_loader/extensions/test_ros2_foxy.py
rename to tests/legacy/unit/project_loader/extensions/test_ros2_foxy.py
index d4d30c6904..91a7dc40d6 100644
--- a/tests/unit/project_loader/extensions/test_ros2_foxy.py
+++ b/tests/legacy/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/legacy/unit/project_loader/extensions/test_utils.py
similarity index 98%
rename from tests/unit/project_loader/extensions/test_utils.py
rename to tests/legacy/unit/project_loader/extensions/test_utils.py
index c77a6f79ed..cfd2d51bc4 100644
--- a/tests/unit/project_loader/extensions/test_utils.py
+++ b/tests/legacy/unit/project_loader/extensions/test_utils.py
@@ -19,10 +19,10 @@
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
-from tests import fixture_setup
+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.legacy 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/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 91%
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
index 603ed53257..097b3ae969 100644
--- 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
@@ -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/legacy/unit/project_loader/grammar_processing/test_part_grammar_processor.py
similarity index 85%
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
index e85d90b39e..61e1a15914 100644
--- 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
@@ -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,
)
@@ -196,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()
@@ -229,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}"]
@@ -308,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)
@@ -318,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",
@@ -346,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,
)
@@ -361,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",
@@ -385,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,
)
@@ -458,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:
@@ -488,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,
)
@@ -503,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:
@@ -523,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,
)
@@ -597,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:
@@ -627,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,
)
@@ -642,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:
@@ -662,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,
)
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 91%
rename from tests/unit/project_loader/inspection/test_latest_step.py
rename to tests/legacy/unit/project_loader/inspection/test_latest_step.py
index 1930f1eaef..a174987bcf 100644
--- a/tests/unit/project_loader/inspection/test_latest_step.py
+++ b/tests/legacy/unit/project_loader/inspection/test_latest_step.py
@@ -18,10 +18,10 @@
from testtools.matchers import Equals
-from snapcraft import project
-from snapcraft.internal import steps
-from snapcraft.internal.project_loader import inspection
-from tests import unit
+from snapcraft_legacy import project
+from snapcraft_legacy.internal import steps
+from snapcraft_legacy.internal.project_loader import inspection
+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 98%
rename from tests/unit/project_loader/inspection/test_lifecycle_status.py
rename to tests/legacy/unit/project_loader/inspection/test_lifecycle_status.py
index 1bb1387fb0..4c268d2e53 100644
--- a/tests/unit/project_loader/inspection/test_lifecycle_status.py
+++ b/tests/legacy/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/legacy/unit/project_loader/inspection/test_provides.py
similarity index 96%
rename from tests/unit/project_loader/inspection/test_provides.py
rename to tests/legacy/unit/project_loader/inspection/test_provides.py
index d7199f3e07..546497ea45 100644
--- a/tests/unit/project_loader/inspection/test_provides.py
+++ b/tests/legacy/unit/project_loader/inspection/test_provides.py
@@ -18,9 +18,9 @@
from testtools.matchers import Equals
-from snapcraft import project
-from snapcraft.internal.project_loader import inspection
-from tests import unit
+from snapcraft_legacy import project
+from snapcraft_legacy.internal.project_loader import inspection
+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 97%
rename from tests/unit/project_loader/test_build_packages.py
rename to tests/legacy/unit/project_loader/test_build_packages.py
index fa0b128917..d1f918168c 100644
--- a/tests/unit/project_loader/test_build_packages.py
+++ b/tests/legacy/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_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 98%
rename from tests/unit/project_loader/test_config.py
rename to tests/legacy/unit/project_loader/test_config.py
index 655b9f62cb..5b32509c3f 100644
--- a/tests/unit/project_loader/test_config.py
+++ b/tests/legacy/unit/project_loader/test_config.py
@@ -18,9 +18,9 @@
from testtools.matchers import Contains, Equals
-import snapcraft.internal.project_loader._config as _config
-from snapcraft.internal.project_loader import errors
-from tests import unit
+import snapcraft_legacy.internal.project_loader._config as _config
+from snapcraft_legacy.internal.project_loader import errors
+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 97%
rename from tests/unit/project_loader/test_environment.py
rename to tests/legacy/unit/project_loader/test_environment.py
index 78436ff6eb..13e330614a 100644
--- a/tests/unit/project_loader/test_environment.py
+++ b/tests/legacy/unit/project_loader/test_environment.py
@@ -24,9 +24,9 @@
import fixtures
from testtools.matchers import Contains, Equals, GreaterThan, Not
-import snapcraft
-from snapcraft.internal import common
-from tests.fixture_setup.os_release import FakeOsRelease
+import snapcraft_legacy
+from snapcraft_legacy.internal import common
+from tests.legacy.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
@@ -305,7 +307,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"),
@@ -445,7 +447,7 @@ def test_content_dirs_default(self):
self.assertThat(env, Contains('SNAPCRAFT_CONTENT_DIRS=""'))
@mock.patch(
- "snapcraft.project._project.Project._get_provider_content_dirs",
+ "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):
diff --git a/tests/unit/project_loader/test_errors.py b/tests/legacy/unit/project_loader/test_errors.py
similarity index 94%
rename from tests/unit/project_loader/test_errors.py
rename to tests/legacy/unit/project_loader/test_errors.py
index b3ee3a5daa..4c5bcbb5b4 100644
--- a/tests/unit/project_loader/test_errors.py
+++ b/tests/legacy/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/legacy/unit/project_loader/test_parts.py
similarity index 97%
rename from tests/unit/project_loader/test_parts.py
rename to tests/legacy/unit/project_loader/test_parts.py
index 4c4bdac3a1..e2e9aaf06f 100644
--- a/tests/unit/project_loader/test_parts.py
+++ b/tests/legacy/unit/project_loader/test_parts.py
@@ -19,9 +19,9 @@
from testtools.matchers import Equals
-from snapcraft.internal import project_loader
-from snapcraft.project import Project
-from tests import fixture_setup
+from snapcraft_legacy.internal import project_loader
+from snapcraft_legacy.project import Project
+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 98%
rename from tests/unit/project_loader/test_replace_attr.py
rename to tests/legacy/unit/project_loader/test_replace_attr.py
index a6c81e8796..e3af832387 100644
--- a/tests/unit/project_loader/test_replace_attr.py
+++ b/tests/legacy/unit/project_loader/test_replace_attr.py
@@ -16,8 +16,8 @@
from testtools.matchers import Equals
-from snapcraft.internal import project_loader
-from tests import unit
+from snapcraft_legacy.internal import project_loader
+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 96%
rename from tests/unit/project_loader/test_schema.py
rename to tests/legacy/unit/project_loader/test_schema.py
index ff283bd1e5..f16a952c63 100644
--- a/tests/unit/project_loader/test_schema.py
+++ b/tests/legacy/unit/project_loader/test_schema.py
@@ -23,11 +23,11 @@
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
-from tests import fixture_setup
+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.legacy import fixture_setup
from . import ProjectLoaderBaseTest
@@ -280,13 +280,13 @@ 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,
)
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/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 97%
rename from tests/unit/remote_build/test_errors.py
rename to tests/legacy/unit/remote_build/test_errors.py
index c4e47f7884..05865a6339 100644
--- a/tests/unit/remote_build/test_errors.py
+++ b/tests/legacy/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/legacy/unit/remote_build/test_info_file.py
similarity index 94%
rename from tests/unit/remote_build/test_info_file.py
rename to tests/legacy/unit/remote_build/test_info_file.py
index 924b474d47..d442955e8d 100644
--- a/tests/unit/remote_build/test_info_file.py
+++ b/tests/legacy/unit/remote_build/test_info_file.py
@@ -19,8 +19,8 @@
import fixtures
from testtools.matchers import Equals, FileExists
-from snapcraft.internal.remote_build import InfoFile
-from tests import unit
+from snapcraft_legacy.internal.remote_build import InfoFile
+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 94%
rename from tests/unit/remote_build/test_launchpad.py
rename to tests/legacy/unit/remote_build/test_launchpad.py
index 803d52b4ec..626c85dcba 100644
--- a/tests/unit/remote_build/test_launchpad.py
+++ b/tests/legacy/unit/remote_build/test_launchpad.py
@@ -20,11 +20,11 @@
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
-from tests import unit
+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.legacy 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,
@@ -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)
@@ -327,7 +327,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()
@@ -361,9 +361,10 @@ 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=[]
+ "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):
@@ -389,7 +390,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
@@ -428,7 +429,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/legacy/unit/remote_build/test_worktree.py
similarity index 97%
rename from tests/unit/remote_build/test_worktree.py
rename to tests/legacy/unit/remote_build/test_worktree.py
index 1614c9cd3b..e5de547195 100644
--- a/tests/unit/remote_build/test_worktree.py
+++ b/tests/legacy/unit/remote_build/test_worktree.py
@@ -21,10 +21,10 @@
from testtools.matchers import Equals
-from snapcraft import yaml_utils
-from snapcraft.internal.remote_build import WorkTree
-from snapcraft.project import Project
-from tests import fixture_setup, unit
+from snapcraft_legacy import yaml_utils
+from snapcraft_legacy.internal.remote_build import WorkTree
+from snapcraft_legacy.project import Project
+from tests.legacy import fixture_setup, unit
from . import TestDir
diff --git a/tests/legacy/unit/repo/__init__.py b/tests/legacy/unit/repo/__init__.py
new file mode 100644
index 0000000000..2777051ccb
--- /dev/null
+++ b/tests/legacy/unit/repo/__init__.py
@@ -0,0 +1,32 @@
+# -*- 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 logging
+import tempfile
+
+import fixtures
+
+from tests.legacy import unit
+
+
+class RepoBaseTestCase(unit.TestCase):
+ def setUp(self):
+ super().setUp()
+ fake_logger = fixtures.FakeLogger(level=logging.ERROR)
+ self.useFixture(fake_logger)
+ tempdirObj = tempfile.TemporaryDirectory()
+ self.addCleanup(tempdirObj.cleanup)
+ self.tempdir = tempdirObj.name
diff --git a/tests/unit/repo/test_apt_cache.py b/tests/legacy/unit/repo/test_apt_cache.py
similarity index 94%
rename from tests/unit/repo/test_apt_cache.py
rename to tests/legacy/unit/repo/test_apt_cache.py
index 3025f82e97..676154fa02 100644
--- a/tests/unit/repo/test_apt_cache.py
+++ b/tests/legacy/unit/repo/test_apt_cache.py
@@ -22,8 +22,8 @@
import fixtures
from testtools.matchers import Equals
-from snapcraft.internal.repo.apt_cache import AptCache
-from tests import unit
+from snapcraft_legacy.internal.repo.apt_cache import AptCache
+from tests.legacy import unit
class TestAptStageCache(unit.TestCase):
@@ -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/legacy/unit/repo/test_apt_key_manager.py b/tests/legacy/unit/repo/test_apt_key_manager.py
new file mode 100644
index 0000000000..dd25c2d8ea
--- /dev/null
+++ b/tests/legacy/unit/repo/test_apt_key_manager.py
@@ -0,0 +1,316 @@
+# -*- 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 subprocess
+from unittest import mock
+from unittest.mock import call
+
+import gnupg
+import pytest
+
+from snapcraft_legacy.internal.meta.package_repository import (
+ PackageRepositoryApt,
+ PackageRepositoryAptPpa,
+)
+from snapcraft_legacy.internal.repo import apt_ppa, errors
+from snapcraft_legacy.internal.repo.apt_key_manager import AptKeyManager
+
+
+@pytest.fixture(autouse=True)
+def mock_environ_copy():
+ with mock.patch("os.environ.copy") as m:
+ yield m
+
+
+@pytest.fixture(autouse=True)
+def mock_gnupg(tmp_path, autouse=True):
+ with mock.patch("gnupg.GPG", spec=gnupg.GPG) as m:
+ m.return_value.import_keys.return_value.fingerprints = [
+ "FAKE-KEY-ID-FROM-GNUPG"
+ ]
+ yield m
+
+
+@pytest.fixture(autouse=True)
+def mock_run():
+ with mock.patch("subprocess.run", spec=subprocess.run) as m:
+ yield m
+
+
+@pytest.fixture(autouse=True)
+def mock_apt_ppa_get_signing_key():
+ with mock.patch(
+ "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:
+ yield m
+
+
+@pytest.fixture
+def key_assets(tmp_path):
+ key_assets = tmp_path / "key-assets"
+ key_assets.mkdir(parents=True)
+ yield key_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(
+ ["sudo", "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(
+ ["sudo", "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 exc_info:
+ apt_gpg.install_key(key="FAKEKEY")
+
+ assert exc_info.value._output == "some error"
+ assert exc_info.value._key == "FAKEKEY"
+
+
+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(
+ [
+ "sudo",
+ "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 exc_info:
+ apt_gpg.install_key_from_keyserver(
+ key_id="fake-key-id", key_server="fake-server"
+ )
+
+ assert exc_info.value._output == "some error"
+ assert exc_info.value._key_id == "fake-key-id"
+
+
+@mock.patch(
+ "snapcraft_legacy.internal.repo.apt_key_manager.AptKeyManager.is_key_installed"
+)
+@pytest.mark.parametrize(
+ "is_installed", [True, False],
+)
+def test_install_package_repository_key_already_installed(
+ mock_is_key_installed, is_installed, apt_gpg,
+):
+ mock_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
+
+
+@mock.patch(
+ "snapcraft_legacy.internal.repo.apt_key_manager.AptKeyManager.is_key_installed",
+ return_value=False,
+)
+@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,
+):
+ 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")]
+
+
+@mock.patch(
+ "snapcraft_legacy.internal.repo.apt_key_manager.AptKeyManager.is_key_installed",
+ return_value=False,
+)
+@mock.patch(
+ "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,
+):
+ 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")
+ ]
+
+
+@mock.patch(
+ "snapcraft_legacy.internal.repo.apt_key_manager.AptKeyManager.is_key_installed",
+ return_value=False,
+)
+@mock.patch(
+ "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,
+):
+ 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/legacy/unit/repo/test_apt_ppa.py b/tests/legacy/unit/repo/test_apt_ppa.py
new file mode 100644
index 0000000000..033ef83ed7
--- /dev/null
+++ b/tests/legacy/unit/repo/test_apt_ppa.py
@@ -0,0 +1,60 @@
+# -*- 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 .
+
+
+from unittest import mock
+from unittest.mock import call
+
+import launchpadlib
+import pytest
+
+from snapcraft_legacy.internal.repo import apt_ppa, errors
+
+
+@pytest.fixture
+def mock_launchpad(autouse=True):
+ with mock.patch(
+ "snapcraft_legacy.internal.repo.apt_ppa.Launchpad",
+ spec=launchpadlib.launchpad.Launchpad,
+ ) as m:
+ 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 exc_info:
+ apt_ppa.split_ppa_parts(ppa="ppa-missing-slash")
+
+ assert exc_info.value._ppa == "ppa-missing-slash"
+
+
+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/legacy/unit/repo/test_apt_sources_manager.py b/tests/legacy/unit/repo/test_apt_sources_manager.py
new file mode 100644
index 0000000000..a955553fe1
--- /dev/null
+++ b/tests/legacy/unit/repo/test_apt_sources_manager.py
@@ -0,0 +1,260 @@
+# -*- 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 pathlib
+import subprocess
+from textwrap import dedent
+from unittest import mock
+from unittest.mock import call
+
+import pytest
+
+from snapcraft_legacy.internal.meta.package_repository import (
+ PackageRepositoryApt,
+ PackageRepositoryAptPpa,
+)
+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_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:
+ yield m
+
+
+@pytest.fixture(autouse=True)
+def mock_environ_copy():
+ with mock.patch("os.environ.copy") as m:
+ yield m
+
+
+@pytest.fixture(autouse=True)
+def mock_host_arch():
+ with mock.patch(
+ "snapcraft_legacy.internal.repo.apt_sources_manager.ProjectOptions"
+ ) as m:
+ m.return_value.deb_arch = "FAKE-HOST-ARCH"
+ yield m
+
+
+@pytest.fixture(autouse=True)
+def mock_run():
+ with mock.patch("subprocess.run") as m:
+ yield m
+
+
+@pytest.fixture()
+def mock_sudo_write():
+ def write_file(*, dst_path: pathlib.Path, content: bytes) -> None:
+ dst_path.write_bytes(content)
+
+ with mock.patch(
+ "snapcraft_legacy.internal.repo.apt_sources_manager._sudo_write_file"
+ ) as m:
+ m.side_effect = write_file
+ yield m
+
+
+@pytest.fixture(autouse=True)
+def mock_version_codename():
+ with mock.patch(
+ "snapcraft_legacy.internal.os_release.OsRelease.version_codename",
+ return_value="FAKE-CODENAME",
+ ) as m:
+ yield m
+
+
+@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,)
+
+
+@mock.patch("tempfile.NamedTemporaryFile")
+@mock.patch("os.unlink")
+def test_sudo_write_file(mock_unlink, mock_tempfile, mock_run, tmp_path):
+ mock_tempfile.return_value.__enter__.return_value.name = "/tmp/foobar"
+
+ apt_sources_manager._sudo_write_file(dst_path="/foo/bar", content=b"some-content")
+
+ assert mock_tempfile.mock_calls == [
+ call(delete=False),
+ call().__enter__(),
+ call().__enter__().write(b"some-content"),
+ call().__enter__().flush(),
+ call().__exit__(None, None, None),
+ ]
+ assert mock_run.mock_calls == [
+ call(
+ [
+ "sudo",
+ "install",
+ "--owner=root",
+ "--group=root",
+ "--mode=0644",
+ "/tmp/foobar",
+ "/foo/bar",
+ ],
+ check=True,
+ )
+ ]
+ assert mock_unlink.mock_calls == [call("/tmp/foobar")]
+
+
+def test_sudo_write_file_fails(mock_run):
+ mock_run.side_effect = subprocess.CalledProcessError(
+ cmd=["sudo"], returncode=1, output=b"some error"
+ )
+
+ with pytest.raises(RuntimeError) as error:
+ apt_sources_manager._sudo_write_file(
+ dst_path="/foo/bar", content=b"some-content"
+ )
+
+ assert (
+ str(error.value).startswith(
+ "Failed to install repository config with: ['sudo', 'install'"
+ )
+ is True
+ )
+
+
+@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, mock_sudo_write):
+ 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
+ assert mock_sudo_write.mock_calls == [call(content=content, dst_path=sources_path,)]
+
+ # Verify a second-run does not incur any changes.
+ mock_sudo_write.reset_mock()
+
+ changed = apt_sources_mgr.install_package_repository_sources(
+ package_repo=package_repo
+ )
+
+ assert changed is False
+ assert sources_path.read_bytes() == content
+ assert mock_sudo_write.mock_calls == []
+
+
+def test_install_ppa_invalid(apt_sources_mgr):
+ repo = PackageRepositoryAptPpa(ppa="ppa-missing-slash")
+
+ with pytest.raises(errors.AptPPAInstallError) as exc_info:
+ apt_sources_mgr.install_package_repository_sources(package_repo=repo)
+
+ assert exc_info.value._ppa == "ppa-missing-slash"
diff --git a/tests/unit/repo/test_base.py b/tests/legacy/unit/repo/test_base.py
similarity index 98%
rename from tests/unit/repo/test_base.py
rename to tests/legacy/unit/repo/test_base.py
index a922af9104..3aaa820990 100644
--- a/tests/unit/repo/test_base.py
+++ b/tests/legacy/unit/repo/test_base.py
@@ -20,8 +20,8 @@
from testtools.matchers import Equals, FileContains, FileExists, Not
-from snapcraft.internal.repo._base import BaseRepo, get_pkg_name_parts
-from tests import unit
+from snapcraft_legacy.internal.repo._base import BaseRepo, get_pkg_name_parts
+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 96%
rename from tests/unit/repo/test_deb.py
rename to tests/legacy/unit/repo/test_deb.py
index c3b98b138c..cb64f6b0f3 100644
--- a/tests/unit/repo/test_deb.py
+++ b/tests/legacy/unit/repo/test_deb.py
@@ -26,10 +26,10 @@
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 tests import unit
+from snapcraft_legacy.internal import repo
+from snapcraft_legacy.internal.repo import errors
+from snapcraft_legacy.internal.repo.deb_package import DebPackage
+from tests.legacy import unit
@pytest.fixture(autouse=True)
@@ -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"
)
)
@@ -516,15 +516,13 @@ def fake_dpkg_query(*args, **kwargs):
).encode()
elif "symlink" in args[0][2].as_posix():
raise CalledProcessError(
- 1,
- f"dpkg-query: no path found matching pattern {args[0][2]}",
+ 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,
- f"dpkg-query: no path found matching pattern {args[0][2]}",
+ 1, f"dpkg-query: no path found matching pattern {args[0][2]}",
)
self.useFixture(
diff --git a/tests/unit/repo/test_deb_package.py b/tests/legacy/unit/repo/test_deb_package.py
similarity index 95%
rename from tests/unit/repo/test_deb_package.py
rename to tests/legacy/unit/repo/test_deb_package.py
index 097f45d838..3b9b9058ac 100644
--- a/tests/unit/repo/test_deb_package.py
+++ b/tests/legacy/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/legacy/unit/repo/test_errors.py
similarity index 99%
rename from tests/unit/repo/test_errors.py
rename to tests/legacy/unit/repo/test_errors.py
index b41bbfe243..dc336d66eb 100644
--- a/tests/unit/repo/test_errors.py
+++ b/tests/legacy/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/legacy/unit/repo/test_snaps.py
similarity index 98%
rename from tests/unit/repo/test_snaps.py
rename to tests/legacy/unit/repo/test_snaps.py
index cfdac057f0..46b2ab1c80 100644
--- a/tests/unit/repo/test_snaps.py
+++ b/tests/legacy/unit/repo/test_snaps.py
@@ -19,8 +19,8 @@
import fixtures
from testtools.matchers import Equals, FileContains, FileExists, Is
-from snapcraft.internal.repo import errors, snaps
-from tests import unit
+from snapcraft_legacy.internal.repo import errors, snaps
+from tests.legacy import unit
class SnapPackageCurrentChannelTest(unit.TestCase):
@@ -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/legacy/unit/repo/test_ua_manager.py
similarity index 97%
rename from tests/unit/repo/test_ua_manager.py
rename to tests/legacy/unit/repo/test_ua_manager.py
index df7713323e..5bb314fa00 100644
--- a/tests/unit/repo/test_ua_manager.py
+++ b/tests/legacy/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/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 99%
rename from tests/unit/review_tools/test_errors.py
rename to tests/legacy/unit/review_tools/test_errors.py
index d785b85c16..c59ffb5305 100644
--- a/tests/unit/review_tools/test_errors.py
+++ b/tests/legacy/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/legacy/unit/review_tools/test_runner.py
similarity index 96%
rename from tests/unit/review_tools/test_runner.py
rename to tests/legacy/unit/review_tools/test_runner.py
index aa22d0972d..67ccc14b51 100644
--- a/tests/unit/review_tools/test_runner.py
+++ b/tests/legacy/unit/review_tools/test_runner.py
@@ -20,8 +20,8 @@
import fixtures
-from snapcraft.internal import review_tools
-from tests import unit
+from snapcraft_legacy.internal import review_tools
+from tests.legacy import unit
class RunTest(unit.TestCase):
@@ -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/__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 97%
rename from tests/unit/sources/test_7z.py
rename to tests/legacy/unit/sources/test_7z.py
index 04cf445e8f..067e793390 100644
--- a/tests/unit/sources/test_7z.py
+++ b/tests/legacy/unit/sources/test_7z.py
@@ -22,8 +22,8 @@
import fixtures
from testtools.matchers import Equals, MatchesRegex
-from snapcraft.internal import sources
-from tests import unit
+from snapcraft_legacy.internal import sources
+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 84%
rename from tests/unit/sources/test_base.py
rename to tests/legacy/unit/sources/test_base.py
index b80baaae3e..bb04171d77 100644
--- a/tests/unit/sources/test_base.py
+++ b/tests/legacy/unit/sources/test_base.py
@@ -20,8 +20,8 @@
import requests
from testtools.matchers import Contains, Equals
-from snapcraft.internal.sources import _base, errors
-from tests import unit
+from snapcraft_legacy.internal.sources import _base, errors
+from tests.legacy import unit
class TestFileBase(unit.TestCase):
@@ -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/legacy/unit/sources/test_bazaar.py
similarity index 97%
rename from tests/unit/sources/test_bazaar.py
rename to tests/legacy/unit/sources/test_bazaar.py
index 0f12c746ea..afc4c43c58 100644
--- a/tests/unit/sources/test_bazaar.py
+++ b/tests/legacy/unit/sources/test_bazaar.py
@@ -21,8 +21,8 @@
import fixtures
from testtools.matchers import Equals
-from snapcraft.internal import sources
-from tests import unit
+from snapcraft_legacy.internal import sources
+from tests.legacy import unit
# LP: #1733584
@@ -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/legacy/unit/sources/test_checksum.py
similarity index 94%
rename from tests/unit/sources/test_checksum.py
rename to tests/legacy/unit/sources/test_checksum.py
index 421e5d3516..ccf47d12e0 100644
--- a/tests/unit/sources/test_checksum.py
+++ b/tests/legacy/unit/sources/test_checksum.py
@@ -20,9 +20,9 @@
from testtools.matchers import Equals
-from snapcraft.internal.sources import errors
-from snapcraft.internal.sources._checksum import verify_checksum
-from tests import unit
+from snapcraft_legacy.internal.sources import errors
+from snapcraft_legacy.internal.sources._checksum import verify_checksum
+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 97%
rename from tests/unit/sources/test_deb.py
rename to tests/legacy/unit/sources/test_deb.py
index 5a719223d2..a55faf3db2 100644
--- a/tests/unit/sources/test_deb.py
+++ b/tests/legacy/unit/sources/test_deb.py
@@ -20,8 +20,8 @@
from testtools.matchers import Equals
-from snapcraft.internal import sources
-from tests import unit
+from snapcraft_legacy.internal import sources
+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 98%
rename from tests/unit/sources/test_errors.py
rename to tests/legacy/unit/sources/test_errors.py
index c087f2c720..691de2a5f4 100644
--- a/tests/unit/sources/test_errors.py
+++ b/tests/legacy/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/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 fd238b8494..cb630b87b5 100644
--- a/tests/unit/sources/test_git.py
+++ b/tests/legacy/unit/sources/test_git.py
@@ -22,9 +22,9 @@
import fixtures
from testtools.matchers import Equals
-from snapcraft.internal import sources
-from snapcraft.internal.sources import errors
-from tests import unit
+from snapcraft_legacy.internal import sources
+from snapcraft_legacy.internal.sources import errors
+from tests.legacy 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/legacy/unit/sources/test_local.py
similarity index 98%
rename from tests/unit/sources/test_local.py
rename to tests/legacy/unit/sources/test_local.py
index 3a23ad7e1d..43c444bf90 100644
--- a/tests/unit/sources/test_local.py
+++ b/tests/legacy/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 tests import unit
+from snapcraft_legacy.internal import common, errors, sources
+from tests.legacy 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/legacy/unit/sources/test_mercurial.py
similarity index 98%
rename from tests/unit/sources/test_mercurial.py
rename to tests/legacy/unit/sources/test_mercurial.py
index 91865937d9..e4dc4f2718 100644
--- a/tests/unit/sources/test_mercurial.py
+++ b/tests/legacy/unit/sources/test_mercurial.py
@@ -21,15 +21,15 @@
import fixtures
from testtools.matchers import Equals
-from snapcraft.internal import sources
-from tests import unit
+from snapcraft_legacy.internal import sources
+from tests.legacy import unit
# LP: #1733584
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/legacy/unit/sources/test_rpm.py
similarity index 97%
rename from tests/unit/sources/test_rpm.py
rename to tests/legacy/unit/sources/test_rpm.py
index d86a121682..ac47427a2a 100644
--- a/tests/unit/sources/test_rpm.py
+++ b/tests/legacy/unit/sources/test_rpm.py
@@ -22,8 +22,8 @@
from testtools.matchers import Equals, MatchesRegex
-from snapcraft.internal import sources
-from tests import unit
+from snapcraft_legacy.internal import sources
+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 88%
rename from tests/unit/sources/test_script.py
rename to tests/legacy/unit/sources/test_script.py
index 3486eed610..7cb01c405c 100644
--- a/tests/unit/sources/test_script.py
+++ b/tests/legacy/unit/sources/test_script.py
@@ -19,8 +19,8 @@
from testtools.matchers import FileExists
-from snapcraft.internal.sources import Script
-from tests import unit
+from snapcraft_legacy.internal.sources import Script
+from tests.legacy import unit
class TestScript(unit.TestCase):
@@ -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/legacy/unit/sources/test_snap.py
similarity index 97%
rename from tests/unit/sources/test_snap.py
rename to tests/legacy/unit/sources/test_snap.py
index 83f151470d..b7e7177201 100644
--- a/tests/unit/sources/test_snap.py
+++ b/tests/legacy/unit/sources/test_snap.py
@@ -21,8 +21,8 @@
from testtools.matchers import DirExists, Equals, FileExists, MatchesRegex
-from snapcraft.internal import sources
-from tests import unit
+from snapcraft_legacy.internal import sources
+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 99%
rename from tests/unit/sources/test_sources.py
rename to tests/legacy/unit/sources/test_sources.py
index 130bf31b8b..a895b7e8b3 100644
--- a/tests/unit/sources/test_sources.py
+++ b/tests/legacy/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/legacy/unit/sources/test_subversion.py
similarity index 97%
rename from tests/unit/sources/test_subversion.py
rename to tests/legacy/unit/sources/test_subversion.py
index f183d1a12a..c62f63ade2 100644
--- a/tests/unit/sources/test_subversion.py
+++ b/tests/legacy/unit/sources/test_subversion.py
@@ -21,8 +21,8 @@
import fixtures
from testtools.matchers import Equals
-from snapcraft.internal import sources
-from tests import unit
+from snapcraft_legacy.internal import sources
+from tests.legacy import unit
# LP: #1733584
@@ -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/legacy/unit/sources/test_tar.py
similarity index 96%
rename from tests/unit/sources/test_tar.py
rename to tests/legacy/unit/sources/test_tar.py
index 31b8b93a84..67c07bdead 100644
--- a/tests/unit/sources/test_tar.py
+++ b/tests/legacy/unit/sources/test_tar.py
@@ -21,12 +21,12 @@
import requests
from testtools.matchers import Equals
-from snapcraft.internal import sources
-from tests import unit
+from snapcraft_legacy.internal import sources
+from tests.legacy 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/legacy/unit/sources/test_zip.py
similarity index 96%
rename from tests/unit/sources/test_zip.py
rename to tests/legacy/unit/sources/test_zip.py
index dd0ecf1b84..1237c83eab 100644
--- a/tests/unit/sources/test_zip.py
+++ b/tests/legacy/unit/sources/test_zip.py
@@ -19,8 +19,8 @@
from testtools.matchers import Equals
-from snapcraft.internal import sources
-from tests import unit
+from snapcraft_legacy.internal import sources
+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 98%
rename from tests/unit/states/conftest.py
rename to tests/legacy/unit/states/conftest.py
index fbce25210d..09bbd3db8c 100644
--- a/tests/unit/states/conftest.py
+++ b/tests/legacy/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/legacy/unit/states/test_build.py
similarity index 90%
rename from tests/unit/states/test_build.py
rename to tests/legacy/unit/states/test_build.py
index b068c7d209..837185447c 100644
--- a/tests/unit/states/test_build.py
+++ b/tests/legacy/unit/states/test_build.py
@@ -18,9 +18,9 @@
from testtools.matchers import Equals
-import snapcraft.internal
-from snapcraft import yaml_utils
-from tests import unit
+import snapcraft_legacy.internal
+from snapcraft_legacy import yaml_utils
+from tests.legacy 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/legacy/unit/states/test_global_state.py
similarity index 99%
rename from tests/unit/states/test_global_state.py
rename to tests/legacy/unit/states/test_global_state.py
index 428909d369..d3541d490c 100644
--- a/tests/unit/states/test_global_state.py
+++ b/tests/legacy/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/legacy/unit/states/test_prime.py
similarity index 88%
rename from tests/unit/states/test_prime.py
rename to tests/legacy/unit/states/test_prime.py
index c524c242e5..d53deee0cb 100644
--- a/tests/unit/states/test_prime.py
+++ b/tests/legacy/unit/states/test_prime.py
@@ -18,9 +18,9 @@
from testtools.matchers import Equals
-import snapcraft.internal
-from snapcraft import yaml_utils
-from tests import unit
+import snapcraft_legacy.internal
+from snapcraft_legacy import yaml_utils
+from tests.legacy 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/legacy/unit/states/test_pull.py
similarity index 90%
rename from tests/unit/states/test_pull.py
rename to tests/legacy/unit/states/test_pull.py
index 42a0f34a64..568d92ade2 100644
--- a/tests/unit/states/test_pull.py
+++ b/tests/legacy/unit/states/test_pull.py
@@ -18,9 +18,9 @@
from testtools.matchers import Equals
-import snapcraft.internal
-from snapcraft import yaml_utils
-from tests import unit
+import snapcraft_legacy.internal
+from snapcraft_legacy import yaml_utils
+from tests.legacy 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
)
@@ -97,8 +97,7 @@ def test_properties_of_interest(self):
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"),
+ properties["source-submodules"], Equals("test-source-submodules"),
)
def test_project_options_of_interest(self):
diff --git a/tests/unit/states/test_stage.py b/tests/legacy/unit/states/test_stage.py
similarity index 88%
rename from tests/unit/states/test_stage.py
rename to tests/legacy/unit/states/test_stage.py
index 483971419a..138350e3a7 100644
--- a/tests/unit/states/test_stage.py
+++ b/tests/legacy/unit/states/test_stage.py
@@ -18,9 +18,9 @@
from testtools.matchers import Equals
-import snapcraft.internal
-from snapcraft import yaml_utils
-from tests import unit
+import snapcraft_legacy.internal
+from snapcraft_legacy import yaml_utils
+from tests.legacy 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/legacy/unit/states/test_state.py
similarity index 97%
rename from tests/unit/states/test_state.py
rename to tests/legacy/unit/states/test_state.py
index 03c73448d7..833dc549cd 100644
--- a/tests/unit/states/test_state.py
+++ b/tests/legacy/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/__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/http_client/test_agent.py b/tests/legacy/unit/store/test_agent.py
similarity index 91%
rename from tests/unit/store/http_client/test_agent.py
rename to tests/legacy/unit/store/test_agent.py
index 0cab921263..0150c68bc9 100644
--- a/tests/unit/store/http_client/test_agent.py
+++ b/tests/legacy/unit/store/test_agent.py
@@ -19,11 +19,11 @@
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 tests import unit
-from tests.fixture_setup.os_release import FakeOsRelease
+from snapcraft_legacy import ProjectOptions
+from snapcraft_legacy import __version__ as snapcraft_version
+from snapcraft_legacy.storeapi import agent
+from tests.legacy import unit
+from tests.legacy.fixture_setup.os_release import FakeOsRelease
class UserAgentTestCase(unit.TestCase):
diff --git a/tests/unit/store/test_channels.py b/tests/legacy/unit/store/test_channels.py
similarity index 98%
rename from tests/unit/store/test_channels.py
rename to tests/legacy/unit/store/test_channels.py
index 14b5182277..9b674d4323 100644
--- a/tests/unit/store/test_channels.py
+++ b/tests/legacy/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/legacy/unit/store/test_errors.py
similarity index 98%
rename from tests/unit/store/test_errors.py
rename to tests/legacy/unit/store/test_errors.py
index 403dc481aa..725356f187 100644
--- a/tests/unit/store/test_errors.py
+++ b/tests/legacy/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/legacy/unit/store/test_metrics.py
similarity index 99%
rename from tests/unit/store/test_metrics.py
rename to tests/legacy/unit/store/test_metrics.py
index 508d242473..892d85eae9 100644
--- a/tests/unit/store/test_metrics.py
+++ b/tests/legacy/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/legacy/unit/store/test_status.py
similarity index 98%
rename from tests/unit/store/test_status.py
rename to tests/legacy/unit/store/test_status.py
index b5740f626e..f815ada664 100644
--- a/tests/unit/store/test_status.py
+++ b/tests/legacy/unit/store/test_status.py
@@ -16,8 +16,8 @@
from testtools.matchers import Equals, HasLength
-from snapcraft.storeapi import channels, errors, status
-from tests import unit
+from snapcraft_legacy.storeapi import channels, errors, status
+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 56%
rename from tests/unit/store/test_store_client.py
rename to tests/legacy/unit/store/test_store_client.py
index 006a6c8c4e..e175a5ab63 100644
--- a/tests/unit/store/test_store_client.py
+++ b/tests/legacy/unit/store/test_store_client.py
@@ -17,10 +17,8 @@
import json
import logging
import os
-import pathlib
import tempfile
from textwrap import dedent
-from unittest import mock
import fixtures
import pytest
@@ -34,143 +32,20 @@
Not,
)
-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 tests import fixture_setup, unit
+from snapcraft_legacy import storeapi
+from snapcraft_legacy.storeapi import errors, metrics
+from snapcraft_legacy.storeapi.v2 import 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 +54,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 +71,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 +78,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 +92,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 +114,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 +122,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 +135,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 +145,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 +156,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,27 +168,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(
@@ -418,91 +252,9 @@ def test_get_account_information_successfully(self):
),
)
- 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(
- {
- "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",
- },
- }
- },
- }
- ),
- )
- 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 +265,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 +282,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 +312,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 +330,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 +341,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 +352,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 +363,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 +375,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 +388,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 +552,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 +597,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 +605,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 +614,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,191 +628,9 @@ 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):
- super().setUp()
- self.snap_path = os.path.join(
- os.path.dirname(tests.__file__), "data", "test-snap.snap"
- )
- # These should eventually converge to the same module
- pbars = (
- "snapcraft.storeapi._upload.ProgressBar",
- "snapcraft.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.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))
- 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_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))
- 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.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))
- 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.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))
- 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):
- self.client.login(email="dummy", password="test correct password")
- 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):
- self.client.login(email="dummy", password="test correct password")
- 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):
- 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 +644,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 +660,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 +682,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,
@@ -1239,127 +700,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_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,
- "snap-id",
- ["invalid"],
- )
- self.assertThat(
- str(raised),
- Equals(
- "Could not close channel: The 'channels' field content " "is not valid."
- ),
- )
-
- 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,
- "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_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"]
- )
- 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()
@@ -1442,11 +782,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 +792,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,54 +805,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),
- )
-
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 +827,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 +839,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 +868,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,7 +890,6 @@ 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),
@@ -1622,7 +898,6 @@ def test_whoami(self):
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,
@@ -1637,7 +912,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,
@@ -1649,16 +923,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):
@@ -1671,18 +935,7 @@ 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.__file__), "data", "test-snap.snap")
- 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()
@@ -1781,19 +1034,7 @@ 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.__file__), "data", "test-snap.snap")
- 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()
@@ -1876,11 +1117,7 @@ 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.__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/unit/store/v2/__init__.py b/tests/legacy/unit/store/v2/__init__.py
similarity index 100%
rename from tests/unit/store/v2/__init__.py
rename to tests/legacy/unit/store/v2/__init__.py
diff --git a/tests/unit/store/v2/test_releases.py b/tests/legacy/unit/store/v2/test_releases.py
similarity index 99%
rename from tests/unit/store/v2/test_releases.py
rename to tests/legacy/unit/store/v2/test_releases.py
index ad692e38b0..28bbe5676d 100644
--- a/tests/unit/store/v2/test_releases.py
+++ b/tests/legacy/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/legacy/unit/store/v2/test_validation_sets.py
similarity index 98%
rename from tests/unit/store/v2/test_validation_sets.py
rename to tests/legacy/unit/store/v2/test_validation_sets.py
index a6598f8e63..e63db9188f 100644
--- a/tests/unit/store/v2/test_validation_sets.py
+++ b/tests/legacy/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/legacy/unit/store/v2/test_whoami.py
similarity index 97%
rename from tests/unit/store/v2/test_whoami.py
rename to tests/legacy/unit/store/v2/test_whoami.py
index 2e5bf7f884..11620e1549 100644
--- a/tests/unit/store/v2/test_whoami.py
+++ b/tests/legacy/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/legacy/unit/test_common.py
similarity index 98%
rename from tests/unit/test_common.py
rename to tests/legacy/unit/test_common.py
index 0be663d380..0ecd312254 100644
--- a/tests/unit/test_common.py
+++ b/tests/legacy/unit/test_common.py
@@ -19,8 +19,8 @@
import pytest
from testtools.matchers import Equals
-from snapcraft.internal import common, errors
-from tests import unit
+from snapcraft_legacy.internal import common, errors
+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 95%
rename from tests/unit/test_config.py
rename to tests/legacy/unit/test_config.py
index 3e3db651e4..960bc22a38 100644
--- a/tests/unit/test_config.py
+++ b/tests/legacy/unit/test_config.py
@@ -19,9 +19,9 @@
import xdg
from testtools.matchers import Equals
-from snapcraft import config
-from snapcraft.internal.errors import SnapcraftInvalidCLIConfigError
-from tests import unit
+from snapcraft_legacy import config
+from snapcraft_legacy.internal.errors import SnapcraftInvalidCLIConfigError
+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 97%
rename from tests/unit/test_elf.py
rename to tests/legacy/unit/test_elf.py
index 03622bf291..2f2089cb6f 100644
--- a/tests/unit/test_elf.py
+++ b/tests/legacy/unit/test_elf.py
@@ -24,9 +24,9 @@
import pytest
from testtools.matchers import Contains, EndsWith, Equals, NotEquals, StartsWith
-from snapcraft import ProjectOptions
-from snapcraft.internal import elf, errors
-from tests import fixture_setup, unit
+from snapcraft_legacy import ProjectOptions
+from snapcraft_legacy.internal import elf, errors
+from tests.legacy import fixture_setup, unit
class TestElfBase(unit.TestCase):
@@ -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):
@@ -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/legacy/unit/test_errors.py
similarity index 99%
rename from tests/unit/test_errors.py
rename to tests/legacy/unit/test_errors.py
index 51211f2e88..56da142dba 100644
--- a/tests/unit/test_errors.py
+++ b/tests/legacy/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/legacy/unit/test_file_utils.py
similarity index 98%
rename from tests/unit/test_file_utils.py
rename to tests/legacy/unit/test_file_utils.py
index 613a05a2e6..f5c1c479e7 100644
--- a/tests/unit/test_file_utils.py
+++ b/tests/legacy/unit/test_file_utils.py
@@ -25,9 +25,9 @@
import testtools
from testtools.matchers import Equals
-from snapcraft import file_utils
-from snapcraft.internal import common, errors
-from tests import fixture_setup, unit
+from snapcraft_legacy import file_utils
+from snapcraft_legacy.internal import common, errors
+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 97%
rename from tests/unit/test_formatting_utils.py
rename to tests/legacy/unit/test_formatting_utils.py
index 7269eeda4e..f5607b2b56 100644
--- a/tests/unit/test_formatting_utils.py
+++ b/tests/legacy/unit/test_formatting_utils.py
@@ -17,8 +17,8 @@
import pytest
from testtools.matchers import Equals
-from snapcraft import formatting_utils
-from tests import unit
+from snapcraft_legacy import formatting_utils
+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 97%
rename from tests/unit/test_indicators.py
rename to tests/legacy/unit/test_indicators.py
index ea9db69d1c..7243b12e03 100644
--- a/tests/unit/test_indicators.py
+++ b/tests/legacy/unit/test_indicators.py
@@ -21,8 +21,8 @@
import progressbar
import requests
-from snapcraft.internal import indicators
-from tests import unit
+from snapcraft_legacy.internal import indicators
+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 88%
rename from tests/unit/test_init.py
rename to tests/legacy/unit/test_init.py
index 88066c2baa..26bf7605f8 100644
--- a/tests/unit/test_init.py
+++ b/tests/legacy/unit/test_init.py
@@ -17,12 +17,12 @@
import fixtures
from testtools.matchers import Equals
-import snapcraft
-from tests import unit
+import snapcraft_legacy
+from tests.legacy import unit
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/legacy/unit/test_log.py
similarity index 98%
rename from tests/unit/test_log.py
rename to tests/legacy/unit/test_log.py
index b62a237f4c..fb7dcad8b7 100644
--- a/tests/unit/test_log.py
+++ b/tests/legacy/unit/test_log.py
@@ -18,8 +18,8 @@
from testtools.matchers import Contains, Equals, Not
-from snapcraft.internal import log
-from tests import fixture_setup, unit
+from snapcraft_legacy.internal import log
+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 98%
rename from tests/unit/test_mangling.py
rename to tests/legacy/unit/test_mangling.py
index 942fe9634b..e60b7f8f36 100644
--- a/tests/unit/test_mangling.py
+++ b/tests/legacy/unit/test_mangling.py
@@ -19,8 +19,8 @@
from testtools.matchers import FileContains, FileExists, Not
-from snapcraft.internal import mangling
-from tests import fixture_setup, unit
+from snapcraft_legacy.internal import mangling
+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 98%
rename from tests/unit/test_mountinfo.py
rename to tests/legacy/unit/test_mountinfo.py
index 209cc13408..1390270180 100644
--- a/tests/unit/test_mountinfo.py
+++ b/tests/legacy/unit/test_mountinfo.py
@@ -20,8 +20,8 @@
import fixtures
from testtools.matchers import Equals, HasLength
-from snapcraft.internal import errors, mountinfo
-from tests import unit
+from snapcraft_legacy.internal import errors, mountinfo
+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 91%
rename from tests/unit/test_options.py
rename to tests/legacy/unit/test_options.py
index 2002b44d27..c8f558ada6 100644
--- a/tests/unit/test_options.py
+++ b/tests/legacy/unit/test_options.py
@@ -21,14 +21,14 @@
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,
)
-from tests import unit
+from tests.legacy import unit
class TestNativeOptions:
@@ -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/legacy/unit/test_os_release.py b/tests/legacy/unit/test_os_release.py
new file mode 100644
index 0000000000..853ed9fee8
--- /dev/null
+++ b/tests/legacy/unit/test_os_release.py
@@ -0,0 +1,139 @@
+# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*-
+#
+# Copyright (C) 2015-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 textwrap import dedent
+
+from testtools.matchers import Equals
+
+from snapcraft_legacy.internal import errors, os_release
+from tests.legacy import unit
+
+
+class OsReleaseTestCase(unit.TestCase):
+ def _write_os_release(self, contents):
+ path = "os-release"
+ with open(path, "w") as f:
+ f.write(contents)
+ return path
+
+ def test_blank_lines(self):
+ release = os_release.OsRelease(
+ os_release_file=self._write_os_release(
+ dedent(
+ """\
+ NAME="Arch Linux"
+
+ PRETTY_NAME="Arch Linux"
+ ID=arch
+ ID_LIKE=archlinux
+ VERSION_ID="foo"
+ VERSION_CODENAME="bar"
+
+ """
+ )
+ )
+ )
+
+ self.assertThat(release.id(), Equals("arch"))
+ self.assertThat(release.name(), Equals("Arch Linux"))
+ self.assertThat(release.version_id(), Equals("foo"))
+ self.assertThat(release.version_codename(), Equals("bar"))
+
+ def test_no_id(self):
+ release = os_release.OsRelease(
+ os_release_file=self._write_os_release(
+ dedent(
+ """\
+ NAME="Arch Linux"
+ PRETTY_NAME="Arch Linux"
+ ID_LIKE=archlinux
+ VERSION_ID="foo"
+ VERSION_CODENAME="bar"
+ """
+ )
+ )
+ )
+
+ self.assertRaises(errors.OsReleaseIdError, release.id)
+
+ def test_no_name(self):
+ release = os_release.OsRelease(
+ os_release_file=self._write_os_release(
+ dedent(
+ """\
+ ID=arch
+ PRETTY_NAME="Arch Linux"
+ ID_LIKE=archlinux
+ VERSION_ID="foo"
+ VERSION_CODENAME="bar"
+ """
+ )
+ )
+ )
+
+ self.assertRaises(errors.OsReleaseNameError, release.name)
+
+ def test_no_version_id(self):
+ release = os_release.OsRelease(
+ os_release_file=self._write_os_release(
+ dedent(
+ """\
+ NAME="Arch Linux"
+ ID=arch
+ PRETTY_NAME="Arch Linux"
+ ID_LIKE=archlinux
+ VERSION_CODENAME="bar"
+ """
+ )
+ )
+ )
+
+ self.assertRaises(errors.OsReleaseVersionIdError, release.version_id)
+
+ def test_no_version_codename(self):
+ """Test that version codename can also come from VERSION_ID"""
+ release = os_release.OsRelease(
+ os_release_file=self._write_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"
+ """
+ )
+ )
+ )
+
+ self.assertThat(release.version_codename(), Equals("trusty"))
+
+ def test_no_version_codename_or_version_id(self):
+ release = os_release.OsRelease(
+ os_release_file=self._write_os_release(
+ dedent(
+ """\
+ NAME="Ubuntu"
+ ID=ubuntu
+ ID_LIKE=debian
+ PRETTY_NAME="Ubuntu 16.04.3 LTS"
+ """
+ )
+ )
+ )
+
+ self.assertRaises(errors.OsReleaseCodenameError, release.version_codename)
diff --git a/tests/unit/test_steps.py b/tests/legacy/unit/test_steps.py
similarity index 97%
rename from tests/unit/test_steps.py
rename to tests/legacy/unit/test_steps.py
index 497f151d37..eb8f6df922 100644
--- a/tests/unit/test_steps.py
+++ b/tests/legacy/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/legacy/unit/test_target_arch.py
similarity index 95%
rename from tests/unit/test_target_arch.py
rename to tests/legacy/unit/test_target_arch.py
index 20ea037ae0..039fff1455 100644
--- a/tests/unit/test_target_arch.py
+++ b/tests/legacy/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/legacy/unit/test_xattrs.py
similarity index 96%
rename from tests/unit/test_xattrs.py
rename to tests/legacy/unit/test_xattrs.py
index 4608508a73..9d15fe95f0 100644
--- a/tests/unit/test_xattrs.py
+++ b/tests/legacy/unit/test_xattrs.py
@@ -20,9 +20,9 @@
from testtools.matchers import Equals
-from snapcraft.internal import xattrs
-from snapcraft.internal.errors import XAttributeTooLongError
-from tests import unit
+from snapcraft_legacy.internal import xattrs
+from snapcraft_legacy.internal.errors import XAttributeTooLongError
+from tests.legacy import unit
class TestXattrs(unit.TestCase):
diff --git a/tests/unit/yaml_utils/__init__.py b/tests/legacy/unit/yaml_utils/__init__.py
similarity index 100%
rename from tests/unit/yaml_utils/__init__.py
rename to tests/legacy/unit/yaml_utils/__init__.py
diff --git a/tests/unit/yaml_utils/test_errors.py b/tests/legacy/unit/yaml_utils/test_errors.py
similarity index 94%
rename from tests/unit/yaml_utils/test_errors.py
rename to tests/legacy/unit/yaml_utils/test_errors.py
index 1e2f7c3b9e..7871057bdc 100644
--- a/tests/unit/yaml_utils/test_errors.py
+++ b/tests/legacy/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/legacy/unit/yaml_utils/test_yaml_utils.py
similarity index 96%
rename from tests/unit/yaml_utils/test_yaml_utils.py
rename to tests/legacy/unit/yaml_utils/test_yaml_utils.py
index 35310dea7e..c26628c13a 100644
--- a/tests/unit/yaml_utils/test_yaml_utils.py
+++ b/tests/legacy/unit/yaml_utils/test_yaml_utils.py
@@ -15,11 +15,11 @@
# along with this program. If not, see .
import io
-import pytest
+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/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]:
diff --git a/tests/spread/core22/appstream-desktop/expected_appstream-desktop.desktop b/tests/spread/core22/appstream-desktop/expected_appstream-desktop.desktop
new file mode 100644
index 0000000000..c11175ebd0
--- /dev/null
+++ b/tests/spread/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/core22/appstream-desktop/expected_snap.yaml b/tests/spread/core22/appstream-desktop/expected_snap.yaml
new file mode 100644
index 0000000000..81a303ae70
--- /dev/null
+++ b/tests/spread/core22/appstream-desktop/expected_snap.yaml
@@ -0,0 +1,28 @@
+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.
+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/core22/appstream-desktop/snaps/appstream-desktop/appstream-desktop b/tests/spread/core22/appstream-desktop/snaps/appstream-desktop/appstream-desktop
new file mode 100755
index 0000000000..10501a451d
--- /dev/null
+++ b/tests/spread/core22/appstream-desktop/snaps/appstream-desktop/appstream-desktop
@@ -0,0 +1,3 @@
+#!/bin/sh
+
+echo "appstream desktop"
diff --git a/tests/spread/core22/appstream-desktop/snaps/appstream-desktop/io.snapcraft.appstream.desktop b/tests/spread/core22/appstream-desktop/snaps/appstream-desktop/io.snapcraft.appstream.desktop
new file mode 100644
index 0000000000..85db82eab9
--- /dev/null
+++ b/tests/spread/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/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
new file mode 100644
index 0000000000..11affa5ea9
--- /dev/null
+++ b/tests/spread/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/core22/appstream-desktop/snaps/appstream-desktop/snap/snapcraft.yaml b/tests/spread/core22/appstream-desktop/snaps/appstream-desktop/snap/snapcraft.yaml
new file mode 100644
index 0000000000..d5329c80d4
--- /dev/null
+++ b/tests/spread/core22/appstream-desktop/snaps/appstream-desktop/snap/snapcraft.yaml
@@ -0,0 +1,23 @@
+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: .
+ 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/core22/appstream-desktop/snaps/appstream-desktop/snapcraft.svg b/tests/spread/core22/appstream-desktop/snaps/appstream-desktop/snapcraft.svg
new file mode 100644
index 0000000000..38cdee4083
--- /dev/null
+++ b/tests/spread/core22/appstream-desktop/snaps/appstream-desktop/snapcraft.svg
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/spread/core22/appstream-desktop/task.yaml b/tests/spread/core22/appstream-desktop/task.yaml
new file mode 100644
index 0000000000..a700e69059
--- /dev/null
+++ b/tests/spread/core22/appstream-desktop/task.yaml
@@ -0,0 +1,27 @@
+summary: Build a snap that tests appstream settings
+
+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
+
+ 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/spread/core22/clean/snap/snapcraft.yaml b/tests/spread/core22/clean/snap/snapcraft.yaml
new file mode 100644
index 0000000000..028287c079
--- /dev/null
+++ b/tests/spread/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/core22/clean/task.yaml b/tests/spread/core22/clean/task.yaml
new file mode 100644
index 0000000000..41ce466a19
--- /dev/null
+++ b/tests/spread/core22/clean/task.yaml
@@ -0,0 +1,63 @@
+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: |
+ # Unset SNAPCRAFT_BUILD_ENVIRONMENT=host.
+ unset SNAPCRAFT_BUILD_ENVIRONMENT
+
+ 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
diff --git a/tests/spread/core22/craftctl/task.yaml b/tests/spread/core22/craftctl/task.yaml
new file mode 100644
index 0000000000..21e05dfe3b
--- /dev/null
+++ b/tests/spread/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/core22/craftctl/test-craftctl-default/Makefile b/tests/spread/core22/craftctl/test-craftctl-default/Makefile
new file mode 100644
index 0000000000..aa47d9c2bc
--- /dev/null
+++ b/tests/spread/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/core22/craftctl/test-craftctl-default/hello.c b/tests/spread/core22/craftctl/test-craftctl-default/hello.c
new file mode 100644
index 0000000000..7583bb3313
--- /dev/null
+++ b/tests/spread/core22/craftctl/test-craftctl-default/hello.c
@@ -0,0 +1,6 @@
+#include
+
+int main()
+{
+ printf("hello\n");
+}
diff --git a/tests/spread/core22/craftctl/test-craftctl-default/snap/snapcraft.yaml b/tests/spread/core22/craftctl/test-craftctl-default/snap/snapcraft.yaml
new file mode 100644
index 0000000000..1dbb998f5d
--- /dev/null
+++ b/tests/spread/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/core22/craftctl/test-craftctl-get-set/snap/snapcraft.yaml b/tests/spread/core22/craftctl/test-craftctl-get-set/snap/snapcraft.yaml
new file mode 100644
index 0000000000..0e17298a5a
--- /dev/null
+++ b/tests/spread/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/core22/environment/task.yaml b/tests/spread/core22/environment/task.yaml
new file mode 100644
index 0000000000..116730bac7
--- /dev/null
+++ b/tests/spread/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/core22/environment/test-variables/snap/snapcraft.yaml b/tests/spread/core22/environment/test-variables/snap/snapcraft.yaml
new file mode 100644
index 0000000000..6bca17edfa
--- /dev/null
+++ b/tests/spread/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/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/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
new file mode 100644
index 0000000000..f1976277b3
--- /dev/null
+++ b/tests/spread/core22/package-repositories/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/core22/package-repositories/test-apt-key-fingerprint/snap/snapcraft.yaml b/tests/spread/core22/package-repositories/test-apt-key-fingerprint/snap/snapcraft.yaml
new file mode 100644
index 0000000000..cc90a644a0
--- /dev/null
+++ b/tests/spread/core22/package-repositories/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/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
new file mode 100644
index 0000000000..f1976277b3
--- /dev/null
+++ b/tests/spread/core22/package-repositories/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/core22/package-repositories/test-apt-key-name/snap/snapcraft.yaml b/tests/spread/core22/package-repositories/test-apt-key-name/snap/snapcraft.yaml
new file mode 100644
index 0000000000..9d65650e1f
--- /dev/null
+++ b/tests/spread/core22/package-repositories/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/core22/package-repositories/test-apt-keyserver/snap/snapcraft.yaml b/tests/spread/core22/package-repositories/test-apt-keyserver/snap/snapcraft.yaml
new file mode 100644
index 0000000000..65ace50d40
--- /dev/null
+++ b/tests/spread/core22/package-repositories/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/core22/package-repositories/test-apt-path/snap/snapcraft.yaml b/tests/spread/core22/package-repositories/test-apt-path/snap/snapcraft.yaml
new file mode 100644
index 0000000000..f82ce23637
--- /dev/null
+++ b/tests/spread/core22/package-repositories/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/core22/package-repositories/test-apt-ppa/snap/snapcraft.yaml b/tests/spread/core22/package-repositories/test-apt-ppa/snap/snapcraft.yaml
new file mode 100644
index 0000000000..1f12cfdb09
--- /dev/null
+++ b/tests/spread/core22/package-repositories/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/core22/packing/snap/snapcraft.yaml b/tests/spread/core22/packing/snap/snapcraft.yaml
new file mode 100644
index 0000000000..3222b1c8e5
--- /dev/null
+++ b/tests/spread/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/core22/packing/task.yaml b/tests/spread/core22/packing/task.yaml
new file mode 100644
index 0000000000..00dcac93df
--- /dev/null
+++ b/tests/spread/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
diff --git a/tests/spread/core22/scriptlets/scriptlet-failures/snap/snapcraft.yaml b/tests/spread/core22/scriptlets/scriptlet-failures/snap/snapcraft.yaml
new file mode 100644
index 0000000000..bec55a0e4d
--- /dev/null
+++ b/tests/spread/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/core22/scriptlets/task.yaml b/tests/spread/core22/scriptlets/task.yaml
new file mode 100644
index 0000000000..13c8b4fc33
--- /dev/null
+++ b/tests/spread/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
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/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'"
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/general/sources/snaps/git-submodules/snapcraft.yaml b/tests/spread/general/sources/snaps/git-submodules/snapcraft.yaml
index 6008ec95d0..c5c42bdc6d 100644
--- a/tests/spread/general/sources/snaps/git-submodules/snapcraft.yaml
+++ b/tests/spread/general/sources/snaps/git-submodules/snapcraft.yaml
@@ -1,4 +1,4 @@
-name: git-submodules
+name: git-recurse-submodules
base: core20
version: "0.1"
summary: Test the use of source-submodules
@@ -8,7 +8,7 @@ confinement: strict
parts:
git:
plugin: dump
- source: https://github.com/snapcore/core20
+ source: https://github.com/snapcore/core18
source-type: git
source-submodules:
- submodule_1
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/store/task.yaml b/tests/spread/general/store/task.yaml
index c1b16bc157..5498c31b48 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,15 +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
- if [ "${SPREAD_VARIANT}" = "CANDID" ]; then
- snapcraft login --experimental-login --with login
- else
- snapcraft login --with login
- fi
+ # Login mechanism
+ export SNAPCRAFT_STORE_AUTH="${SPREAD_VARIANT}"
# Who Am I?
snapcraft whoami
@@ -76,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
@@ -87,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
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/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/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/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/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"
diff --git a/tests/unit/__init__.py b/tests/unit/__init__.py
index b201ad1278..e69de29bb2 100644
--- a/tests/unit/__init__.py
+++ b/tests/unit/__init__.py
@@ -1,320 +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.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,
- source_submodules=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.source_submodules = source_submodules
- 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.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
- )
- 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/cli/test_default_command.py b/tests/unit/cli/test_default_command.py
new file mode 100644
index 0000000000..4cc22ea845
--- /dev/null
+++ b/tests/unit/cli/test_default_command.py
@@ -0,0 +1,112 @@
+# -*- 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(
+ debug=False,
+ directory=None,
+ 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,
+ )
+ )
+ ]
+
+
+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 == [
+ call(
+ argparse.Namespace(
+ directory=None,
+ output=None,
+ debug=False,
+ destructive_mode=True,
+ use_lxd=False,
+ enable_experimental_extensions=False,
+ enable_developer_debug=False,
+ enable_experimental_target_arch=False,
+ target_arch=None,
+ 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,
+ debug=False,
+ destructive_mode=False,
+ use_lxd=True,
+ 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_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",
+ debug=False,
+ 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_lifecycle.py b/tests/unit/cli/test_lifecycle.py
index 1165e11afd..9f4daa2204 100644
--- a/tests/unit/cli/test_lifecycle.py
+++ b/tests/unit/cli/test_lifecycle.py
@@ -1,6 +1,6 @@
# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*-
#
-# Copyright (C) 2020 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
@@ -14,45 +14,436 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
-from unittest import mock
+import argparse
+import sys
+from unittest.mock import call
import pytest
-from snapcraft.cli import lifecycle
+from snapcraft import cli
@pytest.mark.parametrize(
- "output,pack_name,pack_dir",
+ "cmd,run_method",
[
- ("/tmp/output.snap", "output.snap", "/tmp"),
- ("/tmp", None, "/tmp"),
- ("output.snap", "output.snap", None),
+ ("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=[],
+ debug=False,
+ 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(
- "compression",
- ["xz", "lzo", None],
+ "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"),
+ ],
)
-@mock.patch("snapcraft.file_utils.get_host_tool_path", return_value="/bin/snap")
-@mock.patch("snapcraft.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)
+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"],
+ debug=False,
+ 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,
+ )
+ )
+ ]
+
- assert mock_host_tool.mock_calls == [
- mock.call(command_name="snap", package_name="snapd")
+@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",
+ [
+ "cmd",
+ cmd,
+ "--destructive-mode",
+ "part1",
+ "part2",
+ ],
+ )
+ mock_lifecycle_cmd = mocker.patch(run_method)
+ cli.run()
+ assert mock_lifecycle_cmd.mock_calls == [
+ 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,
+ enable_experimental_target_arch=False,
+ target_arch=None,
+ provider=None,
+ )
+ )
]
- pack_command = ["/bin/snap", "pack"]
- if compression:
- pack_command.extend(["--compression", compression])
+@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"],
+ debug=False,
+ destructive_mode=False,
+ shell=False,
+ shell_after=False,
+ use_lxd=True,
+ enable_experimental_extensions=False,
+ enable_developer_debug=False,
+ enable_experimental_target_arch=False,
+ target_arch=None,
+ provider=None,
+ )
+ )
+ ]
- if pack_name:
- pack_command.extend(["--filename", pack_name])
- pack_command.append("/my/snap")
+@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,
+ )
+ )
+ ]
- if pack_dir:
- pack_command.append(pack_dir)
- assert mock_run_pack.mock_calls == [mock.call(pack_command)]
+@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",
+ ],
+ )
+ 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=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,
+ enable_developer_debug=False,
+ enable_experimental_target_arch=False,
+ target_arch=None,
+ provider=None,
+ )
+ )
+ ]
+
+
+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,
+ debug=False,
+ destructive_mode=False,
+ 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_destructive_mode(mocker):
+ mocker.patch.object(
+ sys,
+ "argv",
+ ["cmd", "pack", "--destructive-mode"],
+ )
+ 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=False,
+ destructive_mode=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_use_lxd(mocker):
+ mocker.patch.object(
+ sys,
+ "argv",
+ ["cmd", "pack", "--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,
+ debug=False,
+ destructive_mode=False,
+ use_lxd=True,
+ enable_experimental_extensions=False,
+ enable_developer_debug=False,
+ enable_experimental_target_arch=False,
+ target_arch=None,
+ provider=None,
+ )
+ )
+ ]
+
+
+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"])
+ 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",
+ debug=False,
+ destructive_mode=False,
+ 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_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(
+ debug=False,
+ destructive_mode=False,
+ directory="name",
+ output=None,
+ 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_version.py b/tests/unit/cli/test_version.py
new file mode 100644
index 0000000000..c352240894
--- /dev/null
+++ b/tests/unit/cli/test_version.py
@@ -0,0 +1,40 @@
+# -*- 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
+
+from snapcraft import __version__, cli
+
+
+def test_version_command(mocker):
+ mocker.patch.object(sys, "argv", ["cmd", "version"])
+ mock_version_cmd = mocker.patch("snapcraft.commands.version.VersionCommand.run")
+ cli.run()
+ assert mock_version_cmd.mock_calls == [call(argparse.Namespace())]
+
+
+def test_version_argument(mocker, emitter):
+ mocker.patch.object(sys, "argv", ["cmd", "--version"])
+ cli.run()
+ 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_message(f"snapcraft {__version__}")
diff --git a/tests/unit/commands/__init__.py b/tests/unit/commands/__init__.py
index 888c8979b0..e69de29bb2 100644
--- a/tests/unit/commands/__init__.py
+++ b/tests/unit/commands/__init__.py
@@ -1,463 +0,0 @@
-# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*-
-#
-# Copyright (C) 2015-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 json
-import subprocess
-from pathlib import PosixPath
-from textwrap import dedent
-from unittest import mock
-
-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 tests import fixture_setup, unit
-
-_sample_keys = [
- {
- "name": "default",
- "sha3-384": "vdEeQvRxmZ26npJCFaGnl-VfGz0lU2jZZkWp_s7E-RxVCNtH2_mtjcxq2NkDKkIp",
- },
- {
- "name": "another",
- "sha3-384": "JsfToV5hO2eN9l89pYYCKXUioTERrZIIHUgQQd47jW8YNNBskupiIjWYd3KXLY_D",
- },
-]
-
-
-def get_sample_key(name):
- for key in _sample_keys:
- if key["name"] == name:
- return key
- raise KeyError(name)
-
-
-original_check_output = subprocess.check_output
-
-
-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"):
- return original_check_output(command, *args, **kwargs)
- elif command[0].endswith("snap") and command[1:] == ["keys", "--json"]:
- return json.dumps(_sample_keys)
- elif command[0].endswith("snap") and command[1] == "export-key":
- if not command[2].startswith("--account="):
- raise AssertionError("Unhandled command: {}".format(command))
- account_id = command[2][len("--account=") :]
- name = command[3]
- # This isn't a full account-key-request assertion, but it's enough
- # for testing.
- return dedent(
- """\
- type: account-key-request
- account-id: {account_id}
- name: {name}
- public-key-sha3-384: {sha3_384}
- """
- ).format(
- account_id=account_id, name=name, sha3_384=get_sample_key(name)["sha3-384"]
- )
- elif command[0].endswith("snap") and command[1:] == [
- "create-key",
- "new-key",
- ]:
- pass
- else:
- raise AssertionError("Unhandled command: {}".format(command))
-
-
-class CommandBaseTestCase(unit.TestCase):
- def setUp(self):
- super().setUp()
- self.runner = CliRunner()
-
- 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)
- )
-
- with mock.patch("sys.argv", args):
- return self.runner.invoke(run, args, catch_exceptions=False, **kwargs)
-
-
-class LifecycleCommandsBaseTestCase(CommandBaseTestCase):
- def setUp(self):
- super().setUp()
-
- self.useFixture(fixtures.EnvironmentVariable("SNAPCRAFT_BUILD_ENVIRONMENT"))
-
- self.fake_lifecycle_clean = fixtures.MockPatch(
- "snapcraft.internal.lifecycle.clean"
- )
- self.useFixture(self.fake_lifecycle_clean)
-
- self.fake_lifecycle_execute = fixtures.MockPatch(
- "snapcraft.internal.lifecycle.execute"
- )
- self.useFixture(self.fake_lifecycle_execute)
-
- self.fake_pack = fixtures.MockPatch("snapcraft.cli.lifecycle._pack")
- self.useFixture(self.fake_pack)
-
- self.snapcraft_yaml = fixture_setup.SnapcraftYaml(
- self.path,
- parts={
- "part0": {"plugin": "nil"},
- "part1": {"plugin": "nil"},
- "part2": {"plugin": "nil"},
- },
- )
- self.useFixture(self.snapcraft_yaml)
-
- self.provider_class_mock = mock.MagicMock()
- self.provider_mock = mock.MagicMock()
- self.provider_class_mock.return_value.__enter__.return_value = (
- self.provider_mock
- )
-
- self.fake_get_provider_for = fixtures.MockPatch(
- "snapcraft.internal.build_providers.get_provider_for",
- return_value=self.provider_class_mock,
- )
- self.useFixture(self.fake_get_provider_for)
-
- def assert_clean_not_called(self):
- self.fake_lifecycle_clean.mock.assert_not_called()
- self.provider_mock.clean.assert_not_called()
- self.provider_mock.clean_project.assert_not_called()
-
-
-class StoreCommandsBaseTestCase(CommandBaseTestCase):
- def setUp(self):
- super().setUp()
- self.fake_store = fixture_setup.FakeStore()
- self.useFixture(self.fake_store)
- self.client = storeapi.StoreClient()
-
-
-class FakeStoreCommandsBaseTestCase(CommandBaseTestCase):
- def setUp(self):
- super().setUp()
-
- # Our experimental environment variable is sticky
- self.useFixture(
- fixtures.EnvironmentVariable(
- "SNAPCRAFT_EXPERIMENTAL_PROGRESSIVE_RELEASES", None
- )
- )
-
- self.fake_store_login = fixtures.MockPatchObject(storeapi.StoreClient, "login")
- self.useFixture(self.fake_store_login)
-
- self.fake_store_register = fixtures.MockPatchObject(
- storeapi._dashboard_api.DashboardAPI, "register"
- )
- self.useFixture(self.fake_store_register)
-
- self.fake_store_account_info_data = {
- "account_id": "abcd",
- "account_keys": list(),
- "snaps": {
- "16": {
- "snap-test": {
- "snap-id": "snap-test-snap-id",
- "status": "Approved",
- "private": False,
- "since": "2016-12-12T01:01Z",
- "price": "0",
- },
- "basic": {
- "snap-id": "basic-snap-id",
- "status": "Approved",
- "private": False,
- "since": "2016-12-12T01:01Z",
- "price": "0",
- },
- }
- },
- }
-
- self.fake_store_account_info = fixtures.MockPatchObject(
- storeapi._dashboard_api.DashboardAPI,
- "get_account_information",
- return_value=self.fake_store_account_info_data,
- )
- self.useFixture(self.fake_store_account_info)
-
- self.fake_store_status = fixtures.MockPatchObject(
- storeapi._dashboard_api.DashboardAPI, "snap_status", return_value=dict()
- )
- self.useFixture(self.fake_store_status)
-
- self.fake_store_release = fixtures.MockPatchObject(
- storeapi.StoreClient, "release"
- )
- self.useFixture(self.fake_store_release)
-
- self.fake_store_register_key = fixtures.MockPatchObject(
- storeapi._dashboard_api.DashboardAPI, "register_key"
- )
- 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(
- status=metrics.MetricsStatus["OK"],
- snap_id="test-snap-id",
- metric_name="daily_device_change",
- buckets=["2021-01-01", "2021-01-02", "2021-01-03"],
- series=[
- metrics.Series(
- name="continued",
- values=[10, 11, 12],
- currently_released=None,
- ),
- metrics.Series(
- name="lost", values=[1, 2, 3], currently_released=None
- ),
- metrics.Series(
- name="new", values=[2, 3, 4], currently_released=None
- ),
- ],
- )
- ]
- )
- self.fake_store_get_metrics = fixtures.MockPatchObject(
- storeapi.StoreClient, "get_metrics", return_value=self.metrics
- )
- self.useFixture(self.fake_store_get_metrics)
-
- self.releases = Releases.unmarshal(
- {
- "revisions": [
- {
- "architectures": ["i386"],
- "base": "core20",
- "build_url": None,
- "confinement": "strict",
- "created_at": " 2016-09-27T19:23:40Z",
- "grade": "stable",
- "revision": 2,
- "sha3-384": "a9060ef4872ccacbfa440617a76fcd84967896b28d0d1eb7571f00a1098d766e7e93353b084ba6ad841d7b14b95ede48",
- "size": 20,
- "status": "Published",
- "version": "2.0.1",
- },
- {
- "architectures": ["amd64"],
- "base": "core20",
- "build_url": None,
- "confinement": "strict",
- "created_at": "2016-09-27T18:38:43Z",
- "grade": "stable",
- "revision": 1,
- "sha3-384": "a9060ef4872ccacbfa440617a76fcd84967896b28d0d1eb7571f00a1098d766e7e93353b084ba6ad841d7b14b95ede48",
- "size": 20,
- "status": "Published",
- "version": "2.0.2",
- },
- ],
- "releases": [
- {
- "architecture": "amd64",
- "branch": None,
- "channel": "latest/stable",
- "expiration-date": None,
- "revision": 1,
- "risk": "stable",
- "track": "latest",
- "when": "2020-02-12T17:51:40.891996Z",
- },
- {
- "architecture": "i386",
- "branch": None,
- "channel": "latest/stable",
- "expiration-date": None,
- "revision": None,
- "risk": "stable",
- "track": "latest",
- "when": "2020-02-11T17:51:40.891996Z",
- },
- {
- "architecture": "amd64",
- "branch": None,
- "channel": "latest/edge",
- "expiration-date": None,
- "revision": 1,
- "risk": "stable",
- "track": "latest",
- "when": "2020-01-12T17:51:40.891996Z",
- },
- ],
- }
- )
- self.fake_store_get_releases = fixtures.MockPatchObject(
- storeapi.StoreClient, "get_snap_releases", return_value=self.releases
- )
- 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
- )
- self.useFixture(self.fake_check_output)
-
- # Pretend that the snap command is available
- self.fake_package_installed = fixtures.MockPatch(
- "snapcraft.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..77764dcce0 100644
--- a/tests/unit/commands/conftest.py
+++ b/tests/unit/commands/conftest.py
@@ -1,6 +1,6 @@
# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*-
#
-# Copyright (C) 2017-2021 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
@@ -14,20 +14,21 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
-from typing import List
import pytest
-from click.testing import CliRunner
-
-from snapcraft.cli._runner import run
@pytest.fixture
-def click_run():
- """Run commands using Click's testing backend."""
- cli = CliRunner()
+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
- def runner(args: List[str]):
- return cli.invoke(run, args)
- return runner
+@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/__init__.py b/tests/unit/commands/store/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
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
new file mode 100644
index 0000000000..eada9c74e5
--- /dev/null
+++ b/tests/unit/commands/store/test_client.py
@@ -0,0 +1,794 @@
+# -*- 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 textwrap
+import time
+from unittest.mock import call
+
+import craft_store
+import pytest
+import requests
+from craft_store import endpoints
+
+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 no_wait(monkeypatch):
+ monkeypatch.setattr(time, "sleep", lambda x: None)
+
+
+@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 #
+####################
+
+
+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_login_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",
+ )
+ ]
+
+
+###########
+# 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(),
+ ]
+
+
+###########
+# 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"]},
+ )
+ ]
+
+
+###################
+# 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"},
+ )
+ ]
+
+
+#################
+# 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/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..c1b7dfc623
--- /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_message("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_message("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_message("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_message("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_message(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_message(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_message("Credentials cleared")
diff --git a/tests/unit/commands/test_close.py b/tests/unit/commands/test_close.py
deleted file mode 100644
index 031e113914..0000000000
--- a/tests/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.storeapi.errors
-from snapcraft 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.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/unit/commands/test_expand_extensions.py b/tests/unit/commands/test_expand_extensions.py
new file mode 100644
index 0000000000..f218df2b8e
--- /dev/null
+++ b/tests/unit/commands/test_expand_extensions.py
@@ -0,0 +1,76 @@
+# -*- 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_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_export_login.py b/tests/unit/commands/test_export_login.py
deleted file mode 100644
index 1c086db5ad..0000000000
--- a/tests/unit/commands/test_export_login.py
+++ /dev/null
@@ -1,229 +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
-import pytest
-from testtools.matchers import Contains, Equals, MatchesRegex, Not
-
-from snapcraft 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,
- expires=None,
- save=False,
- config_fd=None,
- )
-
- 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,
- expires=None,
- save=False,
- config_fd=None,
- )
-
- 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:00", "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,
- expires="2018-02-01T00:00:00",
- save=False,
- config_fd=None,
- )
-
- def test_successful_login_with_2fa(self):
- self.fake_store_login.mock.side_effect = [
- storeapi.http_clients.errors.StoreTwoFactorAuthenticationRequired(),
- None,
- ]
-
- 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)
- )
- 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"?'
- )
diff --git a/tests/unit/commands/test_lifecycle.py b/tests/unit/commands/test_lifecycle.py
new file mode 100644
index 0000000000..06bab102f3
--- /dev/null
+++ b/tests/unit/commands/test_lifecycle.py
@@ -0,0 +1,97 @@
+# -*- 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,
+ CleanCommand,
+ PackCommand,
+ PrimeCommand,
+ PullCommand,
+ SnapCommand,
+ StageCommand,
+)
+
+
+@pytest.mark.parametrize(
+ "cmd_name,cmd_class",
+ [
+ ("pull", PullCommand),
+ ("build", BuildCommand),
+ ("stage", StageCommand),
+ ("prime", PrimeCommand),
+ ("clean", CleanCommand),
+ ],
+)
+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(cmd_name, argparse.Namespace(parts=["part1", "part2"]))
+ ]
+
+
+@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 = cmd_class(None)
+ cmd.run(argparse.Namespace(directory=None, output=None, compression=None))
+ assert lifecycle_run_mock.mock_calls == [
+ 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):
+ 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/commands/test_list.py b/tests/unit/commands/test_list.py
deleted file mode 100644
index 8332351c82..0000000000
--- a/tests/unit/commands/test_list.py
+++ /dev/null
@@ -1,131 +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 snapcraft import storeapi
-
-from . import 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.storeapi
- self.fake_store_account_info.mock.side_effect = [
- storeapi.http_clients.errors.InvalidCredentialsError("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/unit/commands/test_list_extensions.py b/tests/unit/commands/test_list_extensions.py
new file mode 100644
index 0000000000..979c07ac18
--- /dev/null
+++ b/tests/unit/commands/test_list_extensions.py
@@ -0,0 +1,71 @@
+# -*- 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_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"""
+ )
+ )
+
+
+@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_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_list_tracks.py b/tests/unit/commands/test_list_tracks.py
deleted file mode 100644
index 4e85b806b8..0000000000
--- a/tests/unit/commands/test_list_tracks.py
+++ /dev/null
@@ -1,61 +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 snapcraft import storeapi
-
-from . import 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 = [
- storeapi.http_clients.errors.InvalidCredentialsError("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/unit/commands/test_login.py b/tests/unit/commands/test_login.py
deleted file mode 100644
index 94bb1aabed..0000000000
--- a/tests/unit/commands/test_login.py
+++ /dev/null
@@ -1,284 +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 pathlib
-import re
-from unittest import mock
-
-import fixtures
-import pytest
-from simplejson.scanner import JSONDecodeError
-from testtools.matchers import Contains, Equals, MatchesRegex, Not
-
-from snapcraft import storeapi
-
-from . import 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,
- expires=None,
- save=True,
- config_fd=None,
- )
-
- def test_login_with_2fa(self):
- self.fake_store_login.mock.side_effect = [
- storeapi.http_clients.errors.StoreTwoFactorAuthenticationRequired(),
- 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,
- expires=None,
- save=True,
- config_fd=None,
- ),
- mock.call(
- email="user@example.com",
- password="secret",
- otp="123456",
- acls=None,
- packages=None,
- channels=None,
- expires=None,
- save=True,
- config_fd=None,
- ),
- ]
- )
-
- 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)
- )
-
- 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/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'}}}}"
+ )
diff --git a/tests/unit/commands/test_names.py b/tests/unit/commands/test_names.py
new file mode 100644
index 0000000000..f16d51c979
--- /dev/null
+++ b/tests/unit/commands/test_names.py
@@ -0,0 +1,238 @@
+# -*- 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 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_message("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_message("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_messages(["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_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'."""
+ ),
+ intermediate=True,
+ )
+ emitter.assert_message(
+ "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_message("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)]
+ if command_class.hidden:
+ emitter.assert_progress("This command is deprecated: use 'names' instead")
+ emitter.assert_message(
+ dedent(
+ """\
+ Name Since Visibility Notes
+ test-snap-private 2016-07-26T20:18:32Z private -
+ test-snap-public 2016-07-26T20:18:32Z public -"""
+ )
+ )
diff --git a/tests/unit/commands/test_register.py b/tests/unit/commands/test_register.py
deleted file mode 100644
index 31a48542fa..0000000000
--- a/tests/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 import storeapi
-
-from . import 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 = [
- storeapi.http_clients.errors.InvalidCredentialsError("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/test_release.py b/tests/unit/commands/test_release.py
deleted file mode 100644
index 3c14549a36..0000000000
--- a/tests/unit/commands/test_release.py
+++ /dev/null
@@ -1,281 +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 import storeapi
-from snapcraft.storeapi.v2.channel_map import (
- MappedChannel,
- Progressive,
- Revision,
- SnapChannel,
-)
-
-from . import 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 = [
- storeapi.http_clients.errors.InvalidCredentialsError("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/unit/commands/test_status.py b/tests/unit/commands/test_status.py
index 2f45e2e48f..ce580fda0f 100644
--- a/tests/unit/commands/test_status.py
+++ b/tests/unit/commands/test_status.py
@@ -1,468 +1,605 @@
-# -*- 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 import storeapi
-from snapcraft.storeapi.v2.channel_map import (
- MappedChannel,
- Progressive,
- Revision,
- SnapChannel,
-)
-
-from . import 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 = [
- storeapi.http_clients.errors.InvalidCredentialsError("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 ↑ ↑
- """
- )
+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
- 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")
- )
+ cmd = commands.StoreStatusCommand(None)
- 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 ↑ ↑
- """
- )
- ),
+ cmd.run(
+ argparse.Namespace(
+ name="test-snap",
+ arch=None,
+ track=None,
)
+ )
- def test_status_no_releases(self):
- self.channel_map.channel_map = []
+ emitter.assert_message(
+ "Track Arch Channel Version Revision Progress\n"
+ "2.1 amd64 stable 10 20 -\n"
+ " candidate ↑ ↑ -\n"
+ " beta ↑ ↑ -\n"
+ " edge ↑ ↑ -"
+ ""
+ )
- 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.")
- )
+@pytest.mark.usefixtures("memory_keyring")
+def test_no_releases(emitter, fake_store_get_status_map, channel_map_result):
+ channel_map_result.channel_map = []
- 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")
- )
+ cmd = commands.StoreStatusCommand(None)
- result = self.run_command(
- ["status", "snap-test", "--experimental-progressive-releases"]
+ cmd.run(
+ argparse.Namespace(
+ name="test-snap",
+ arch=None,
+ track=None,
)
+ )
- 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 ↑ ↑ -
- """
- )
- ),
- )
+ emitter.assert_message("This snap has no released revisions")
- 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 ↑ ↑
- """
- )
+@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
),
)
-
- 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 ↑ ↑
- """
- )
+ )
+ 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
),
)
-
- 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 ↑ ↑
- """
- )
+ )
+ 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
),
)
-
- 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 ↑ ↑
- """
- )
+ )
+ 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
),
)
-
- 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 ↑ ↑
- """
- )
+ )
+ 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
),
)
-
- 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 ↑ ↑
- """
- )
+ )
+ 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
),
)
-
- 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"]
+ )
+ 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
+ ),
)
-
- 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 ↑ ↑ -
- """
- )
+ )
+ 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)
- 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"]
- )
+ cmd.run(argparse.Namespace(name="test-snap"))
- 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 ↑ ↑ -
- """
- )
- ),
- )
+ emitter.assert_message(
+ "Name Status Creation-Date Version-Pattern\n"
+ "latest active - -\n"
+ "2.0 default 2019-10-17T14:11:59Z 2\\.*"
+ )
diff --git a/tests/unit/commands/test_upload.py b/tests/unit/commands/test_upload.py
index a7757c8ac5..5e71ad4f38 100644
--- a/tests/unit/commands/test_upload.py
+++ b/tests/unit/commands/test_upload.py
@@ -1,565 +1,132 @@
-# -*- 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 logging
-import os
-from unittest import mock
-
-import fixtures
-from testtools.matchers import Contains, Equals, FileExists, Not
-from xdg import BaseDirectory
-
-import tests
-from snapcraft import file_utils, internal, storeapi
-from snapcraft.internal import review_tools
-from snapcraft.storeapi.errors import (
- StoreDeltaApplicationError,
- StoreUpDownError,
- StoreUploadError,
-)
-
-from . import FakeStoreCommandsBaseTestCase
-
-
-class UploadCommandBaseTestCase(FakeStoreCommandsBaseTestCase):
- def setUp(self):
- super().setUp()
-
- self.snap_file = os.path.join(
- os.path.dirname(tests.__file__), "data", "test-snap.snap"
- )
-
- self.fake_review_tools_run = fixtures.MockPatch(
- "snapcraft.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
- )
- 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,
+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,
- 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.__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(
+ assert fake_store_verify_upload.mock_calls == [call(ANY, snap_name="basic")]
+ assert fake_store_notify_upload.mock_calls == [
+ call(
+ ANY,
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 = [
- storeapi.http_clients.errors.InvalidCredentialsError("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.__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,
+ upload_id="2ecbfac1-3448-4e7d-85a4-7919b999f120",
built_at=None,
channels=None,
- delta_format=None,
- delta_hash=None,
- source_hash=None,
- target_hash=None,
+ snap_file_size=4096,
)
+ ]
+ emitter.assert_message("Revision 10 created for 'basic'")
- 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,
- )
+@pytest.mark.usefixtures("memory_keyring")
+def test_default_channels(
+ emitter, fake_store_notify_upload, fake_store_verify_upload, snap_file
+):
+ cmd = commands.StoreUploadCommand(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"]
+ cmd.run(
+ argparse.Namespace(
+ snap_file=snap_file,
+ channels="stable,edge",
)
+ )
- 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(
+ assert fake_store_verify_upload.mock_calls == [call(ANY, snap_name="basic")]
+ assert fake_store_notify_upload.mock_calls == [
+ call(
+ ANY,
snap_name="basic",
- snap_filename=self.snap_file,
+ upload_id="2ecbfac1-3448-4e7d-85a4-7919b999f120",
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"]
+ channels=["stable", "edge"],
+ snap_file_size=4096,
)
+ ]
+ emitter.assert_message(
+ "Revision 10 created for 'basic' and released to 'edge' and 'stable'"
+ )
- 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."
- ),
- )
+def test_invalid_file():
+ cmd = commands.StoreUploadCommand(None)
-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",
+ with pytest.raises(craft_cli.errors.ArgumentParsingError) as raised:
+ cmd.run(
+ argparse.Namespace(
+ snap_file="invalid.snap",
+ channels=None,
+ )
)
- 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._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))
-
- class _FakeResponse:
- status_code = 501
- reason = "disabled"
-
- def json(self):
- return {
- "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,
- ]
-
- # Upload and ensure fallback is called
- with mock.patch("snapcraft.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",
- ]
- )
+ assert str(raised.value) == "'invalid.snap' is not a valid file"
diff --git a/tests/unit/commands/test_version.py b/tests/unit/commands/test_version.py
index 78d077ed79..0f2c11043a 100644
--- a/tests/unit/commands/test_version.py
+++ b/tests/unit/commands/test_version.py
@@ -1,6 +1,6 @@
# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*-
#
-# Copyright (C) 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,21 +13,14 @@
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
-from testtools.matchers import Equals
-from . import CommandBaseTestCase
+from argparse import Namespace
+from snapcraft import __version__
+from snapcraft.commands.version import VersionCommand
-class VersionCommandTestCase(CommandBaseTestCase):
- def test_has_version(self):
- result = self.run_command(["--version"])
- self.assertThat(result.exit_code, Equals(0))
- def test_has_version_without_hyphens(self):
- result = self.run_command(["version"])
- self.assertThat(result.exit_code, Equals(0))
-
- def test_method_return_same_value(self):
- result1 = self.run_command(["version"])
- result2 = self.run_command(["--version"])
- self.assertEqual(result1.output, result2.output)
+def test_version_command(emitter):
+ cmd = VersionCommand(None)
+ cmd.run(Namespace())
+ emitter.assert_message(f"snapcraft {__version__}")
diff --git a/tests/unit/commands/test_whoami.py b/tests/unit/commands/test_whoami.py
deleted file mode 100644
index 285d276b97..0000000000
--- a/tests/unit/commands/test_whoami.py
+++ /dev/null
@@ -1,51 +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.storeapi.v2 import whoami
-from snapcraft.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("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/conftest.py b/tests/unit/conftest.py
index 9e73452b65..c42df88b81 100644
--- a/tests/unit/conftest.py
+++ b/tests/unit/conftest.py
@@ -1,6 +1,6 @@
# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*-
#
-# Copyright (C) 2020 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
@@ -14,95 +14,186 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
-import os
-import pathlib
-from typing import List
-from unittest import mock
+from typing import Any, Dict, Optional, Tuple
import pytest
-import xdg
+from snapcraft import extensions
-def pytest_generate_tests(metafunc):
- idlist = []
- argvalues = []
- if metafunc.cls is None:
- return
- for scenario in metafunc.cls.scenarios:
- idlist.append(scenario[0])
- items = scenario[1].items()
- argnames = [x[0] for x in items]
- argvalues.append([x[1] for x in items])
- metafunc.parametrize(argnames, argvalues, ids=idlist, scope="class")
+@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 mock_subprocess_run():
- """A no-op subprocess.run mock."""
- patcher = mock.patch("subprocess.run")
- yield patcher.start()
- patcher.stop()
+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 tmp_work_path(tmp_path):
- """Setup a temporary directory and chdir to it."""
- os.chdir(tmp_path)
- return tmp_path
+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 xdg_dirs(tmp_path, monkeypatch):
- """Setup XDG directories in a temporary directory."""
- monkeypatch.setattr(
- xdg.BaseDirectory, "xdg_config_home", (tmp_path / ".config").as_posix()
- )
- monkeypatch.setattr(
- xdg.BaseDirectory, "xdg_data_home", (tmp_path / ".local").as_posix()
- )
- monkeypatch.setattr(
- xdg.BaseDirectory, "xdg_cache_home", (tmp_path / ".cache").as_posix()
- )
- monkeypatch.setattr(
- xdg.BaseDirectory,
- "xdg_config_dirs",
- lambda: [(tmp_path / ".config").as_posix()],
- )
- monkeypatch.setattr(
- xdg.BaseDirectory, "xdg_data_dirs", lambda: [(tmp_path / ".config").as_posix()]
- )
-
- monkeypatch.setenv("XDG_CONFIG_HOME", (tmp_path / ".config").as_posix())
- monkeypatch.setenv("XDG_DATA_HOME", (tmp_path / ".local").as_posix())
- monkeypatch.setenv("XDG_CACHE_HOME", (tmp_path / ".cache").as_posix())
-
- return tmp_path
-
-
-@pytest.fixture()
-def in_snap(monkeypatch):
- """Simualte being run from within the context of the Snapcraft snap."""
- monkeypatch.setenv("SNAP", "/snap/snapcraft/current")
- monkeypatch.setenv("SNAP_NAME", "snapcraft")
- monkeypatch.setenv("SNAP_VERSION", "4.0")
-
-
-@pytest.fixture()
-def fake_exists(monkeypatch):
- """Fakely return True when checking for preconfigured paths."""
-
- class FileCheck:
- def __init__(self) -> None:
- self._original_exists = os.path.exists
- self.paths: List[str] = list()
-
- def exists(self, path: str) -> bool:
- if pathlib.Path(path) in self.paths:
- return True
- return self._original_exists(path)
-
- file_checker = FileCheck()
- monkeypatch.setattr(os.path, "exists", file_checker.exists)
-
- return file_checker
+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")
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..a335543170
--- /dev/null
+++ b/tests/unit/extensions/conftest.py
@@ -0,0 +1,164 @@
+# -*- 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):
+ """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")
diff --git a/tests/unit/extensions/test_extensions.py b/tests/unit/extensions/test_extensions.py
new file mode 100644
index 0000000000..e1ddb89ce4
--- /dev/null
+++ b/tests/unit/extensions/test_extensions.py
@@ -0,0 +1,225 @@
+# -*- 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_message(
+ "*EXPERIMENTAL* extension 'fake-extension-experimental' enabled",
+ intermediate=True,
+ )
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
+ )
diff --git a/tests/unit/meta/test_appstream.py b/tests/unit/meta/test_appstream.py
new file mode 100644
index 0000000000..2acc27df9d
--- /dev/null
+++ b/tests/unit/meta/test_appstream.py
@@ -0,0 +1,805 @@
+# -*- 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 import errors
+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}{key}>
+ """
+ )
+ )
+
+ 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:
+
+ 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:
+
+ 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:
+
+ Build snaps .
+ Construye snaps.
+ Publish snaps to the store.
+ Publica snaps en la tienda.
+
+ 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:
+
+ Build snaps
.
+ Construye snaps.
+ Publish snaps to the store.
+ Publica snaps en la tienda.
+
+ 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:
+
+ Build snaps.
+ Construye snaps.
+ Publish snaps to the store.
+ Publica snaps en la tienda.
+
+ 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_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': "
+ "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:
+ """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"),
+ ]
diff --git a/tests/unit/meta/test_snap_yaml.py b/tests/unit/meta/test_snap_yaml.py
new file mode 100644
index 0000000000..ce2411dcc8
--- /dev/null
+++ b/tests/unit/meta/test_snap_yaml.py
@@ -0,0 +1,294 @@
+# -*- 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.
+
+ 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.
+ 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
+ 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
+ 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: devel
+ confinement: strict
+
+ environment:
+ GLOBAL_VARIABLE: test-global-variable
+
+ parts:
+ part1:
+ plugin: nil
+
+ 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
+ sockets:
+ test-socket-1:
+ listen-stream: /tmp/test-socket.sock
+ socket-mode: 0
+ 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
+
+ hooks:
+ configure:
+ command-chain: ["test"]
+ environment:
+ test-variable-1: "test"
+ test-variable-2: "test"
+ plugs:
+ - home
+ - network
+ 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)
+ yield Project.unmarshal(data)
+
+
+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()
+
+ 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
+ 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
+ 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: devel
+ 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
+ hooks:
+ configure:
+ command-chain:
+ - test
+ environment:
+ test-variable-1: test
+ test-variable-2: test
+ plugs:
+ - home
+ - network
+ 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/parts/__init__.py b/tests/unit/parts/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
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}"
+ )
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_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"],
+ },
+ }
diff --git a/tests/unit/parts/test_lifecycle.py b/tests/unit/parts/test_lifecycle.py
new file mode 100644
index 0000000000..eca7c4e724
--- /dev/null
+++ b/tests/unit/parts/test_lifecycle.py
@@ -0,0 +1,779 @@
+# -*- 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 PropertyMock, call
+
+import pytest
+from craft_parts import Action, Step
+
+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 MANDATORY_ADOPTABLE_FIELDS, Project
+
+_SNAPCRAFT_YAML_FILENAMES = [
+ "snap/snapcraft.yaml",
+ "build-aux/snap/snapcraft.yaml",
+ "snapcraft.yaml",
+ ".snapcraft.yaml",
+]
+
+
+@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(
+ *, base: str, filename: str = "snap/snapcraft.yaml"
+ ) -> Dict[str, Any]:
+ content = textwrap.dedent(
+ f"""
+ name: mytest
+ version: '0.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",
+ "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.",
+ "type": None,
+ "confinement": "strict",
+ "icon": None,
+ "layout": None,
+ "license": None,
+ "grade": "stable",
+ "architectures": [],
+ "package-repositories": [],
+ "assumes": [],
+ "hooks": None,
+ "passthrough": None,
+ "apps": None,
+ "plugs": None,
+ "slots": None,
+ "parts": {"part1": {"plugin": "nil"}},
+ "epoch": None,
+ }
+
+ 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:
+ parts_lifecycle.run("pull", argparse.Namespace())
+
+ assert str(raised.value) == (
+ "Could not find snap/snapcraft.yaml. Are you sure you are in the right "
+ "directory?"
+ )
+ assert raised.value.resolution == "To 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_command_mock = mocker.patch("snapcraft.parts.lifecycle._run_command")
+
+ parts_lifecycle.run(
+ "pull",
+ argparse.Namespace(
+ parts=["part1"], destructive_mode=True, use_lxd=False, provider=None
+ ),
+ )
+
+ 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=project,
+ parse_info={},
+ assets_dir=assets_dir,
+ parsed_args=argparse.Namespace(
+ parts=["part1"], destructive_mode=True, use_lxd=False, provider=None
+ ),
+ ),
+ ]
+
+
+@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")
+ 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", "snap", "clean"])
+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",
+ [
+ ("pull", "pull"),
+ ("build", "build"),
+ ("stage", "stage"),
+ ("prime", "prime"),
+ ],
+)
+@pytest.mark.parametrize("debug_shell", [None, "debug", "shell", "shell_after"])
+def test_lifecycle_run_command_step(
+ 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=parsed_args
+ )
+
+ 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 == []
+
+
+@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(
+ 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=False,
+ use_lxd=False,
+ parts=[],
+ ),
+ )
+
+ 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")
+ ]
+
+
+@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")
+ 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(
+ 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=False,
+ use_lxd=False,
+ parts=[],
+ ),
+ )
+
+ assert run_in_provider_mock.mock_calls == []
+ 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")
+ ]
+
+
+@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")
+ 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(
+ cmd,
+ project=project,
+ parse_info={},
+ assets_dir=Path(),
+ 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", debug=False, shell=False, shell_after=False)
+ ]
+ assert pack_mock.mock_calls == [
+ call(new_dir / "home/prime", output=None, compression="xz")
+ ]
+
+
+@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(
+ cmd,
+ project=project,
+ parse_info={},
+ assets_dir=Path(),
+ parsed_args=argparse.Namespace(
+ directory=None,
+ output=None,
+ destructive_mode=False,
+ use_lxd=False,
+ parts=[],
+ ),
+ )
+
+ assert run_mock.mock_calls == []
+ assert run_in_provider_mock.mock_calls == [
+ call(
+ project,
+ cmd,
+ argparse.Namespace(
+ directory=None,
+ output=None,
+ destructive_mode=False,
+ use_lxd=False,
+ parts=[],
+ ),
+ )
+ ]
+
+
+@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(),
+ parse_info={},
+ parsed_args=argparse.Namespace(
+ directory=None,
+ output=None,
+ debug=False,
+ destructive_mode=False,
+ shell=False,
+ shell_after=False,
+ use_lxd=False,
+ parts=[],
+ ),
+ )
+
+ assert str(raised.value) == (
+ "error setting grade: unexpected value; permitted: 'stable', 'devel'"
+ )
+ assert run_mock.mock_calls == [
+ call("prime", debug=False, shell=False, shell_after=False)
+ ]
+ assert pack_mock.mock_calls == []
+
+
+@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")
+ yaml_data.pop(field)
+ yaml_data["adopt-info"] = "part"
+ project = Project.unmarshal(yaml_data)
+
+ with pytest.raises(errors.SnapcraftError) as raised:
+ 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"
+
+
+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,
+ parse_info={},
+ 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,
+ parse_info={},
+ 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,
+ parse_info={},
+ 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,
+ parse_info={},
+ 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,
+ parse_info={},
+ 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"])]
+
+
+@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")
+ 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",
+ "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"}
+
+
+def test_get_snap_project_no_base(snapcraft_yaml, new_dir):
+ 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"
+ )
+
+
+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",
+ "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
new file mode 100644
index 0000000000..f058c78e04
--- /dev/null
+++ b/tests/unit/parts/test_parts.py
@@ -0,0 +1,192 @@
+# -*- 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
+from unittest.mock import ANY, call
+
+import craft_parts
+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(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,
+ 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")
+ 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_build_snaps=["core22"],
+ project_name="test-project",
+ project_vars_part_name=None,
+ project_vars={"version": "1", "grade": "stable"},
+ )
+ ]
+ emitter.assert_progress(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,
+ assets_dir=new_dir,
+ part_names=[],
+ package_repositories=[],
+ adopt_info=None,
+ parse_info={},
+ project_name="test-project",
+ project_vars={"version": "1", "grade": "stable"},
+ )
+ 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,
+ assets_dir=new_dir,
+ part_names=[],
+ 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"))
+ 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,
+ assets_dir=new_dir,
+ part_names=[],
+ package_repositories=[],
+ adopt_info=None,
+ project_name="test-project",
+ parse_info={},
+ 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'."
+ )
+
+
+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_name="test-project",
+ parse_info={},
+ project_vars={"version": "1", "grade": "stable"},
+ )
+ lifecycle.clean(part_names=None)
+ emitter.assert_message("Cleaning all parts", intermediate=True)
+
+
+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_name="test-project",
+ parse_info={},
+ project_vars={"version": "1", "grade": "stable"},
+ )
+ lifecycle.clean(part_names=["p1"])
+ emitter.assert_message("Cleaning parts: p1", intermediate=True)
+
+
+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,
+ part_names=[],
+ package_repositories=[
+ {
+ "type": "apt",
+ "ppa": "test/somerepo",
+ },
+ ],
+ adopt_info=None,
+ project_name="test-project",
+ parse_info={},
+ project_vars={"version": "1", "grade": "stable"},
+ extra_build_snaps=["core22"],
+ )
+ 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_build_snaps=["core22"],
+ project_name="test-project",
+ project_vars_part_name=None,
+ project_vars={"version": "1", "grade": "stable"},
+ )
+ ]
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."
+ )
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:
+
+ Build snaps.
+ Construye snaps.
+ Publish snaps to the store.
+ Publica snaps en la tienda.
+
+
+
+ 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/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)
diff --git a/tests/unit/parts/test_yaml_utils.py b/tests/unit/parts/test_yaml_utils.py
new file mode 100644
index 0000000000..930fb5d8a1
--- /dev/null
+++ b/tests/unit/parts/test_yaml_utils.py
@@ -0,0 +1,124 @@
+# -*- 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(
+ """\
+ base: core22
+ entry:
+ sub-entry:
+ - list1
+ - list2
+ scalar: scalar-value
+ """
+ )
+ )
+ )
+ == {
+ "base": "core22",
+ "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(
+ """\
+ base: core22
+ entry: value1
+ entry: value2
+ """
+ )
+ )
+ )
+
+ assert str(raised.value) == dedent(
+ """\
+ snapcraft.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(
+ """\
+ base: core22
+ entry: {{value}}
+ """
+ )
+ )
+ )
+
+ assert str(raised.value) == dedent(
+ """\
+ snapcraft.yaml parsing error: while constructing a mapping
+ in "", line 2, column 8
+ found unhashable key
+ 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"
diff --git a/tests/unit/project_loader/grammar/test_compound_statement.py b/tests/unit/project_loader/grammar/test_compound_statement.py
deleted file mode 100644
index c9b9df8c94..0000000000
--- a/tests/unit/project_loader/grammar/test_compound_statement.py
+++ /dev/null
@@ -1,269 +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
-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
-
-
-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.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.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/unit/project_loader/grammar/test_on_statement.py b/tests/unit/project_loader/grammar/test_on_statement.py
deleted file mode 100644
index 821af90c07..0000000000
--- a/tests/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
-import snapcraft.internal.project_loader.grammar._on as on
-from snapcraft.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.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.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.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/unit/project_loader/grammar/test_processor.py b/tests/unit/project_loader/grammar/test_processor.py
deleted file mode 100644
index 0a083c0649..0000000000
--- a/tests/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
-import snapcraft.internal.project_loader.grammar._to as _to
-from snapcraft.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.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.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.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.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/unit/project_loader/grammar/test_to_statement.py b/tests/unit/project_loader/grammar/test_to_statement.py
deleted file mode 100644
index a66a76832c..0000000000
--- a/tests/unit/project_loader/grammar/test_to_statement.py
+++ /dev/null
@@ -1,301 +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
-import snapcraft.internal.project_loader.grammar._to as to
-from snapcraft.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.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.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.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/unit/project_loader/grammar/test_try_statement.py b/tests/unit/project_loader/grammar/test_try_statement.py
deleted file mode 100644
index 32010fe0c9..0000000000
--- a/tests/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
-import snapcraft.internal.project_loader.grammar._try as _try
-from snapcraft.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.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.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/unit/repo/__init__.py b/tests/unit/repo/__init__.py
index 5148651223..e69de29bb2 100644
--- a/tests/unit/repo/__init__.py
+++ b/tests/unit/repo/__init__.py
@@ -1,32 +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 logging
-import tempfile
-
-import fixtures
-
-from tests import unit
-
-
-class RepoBaseTestCase(unit.TestCase):
- def setUp(self):
- super().setUp()
- fake_logger = fixtures.FakeLogger(level=logging.ERROR)
- self.useFixture(fake_logger)
- tempdirObj = tempfile.TemporaryDirectory()
- self.addCleanup(tempdirObj.cleanup)
- self.tempdir = tempdirObj.name
diff --git a/tests/unit/repo/test_apt_key_manager.py b/tests/unit/repo/test_apt_key_manager.py
index 26b610beff..4f3929b3e2 100644
--- a/tests/unit/repo/test_apt_key_manager.py
+++ b/tests/unit/repo/test_apt_key_manager.py
@@ -1,6 +1,6 @@
# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*-
#
-# Copyright (C) 2020 Canonical Ltd
+# 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
@@ -22,50 +22,45 @@
import gnupg
import pytest
-from snapcraft.internal.meta.package_repository import (
+from snapcraft.repo import apt_ppa, errors
+from snapcraft.repo.apt_key_manager import AptKeyManager
+from snapcraft.repo.package_repository import (
PackageRepositoryApt,
- PackageRepositoryAptPpa,
+ PackageRepositoryAptPPA,
)
-from snapcraft.internal.repo import apt_ppa, errors
-from snapcraft.internal.repo.apt_key_manager import AptKeyManager
@pytest.fixture(autouse=True)
-def mock_environ_copy():
- with mock.patch("os.environ.copy") as m:
- yield m
+def mock_environ_copy(mocker):
+ yield mocker.patch("os.environ.copy")
@pytest.fixture(autouse=True)
-def mock_gnupg(tmp_path, autouse=True):
- with mock.patch("gnupg.GPG", spec=gnupg.GPG) as m:
- m.return_value.import_keys.return_value.fingerprints = [
- "FAKE-KEY-ID-FROM-GNUPG"
- ]
- yield m
+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():
- with mock.patch("subprocess.run", spec=subprocess.run) as m:
- yield m
+def mock_run(mocker):
+ yield mocker.patch("subprocess.run", spec=subprocess.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",
+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",
- ) as m:
- yield m
+ )
@pytest.fixture
def key_assets(tmp_path):
- key_assets = tmp_path / "key-assets"
- key_assets.mkdir(parents=True)
- yield key_assets
+ assets = tmp_path / "key-assets"
+ assets.mkdir(parents=True)
+ yield assets
@pytest.fixture
@@ -138,7 +133,7 @@ def test_is_key_installed(
assert is_installed is expected
assert mock_run.mock_calls == [
call(
- ["sudo", "apt-key", "export", "foo"],
+ ["apt-key", "export", "foo"],
check=True,
stderr=subprocess.STDOUT,
stdout=subprocess.PIPE,
@@ -169,7 +164,7 @@ def test_install_key(
assert mock_run.mock_calls == [
call(
- ["sudo", "apt-key", "--keyring", str(gpg_keyring), "add", "-"],
+ ["apt-key", "--keyring", str(gpg_keyring), "add", "-"],
check=True,
env={"LANG": "C.UTF-8"},
input=b"some-fake-key",
@@ -184,11 +179,10 @@ def test_install_key_with_apt_key_failure(apt_gpg, mock_run):
cmd=["foo"], returncode=1, output=b"some error"
)
- with pytest.raises(errors.AptGPGKeyInstallError) as exc_info:
+ with pytest.raises(errors.AptGPGKeyInstallError) as raised:
apt_gpg.install_key(key="FAKEKEY")
- assert exc_info.value._output == "some error"
- assert exc_info.value._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):
@@ -197,7 +191,6 @@ def test_install_key_from_keyserver(apt_gpg, gpg_keyring, mock_run):
assert mock_run.mock_calls == [
call(
[
- "sudo",
"apt-key",
"--keyring",
str(gpg_keyring),
@@ -222,26 +215,25 @@ def test_install_key_from_keyserver_with_apt_key_failure(
cmd=["apt-key"], returncode=1, output=b"some error"
)
- with pytest.raises(errors.AptGPGKeyInstallError) as exc_info:
+ with pytest.raises(errors.AptGPGKeyInstallError) as raised:
apt_gpg.install_key_from_keyserver(
key_id="fake-key-id", key_server="fake-server"
)
- assert exc_info.value._output == "some error"
- assert exc_info.value._key_id == "fake-key-id"
+ assert str(raised.value) == "Failed to install GPG key: some error"
-@mock.patch("snapcraft.internal.repo.apt_key_manager.AptKeyManager.is_key_installed")
@pytest.mark.parametrize(
"is_installed",
[True, False],
)
def test_install_package_repository_key_already_installed(
- mock_is_key_installed,
- is_installed,
- apt_gpg,
+ is_installed, apt_gpg, mocker
):
- mock_is_key_installed.return_value = is_installed
+ 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,
@@ -255,17 +247,15 @@ def test_install_package_repository_key_already_installed(
assert updated is not is_installed
-@mock.patch(
- "snapcraft.internal.repo.apt_key_manager.AptKeyManager.is_key_installed",
- return_value=False,
-)
-@mock.patch("snapcraft.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,
-):
+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")
@@ -283,18 +273,15 @@ def test_install_package_repository_key_from_asset(
assert mock_install_key.mock_calls == [call(key="key-data")]
-@mock.patch(
- "snapcraft.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"
-)
-def test_install_package_repository_key_apt_from_keyserver(
- mock_install_key_from_keyserver,
- mock_is_key_installed,
- apt_gpg,
-):
+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(
@@ -313,22 +300,16 @@ def test_install_package_repository_key_apt_from_keyserver(
]
-@mock.patch(
- "snapcraft.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"
-)
-def test_install_package_repository_key_ppa_from_keyserver(
- mock_install_key_from_keyserver,
- mock_is_key_installed,
- apt_gpg,
-):
- package_repo = PackageRepositoryAptPpa(
- ppa="test/ppa",
+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
diff --git a/tests/unit/repo/test_apt_ppa.py b/tests/unit/repo/test_apt_ppa.py
index 1acaf19b0d..e65d23f147 100644
--- a/tests/unit/repo/test_apt_ppa.py
+++ b/tests/unit/repo/test_apt_ppa.py
@@ -1,6 +1,6 @@
# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*-
#
-# Copyright (C) 2020 Canonical Ltd
+# 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
@@ -15,25 +15,23 @@
# along with this program. If not, see .
-from unittest import mock
from unittest.mock import call
import launchpadlib
import pytest
-from snapcraft.internal.repo import apt_ppa, errors
+from snapcraft.repo import apt_ppa, errors
-@pytest.fixture
-def mock_launchpad(autouse=True):
- with mock.patch(
- "snapcraft.internal.repo.apt_ppa.Launchpad",
- spec=launchpadlib.launchpad.Launchpad,
- ) as m:
- m.login_anonymously.return_value.load.return_value.signing_key_fingerprint = (
- "FAKE-PPA-SIGNING-KEY"
- )
- yield m
+@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():
@@ -44,10 +42,12 @@ def test_split_ppa_parts():
def test_split_ppa_parts_invalid():
- with pytest.raises(errors.AptPPAInstallError) as exc_info:
+ with pytest.raises(errors.AptPPAInstallError) as raised:
apt_ppa.split_ppa_parts(ppa="ppa-missing-slash")
- assert exc_info.value._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(
diff --git a/tests/unit/repo/test_apt_sources_manager.py b/tests/unit/repo/test_apt_sources_manager.py
index 200e9afcd4..19d95fcf0f 100644
--- a/tests/unit/repo/test_apt_sources_manager.py
+++ b/tests/unit/repo/test_apt_sources_manager.py
@@ -1,6 +1,6 @@
# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*-
#
-# Copyright (C) 2021 Canonical Ltd
+# 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
@@ -15,69 +15,50 @@
# along with this program. If not, see .
-import pathlib
-import subprocess
from textwrap import dedent
-from unittest import mock
-from unittest.mock import call
import pytest
-from snapcraft.internal.meta.package_repository import (
+from snapcraft.repo import apt_ppa, apt_sources_manager, errors
+from snapcraft.repo.package_repository import (
PackageRepositoryApt,
- PackageRepositoryAptPpa,
+ PackageRepositoryAptPPA,
)
-from snapcraft.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",
+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",
- ) as m:
- yield m
-
-
-@pytest.fixture(autouse=True)
-def mock_environ_copy():
- with mock.patch("os.environ.copy") as m:
- yield m
+ )
@pytest.fixture(autouse=True)
-def mock_host_arch():
- with mock.patch("snapcraft.internal.repo.apt_sources_manager.ProjectOptions") as m:
- m.return_value.deb_arch = "FAKE-HOST-ARCH"
- yield m
+def mock_environ_copy(mocker):
+ yield mocker.patch("os.environ.copy")
@pytest.fixture(autouse=True)
-def mock_run():
- with mock.patch("subprocess.run") as m:
- yield m
+def mock_host_arch(mocker):
+ m = mocker.patch("snapcraft.utils.get_host_architecture")
+ m.return_value = "FAKE-HOST-ARCH"
+ yield m
-@pytest.fixture()
-def mock_sudo_write():
- 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"
- ) as m:
- m.side_effect = write_file
- yield m
+@pytest.fixture(autouse=True)
+def mock_run(mocker):
+ yield mocker.patch("subprocess.run")
@pytest.fixture(autouse=True)
-def mock_version_codename():
- with mock.patch(
- "snapcraft.internal.os_release.OsRelease.version_codename",
+def mock_version_codename(mocker):
+ yield mocker.patch(
+ "snapcraft.os_release.OsRelease.version_codename",
return_value="FAKE-CODENAME",
- ) as m:
- yield m
+ )
@pytest.fixture
@@ -90,55 +71,6 @@ def apt_sources_mgr(tmp_path):
)
-@mock.patch("tempfile.NamedTemporaryFile")
-@mock.patch("os.unlink")
-def test_sudo_write_file(mock_unlink, mock_tempfile, mock_run, tmp_path):
- mock_tempfile.return_value.__enter__.return_value.name = "/tmp/foobar"
-
- apt_sources_manager._sudo_write_file(dst_path="/foo/bar", content=b"some-content")
-
- assert mock_tempfile.mock_calls == [
- call(delete=False),
- call().__enter__(),
- call().__enter__().write(b"some-content"),
- call().__enter__().flush(),
- call().__exit__(None, None, None),
- ]
- assert mock_run.mock_calls == [
- call(
- [
- "sudo",
- "install",
- "--owner=root",
- "--group=root",
- "--mode=0644",
- "/tmp/foobar",
- "/foo/bar",
- ],
- check=True,
- )
- ]
- assert mock_unlink.mock_calls == [call("/tmp/foobar")]
-
-
-def test_sudo_write_file_fails(mock_run):
- mock_run.side_effect = subprocess.CalledProcessError(
- cmd=["sudo"], returncode=1, output=b"some error"
- )
-
- with pytest.raises(RuntimeError) as error:
- apt_sources_manager._sudo_write_file(
- dst_path="/foo/bar", content=b"some-content"
- )
-
- assert (
- str(error.value).startswith(
- "Failed to install repository config with: ['sudo', 'install'"
- )
- is True
- )
-
-
@pytest.mark.parametrize(
"package_repo,name,content",
[
@@ -216,7 +148,7 @@ def test_sudo_write_file_fails(mock_run):
).encode(),
),
(
- PackageRepositoryAptPpa(ppa="test/ppa"),
+ PackageRepositoryAptPPA(ppa="test/ppa"),
"snapcraft-ppa-test_ppa.sources",
dedent(
"""\
@@ -230,7 +162,7 @@ def test_sudo_write_file_fails(mock_run):
),
],
)
-def test_install(package_repo, name, content, apt_sources_mgr, mock_sudo_write):
+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(
@@ -239,29 +171,22 @@ def test_install(package_repo, name, content, apt_sources_mgr, mock_sudo_write):
assert changed is True
assert sources_path.read_bytes() == content
- assert mock_sudo_write.mock_calls == [
- call(
- content=content,
- dst_path=sources_path,
- )
- ]
# Verify a second-run does not incur any changes.
- mock_sudo_write.reset_mock()
-
changed = apt_sources_mgr.install_package_repository_sources(
package_repo=package_repo
)
assert changed is False
assert sources_path.read_bytes() == content
- assert mock_sudo_write.mock_calls == []
def test_install_ppa_invalid(apt_sources_mgr):
- repo = PackageRepositoryAptPpa(ppa="ppa-missing-slash")
+ repo = PackageRepositoryAptPPA(ppa="ppa-missing-slash")
- with pytest.raises(errors.AptPPAInstallError) as exc_info:
+ with pytest.raises(errors.AptPPAInstallError) as raised:
apt_sources_mgr.install_package_repository_sources(package_repo=repo)
- assert exc_info.value._ppa == "ppa-missing-slash"
+ assert str(raised.value) == (
+ "Failed to install PPA 'ppa-missing-slash': invalid PPA format"
+ )
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_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/repo/test_projects.py b/tests/unit/repo/test_projects.py
new file mode 100644
index 0000000000..f04bc73623
--- /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": "BCDEF12345" * 4,
+ },
+ {
+ "type": "apt",
+ "url": "https://some/url",
+ "key-id": "BCDEF12345" * 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 == "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
+ )
+ 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",
+ [
+ ("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):
+ 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": "ABCDE12345" * 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/store/http_client/test_candid_client.py b/tests/unit/store/http_client/test_candid_client.py
deleted file mode 100644
index 76390322c6..0000000000
--- a/tests/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.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/unit/store/http_client/test_config.py b/tests/unit/store/http_client/test_config.py
deleted file mode 100644
index 9bb641f858..0000000000
--- a/tests/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.storeapi.http_clients import errors
-from snapcraft.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/unit/store/http_client/test_errors.py b/tests/unit/store/http_client/test_errors.py
deleted file mode 100644
index 69453870d9..0000000000
--- a/tests/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.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/unit/store/http_client/test_ubuntu_one_auth_client.py b/tests/unit/store/http_client/test_ubuntu_one_auth_client.py
deleted file mode 100644
index 1ea07805aa..0000000000
--- a/tests/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.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/unit/store/v2/test_channel_map.py b/tests/unit/store/v2/test_channel_map.py
deleted file mode 100644
index 22d8bb4c47..0000000000
--- a/tests/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.storeapi.v2 import channel_map
-from tests 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/test_os_release.py b/tests/unit/test_os_release.py
index da25cfa4cc..80116c9cf0 100644
--- a/tests/unit/test_os_release.py
+++ b/tests/unit/test_os_release.py
@@ -1,6 +1,6 @@
# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*-
#
-# Copyright (C) 2015-2018 Canonical Ltd
+# 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
@@ -14,126 +14,135 @@
# 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
-from testtools.matchers import Equals
+import pytest
-from snapcraft.internal import errors, os_release
-from tests import unit
+from snapcraft import errors, os_release
-class OsReleaseTestCase(unit.TestCase):
- def _write_os_release(self, contents):
- path = "os-release"
- with open(path, "w") as f:
- f.write(contents)
- return path
+@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
- def test_blank_lines(self):
- release = os_release.OsRelease(
- os_release_file=self._write_os_release(
- dedent(
- """\
- NAME="Arch Linux"
+ return _release_data
- PRETTY_NAME="Arch Linux"
- ID=arch
- ID_LIKE=archlinux
- VERSION_ID="foo"
- VERSION_CODENAME="bar"
+
+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"
"""
- )
- )
)
-
- self.assertThat(release.id(), Equals("arch"))
- self.assertThat(release.name(), Equals("Arch Linux"))
- self.assertThat(release.version_id(), Equals("foo"))
- self.assertThat(release.version_codename(), Equals("bar"))
-
- def test_no_id(self):
- release = os_release.OsRelease(
- os_release_file=self._write_os_release(
- dedent(
- """\
- NAME="Arch Linux"
- PRETTY_NAME="Arch Linux"
- 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()
- self.assertRaises(errors.OsReleaseIdError, release.id)
-
- def test_no_name(self):
- release = os_release.OsRelease(
- os_release_file=self._write_os_release(
- dedent(
- """\
- ID=arch
- PRETTY_NAME="Arch Linux"
- ID_LIKE=archlinux
- VERSION_ID="foo"
- VERSION_CODENAME="bar"
+ 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"
- self.assertRaises(errors.OsReleaseNameError, release.name)
-
- def test_no_version_id(self):
- release = os_release.OsRelease(
- os_release_file=self._write_os_release(
- dedent(
- """\
- NAME="Arch Linux"
- ID=arch
- PRETTY_NAME="Arch Linux"
- ID_LIKE=archlinux
- VERSION_CODENAME="bar"
+
+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"
+
- self.assertRaises(errors.OsReleaseVersionIdError, release.version_id)
-
- def test_no_version_codename(self):
- """Test that version codename can also come from VERSION_ID"""
- release = os_release.OsRelease(
- os_release_file=self._write_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"
+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"
"""
- )
- )
)
+ )
- self.assertThat(release.version_codename(), Equals("trusty"))
-
- def test_no_version_codename_or_version_id(self):
- release = os_release.OsRelease(
- os_release_file=self._write_os_release(
- dedent(
- """\
- NAME="Ubuntu"
- ID=ubuntu
- ID_LIKE=debian
- PRETTY_NAME="Ubuntu 16.04.3 LTS"
+ 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()
- self.assertRaises(errors.OsReleaseCodenameError, release.version_codename)
+ assert str(raised.value) == "Unable to determine host OS version codename"
diff --git a/tests/unit/test_pack.py b/tests/unit/test_pack.py
new file mode 100644
index 0000000000..87158bc2b3
--- /dev/null
+++ b/tests/unit/test_pack.py
@@ -0,0 +1,97 @@
+# -*- 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,
+ universal_newlines=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,
+ universal_newlines=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,
+ universal_newlines=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,
+ universal_newlines=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,
+ universal_newlines=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."
+ )
diff --git a/tests/unit/test_projects.py b/tests/unit/test_projects.py
new file mode 100644
index 0000000000..9eadff80a1
--- /dev/null
+++ b/tests/unit/test_projects.py
@@ -0,0 +1,1136 @@
+# -*- 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 pydantic
+import pytest
+
+from snapcraft import errors
+from snapcraft.projects import (
+ MANDATORY_ADOPTABLE_FIELDS,
+ ContentPlug,
+ GrammarAwareProject,
+ Hook,
+ Project,
+)
+
+# pylint: disable=too-many-lines
+
+
+@pytest.fixture
+def project_yaml_data():
+ def _project_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 _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, project_yaml_data):
+ project = Project.unmarshal(project_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 is None
+ assert project.icon is None
+ 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
+ assert project.apps is None
+ assert project.plugs is None
+ 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"}})
+ 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.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 is None
+ assert app.plugs is None
+ assert app.aliases is None
+ assert app.environment is None
+ assert app.command_chain == []
+
+
+class TestProjectValidation:
+ """Validate top-level project items."""
+
+ @pytest.mark.parametrize("field", ["name", "confinement", "parts"])
+ 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):
+ 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, project_yaml_data):
+ data = project_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_adoptable_fields_definition(self):
+ assert MANDATORY_ADOPTABLE_FIELDS == (
+ "version",
+ "summary",
+ "description",
+ )
+
+ @pytest.mark.parametrize("field", MANDATORY_ADOPTABLE_FIELDS)
+ def test_adoptable_fields(self, field, project_yaml_data):
+ data = project_yaml_data()
+ data.pop(field)
+ error = f"Snap {field} is required if not using adopt-info"
+ with pytest.raises(errors.ProjectValidationError, match=error):
+ Project.unmarshal(data)
+
+ @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)
+ data["adopt-info"] = "part1"
+ project = Project.unmarshal(data)
+ assert getattr(project, field) is None
+
+ @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)
+ setattr(project, field, None)
+
+ @pytest.mark.parametrize(
+ "name",
+ [
+ "name",
+ "name-with-dashes",
+ "name0123",
+ "0123name",
+ "a234567890123456789012345678901234567890",
+ ],
+ )
+ 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(
+ "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, project_yaml_data):
+ with pytest.raises(errors.ProjectValidationError, match=error):
+ Project.unmarshal(project_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, project_yaml_data):
+ project = Project.unmarshal(project_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, project_yaml_data):
+ with pytest.raises(errors.ProjectValidationError, match=error):
+ 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, project_yaml_data):
+ data = project_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, project_yaml_data):
+ data = project_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, project_yaml_data):
+ data = project_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)
+
+ @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))
+ assert project.summary == summary
+
+ 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(project_yaml_data(summary=summary))
+
+ @pytest.mark.parametrize(
+ "epoch",
+ [
+ "0",
+ "1",
+ "1*",
+ "12345",
+ "12345*",
+ ],
+ )
+ 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(
+ "epoch",
+ [
+ "",
+ "invalid",
+ "0*",
+ "012345",
+ "-1",
+ "*1",
+ "1**",
+ ],
+ )
+ 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(project_yaml_data(epoch=epoch))
+
+ def test_project_package_repository(self, project_yaml_data):
+ repos = [
+ {
+ "type": "apt",
+ "ppa": "test/somerepo",
+ },
+ {
+ "type": "apt",
+ "url": "https://some/url",
+ "key-id": "ABCDE12345" * 4,
+ },
+ ]
+ project = Project.unmarshal(project_yaml_data(package_repositories=repos))
+ assert project.package_repositories == repos
+
+ def test_project_package_repository_missing_fields(self, project_yaml_data):
+ repos = [
+ {
+ "type": "apt",
+ },
+ ]
+ error = r".*- field 'url' required .*\n- field 'key-id' required"
+ with pytest.raises(errors.ProjectValidationError, match=error):
+ Project.unmarshal(project_yaml_data(package_repositories=repos))
+
+ def test_project_package_repository_extra_fields(self, project_yaml_data):
+ repos = [
+ {
+ "type": "apt",
+ "extra": "something",
+ },
+ ]
+ error = r".*- extra field 'extra' not permitted"
+ with pytest.raises(errors.ProjectValidationError, match=error):
+ Project.unmarshal(project_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, project_yaml_data):
+ project = Project.unmarshal(project_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, project_yaml_data):
+ error = ".*value is not a valid dict"
+ 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))
+
+ 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"]
+
+ @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."""
+
+ @pytest.mark.parametrize(
+ "hooks",
+ [
+ {"configure": {}},
+ {
+ "configure": {
+ "command-chain": ["test-1", "test-2"],
+ "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."""
+
+ 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, app_yaml_data):
+ data = app_yaml_data(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(
+ "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, app_yaml_data):
+ data = app_yaml_data(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, app_yaml_data):
+ data = app_yaml_data(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, app_yaml_data):
+ data = app_yaml_data(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, app_yaml_data):
+ data = app_yaml_data(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)
+
+ 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",
+ [
+ {"SINGLE_VARIABLE": "foo"},
+ {"FIRST_VARIABLE": "foo", "SECOND_VARIABLE": "bar"},
+ ],
+ )
+ 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
+
+ @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, 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 = 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)
+
+
+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"},
+ ],
+ }
+ }
+ )
+
+ error = r".*- 'try' was removed from grammar, use 'on ' instead"
+ with pytest.raises(errors.ProjectValidationError, match=error):
+ GrammarAwareProject.validate_grammar(data)
+
+ def test_grammar_type_error(self, project_yaml_data):
+ data = project_yaml_data(
+ parts={
+ "p1": {
+ "plugin": "nil",
+ "source": [
+ {"on amd64": [25]},
+ ],
+ }
+ }
+ )
+
+ error = r".*- value must be a string: \[25\]"
+ with pytest.raises(errors.ProjectValidationError, match=error):
+ GrammarAwareProject.validate_grammar(data)
+
+ def test_grammar_syntax_error(self, project_yaml_data):
+ data = project_yaml_data(
+ parts={
+ "p1": {
+ "plugin": "nil",
+ "source": [
+ {"on amd64,,arm64": "foo"},
+ ],
+ }
+ }
+ )
+
+ error = r".*- syntax error in 'on' selector"
+ with pytest.raises(errors.ProjectValidationError, match=error):
+ GrammarAwareProject.validate_grammar(data)
diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py
new file mode 100644
index 0000000000..9ae6c6a4c9
--- /dev/null
+++ b/tests/unit/test_utils.py
@@ -0,0 +1,244 @@
+# -*- 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 textwrap import dedent
+
+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)
+
+
+#####################
+# Get Host Platform #
+#####################
+
+
+@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
+ 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_machine,platform_architecture,deb_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_machine, platform_architecture, mocker, deb_arch
+):
+ """Test all platform mappings in addition to unknown."""
+ mocker.patch("platform.machine", return_value=platform_machine)
+ mocker.patch("platform.architecture", return_value=platform_architecture)
+
+ 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
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/tools/freeze-requirements.sh b/tools/freeze-requirements.sh
index b88d7d6cb0..5f0279c9af 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,10 +14,10 @@ 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"
+ sed -i '/^setuptools/d' "$req_file"
echo 'setuptools==49.6.0' >> "$req_file"
# Pinned pyinstaller for windows.
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