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
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Comment on lines +132 to +139
## Example .xts File

```yaml
Expand Down
146 changes: 135 additions & 11 deletions src/xts_core/xts.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment on lines 4 to 8
# *
# *
Expand Down Expand Up @@ -38,6 +35,7 @@
import re
import sys
import json
from pathlib import Path

import yaml
try:
Expand Down Expand Up @@ -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)

Comment on lines 191 to 198
if resolved_xts_path is None:
Expand All @@ -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])
Expand All @@ -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.'
)

Comment on lines +248 to +260
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
Comment on lines +261 to +275

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()
Comment on lines +282 to +284

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}'

Comment on lines +286 to +291
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 <path> --name <alias>.')
Comment on lines +347 to +357

def main():
XTS().run()

Expand Down
50 changes: 49 additions & 1 deletion test/test_xts_all_cases.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Comment on lines +123 to +130


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"
Expand Down
Loading