From f61fe037a787a7ec007abf8fa56d1d907a0d74f1 Mon Sep 17 00:00:00 2001 From: Abhijeet Saroha Date: Thu, 10 Oct 2024 18:39:38 +0530 Subject: [PATCH 1/6] add matrix functionality --- src/makim/core.py | 114 ++++++++++++++++++------ tests/smoke/.makim-matrix-strategy.yaml | 83 +++++++++++++++++ 2 files changed, 169 insertions(+), 28 deletions(-) create mode 100644 tests/smoke/.makim-matrix-strategy.yaml diff --git a/src/makim/core.py b/src/makim/core.py index ae4bce0..f08321a 100644 --- a/src/makim/core.py +++ b/src/makim/core.py @@ -112,6 +112,13 @@ class Makim: group_data: dict[str, Any] = {} task_name: str = '' task_data: dict[str, Any] = {} + # matrix variables + matrix_config: dict[ + str, Any + ] = {} # stores the original matrix configuration + matrix_current: dict[ + str, Any + ] = {} # stores current matrix combination values def __init__(self) -> None: """Prepare the Makim class with the default configuration.""" @@ -363,7 +370,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.') @@ -451,6 +458,42 @@ def _load_task_args(self) -> None: else (False if is_bool else None) ) + 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 [{}] + + # 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 + ] + + # Generate all combinations + from itertools import product + + 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): @@ -518,19 +561,21 @@ 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, ) + # Get matrix configuration if it exists + self.matrix_config = self.task_data.get('matrix', {}) + matrix_combinations = self._generate_matrix_combinations( + self.matrix_config + ) + env, variables = self._load_scoped_data('task') - for k, v in env.items(): - os.environ[k] = v self.env_scoped = deepcopy(env) @@ -565,36 +610,49 @@ 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: + self.matrix_current = matrix_vars + + # Create a copy of variables and update with matrix values + current_vars = deepcopy(variables) + current_vars['matrix'] = self.matrix_current + + # Render command with current matrix values + current_cmd = TEMPLATE.from_string(cmd).render( + args=args_input, env=env, vars=current_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: + MakimLogs.print_info('=' * width) + MakimLogs.print_info( + 'TARGET: ' + f'{self.group_name}.{self.task_name}' + ) + MakimLogs.print_info('ARGS:') + MakimLogs.print_info(pprint.pformat(args_input)) + MakimLogs.print_info('VARS:') + MakimLogs.print_info(pprint.pformat(current_vars)) + MakimLogs.print_info('ENV:') + MakimLogs.print_info(str(env)) + if matrix_vars: + MakimLogs.print_info('MATRIX:') + MakimLogs.print_info(pprint.pformat(matrix_vars)) + MakimLogs.print_info('-' * 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: diff --git a/tests/smoke/.makim-matrix-strategy.yaml b/tests/smoke/.makim-matrix-strategy.yaml new file mode 100644 index 0000000..4f20dfb --- /dev/null +++ b/tests/smoke/.makim-matrix-strategy.yaml @@ -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 %} From 678e808fe3e9bb8c8d48b334b8acc540c2dc6dd7 Mon Sep 17 00:00:00 2001 From: Abhijeet Saroha Date: Thu, 10 Oct 2024 18:58:18 +0530 Subject: [PATCH 2/6] re-order the imports --- src/makim/core.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/makim/core.py b/src/makim/core.py index f08321a..2d58f00 100644 --- a/src/makim/core.py +++ b/src/makim/core.py @@ -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 @@ -485,9 +486,6 @@ def _generate_matrix_combinations( for k in keys ] - # Generate all combinations - from itertools import product - combinations = [] for combo in product(*values): combinations.append(dict(zip(keys, combo))) From 8536dd564ae6bfe6d2fff0b43bc9c0a44c97fa8b Mon Sep 17 00:00:00 2001 From: Abhijeet Saroha Date: Fri, 11 Oct 2024 20:00:15 +0530 Subject: [PATCH 3/6] add matrix_vars to TEMPALTE --- src/makim/core.py | 2 +- tests/smoke/.makim-matrix-strategy.yaml | 20 ++++++++++---------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/makim/core.py b/src/makim/core.py index 2d58f00..55ab087 100644 --- a/src/makim/core.py +++ b/src/makim/core.py @@ -620,7 +620,7 @@ def _run_command(self, args: dict[str, Any]) -> None: # Render command with current matrix values current_cmd = TEMPLATE.from_string(cmd).render( - args=args_input, env=env, vars=current_vars + args=args_input, env=env, vars=current_vars, matrix=matrix_vars ) if self.verbose: diff --git a/tests/smoke/.makim-matrix-strategy.yaml b/tests/smoke/.makim-matrix-strategy.yaml index 4f20dfb..fd4ff9f 100644 --- a/tests/smoke/.makim-matrix-strategy.yaml +++ b/tests/smoke/.makim-matrix-strategy.yaml @@ -16,9 +16,9 @@ groups: - 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 }}" + 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 @@ -35,9 +35,9 @@ groups: default: false help: Automatically fix issues run: | - echo "[Lint] Running {{ matrix.type }} on {{ matrix.path }}" + echo "[Lint] Running ${{ matrix.type }} on ${{ matrix.path }}" {% if args.fix %} - echo "[Lint] Auto-fixing enabled for {{ matrix.type }}" + echo "[Lint] Auto-fixing enabled for ${{ matrix.type }}" {% endif %} test: @@ -55,9 +55,9 @@ groups: - basic - coverage run: | - echo "[Test] Using Node.js {{ matrix.node }}" - echo "[Test] Mode: {{ matrix.mode }}" - echo "[Test] Timeout set to {{ vars.timeout }}" + echo "[Test] Using Node.js ${{ matrix.node }}" + echo "[Test] Mode: ${{ matrix.mode }}" + echo "[Test] Timeout set to ${{ vars.timeout }}" browser: help: Run browser tests @@ -74,8 +74,8 @@ groups: default: true help: Run in headless mode run: | - echo "[Browser Test] Testing on {{ matrix.browser }}" - echo "[Browser Test] Viewport: {{ matrix.viewport }}" + echo "[Browser Test] Testing on ${{ matrix.browser }}" + echo "[Browser Test] Viewport: ${{ matrix.viewport }}" {% if args.headless %} echo "[Browser Test] Running in headless mode" {% else %} From eff1d49fc370db99b6e76604f616dda8731f0739 Mon Sep 17 00:00:00 2001 From: Abhijeet Saroha Date: Fri, 11 Oct 2024 20:53:38 +0530 Subject: [PATCH 4/6] update test list (tests.smoke) --- .makim.yaml | 22 ++++++++++++++ src/makim/core.py | 74 +++++++++++++++++++++++++++++------------------ 2 files changed, 68 insertions(+), 28 deletions(-) diff --git a/.makim.yaml b/.makim.yaml index f694b93..2425236 100644 --- a/.makim.yaml +++ b/.makim.yaml @@ -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 @@ -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: diff --git a/src/makim/core.py b/src/makim/core.py index 55ab087..06e1f7a 100644 --- a/src/makim/core.py +++ b/src/makim/core.py @@ -113,13 +113,6 @@ class Makim: group_data: dict[str, Any] = {} task_name: str = '' task_data: dict[str, Any] = {} - # matrix variables - matrix_config: dict[ - str, Any - ] = {} # stores the original matrix configuration - matrix_current: dict[ - str, Any - ] = {} # stores current matrix combination values def __init__(self) -> None: """Prepare the Makim class with the default configuration.""" @@ -459,6 +452,33 @@ 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]]: @@ -568,13 +588,13 @@ def _run_command(self, args: dict[str, Any]) -> None: ) # Get matrix configuration if it exists - self.matrix_config = self.task_data.get('matrix', {}) - matrix_combinations = self._generate_matrix_combinations( - self.matrix_config - ) + 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} @@ -612,11 +632,14 @@ def _run_command(self, args: dict[str, Any]) -> None: # Run command for each matrix combination for matrix_vars in matrix_combinations: - self.matrix_current = matrix_vars + # 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) - current_vars['matrix'] = self.matrix_current + if matrix_vars: + current_vars['matrix'] = matrix_vars # Render command with current matrix values current_cmd = TEMPLATE.from_string(cmd).render( @@ -624,20 +647,15 @@ def _run_command(self, args: dict[str, Any]) -> None: ) if self.verbose: - MakimLogs.print_info('=' * width) - MakimLogs.print_info( - 'TARGET: ' + f'{self.group_name}.{self.task_name}' - ) - MakimLogs.print_info('ARGS:') - MakimLogs.print_info(pprint.pformat(args_input)) - MakimLogs.print_info('VARS:') - MakimLogs.print_info(pprint.pformat(current_vars)) - MakimLogs.print_info('ENV:') - MakimLogs.print_info(str(env)) - if matrix_vars: - MakimLogs.print_info('MATRIX:') - MakimLogs.print_info(pprint.pformat(matrix_vars)) - MakimLogs.print_info('-' * width) + # 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>>> ') ) From 7dbd1cde557fe6bcdc7b7bb081a8575b2bfbe92d Mon Sep 17 00:00:00 2001 From: Abhijeet Saroha Date: Fri, 11 Oct 2024 20:58:32 +0530 Subject: [PATCH 5/6] Trigger Checks From 3ffebe8f1444f7d2e91159f75f82ca0220890318 Mon Sep 17 00:00:00 2001 From: Abhijeet Saroha Date: Mon, 14 Oct 2024 23:20:01 +0530 Subject: [PATCH 6/6] update the code --- src/makim/core.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/makim/core.py b/src/makim/core.py index 06e1f7a..47a5202 100644 --- a/src/makim/core.py +++ b/src/makim/core.py @@ -495,7 +495,7 @@ def _generate_matrix_combinations( List of dictionaries, each containing one possible combination """ if not matrix_config: - return [{}] + return [] # Convert matrix config into format suitable for product keys = list(matrix_config.keys()) @@ -588,7 +588,7 @@ def _run_command(self, args: dict[str, Any]) -> None: ) # Get matrix configuration if it exists - 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 @@ -631,7 +631,7 @@ def _run_command(self, args: dict[str, Any]) -> None: width, _ = get_terminal_size() # Run command for each matrix combination - 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