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

feat: add matrix strategy #118

Merged
merged 7 commits into from
Oct 15, 2024
Merged
Show file tree
Hide file tree
Changes from 5 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
22 changes: 22 additions & 0 deletions .makim.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ groups:
- task: smoke-tests.dir-relative-path
- task: smoke-tests.interactive-args
- task: smoke-tests.run-hooks
- task: smoke-tests.matrix

ci:
help: Run all tasks used on CI
Expand Down Expand Up @@ -391,6 +392,27 @@ groups:
makim $VERBOSE_FLAG --file $MAKIM_FILE --version
makim $VERBOSE_FLAG --file $MAKIM_FILE build.compile

matrix:
help: Test makim with matrix combination
args:
verbose-mode:
help: Run the all the tests in verbose mode
type: bool
action: store_true
env:
MAKIM_FILE: tests/smoke/.makim-matrix-strategy.yaml
backend: bash
run: |
export VERBOSE_FLAG='${{ "--verbose" if args.verbose_mode else "" }}'
makim $VERBOSE_FLAG --file $MAKIM_FILE --help
makim $VERBOSE_FLAG --file $MAKIM_FILE --version
makim $VERBOSE_FLAG --file $MAKIM_FILE build.setup
makim $VERBOSE_FLAG --file $MAKIM_FILE build.lint
makim $VERBOSE_FLAG --file $MAKIM_FILE build.lint --fix
makim $VERBOSE_FLAG --file $MAKIM_FILE test.unit
makim $VERBOSE_FLAG --file $MAKIM_FILE test.browser
makim $VERBOSE_FLAG --file $MAKIM_FILE test.browser --headless

error:
help: This group helps tests failure tasks
tasks:
Expand Down
132 changes: 103 additions & 29 deletions src/makim/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import warnings

from copy import deepcopy
from itertools import product
from pathlib import Path
from typing import Any, Dict, List, Optional, Union, cast

Expand Down Expand Up @@ -363,7 +364,7 @@ def _load_dotenv(self, data_scope: dict[str, Any]) -> dict[str, str]:

def _load_scoped_data(
self, scope: str
) -> tuple[dict[str, str], dict[str, str]]:
) -> tuple[dict[str, str], dict[str, Any]]:
scope_options = ('global', 'group', 'task')
if scope not in scope_options:
raise Exception(f'The given scope `{scope}` is not valid.')
Expand Down Expand Up @@ -451,6 +452,66 @@ def _load_task_args(self) -> None:
else (False if is_bool else None)
)

def _log_command_execution(
self, execution_context: dict[str, Any], width: int
) -> None:
"""Log the command execution details.

execution_context should contain: args_input, current_vars,
env, and optionally matrix_vars
"""
MakimLogs.print_info('=' * width)
MakimLogs.print_info(
'TARGET: ' + f'{self.group_name}.{self.task_name}'
)
MakimLogs.print_info('ARGS:')
MakimLogs.print_info(pprint.pformat(execution_context['args_input']))
MakimLogs.print_info('VARS:')
MakimLogs.print_info(pprint.pformat(execution_context['current_vars']))
MakimLogs.print_info('ENV:')
MakimLogs.print_info(str(execution_context['env']))

if execution_context.get('matrix_vars'):
MakimLogs.print_info('MATRIX:')
MakimLogs.print_info(
pprint.pformat(execution_context['matrix_vars'])
)

MakimLogs.print_info('-' * width)

def _generate_matrix_combinations(
self, matrix_config: dict[str, Any]
) -> list[dict[str, Any]]:
"""Generate all possible combinations from matrix configuration.

Parameters
----------
matrix_config : dict
Dictionary containing matrix variables and their possible values

Returns
-------
list[dict]
List of dictionaries, each containing one possible combination
"""
if not matrix_config:
return [{}]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return [{}]
return []


# Convert matrix config into format suitable for product
keys = list(matrix_config.keys())
values = [
matrix_config[k]
if isinstance(matrix_config[k], list)
else [matrix_config[k]]
for k in keys
]

combinations = []
for combo in product(*values):
combinations.append(dict(zip(keys, combo)))

return combinations

# run commands
def _run_hooks(self, args: dict[str, Any], hook_type: str) -> None:
if not self.task_data.get('hooks', {}).get(hook_type):
Expand Down Expand Up @@ -518,20 +579,22 @@ def _run_hooks(self, args: dict[str, Any], hook_type: str) -> None:

def _run_command(self, args: dict[str, Any]) -> None:
cmd = self.task_data.get('run', '').strip()
if 'vars' not in self.group_data:
self.group_data['vars'] = {}

if not isinstance(self.group_data['vars'], dict):
if not isinstance(self.group_data.get('vars', {}), dict):
MakimLogs.raise_error(
'`vars` attribute inside the group '
f'{self.group_name} is not a dictionary.',
MakimError.MAKIM_VARS_ATTRIBUTE_INVALID,
)

env, variables = self._load_scoped_data('task')
for k, v in env.items():
os.environ[k] = v
# Get matrix configuration if it exists
matrix_combinations: list[dict[str, Any]] = [{}]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
matrix_combinations: list[dict[str, Any]] = [{}]
matrix_combinations: list[dict[str, Any]] = []

if matrix_config := self.task_data.get('matrix', {}):
matrix_combinations = self._generate_matrix_combinations(
matrix_config
)

env, variables = self._load_scoped_data('task')
self.env_scoped = deepcopy(env)

args_input = {'file': self.file}
Expand Down Expand Up @@ -565,36 +628,47 @@ def _run_command(self, args: dict[str, Any]) -> None:
MakimError.MAKIM_ARGUMENT_REQUIRED,
)

cmd = str(cmd)
cmd = TEMPLATE.from_string(cmd).render(
args=args_input, env=env, vars=variables
)
width, _ = get_terminal_size()

if self.verbose:
MakimLogs.print_info('=' * width)
MakimLogs.print_info(
'TARGET: ' + f'{self.group_name}.{self.task_name}'
# Run command for each matrix combination
for matrix_vars in matrix_combinations:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
for matrix_vars in matrix_combinations:
for matrix_vars in matrix_combinations or [{}]:

# Update environment variables
for k, v in env.items():
os.environ[k] = v

# Create a copy of variables and update with matrix values
current_vars = deepcopy(variables)
if matrix_vars:
current_vars['matrix'] = matrix_vars

# Render command with current matrix values
current_cmd = TEMPLATE.from_string(cmd).render(
args=args_input, env=env, vars=current_vars, matrix=matrix_vars
)
MakimLogs.print_info('ARGS:')
MakimLogs.print_info(pprint.pformat(args_input))
MakimLogs.print_info('VARS:')
MakimLogs.print_info(pprint.pformat(variables))
MakimLogs.print_info('ENV:')
MakimLogs.print_info(str(env))
MakimLogs.print_info('-' * width)
MakimLogs.print_info('>>> ' + cmd.replace('\n', '\n>>> '))
MakimLogs.print_info('=' * width)

if not self.dry_run and cmd:
self._call_shell_app(cmd)

if self.verbose:
# Prepare execution context for logging
execution_context = {
'args_input': args_input,
'current_vars': current_vars,
'env': env,
'matrix_vars': matrix_vars,
}
# Log command execution details
self._log_command_execution(execution_context, width)
MakimLogs.print_info(
'>>> ' + current_cmd.replace('\n', '\n>>> ')
)
MakimLogs.print_info('=' * width)

if not self.dry_run and current_cmd:
self._call_shell_app(current_cmd)

# move back the environment variable to the previous values
os.environ.clear()
os.environ.update(self.env_scoped)

# public methods

# public methods
def load(
self, file: str, dry_run: bool = False, verbose: bool = False
) -> None:
Expand Down
83 changes: 83 additions & 0 deletions tests/smoke/.makim-matrix-strategy.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
version: 1.0.0
groups:
build:
vars:
app_name: "myapp"
base_image: "node:18"
tasks:
setup:
help: Setup development environment
matrix:
env:
- dev
- test
- prod
arch:
- amd64
- arm64
run: |
echo "[Setup] Environment: ${{ matrix.env }}, Architecture: ${{ matrix.arch }}"
echo "[Setup] Using base image: ${{ vars.base_image }}"
echo "[Setup] Installing dependencies for ${{ matrix.env }}"

lint:
help: Run linting checks
matrix:
type:
- eslint
- prettier
path:
- src
- tests
args:
fix:
type: bool
default: false
help: Automatically fix issues
run: |
echo "[Lint] Running ${{ matrix.type }} on ${{ matrix.path }}"
{% if args.fix %}
echo "[Lint] Auto-fixing enabled for ${{ matrix.type }}"
{% endif %}

test:
vars:
timeout: "30s"
tasks:
unit:
help: Run unit tests across Node versions
matrix:
node:
- "16"
- "18"
- "20"
mode:
- basic
- coverage
run: |
echo "[Test] Using Node.js ${{ matrix.node }}"
echo "[Test] Mode: ${{ matrix.mode }}"
echo "[Test] Timeout set to ${{ vars.timeout }}"

browser:
help: Run browser tests
matrix:
browser:
- chrome
- firefox
viewport:
- desktop
- mobile
args:
headless:
type: bool
default: true
help: Run in headless mode
run: |
echo "[Browser Test] Testing on ${{ matrix.browser }}"
echo "[Browser Test] Viewport: ${{ matrix.viewport }}"
{% if args.headless %}
echo "[Browser Test] Running in headless mode"
{% else %}
echo "[Browser Test] Running in GUI mode"
{% endif %}
Loading