Skip to content
Open
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
39 changes: 34 additions & 5 deletions .github/workflows/tests-integration.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,43 @@ on: [pull_request, workflow_dispatch]
jobs:
integration_tests:
runs-on: ubuntu-latest
env:
TERRAFORM_VERSION: 1.9.8
OPENTOFU_VERSION: 1.9.1
BATS_LIB_PATH: /usr/local/lib/bats
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4

- name: build_image
- uses: actions/setup-python@v5
with:
python-version: '3.13'

- name: Install Leverage CLI
run: |
echo "[INFO] Building image"
make build-image
shell: bash
echo "[INFO] Installing the cli, the bats tests drive the installed command"
pip install -e .
leverage --version

- name: Install bats
run: |
echo "[INFO] Installing bats and its libraries"
git clone --depth 1 https://github.com/bats-core/bats-core.git /tmp/bats-core
sudo /tmp/bats-core/install.sh /usr/local
sudo mkdir -p "${BATS_LIB_PATH}"
sudo git clone --depth 1 https://github.com/bats-core/bats-support.git "${BATS_LIB_PATH}/bats-support"
sudo git clone --depth 1 https://github.com/bats-core/bats-assert.git "${BATS_LIB_PATH}/bats-assert"

- name: Install terraform and tofu
run: |
echo "[INFO] Installing terraform ${TERRAFORM_VERSION} and tofu ${OPENTOFU_VERSION}"
curl -fsSL -o /tmp/terraform.zip \
"https://releases.hashicorp.com/terraform/${TERRAFORM_VERSION}/terraform_${TERRAFORM_VERSION}_linux_amd64.zip"
curl -fsSL -o /tmp/tofu.zip \
"https://github.com/opentofu/opentofu/releases/download/v${OPENTOFU_VERSION}/tofu_${OPENTOFU_VERSION}_linux_amd64.zip"
sudo unzip -q -o /tmp/terraform.zip terraform -d /usr/local/bin
sudo unzip -q -o /tmp/tofu.zip tofu -d /usr/local/bin
terraform version
tofu version

- name: run_integration_tests
run: |
Expand Down
10 changes: 3 additions & 7 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
### Testing
- `poetry run pytest` - Run unit tests
- `poetry run pytest --verbose --cov=./ --cov-report=xml` - Run unit tests with coverage
- `make test-unit` - Run unit tests in Docker (with coverage)
- `make test-unit-no-cov` - Run unit tests in Docker (no coverage)
- `make test-int` - Run integration tests using bats in Docker
- `make test-unit` - Run unit tests (with coverage)
- `make test-unit-no-cov` - Run unit tests (no coverage)
- `make test-int` - Run integration tests using bats (requires bats, terraform and tofu)
- `make tests` - Run full test suite (unit + integration)

### Code Quality
Expand All @@ -28,10 +28,6 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
- `poetry build` - Build package using Poetry
- `make clean` - Clean build artifacts

### Docker
- `make build-image` - Build Docker testing image
- All test commands can run in Docker using the testing image

## Architecture

Leverage CLI is a Python-based command-line tool for managing Binbash Leverage projects. It uses host-based execution to run infrastructure tools directly on the system.
Expand Down
32 changes: 0 additions & 32 deletions Dockerfile

This file was deleted.

15 changes: 5 additions & 10 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
.PHONY: help build
LEVERAGE_TESTING_IMAGE := binbash/leverage-cli-testing
LEVERAGE_TESTING_TAG := 2.5.0
LEVERAGE_IMAGE_TAG := 1.3.5-0.2.0
PYPROJECT_FILE := pyproject.toml
INIT_FILE := leverage/__init__.py
PLACEHOLDER := 0.0.0
Expand All @@ -25,17 +22,15 @@ help:
@echo 'Available Commands:'
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | $(SORT) | awk 'BEGIN {FS = ":.*?## "}; {printf " - \033[36m%-18s\033[0m %s\n", $$1, $$2}'

build-image: ## Build docker image for testing
docker build . -t ${LEVERAGE_TESTING_IMAGE}:${LEVERAGE_TESTING_TAG}

test-unit: ## Run unit tests and create a coverage report
docker run --rm --privileged --mount type=bind,src=$(shell pwd),dst=/leverage -t ${LEVERAGE_TESTING_IMAGE}:${LEVERAGE_TESTING_TAG} pytest --verbose --cov=./ --cov-report=xml
pytest --verbose --cov=./leverage/ --cov-report=xml

test-unit-no-cov: ## Run unit tests with no coverage report
docker run --rm --privileged --mount type=bind,src=$(shell pwd),dst=/leverage -t ${LEVERAGE_TESTING_IMAGE}:${LEVERAGE_TESTING_TAG} pytest --verbose --no-cov
pytest --verbose --no-cov

test-int: ## Run integration tests
docker run --rm --privileged --mount type=bind,src=$(shell pwd),dst=/leverage --env LEVERAGE_IMAGE_TAG=${LEVERAGE_IMAGE_TAG} -t ${LEVERAGE_TESTING_IMAGE}:${LEVERAGE_TESTING_TAG} bash -c "bats --verbose-run --show-output-of-passing-tests --print-output-on-failure -T -t -p -r tests/bats"
# No formatter is forced: bats picks the pretty one on a terminal, and tap when there is none
test-int: ## Run integration tests (requires bats, terraform and tofu, see README)
bats --verbose-run --show-output-of-passing-tests --print-output-on-failure -T -r tests/bats

tests: test-unit-no-cov test-int ## Run full set of tests

Expand Down
22 changes: 11 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -183,12 +183,8 @@ poetry run pre-commit install
To run unit tests, pytest is the tool of choice, and the required dependencies are available in the
corresponding `dev-requirements.txt`.

Integration tests are implemented using [bats](https://github.com/bats-core/bats-core/). Bear in mind that bats tests
are meant to be run in a throwaway environment since they perform filesystem manipulations and installation and removal
of packages, and the cleanup may not be completely thorough. As such, is highly recommended to run these tests using the
docker image.

### Manually
Integration tests are implemented using [bats](https://github.com/bats-core/bats-core/). They drive the installed
`leverage` command, and work on temporary directories of their own, so they can be run directly on your machine.

1. Unit tests:

Expand All @@ -207,17 +203,21 @@ brew install bats-support
brew install bats-assert
```

The cli runs the infrastructure binaries directly, so `terraform` and `tofu` need to be installed as well. See
[System requirements](#system-requirements).

```bash
bats -r tests/bats
```

### Using docker image
If `bats-support` and `bats-assert` are not installed in a location bats searches by default, point `BATS_LIB_PATH` at
the directory holding them:

A Docker image suitable for running all tests can be crafted by running `make build-image`. After crafting the image all
tests can be executed.
```bash
BATS_LIB_PATH=/opt/homebrew/lib bats -r tests/bats
```

To run all tests, run `make tests`. Alternatively `make test-unit` or `make test-int` for unit or integration tests
respectively.
Alternatively, `make tests` runs both suites, and `make test-unit` or `make test-int` runs one of them.

## Release Process

Expand Down
7 changes: 0 additions & 7 deletions entrypoint.sh

This file was deleted.

7 changes: 7 additions & 0 deletions leverage/leverage.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,13 @@ def leverage(context, state, verbose):
state.config = conf.load()
except NotARepositoryError:
return

# The `project` commands bootstrap a project, so they run before its configuration exists.
# `project init` creates the git repository, so from that point on the config loads fine but
# still holds no project name, and building the paths would fail on a legitimate invocation.
if context.invoked_subcommand == project.name:
return

state.paths = PathsHandler(state.config)
state.environment = {
"AWS_SHARED_CREDENTIALS_FILE": str(state.paths.aws_credentials_file),
Expand Down
13 changes: 7 additions & 6 deletions tests/bats/leverage.bats
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
setup_file(){
echo "$(tput bold)========================== bats tests session starts ===========================" >&3
# No `tput` here: it needs a terminal, and there is none when running on CI
echo "========================== bats tests session starts ===========================" >&3
}

setup(){
# Bats modules are installed globally
load "/bats-support/load.bash"
load "/bats-assert/load.bash"
# Resolved through BATS_LIB_PATH
bats_load_library bats-support
bats_load_library bats-assert

# Store useful paths
TESTS_ROOT="$( cd "$( dirname "$BATS_TEST_FILENAME" )/.." >/dev/null 2>&1 && pwd )"
Expand Down Expand Up @@ -38,7 +39,7 @@ teardown(){
run leverage run -l

assert_line --partial "Tasks in build file \`build.py\`:"
assert_line --regexp "hello\s+Say hello."
assert_line --regexp "hello[[:space:]]+Say hello."
assert_line --regexp "Powered by Leverage [0-9]+.[0-9]+.[0-9]+"
}

Expand All @@ -52,7 +53,7 @@ teardown(){
run leverage run -l

assert_line --partial "Tasks in build file \`build.py\`:"
assert_line --regexp "hello\s+Say hello."
assert_line --regexp "hello[[:space:]]+Say hello."
assert_line --regexp "Powered by Leverage [0-9]+.[0-9]+.[0-9]+"
}

Expand Down
21 changes: 16 additions & 5 deletions tests/bats/leverage_terraform.bats
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
setup(){
# Bats modules are installed globally
load "/bats-support/load.bash"
load "/bats-assert/load.bash"
# Resolved through BATS_LIB_PATH
bats_load_library bats-support
bats_load_library bats-assert

# Store useful paths
TEST_ROOT="$( cd "$( dirname "$BATS_TEST_FILENAME" )/.." >/dev/null 2>&1 && pwd )"
Expand All @@ -14,13 +14,24 @@ teardown(){
cd "$TESTS_ROOT"
}

@test "Pulls terraform image and prints version" {
@test "Prints terraform version" {
ROOT_DIR=$(_create_leverage_directory_structure)

# Create required build.env in root directory and go there
cd "$ROOT_DIR"

run leverage terraform version

assert_output --regexp "[\S\s]*Terraform v[0-9]{1,2}\.[0-9]{1,2}\.[0-9]{1,2}[\s\S]*"
assert_output --regexp "Terraform v[0-9]{1,2}\.[0-9]{1,2}\.[0-9]{1,2}"
}

@test "Prints tofu version" {
ROOT_DIR=$(_create_leverage_directory_structure)

# Create required build.env in root directory and go there
cd "$ROOT_DIR"

run leverage tofu version

assert_output --regexp "OpenTofu v[0-9]{1,2}\.[0-9]{1,2}\.[0-9]{1,2}"
}
24 changes: 10 additions & 14 deletions tests/bats/no_git_leverage.bats
Original file line number Diff line number Diff line change
@@ -1,22 +1,18 @@
# The name of the file was chosen as to avoid repetition of the leverage package installing step
# in leverage.bats, since bats respects file name order to run the tests

setup(){
# Bats modules are installed globally
load "/bats-support/load.bash"
load "/bats-assert/load.bash"

# Uninstall git
apk del git >/dev/null 2>&1
}
# Resolved through BATS_LIB_PATH
bats_load_library bats-support
bats_load_library bats-assert

teardown(){
# Reinstall git
apk add git >/dev/null 2>&1
# A directory holding nothing but the leverage entry point, to be used as the whole PATH.
# The cli looks for git through `shutil.which`, which resolves it via PATH, and the console
# script has an absolute shebang, so its interpreter remains reachable.
GITLESS_PATH="$BATS_TEST_TMPDIR/gitless"
mkdir -p "$GITLESS_PATH"
ln -sf "$(command -v leverage)" "$GITLESS_PATH/leverage"
}

@test "Does not run if git is not installed in the system" {
run leverage
run env PATH="$GITLESS_PATH" leverage

assert_failure
assert_output "No git installation found in the system. Exiting."
Expand Down
7 changes: 5 additions & 2 deletions tests/bats/utils.bash
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,11 @@ _create_directory_structure(){
└ account
And print the root path
"
ROOT_DIR=$(mktemp -d -t tmpXXXXXX)
printf "PROJECT=ts\nTERRAFORM_IMAGE_TAG=%s\n" "$LEVERAGE_IMAGE_TAG" > $ROOT_DIR/"build.env"
# An explicit template keeps the name free of dots on both GNU and BSD mktemp. `-t` yields
# `tmp.XXXX` on macOS, and the build script is imported under a module name derived from the
# directory, which a dot turns into a package lookup that fails.
ROOT_DIR=$(mktemp -d "${TMPDIR:-/tmp}/leverageXXXXXX")
printf "PROJECT=ts\n" > $ROOT_DIR/"build.env"
mkdir -p "$ROOT_DIR/config"
mkdir -p "$ROOT_DIR/account"
echo $ROOT_DIR
Expand Down
32 changes: 32 additions & 0 deletions tests/test_modules/test_project.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,41 @@
import subprocess
from pathlib import Path

import pytest
from click.testing import CliRunner

from leverage import conf, leverage
from leverage import path as lepath
from leverage._utils import ExitError
from leverage.modules.project import validate_config


def test_project_commands_run_before_the_project_configuration_exists(tmp_path, monkeypatch):
"""
Test that the project commands do not require an already configured project.

`project init` creates the git repository, so from that point on the configuration loads fine
but holds no project name yet. Building the paths there aborts `project create` with
"Project name has not been set", on what is a perfectly legitimate invocation.
"""
root = tmp_path / "new-project"
root.mkdir()
subprocess.run(["git", "init"], cwd=root, check=True, capture_output=True)

monkeypatch.setattr(lepath, "get_root_path", lambda: root)
monkeypatch.setattr(lepath, "get_working_path", lambda: root)
monkeypatch.setattr(conf, "get_root_path", lambda: root)
monkeypatch.setattr(conf, "get_working_path", lambda: root)
monkeypatch.setattr(Path, "cwd", lambda: root)

result = CliRunner().invoke(leverage, ["project", "create"])

# The command is reached, and reports the missing configuration file on its own terms, rather
# than the run being aborted earlier while building the project paths.
assert "Project name has not been set" not in result.output
assert "No configuration file found for the project" in result.output


@pytest.mark.parametrize(
"project_name,short_name",
[
Expand Down
Loading