# Copyright 2022-2026 The Ramble Authors
#
# Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
# https://www.apache.org/licenses/LICENSE-2.0> or the MIT license
# <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your
# option. This file may not be copied, modified, or distributed
# except according to those terms.
import os
import shutil
import pytest
from ramble.error import RambleCommandError
from ramble.main import RambleCommand
from ramble.repository import BadRepoError
import spack.util.spack_yaml as syaml
repo = RambleCommand("repo")
[docs]
def test_help_option():
with pytest.raises(SystemExit):
repo("--help")
assert repo.returncode in (None, 0)
[docs]
def test_create_add_list_remove(mutable_config, tmpdir):
# Create a new repository and check that the expected
# files are there
repo("create", str(tmpdir), "mockrepo")
assert os.path.exists(os.path.join(str(tmpdir), "repo.yaml"))
assert os.path.exists(os.path.join(str(tmpdir), "applications"))
# Add the new repository and check it appears in the list output
repo("add", "-t", "applications", "--scope=site", str(tmpdir))
output = repo("list", "--scope=site", output=str)
assert "mockrepo" in output
# Then remove it and check it's not there
repo("remove", "--scope=site", str(tmpdir))
output = repo("list", "--scope=site", output=str)
assert "mockrepo" not in output
[docs]
@pytest.mark.parametrize("subdir", ["applications", "", "foo"])
def test_create_add_list_remove_flags(mutable_config, tmpdir, subdir):
# Create a new repository and check that the expected
# files are there
repo("create", str(tmpdir), "mockrepo", "-d", subdir)
assert os.path.exists(os.path.join(str(tmpdir), "repo.yaml"))
assert os.path.exists(os.path.join(str(tmpdir), subdir))
# Add the new repository and check it appears in the list output
repo("add", "-t", "applications", "--scope=site", str(tmpdir))
output = repo("list", "--scope=site", output=str)
assert "mockrepo" in output
# Then remove it and check it's not there
repo("remove", "--scope=site", str(tmpdir))
output = repo("list", "--scope=site", output=str)
assert "mockrepo" not in output
[docs]
def test_add_behavior(mutable_config, tmpdir):
# Create an app-only repo
repo("create", str(tmpdir), "mockrepo", "-t", "applications")
assert os.path.exists(os.path.join(str(tmpdir), "application_repo.yaml"))
assert os.path.exists(os.path.join(str(tmpdir), "applications"))
# Complains when specified repo type is not found
with pytest.raises(
BadRepoError, match="Failed to find valid repo with type ObjectTypes.modifiers"
):
repo("add", "-t", "modifiers", "--scope=site", str(tmpdir))
output = repo("list", "--scope=site", output=str)
assert "mockrepo" not in output
# Do not complain when type is not specified
repo("add", "--scope=site", str(tmpdir))
output = repo("list", "--scope=site", output=str)
assert "mockrepo" in output
# Complains if the given path contains no valid repo for all object types
shutil.rmtree(os.path.join(tmpdir, "applications"))
with pytest.raises(BadRepoError, match="not a valid repo for any object types"):
repo("add", "--scope=site", str(tmpdir))
[docs]
def test_remove_from_any_scope(mutable_config, tmpdir):
"""Tests that 'repo rm' without a scope removes from the correct scope."""
repo_path = str(tmpdir.join("test_repo"))
repo_name = "test_repo_namespace"
# Create a new repository
repo("create", repo_path, repo_name)
# Add the new repository to the 'site' scope
repo("add", "-t", "applications", "--scope=site", repo_path)
apps_repos_in_site = mutable_config.get("repos", scope="site")
assert repo_path in apps_repos_in_site, "Repo should be in site config after add."
# Check that it's in the list (merged scopes)
output = repo("list", output=str)
assert repo_name in output
print(output)
# Then remove it without specifying a scope
repo("remove", "--scope=site", repo_path)
# Check it's not in the site scope list anymore
output = repo("list", "--scope=site", output=str)
assert repo_name not in output
print(output)
apps_repos_in_site_after_remove = mutable_config.get("repos", scope="site")
assert (
repo_path not in apps_repos_in_site_after_remove
), "Repo should be removed from site config."
[docs]
def test_add_with_any_type_registers_all_types(mutable_config, tmpdir):
"""
Tests that `repo add` with `type=any` (the default) correctly registers
only the repo types that are actually present in the repository.
"""
repo_path = str(tmpdir)
repo_name = "partial_repo"
# Create a repository with both applications and modifiers
repo("create", repo_path, repo_name)
assert os.path.exists(os.path.join(str(tmpdir), "repo.yaml"))
assert os.path.exists(os.path.join(str(tmpdir), "applications"))
print(repo_path)
# Make the repo incomplete by removing the modifiers directory
shutil.rmtree(os.path.join(repo_path, "modifiers"))
# Add the repository without specifying a type (defaults to `any`)
repo("add", "--scope=site", repo_path)
# Check the configuration to ensure all types were registered
site_repos = mutable_config.get("repos", scope="site")
assert repo_path in site_repos
assert repo_path in site_repos
# The repo name should appear in the overall list
output = repo("list", output=str)
assert repo_name in output
[docs]
def test_add_non_existent_repo(mutable_config, tmpdir):
"""Tests that `repo add` with a non-existent path dies."""
non_existent_path = str(tmpdir.join("non_existent_repo"))
with pytest.raises(RambleCommandError) as e:
repo("add", non_existent_path)
# Check that the exception message contains the expected output
expected_error_message = f"No such file or directory: {non_existent_path}"
assert expected_error_message in str(e.value)
# Ensure the command indeed exited with code 1
assert "Command exited with code 1" in str(e.value)
[docs]
def test_add_repo_missing_config_file(mutable_config, tmpdir):
"""Tests that `repo add` with a directory missing repo.yaml raises BadRepoError directly."""
# Create a directory that will serve as our "repo"
repo_path = tmpdir.mkdir("empty_repo")
# This is normally created by `repo create`, but we are testing the case
# where it's missing (or invalid)
# Create a dummy repo.yaml and then remove it to simulate the missing file
repo_config_file = os.path.join(str(repo_path), "repo.yaml")
with open(repo_config_file, "w") as f:
syaml.dump({"repo": {"namespace": "test_namespace"}}, f)
os.remove(repo_config_file)
with pytest.raises(BadRepoError) as e: # Catch BadRepoError directly
repo("add", str(repo_path))
expected_error_message = f"No valid config file found in '{repo_path}'"
assert expected_error_message in str(e.value)