Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: duplicate code-workspace declarations #596

Merged
merged 4 commits into from
Nov 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ AlgoKit helps you develop Algorand solutions:

AlgoKit can help you deploy and operate Algorand solutions.

AlgoKit comes with out-of-the-box [Continuous Integration / Continuous Deployment (CI/CD) templates](https://github.com/algorandfoundation/algokit-beaker-default-template) that help you rapidly set up best-practice software delivery processes that ensure you build quality in and have a solution that can evolve
AlgoKit comes with out-of-the-box [Continuous Integration / Continuous Deployment (CI/CD) templates](https://github.com/algorandfoundation/algokit-python-template) that help you rapidly set up best-practice software delivery processes that ensure you build quality in and have a solution that can evolve

## What can AlgoKit help me do?

Expand Down Expand Up @@ -266,7 +266,7 @@ Per the above output, the doctor command output is a helpful tool if you need to

This section addresses specific edge cases and issues that some users might encounter when interacting with the CLI. The following table provides solutions to known edge cases:

| Issue Description | OS(s) with observed behaviour | Steps to mitigate | References |
| --------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------- |
| This scenario may arise if installed `python` was build without `--with-ssl` flag enabled, causing pip to fail when trying to install dependencies. | Debian 12 | Run `sudo apt-get install -y libssl-dev` to install the required openssl dependency. Afterwards, ensure to reinstall python with `--with-ssl` flag enabled. This includes options like [building python from source code](https://medium.com/@enahwe/how-to-06bc8a042345) or using tools like [pyenv](https://github.com/pyenv/pyenv). | https://github.com/actions/setup-python/issues/93 |
| `poetry install` invoked directly or via `algokit project bootstrap all` fails on `Could NOT find PkgConfig (missing: PKG_CONFIG_EXECUTABLE)`. | `MacOS` >=14 using `python` 3.13 installed via `homebrew` | Install dependencies deprecated in `3.13` and latest MacOS versions via `brew install pkg-config`, delete the virtual environment folder and retry the `poetry install` command invocation. | N/A |
| Issue Description | OS(s) with observed behaviour | Steps to mitigate | References |
| --------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------- |
| This scenario may arise if installed `python` was build without `--with-ssl` flag enabled, causing pip to fail when trying to install dependencies. | Debian 12 | Run `sudo apt-get install -y libssl-dev` to install the required openssl dependency. Afterwards, ensure to reinstall python with `--with-ssl` flag enabled. This includes options like [building python from source code](https://medium.com/@enahwe/how-to-06bc8a042345) or using tools like [pyenv](https://github.com/pyenv/pyenv). | <https://github.com/actions/setup-python/issues/93> |
| `poetry install` invoked directly or via `algokit project bootstrap all` fails on `Could NOT find PkgConfig (missing: PKG_CONFIG_EXECUTABLE)`. | `MacOS` >=14 using `python` 3.13 installed via `homebrew` | Install dependencies deprecated in `3.13` and latest MacOS versions via `brew install pkg-config`, delete the virtual environment folder and retry the `poetry install` command invocation. | N/A |
32 changes: 19 additions & 13 deletions src/algokit/core/init.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,20 +89,26 @@ def append_project_to_vscode_workspace(project_path: Path, workspace_path: Path)

try:
workspace = _load_vscode_workspace(workspace_path)
# Convert paths to POSIX format for consistent handling, and ensure relative paths are correctly interpreted
processed_project_path = project_path.relative_to(workspace_path.parent).as_posix()
# Normalize the new project path for comparison, ensuring it does not end with a slash unless it's the root
normalized_project_path = processed_project_path if processed_project_path != "." else "./"

# Normalize existing paths in the workspace for comparison
existing_paths = [
folder.get("path", "").rstrip("/").replace("\\", "/") for folder in workspace.get("folders", [])
]
# Ensure the normalized new path is not already in the workspace
if normalized_project_path not in existing_paths:
workspace.setdefault("folders", []).append({"path": processed_project_path})

# Compute the project path relative to the workspace root
processed_project_path = project_path.relative_to(workspace_path.parent)
project_abs_path = (workspace_path.parent / processed_project_path).resolve(strict=False)

# Gather existing paths as absolute paths
existing_abs_paths = []
for folder in workspace.get("folders", []):
folder_path = Path(folder.get("path", "").replace("\\", "/"))
existing_abs_path = (workspace_path.parent / folder_path).resolve(strict=False)
existing_abs_paths.append(existing_abs_path)

# Check if the project path is already in the workspace
if project_abs_path not in existing_abs_paths:
workspace.setdefault("folders", []).append({"path": str(processed_project_path).replace("\\", "/")})
_save_vscode_workspace(workspace_path, workspace)
logger.debug(f"Appended project {project_path} to workspace {workspace_path}.")
logger.debug(f"Appended project {project_path} to workspace {workspace_path}.")
else:
logger.debug(f"Project {project_path} is already in workspace {workspace_path}, not appending.")

except json.JSONDecodeError as json_err:
logger.warning(f"Invalid JSON format in the workspace file {workspace_path}. {json_err}")
except Exception as e:
Expand Down
143 changes: 115 additions & 28 deletions tests/init/test_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import click
import pytest
from _pytest.tmpdir import TempPathFactory
from algokit.core.init import append_project_to_vscode_workspace
from approvaltests.namer import NamerFactory
from approvaltests.pytest.py_test_namer import PyTestNamer
from approvaltests.scrubbers.scrubbers import Scrubber
Expand Down Expand Up @@ -88,9 +89,8 @@ class ExtendedTemplateKey(str, Enum):
TEALSCRIPT = "tealscript"
FULLSTACK = "fullstack"
REACT = "react"
BEAKER = "beaker"
PLAYGROUND = "playground"
BEAKER_WITH_VERSION = "beaker_with_version"
PYTHON_WITH_VERSION = "python_with_version"
SIMPLE = "simple"


Expand All @@ -106,28 +106,24 @@ def _set_blessed_templates(mocker: MockerFixture) -> None:

blessed_templates = {
ExtendedTemplateKey.SIMPLE: BlessedTemplateSource(
url="gh:robdmoore/copier-helloworld",
url="gh:algorandfoundation/algokit-base-template",
description="Does nothing helpful. simple",
),
ExtendedTemplateKey.BEAKER: BlessedTemplateSource(
url="gh:algorandfoundation/algokit-beaker-default-template",
description="Provides a good starting point to build Beaker smart contracts productively.",
),
ExtendedTemplateKey.BEAKER_WITH_VERSION: BlessedTemplateSource(
url="gh:algorandfoundation/algokit-beaker-default-template",
commit="96fc7fd766fac607cdf5d69ee6e85ade04dddd47",
description="Provides a good starting point to build Beaker smart contracts productively, but pinned.",
ExtendedTemplateKey.PYTHON_WITH_VERSION: BlessedTemplateSource(
url="gh:algorandfoundation/algokit-python-template",
commit="f97be2c0e3975adfaeb16ef07a2b4bd6ce2afcff",
description="Provides a good starting point to build python smart contracts productively, but pinned.",
),
ExtendedTemplateKey.FULLSTACK: BlessedTemplateSource(
url="gh:robdmoore/copier-helloworld",
url="gh:algorandfoundation/algokit-base-template",
description="Does nothing helpful. fullstack",
),
ExtendedTemplateKey.PYTHON: BlessedTemplateSource(
url="gh:robdmoore/copier-helloworld",
url="gh:algorandfoundation/algokit-python-template",
description="Does nothing helpful. python",
),
ExtendedTemplateKey.REACT: BlessedTemplateSource(
url="gh:robdmoore/copier-helloworld",
url="gh:algorandfoundation/algokit-base-template",
description="Does nothing helpful. react",
),
ExtendedTemplateKey.BASE: BlessedTemplateSource(
Expand Down Expand Up @@ -567,7 +563,7 @@ def test_init_template_url_and_ref(tmp_path_factory: TempPathFactory, mocker: Mo
cwd = tmp_path_factory.mktemp("cwd")
result = invoke(
"init --name myapp --no-git --no-bootstrap "
"--template-url gh:algorandfoundation/algokit-beaker-default-template "
"--template-url gh:algorandfoundation/algokit-python-template "
f"--template-url-ref {ref} "
"--UNSAFE-SECURITY-accept-template-url --no-workspace",
cwd=cwd,
Expand All @@ -585,7 +581,7 @@ def test_init_blessed_template_url_get_community_warning(
mock_questionary_input.send_text("N") # community warning
result = invoke(
"init --name myapp --no-git "
"--template-url gh:algorandfoundation/algokit-beaker-default-template --defaults "
"--template-url gh:algorandfoundation/algokit-python-template --defaults "
"-a author_name None -a author_email None ",
cwd=cwd,
)
Expand All @@ -601,7 +597,7 @@ def test_init_with_any_template_url_get_community_warning(
mock_questionary_input.send_text("Y")
result = invoke(
"init --name myapp --no-git --no-bootstrap "
"--template-url gh:algorandfoundation/algokit-beaker-default-template --defaults --no-workspace "
"--template-url gh:algorandfoundation/algokit-python-template --defaults --no-workspace "
"-a author_name None -a author_email None ",
cwd=cwd,
)
Expand All @@ -625,7 +621,7 @@ def test_init_with_any_template_url_get_community_warning_with_unsafe_tag(tmp_pa
cwd = tmp_path_factory.mktemp("cwd")
result = invoke(
"init --name myapp --no-git --no-bootstrap "
"--template-url gh:algorandfoundation/algokit-beaker-default-template --defaults --no-workspace "
"--template-url gh:algorandfoundation/algokit-python-template --defaults --no-workspace "
"-a author_name None -a author_email None --UNSAFE-SECURITY-accept-template-url",
cwd=cwd,
)
Expand Down Expand Up @@ -679,7 +675,7 @@ def test_init_with_official_template_name(tmp_path_factory: TempPathFactory) ->
cwd = tmp_path_factory.mktemp("cwd")

result = invoke(
"init --name myapp --no-git --no-bootstrap --template beaker --defaults --no-workspace "
"init --name myapp --no-git --no-bootstrap --template python --defaults --no-workspace "
"-a author_name None -a author_email None ",
cwd=cwd,
)
Expand All @@ -703,7 +699,7 @@ def test_init_with_official_template_name_and_hash(tmp_path_factory: TempPathFac
cwd = tmp_path_factory.mktemp("cwd")

result = invoke(
"init --name myapp --no-git --template beaker_with_version"
"init --name myapp --no-git --template python_with_version"
" --defaults -a run_poetry_install False -a author_name None -a author_email None --no-workspace ",
cwd=cwd,
)
Expand All @@ -725,7 +721,7 @@ def test_init_with_custom_env(tmp_path_factory: TempPathFactory) -> None:

result = invoke(
(
"init --name myapp --no-git --no-bootstrap --template beaker --defaults --no-workspace "
"init --name myapp --no-git --no-bootstrap --template python --defaults --no-workspace "
"-a author_name None -a author_email None "
'-a algod_token "abcdefghijklmnopqrstuvwxyz" -a algod_server http://mylocalserver -a algod_port 1234 '
'-a indexer_token "zyxwvutsrqponmlkjihgfedcba" -a indexer_server http://myotherserver -a indexer_port 6789 '
Expand Down Expand Up @@ -821,7 +817,7 @@ def test_init_template_with_python_task_works(dummy_algokit_template_with_python
],
[
MockQuestionaryAnswer("Custom Template", [MockPipeInput.UP, MockPipeInput.ENTER]),
"gh:robdmoore/copier-helloworld\n", # custom template URL
"gh:algorandfoundation/algokit-base-template\n", # custom template URL
],
],
)
Expand Down Expand Up @@ -896,7 +892,7 @@ def test_init_wizard_v2_github_folder_with_workspace(

# Act
result = invoke(
"init -t beaker --no-git --defaults --name myapp "
"init -t python --no-git --defaults --name myapp "
"--UNSAFE-SECURITY-accept-template-url -a preset_name 'production'",
cwd=cwd,
)
Expand All @@ -922,7 +918,7 @@ def test_init_wizard_v2_github_folder_with_workspace_partial(

# Act
result = invoke(
"init -t beaker --no-git --defaults --name myapp "
"init -t python --no-git --defaults --name myapp "
"--UNSAFE-SECURITY-accept-template-url -a preset_name 'production'",
input="y\n",
cwd=cwd,
Expand All @@ -931,7 +927,7 @@ def test_init_wizard_v2_github_folder_with_workspace_partial(
# Assert
cwd /= "myapp"
assert result.exit_code == 0
assert not (cwd / "projects/myapp/.github/workflows/production-beaker-cd.yaml").exists()
assert not (cwd / "projects/myapp/.github/workflows/cd.yaml").exists()
assert (cwd / ".github/workflows/myapp-cd.yaml").read_text() != ""
assert cwd.glob(".github/workflows/*.yaml")

Expand All @@ -947,7 +943,7 @@ def test_init_wizard_v2_github_folder_no_workspace(

# Act
result = invoke(
"init -t beaker --no-git --defaults --name myapp "
"init -t python --no-git --defaults --name myapp "
"--UNSAFE-SECURITY-accept-template-url -a preset_name 'production' --no-workspace",
cwd=cwd,
)
Expand Down Expand Up @@ -1008,7 +1004,7 @@ def test_init_wizard_v2_append_to_vscode_workspace(

# Act
project_a_result = invoke(
"init -t beaker --no-git --defaults --name myapp "
"init -t python --no-git --defaults --name myapp "
"--UNSAFE-SECURITY-accept-template-url -a preset_name 'production'",
cwd=cwd,
)
Expand All @@ -1017,7 +1013,7 @@ def test_init_wizard_v2_append_to_vscode_workspace(
workspace_file.write_text(workspace_content)

project_b_result = invoke(
"init -t beaker --no-git --defaults --name myapp2 "
"init -t python --no-git --defaults --name myapp2 "
"--UNSAFE-SECURITY-accept-template-url -a preset_name 'starter'",
cwd=cwd / "myapp",
)
Expand All @@ -1031,3 +1027,94 @@ def test_init_wizard_v2_append_to_vscode_workspace(
if expect_warning:
# This assumes the existence of a function `verify` to check for warnings in the output
verify(project_b_result.output)


@pytest.mark.parametrize(
("initial_workspace", "project_path", "expected_workspace", "should_append"),
[
# Test case 1: Different representations of root path
(
{"folders": [{"path": "./"}]},
".",
{"folders": [{"path": "./"}]},
False,
),
# Test case 2: Normalized paths
(
{"folders": [{"path": "projects/app1"}]},
"projects/app1",
{"folders": [{"path": "projects/app1"}]},
False,
),
# Test case 3: Different path separators
(
{"folders": [{"path": "projects\\app1"}]},
"projects/app1",
{"folders": [{"path": "projects\\app1"}]},
False,
),
# Test case 4: Relative paths
(
{"folders": [{"path": "./projects/app1"}]},
"projects/app1",
{"folders": [{"path": "./projects/app1"}]},
False,
),
# Test case 5: New unique path
(
{"folders": [{"path": "projects/app1"}]},
"projects/app2",
{"folders": [{"path": "projects/app1"}, {"path": "projects/app2"}]},
True,
),
# Test case 6: Empty workspace
(
{"folders": []},
"projects/app1",
{"folders": [{"path": "projects/app1"}]},
True,
),
# Test case 7: Path with trailing slash
(
{"folders": [{"path": "projects/app1/"}]},
"projects/app1",
{"folders": [{"path": "projects/app1/"}]},
False,
),
],
)
def test_append_to_workspace_path_normalization(
*,
tmp_path_factory: pytest.TempPathFactory,
initial_workspace: dict,
project_path: str,
expected_workspace: dict,
should_append: bool,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test various path normalization scenarios when appending to workspace."""

# Arrange
tmp_path = tmp_path_factory.mktemp("workspace")
workspace_file = tmp_path / "test.code-workspace"
with workspace_file.open("w") as f:
json.dump(initial_workspace, f)

project_path_obj = tmp_path / project_path
project_path_obj.mkdir(parents=True, exist_ok=True)

# Act
append_project_to_vscode_workspace(project_path_obj, workspace_file)

# Assert
with workspace_file.open("r") as f:
actual_workspace = json.load(f)

assert actual_workspace == expected_workspace

# Check logging
debug_messages = [r.message for r in caplog.records if r.levelname == "DEBUG"]
if should_append:
assert any("Appended project" in msg for msg in debug_messages)
else:
assert any("already in workspace" in msg for msg in debug_messages)
Loading