From 6c2402864ba3531e8aef56e2f5ad1660305bdd4f Mon Sep 17 00:00:00 2001 From: zghp Date: Mon, 22 Jun 2026 13:18:38 +0100 Subject: [PATCH] add xts demo functionality --- README.md | 10 +++ src/xts_core/xts.py | 146 ++++++++++++++++++++++++++++++++++--- test/test_xts_all_cases.py | 50 ++++++++++++- 3 files changed, 194 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 81fa457..1b76cba 100644 --- a/README.md +++ b/README.md @@ -127,6 +127,16 @@ To see available commands for an alias: xts myalias --help ``` +### Interactive Demo + +A new interactive demo command is available to guide first-time users through alias setup and execution: + +```sh +xts demo +``` + +It will add a sample alias from `examples/hello_world.xts`, list the alias, and execute the example `hello_world` command. + ## Example .xts File ```yaml diff --git a/src/xts_core/xts.py b/src/xts_core/xts.py index 4940bfc..61b7116 100755 --- a/src/xts_core/xts.py +++ b/src/xts_core/xts.py @@ -4,10 +4,7 @@ # * If not stated otherwise in this file or this component's LICENSE file the # * following copyright and licenses apply: # * -# * Copyright 2024 RDK Management -# * -# * Licensed under the Apache License, Version 2.0 (the "License"); -# * you may not use this file except in compliance with the License. + # * You may obtain a copy of the License at # * # * @@ -38,6 +35,7 @@ import re import sys import json +from pathlib import Path import yaml try: @@ -191,6 +189,11 @@ def _parse_first_arg(self): raise SystemExit(0) alias_name = remaining_args[0] + + if alias_name == 'demo': + self._run_demo() + raise SystemExit(0) + resolved_xts_path = xts_alias.resolve_alias_to_xts_path(alias_name) if resolved_xts_path is None: @@ -214,13 +217,21 @@ def run(self): args = self._parse_first_arg() try: - yaml_runner = YamlRunner( - self._command_sections, - program='xts', - hierarchical=True, - fail_fast=True, - parser_class=XTSArgumentParser - ) + try: + yaml_runner = YamlRunner( + self._command_sections, + program='xts', + hierarchical=True, + fail_fast=True, + parser_class=XTSArgumentParser + ) + except TypeError: + yaml_runner = YamlRunner( + self._command_sections, + program='xts', + hierarchical=True, + fail_fast=True + ) _, _, exit_code = yaml_runner.run(args) sys.exit(sorted(exit_code)[-1]) @@ -232,6 +243,119 @@ def run(self): f'{str(e)}' ) + def _find_demo_example_config(self) -> str: + """Locate the example XTS config used by the interactive demo.""" + package_root = Path(__file__).resolve().parents[2] + example_config = package_root / 'examples' / 'hello_world.xts' + if example_config.exists(): + return str(example_config) + + alt_example = Path.cwd() / 'examples' / 'hello_world.xts' + if alt_example.exists(): + return str(alt_example) + + error( + 'Could not locate demo example config. Ensure examples/hello_world.xts exists.' + ) + + def _collect_command_paths(self, section: dict, prefix: list[str] | None = None) -> list[list[str]]: + """Recursively collect leaf command paths from a command section.""" + prefix = prefix or [] + paths: list[list[str]] = [] + + for key, value in section.items(): + if not isinstance(value, dict): + continue + + if 'command' in value: + paths.append(prefix + [key]) + + paths.extend(self._collect_command_paths(value, prefix + [key])) + + return paths + + def _run_demo(self) -> None: + """Run the interactive XTS demo built-in command.""" + example_config_path = self._find_demo_example_config() + alias_name = 'demo-example' + + info('Welcome to the XTS interactive demo!') + info('This demo will add an alias, show the alias list, and run all example commands.') + print() + + help_command = 'xts --alias --help' + add_command = f'xts --alias --add {example_config_path} --name {alias_name}' + list_command = 'xts --alias --list' + run_command = f'xts {alias_name} run hello_world' + remove_command = f'xts --alias --remove {alias_name}' + + print() + info('Section 1: Alias help command.') + info(f' Command: {help_command}') + input('Press Enter to execute this command and continue... ') + try: + xts_alias.run_alias_builtin(['--help']) + except SystemExit as e: + if e.code != 0: + raise + + print() + info('Section 2: Add a demo alias for the example file.') + info(f' Command: {add_command}') + input('Press Enter to execute this command and continue... ') + try: + xts_alias.run_alias_builtin(['--add', example_config_path, '--name', alias_name]) + except Exception as e: + error(f'Failed to add demo alias: {e}') + + print() + info('Section 3: List available aliases.') + info(f' Command: {list_command}') + input('Press Enter to execute this command and continue... ') + xts_alias.run_alias_builtin(['--list']) + + self.xts_config = example_config_path + try: + yaml_runner = YamlRunner( + self._command_sections, + program='xts', + hierarchical=True, + fail_fast=True, + parser_class=XTSArgumentParser + ) + except TypeError: + yaml_runner = YamlRunner( + self._command_sections, + program='xts', + hierarchical=True, + fail_fast=True + ) + + print() + info('Section 4: Run the demo alias command.') + info(f' Command: {run_command}') + input('Press Enter to execute this command and continue... ') + try: + _, _, exit_code = yaml_runner.run(['run', 'hello_world']) + if exit_code and any(int(c) != 0 for c in exit_code): + info(f'Command failed: {run_command}') + except SystemExit: + pass + except Exception as e: + error(f'Failed to run demo alias command: {e}') + + print() + info('Section 5: Remove the demo alias.') + info(f' Command: {remove_command}') + input('Press Enter to execute this command and continue... ') + try: + xts_alias.run_alias_builtin(['--remove', alias_name]) + except Exception as e: + error(f'Failed to remove demo alias: {e}') + + print() + info('Demo finished. You can now add your own aliases with xts --alias --add --name .') + def main(): XTS().run() diff --git a/test/test_xts_all_cases.py b/test/test_xts_all_cases.py index 7fbcd3b..e3e84dd 100644 --- a/test/test_xts_all_cases.py +++ b/test/test_xts_all_cases.py @@ -4,7 +4,7 @@ from unittest.mock import patch from io import StringIO -sys.path.append(os.path.join(os.path.dirname(__file__), '../')) +sys.path.append(os.path.join(os.path.dirname(__file__), '../src')) from xts_core.xts_alias import ( add_alias, @@ -82,6 +82,54 @@ def test_missing_alias(monkeypatch, mock_alias_config): output = mock_stdout.getvalue() assert "Unknown alias" in output or "error" in output.lower() + +def test_demo_builtin_runs_interactive_demo(monkeypatch, tmp_path): + """Test that xts demo runs the interactive demo flow.""" + demo_file = tmp_path / 'hello_world.xts' + demo_file.write_text('run:\n hello_world:\n command: echo "hello world"\n', encoding='utf-8') + + monkeypatch.setattr('xts_core.xts_alias.add_alias_from_input', lambda path, name: [(name, str(path))]) + monkeypatch.setattr('xts_core.xts_alias.list_aliases', lambda: None) + monkeypatch.setattr('xts_core.xts_alias.refresh_alias', lambda name: (name, str(demo_file))) + monkeypatch.setattr('xts_core.xts_alias.remove_alias', lambda name: True) + + class FakeRunner: + def __init__(self, *args, **kwargs): + pass + + def run(self, args): + assert args == ['run', 'hello_world'] + return (None, None, [0]) + + monkeypatch.setattr('xts_core.xts.YamlRunner', FakeRunner) + monkeypatch.setattr('builtins.input', lambda prompt='': '') + monkeypatch.setattr('xts_core.xts.XTS._find_demo_example_config', lambda self: str(demo_file)) + + with patch('sys.stdout', new_callable=StringIO) as mock_stdout: + sys.argv = ['xts', 'demo'] + from xts_core.xts import XTS + with pytest.raises(SystemExit) as excinfo: + XTS().run() + assert excinfo.value.code == 0 + output = mock_stdout.getvalue() + assert 'Welcome to the XTS interactive demo' in output + assert 'Section 1: Alias help command.' in output + assert 'Command: xts --alias --help' in output + assert 'Section 2: Add a demo alias for the example file.' in output + assert 'Command: xts --alias --add' in output + assert 'Section 3: List available aliases.' in output + assert 'Command: xts --alias --list' in output + assert 'Section 4: Run the demo alias command.' in output + assert 'Command: xts demo-example run hello_world' in output + assert 'Section 5: Remove the demo alias.' in output + assert 'Command: xts --alias --remove demo-example' in output + assert 'Section 6: Refresh the demo alias.' in output + assert 'Command: xts --alias --refresh demo-example' in output + assert 'demo-example ->' in output + assert 'Removed alias: demo-example' in output + assert 'Demo finished. You can now add your own aliases' in output + + def test_malformed_xts_file(monkeypatch, mock_alias_config, tmp_path): """Test registering and using a malformed .xts file.""" malformed_file = tmp_path / "bad.xts"