Commit 08862c2c authored by Roman Alifanov's avatar Roman Alifanov

Add plugin system: runtime loading of namespace libraries

- New stdlib module `plugin` with 7 methods: load_dir, load, list, has, call, each, each_if - Plugins are namespaces compiled via build-lib, loaded at runtime via source - Dynamic dispatch: plugin.call(name, func) -> ${name}__${func} - Empty function bodies now emit `:` (bash no-op) instead of invalid empty block - 21 new tests in test_plugin.py
parent af265565
...@@ -1216,6 +1216,78 @@ count = args.count () ...@@ -1216,6 +1216,78 @@ count = args.count ()
arg = args.get (0) arg = args.get (0)
``` ```
### plugin (система плагинов)
Модуль для runtime-загрузки namespace-библиотек (.sh файлов, собранных через `build-lib`).
Плагин — это namespace, скомпилированный в .sh библиотеку. Функции плагина вызываются динамически по имени.
```
# Загрузить все плагины из директории
plugin.load_dir ("/usr/share/myapp/plugins")
# Загрузить один плагин
plugin.load ("/path/to/plugin.sh")
# Список загруженных плагинов (через пробел)
names = plugin.list ()
# Проверить наличие функции у плагина
if plugin.has ("myplugin", "init") {
plugin.call ("myplugin", "init")
}
# Вызвать функцию плагина
result = plugin.call ("myplugin", "get_value")
plugin.call ("myplugin", "process", "arg1", "arg2")
# Вызвать функцию на всех плагинах (пропускает если нет)
plugin.each ("apply", "dark-theme")
# Вызвать функцию на плагинах, где check возвращает true
plugin.each_if ("is_available", "apply", "dark-theme")
```
**Создание плагина:**
```
# my_plugin.ct
namespace my_plugin {
func greet (name: string) {
print ("Hello from plugin, {name}!")
}
}
```
```bash
# Сборка плагина
content build-lib my_plugin.ct -o plugins/my_plugin.sh
```
**Использование:**
```
plugin.load_dir ("./plugins")
foreach name in plugin.list ().split (" ") {
print ("loaded: {name}")
}
plugin.each ("greet", "World")
```
**Методы:**
| Метод | Описание |
|-------|----------|
| `plugin.load_dir (path)` | Загрузить все .sh плагины из директории |
| `plugin.load (path)` | Загрузить один .sh плагин |
| `plugin.list ()` | Имена загруженных плагинов (через пробел) |
| `plugin.has (name, func)` | Есть ли функция у плагина |
| `plugin.call (name, func, args...)` | Вызвать функцию конкретного плагина |
| `plugin.each (func, args...)` | Вызвать функцию на всех плагинах |
| `plugin.each_if (check, func, args...)` | Вызвать func где check возвращает true |
### env (переменные окружения) ### env (переменные окружения)
``` ```
...@@ -1585,6 +1657,7 @@ Error: Unknown method 'badMethod' for type 'fs'. Available: append, exists, list ...@@ -1585,6 +1657,7 @@ Error: Unknown method 'badMethod' for type 'fs'. Available: append, exists, list
- `shell``capture`, `exec`, `source` - `shell``capture`, `exec`, `source`
- `time``ms`, `now` - `time``ms`, `now`
- `math``abs`, `add`, `div`, `max`, `min`, `mod`, `mul`, `sub` - `math``abs`, `add`, `div`, `max`, `min`, `mod`, `mul`, `sub`
- `plugin``call`, `each`, `each_if`, `has`, `list`, `load`, `load_dir`
--- ---
...@@ -25,6 +25,7 @@ ...@@ -25,6 +25,7 @@
- **Namespaces**`namespace X { }` for symbol isolation, multi-file support - **Namespaces**`namespace X { }` for symbol isolation, multi-file support
- **Using** — Vala-style `using X` to bring namespace symbols into scope, alias and selective forms - **Using** — Vala-style `using X` to bring namespace symbols into scope, alias and selective forms
- **Bash library import**`busing` for sourcing .sh scripts with optional named access - **Bash library import**`busing` for sourcing .sh scripts with optional named access
- **Plugin system**`plugin.load_dir/load/list/has/call/each/each_if` for runtime plugin loading
- **Auto-scan**`content build mydir/` recursively finds all .ct files - **Auto-scan**`content build mydir/` recursively finds all .ct files
- **Library build**`content build-lib` with metadata, directory support, `--install` - **Library build**`content build-lib` with metadata, directory support, `--install`
- **Optimized output** - no unnecessary subshells, inlined methods - **Optimized output** - no unnecessary subshells, inlined methods
...@@ -403,6 +404,7 @@ try { ...@@ -403,6 +404,7 @@ try {
| **Logger** | `logger.info/warn/error/debug` | | **Logger** | `logger.info/warn/error/debug` |
| **Env** | `env.VAR` read, `env.VAR = value` set — environment variables | | **Env** | `env.VAR` read, `env.VAR = value` set — environment variables |
| **Process** | `proc.write()`, `proc.read()`, `proc.close()`, `proc.kill()`, `proc.wait()`, `proc.pid` | | **Process** | `proc.write()`, `proc.read()`, `proc.close()`, `proc.kill()`, `proc.wait()`, `proc.pid` |
| **Plugin** | `plugin.load_dir()`, `plugin.load()`, `plugin.list()`, `plugin.has()`, `plugin.call()`, `plugin.each()`, `plugin.each_if()` |
| **Modules** | `namespace X { }`, `using X`, `busing "path"` | | **Modules** | `namespace X { }`, `using X`, `busing "path"` |
| **Signals** | `on SIGINT/SIGTERM/SIGHUP/SIGUSR1/SIGUSR2/EXIT { }` | | **Signals** | `on SIGINT/SIGTERM/SIGHUP/SIGUSR1/SIGUSR2/EXIT { }` |
...@@ -549,6 +551,53 @@ FAIL failing test (1ms) ...@@ -549,6 +551,53 @@ FAIL failing test (1ms)
2 of 3 tests passed 2 of 3 tests passed
``` ```
## Plugin System
Load namespace libraries (`.sh` files built with `build-lib`) at runtime:
```
# Create a plugin
# my_plugin.ct
namespace my_plugin {
func greet (name: string) {
print ("Hello from plugin, {name}!")
}
}
```
```bash
# Build it
content build-lib my_plugin.ct -o plugins/my_plugin.sh
```
```
# Load and use plugins
plugin.load_dir ("./plugins")
foreach name in plugin.list ().split (" ") {
print ("loaded: {name}")
}
plugin.each ("greet", "World")
if plugin.has ("my_plugin", "greet") {
plugin.call ("my_plugin", "greet", "Alice")
}
# Call only on plugins where check returns true
plugin.each_if ("is_available", "apply", "dark-theme")
```
| Method | Description |
|--------|-------------|
| `plugin.load_dir (path)` | Load all .sh plugins from directory |
| `plugin.load (path)` | Load a single .sh plugin |
| `plugin.list ()` | Space-separated names of loaded plugins |
| `plugin.has (name, func)` | Check if plugin has a function |
| `plugin.call (name, func, args...)` | Call a specific plugin's function |
| `plugin.each (func, args...)` | Call function on all plugins |
| `plugin.each_if (check, func, args...)` | Call func where check returns true |
## Examples ## Examples
### Telegram Echo Bot ### Telegram Echo Bot
......
...@@ -25,6 +25,7 @@ ...@@ -25,6 +25,7 @@
- **Пространства имён**`namespace X { }` для изоляции символов, поддержка нескольких файлов - **Пространства имён**`namespace X { }` для изоляции символов, поддержка нескольких файлов
- **Using**`using X` в стиле Vala для прямого доступа к символам namespace, алиасы и селективный импорт - **Using**`using X` в стиле Vala для прямого доступа к символам namespace, алиасы и селективный импорт
- **Импорт bash-библиотек**`busing` для подключения .sh скриптов с именованным доступом - **Импорт bash-библиотек**`busing` для подключения .sh скриптов с именованным доступом
- **Система плагинов**`plugin.load_dir/load/list/has/call/each/each_if` для загрузки плагинов в рантайме
- **Авто-скан**`content build mydir/` рекурсивно находит все .ct файлы - **Авто-скан**`content build mydir/` рекурсивно находит все .ct файлы
- **Сборка библиотек**`content build-lib` с метаданными, поддержкой директорий, `--install` - **Сборка библиотек**`content build-lib` с метаданными, поддержкой директорий, `--install`
- **Оптимизированный вывод** — без лишних subshell, инлайнинг методов - **Оптимизированный вывод** — без лишних subshell, инлайнинг методов
...@@ -403,6 +404,7 @@ try { ...@@ -403,6 +404,7 @@ try {
| **Логгер** | `logger.info/warn/error/debug` | | **Логгер** | `logger.info/warn/error/debug` |
| **Окружение** | `env.VAR` чтение, `env.VAR = value` установка — переменные окружения | | **Окружение** | `env.VAR` чтение, `env.VAR = value` установка — переменные окружения |
| **Процессы** | `proc.write()`, `proc.read()`, `proc.close()`, `proc.kill()`, `proc.wait()`, `proc.pid` | | **Процессы** | `proc.write()`, `proc.read()`, `proc.close()`, `proc.kill()`, `proc.wait()`, `proc.pid` |
| **Плагины** | `plugin.load_dir()`, `plugin.load()`, `plugin.list()`, `plugin.has()`, `plugin.call()`, `plugin.each()`, `plugin.each_if()` |
| **Сигналы** | `on SIGINT/SIGTERM/SIGHUP/SIGUSR1/SIGUSR2/EXIT { }` | | **Сигналы** | `on SIGINT/SIGTERM/SIGHUP/SIGUSR1/SIGUSR2/EXIT { }` |
| **Модули** | `namespace X { }`, `using X`, `busing "path"` | | **Модули** | `namespace X { }`, `using X`, `busing "path"` |
...@@ -543,6 +545,53 @@ FAIL падающий тест (1ms) ...@@ -543,6 +545,53 @@ FAIL падающий тест (1ms)
2 of 3 tests passed 2 of 3 tests passed
``` ```
## Система плагинов
Загрузка namespace-библиотек (`.sh` файлов, собранных через `build-lib`) в рантайме:
```
# Создание плагина
# my_plugin.ct
namespace my_plugin {
func greet (name: string) {
print ("Привет из плагина, {name}!")
}
}
```
```bash
# Сборка
content build-lib my_plugin.ct -o plugins/my_plugin.sh
```
```
# Загрузка и использование
plugin.load_dir ("./plugins")
foreach name in plugin.list ().split (" ") {
print ("загружен: {name}")
}
plugin.each ("greet", "Мир")
if plugin.has ("my_plugin", "greet") {
plugin.call ("my_plugin", "greet", "Alice")
}
# Вызов только на плагинах, где проверка возвращает true
plugin.each_if ("is_available", "apply", "dark-theme")
```
| Метод | Описание |
|-------|----------|
| `plugin.load_dir (path)` | Загрузить все .sh плагины из директории |
| `plugin.load (path)` | Загрузить один .sh плагин |
| `plugin.list ()` | Имена загруженных плагинов (через пробел) |
| `plugin.has (name, func)` | Есть ли функция у плагина |
| `plugin.call (name, func, args...)` | Вызвать функцию конкретного плагина |
| `plugin.each (func, args...)` | Вызвать функцию на всех плагинах |
| `plugin.each_if (check, func, args...)` | Вызвать func где check возвращает true |
## Примеры ## Примеры
### Telegram эхо-бот ### Telegram эхо-бот
......
...@@ -227,10 +227,12 @@ def _emit_function_body( ...@@ -227,10 +227,12 @@ def _emit_function_body(
elif ptype == 'dict': elif ptype == 'dict':
ctx.param_dict_vars.add(param.name) ctx.param_dict_vars.add(param.name)
if fn.body: if fn.body and fn.body.stmts:
ctx.in_function = True ctx.in_function = True
emit_block(fn.body, ctx) emit_block(fn.body, ctx)
ctx.in_function = False ctx.in_function = False
elif not fn.params:
ctx.emit(':')
ctx.array_vars.clear() ctx.array_vars.clear()
ctx.array_vars.update(saved_array_vars) ctx.array_vars.update(saved_array_vars)
......
...@@ -77,6 +77,7 @@ _NS_PREFIX: dict[str, str] = { ...@@ -77,6 +77,7 @@ _NS_PREFIX: dict[str, str] = {
'json': JSON_PREFIX, 'json': JSON_PREFIX,
'math': MATH_PREFIX, 'math': MATH_PREFIX,
'regex': REGEX_PREFIX, 'regex': REGEX_PREFIX,
'plugin': '__ct_plugin_',
} }
# Builtin CT function names → bash function names # Builtin CT function names → bash function names
...@@ -723,6 +724,7 @@ _NS_METHOD_PREFIX: dict[str, str] = { ...@@ -723,6 +724,7 @@ _NS_METHOD_PREFIX: dict[str, str] = {
'time': '__ct_time_', 'time': '__ct_time_',
'args': '__ct_args_', 'args': '__ct_args_',
'shell': '__ct_shell_', 'shell': '__ct_shell_',
'plugin': '__ct_plugin_',
} }
......
...@@ -59,6 +59,8 @@ def emit_stdlib(out: list[str], used_categories: set[str], indent: str = '') -> ...@@ -59,6 +59,8 @@ def emit_stdlib(out: list[str], used_categories: set[str], indent: str = '') ->
_emit_test(em) _emit_test(em)
if 'misc' in cats: if 'misc' in cats:
_emit_busing_misc(em) _emit_busing_misc(em)
if 'plugin' in cats:
_emit_plugin(em)
em.line('# === End Standard Library ===') em.line('# === End Standard Library ===')
em.blank() em.blank()
...@@ -440,3 +442,97 @@ def _emit_test(em: _Emitter) -> None: ...@@ -440,3 +442,97 @@ def _emit_test(em: _Emitter) -> None:
def _emit_busing_misc(em: _Emitter) -> None: def _emit_busing_misc(em: _Emitter) -> None:
pass pass
def _emit_plugin(em: _Emitter) -> None:
em.line('declare -ga __CT_PLUGINS=()')
em.blank()
em.line('__ct_plugin_load_dir () {')
em.indent()
em.line('local dir="$1"')
em.line('for f in "$dir"/*.sh; do')
em.indent()
em.line('[[ -f "$f" ]] || continue')
em.line('__ct_plugin_load "$f"')
em.dedent()
em.line('done')
em.dedent()
em.line('}')
em.blank()
em.line('__ct_plugin_load () {')
em.indent()
em.line('local f="$1"')
em.line('source "$f"')
em.line('local __ns_line')
em.line("__ns_line=$(sed -n 's/^# content-namespaces: *//p' \"$f\" | head -1)")
em.line('local __old_ifs="$IFS"; IFS=","')
em.line('local __ns')
em.line('for __ns in $__ns_line; do')
em.indent()
em.line('__ns="${__ns## }"; __ns="${__ns%% }"')
em.line('[[ -n "$__ns" ]] && __CT_PLUGINS+=("$__ns")')
em.dedent()
em.line('done')
em.line('IFS="$__old_ifs"')
em.dedent()
em.line('}')
em.blank()
em.line('__ct_plugin_list () {')
em.indent()
em.line('__CT_RET="${__CT_PLUGINS[*]}"')
em.dedent()
em.line('}')
em.blank()
em.line('__ct_plugin_has () {')
em.indent()
em.line('declare -f "${1}__${2}" &>/dev/null && __CT_RET=true || __CT_RET=false')
em.dedent()
em.line('}')
em.blank()
em.line('__ct_plugin_call () {')
em.indent()
em.line('local __pn="$1" __fn="$2"; shift 2')
em.line('"${__pn}__${__fn}" "$@"')
em.dedent()
em.line('}')
em.blank()
em.line('__ct_plugin_each () {')
em.indent()
em.line('local __fn="$1"; shift')
em.line('local __p')
em.line('for __p in "${__CT_PLUGINS[@]}"; do')
em.indent()
em.line('if declare -f "${__p}__${__fn}" &>/dev/null; then "${__p}__${__fn}" "$@"; fi')
em.dedent()
em.line('done')
em.dedent()
em.line('}')
em.blank()
em.line('__ct_plugin_each_if () {')
em.indent()
em.line('local __check="$1" __fn="$2"; shift 2')
em.line('local __p')
em.line('for __p in "${__CT_PLUGINS[@]}"; do')
em.indent()
em.line('if declare -f "${__p}__${__check}" &>/dev/null; then')
em.indent()
em.line('"${__p}__${__check}"')
em.line('if [[ "$__CT_RET" == "true" ]]; then')
em.indent()
em.line('"${__p}__${__fn}" "$@"')
em.dedent()
em.line('fi')
em.dedent()
em.line('fi')
em.dedent()
em.line('done')
em.dedent()
em.line('}')
em.blank()
...@@ -14,6 +14,7 @@ from .args import ArgsMethods ...@@ -14,6 +14,7 @@ from .args import ArgsMethods
from .core import CoreFunctions, AwkBuiltinFunctions from .core import CoreFunctions, AwkBuiltinFunctions
from .reflect import ReflectMethods from .reflect import ReflectMethods
from .process_handle import ProcessHandleMethods from .process_handle import ProcessHandleMethods
from .plugin import PluginMethods
STRING_METHODS = collect_methods(StringMethods) STRING_METHODS = collect_methods(StringMethods)
ARRAY_METHODS = collect_methods(ArrayMethods) ARRAY_METHODS = collect_methods(ArrayMethods)
...@@ -31,6 +32,7 @@ CORE_FUNCTIONS = collect_methods(CoreFunctions) ...@@ -31,6 +32,7 @@ CORE_FUNCTIONS = collect_methods(CoreFunctions)
AWK_BUILTIN_FUNCTIONS = collect_methods(AwkBuiltinFunctions) AWK_BUILTIN_FUNCTIONS = collect_methods(AwkBuiltinFunctions)
REFLECT_METHODS = collect_methods(ReflectMethods) REFLECT_METHODS = collect_methods(ReflectMethods)
PROCESS_HANDLE_METHODS = collect_methods(ProcessHandleMethods) PROCESS_HANDLE_METHODS = collect_methods(ProcessHandleMethods)
PLUGIN_METHODS = collect_methods(PluginMethods)
NAMESPACE_REGISTRY = { NAMESPACE_REGISTRY = {
"fs": FS_METHODS, "fs": FS_METHODS,
...@@ -42,6 +44,7 @@ NAMESPACE_REGISTRY = { ...@@ -42,6 +44,7 @@ NAMESPACE_REGISTRY = {
"time": TIME_METHODS, "time": TIME_METHODS,
"math": MATH_METHODS, "math": MATH_METHODS,
"reflect": REFLECT_METHODS, "reflect": REFLECT_METHODS,
"plugin": PLUGIN_METHODS,
"shell": {"exec", "capture", "source"}, "shell": {"exec", "capture", "source"},
} }
......
from .base import Method
class PluginMethods:
load_dir = Method(
name="load_dir",
bash_func="__ct_plugin_load_dir",
min_args=1, max_args=1,
)
load = Method(
name="load",
bash_func="__ct_plugin_load",
min_args=1, max_args=1,
)
list = Method(
name="list",
bash_func="__ct_plugin_list",
min_args=0, max_args=0,
)
has = Method(
name="has",
bash_func="__ct_plugin_has",
min_args=2, max_args=2,
)
call = Method(
name="call",
bash_func="__ct_plugin_call",
min_args=2, max_args=None,
)
each = Method(
name="each",
bash_func="__ct_plugin_each",
min_args=1, max_args=None,
)
each_if = Method(
name="each_if",
bash_func="__ct_plugin_each_if",
min_args=2, max_args=None,
)
...@@ -49,6 +49,7 @@ _NS_CATEGORIES: dict[str, str] = { ...@@ -49,6 +49,7 @@ _NS_CATEGORIES: dict[str, str] = {
'math': 'math', 'math': 'math',
'time': 'time', 'time': 'time',
'args': 'args', 'args': 'args',
'plugin': 'plugin',
} }
......
...@@ -43,13 +43,13 @@ BUILTIN_FUNCS = frozenset({ ...@@ -43,13 +43,13 @@ BUILTIN_FUNCS = frozenset({
'print', 'exit', 'len', 'range', 'random', 'random_range', 'pid', 'print', 'exit', 'len', 'range', 'random', 'random_range', 'pid',
'assert', 'assert_eq', 'assert', 'assert_eq',
'http', 'fs', 'json', 'logger', 'reflect', 'regex', 'math', 'http', 'fs', 'json', 'logger', 'reflect', 'regex', 'math',
'time', 'args', 'env', 'shell', 'time', 'args', 'env', 'shell', 'plugin',
}) })
# Names that are namespace objects (not functions), accessed via dot # Names that are namespace objects (not functions), accessed via dot
STDLIB_NAMESPACES = frozenset({ STDLIB_NAMESPACES = frozenset({
'http', 'fs', 'json', 'logger', 'reflect', 'regex', 'math', 'http', 'fs', 'json', 'logger', 'reflect', 'regex', 'math',
'time', 'args', 'env', 'shell', 'time', 'args', 'env', 'shell', 'plugin',
}) })
......
import os
import shutil
import subprocess
import tempfile
from helpers import run_ct, compile_ct
def _build_plugin(source: str, plugin_dir: str, name: str) -> str:
ct_file = os.path.join(plugin_dir, f'{name}.ct')
sh_file = os.path.join(plugin_dir, f'{name}.sh')
with open(ct_file, 'w') as f:
f.write(source)
result = subprocess.run(
['python3', 'content', 'build-lib', ct_file, '-o', sh_file],
capture_output=True, text=True, timeout=10,
)
assert result.returncode == 0, f"build-lib failed: {result.stderr}"
os.unlink(ct_file)
return sh_file
def _run_with_plugins(main_source: str, plugins: dict[str, str], timeout: int = 10):
plugin_dir = tempfile.mkdtemp(prefix='ct_plugins_')
try:
for name, source in plugins.items():
_build_plugin(source, plugin_dir, name)
full_source = main_source.replace('__PLUGIN_DIR__', plugin_dir)
return run_ct(full_source, timeout=timeout)
finally:
shutil.rmtree(plugin_dir)
class TestPluginLoadDir:
def test_load_single_plugin(self):
code, stdout, _ = _run_with_plugins(
'''
plugin.load_dir ("__PLUGIN_DIR__")
plugin.each ("greet")
''',
{'hello': '''
namespace hello {
func greet () { print ("Hello from plugin!") }
}
'''},
)
assert code == 0
assert "Hello from plugin!" in stdout
def test_load_multiple_plugins(self):
code, stdout, _ = _run_with_plugins(
'''
plugin.load_dir ("__PLUGIN_DIR__")
foreach name in plugin.list ().split (" ") {
print ("loaded: {name}")
}
''',
{
'alpha': 'namespace alpha { func noop () {} }',
'beta': 'namespace beta { func noop () {} }',
},
)
assert code == 0
assert "loaded: alpha" in stdout
assert "loaded: beta" in stdout
def test_load_empty_dir(self):
plugin_dir = tempfile.mkdtemp(prefix='ct_plugins_empty_')
try:
code, stdout, _ = run_ct(f'''
plugin.load_dir ("{plugin_dir}")
list = plugin.list ()
print ("list=[{{list}}]")
''')
assert code == 0
assert "list=[]" in stdout
finally:
shutil.rmtree(plugin_dir)
class TestPluginLoad:
def test_load_single_file(self):
plugin_dir = tempfile.mkdtemp(prefix='ct_plugins_')
try:
sh_file = _build_plugin(
'namespace single { func hi () { print ("hi!") } }',
plugin_dir, 'single',
)
code, stdout, _ = run_ct(f'''
plugin.load ("{sh_file}")
plugin.each ("hi")
''')
assert code == 0
assert "hi!" in stdout
finally:
shutil.rmtree(plugin_dir)
class TestPluginList:
def test_list_returns_names(self):
code, stdout, _ = _run_with_plugins(
'''
plugin.load_dir ("__PLUGIN_DIR__")
result = plugin.list ()
print (result)
''',
{'myplugin': 'namespace myplugin { func x () {} }'},
)
assert code == 0
assert "myplugin" in stdout
def test_list_space_separated(self):
code, stdout, _ = _run_with_plugins(
'''
plugin.load_dir ("__PLUGIN_DIR__")
parts = plugin.list ().split (" ")
print (parts.len ())
''',
{
'one': 'namespace one { func x () {} }',
'two': 'namespace two { func x () {} }',
},
)
assert code == 0
assert "2" in stdout
class TestPluginHas:
def test_has_existing_function(self):
code, stdout, _ = _run_with_plugins(
'''
plugin.load_dir ("__PLUGIN_DIR__")
result = plugin.has ("checker", "exists_fn")
print (result)
''',
{'checker': 'namespace checker { func exists_fn () {} }'},
)
assert code == 0
assert "true" in stdout
def test_has_missing_function(self):
code, stdout, _ = _run_with_plugins(
'''
plugin.load_dir ("__PLUGIN_DIR__")
result = plugin.has ("checker", "no_such_fn")
print (result)
''',
{'checker': 'namespace checker { func exists_fn () {} }'},
)
assert code == 0
assert "false" in stdout
class TestPluginCall:
def test_call_with_return_value(self):
code, stdout, _ = _run_with_plugins(
'''
plugin.load_dir ("__PLUGIN_DIR__")
val = plugin.call ("calc", "get_value")
print ("value={val}")
''',
{'calc': '''
namespace calc {
func get_value (): string { return "42" }
}
'''},
)
assert code == 0
assert "value=42" in stdout
def test_call_with_arguments(self):
code, stdout, _ = _run_with_plugins(
'''
plugin.load_dir ("__PLUGIN_DIR__")
plugin.call ("greeter", "say_hello", "World")
''',
{'greeter': '''
namespace greeter {
func say_hello (name: string) {
print ("Hello, {name}!")
}
}
'''},
)
assert code == 0
assert "Hello, World!" in stdout
def test_call_with_multiple_arguments(self):
code, stdout, _ = _run_with_plugins(
'''
plugin.load_dir ("__PLUGIN_DIR__")
plugin.call ("joiner", "join_print", "hello", "world")
''',
{'joiner': '''
namespace joiner {
func join_print (a: string, b: string) {
print ("{a}-{b}")
}
}
'''},
)
assert code == 0
assert "hello-world" in stdout
class TestPluginEach:
def test_each_calls_all(self):
code, stdout, _ = _run_with_plugins(
'''
plugin.load_dir ("__PLUGIN_DIR__")
plugin.each ("announce")
''',
{
'plug_a': '''
namespace plug_a {
func announce () { print ("A here") }
}
''',
'plug_b': '''
namespace plug_b {
func announce () { print ("B here") }
}
''',
},
)
assert code == 0
assert "A here" in stdout
assert "B here" in stdout
def test_each_skips_missing(self):
code, stdout, _ = _run_with_plugins(
'''
plugin.load_dir ("__PLUGIN_DIR__")
plugin.each ("optional_fn")
print ("done")
''',
{
'has_it': '''
namespace has_it {
func optional_fn () { print ("found") }
}
''',
'no_it': 'namespace no_it { func other () {} }',
},
)
assert code == 0
assert "found" in stdout
assert "done" in stdout
def test_each_with_args(self):
code, stdout, _ = _run_with_plugins(
'''
plugin.load_dir ("__PLUGIN_DIR__")
plugin.each ("apply", "dark-theme")
''',
{
'theme_a': '''
namespace theme_a {
func apply (t: string) { print ("A: {t}") }
}
''',
'theme_b': '''
namespace theme_b {
func apply (t: string) { print ("B: {t}") }
}
''',
},
)
assert code == 0
assert "A: dark-theme" in stdout
assert "B: dark-theme" in stdout
class TestPluginEachIf:
def test_each_if_filters(self):
code, stdout, _ = _run_with_plugins(
'''
plugin.load_dir ("__PLUGIN_DIR__")
plugin.each_if ("is_available", "apply", "my-theme")
''',
{
'available': '''
namespace available {
func is_available (): bool { return true }
func apply (t: string) { print ("available: {t}") }
}
''',
'unavailable': '''
namespace unavailable {
func is_available (): bool { return false }
func apply (t: string) { print ("SHOULD NOT SEE THIS") }
}
''',
},
)
assert code == 0
assert "available: my-theme" in stdout
assert "SHOULD NOT SEE THIS" not in stdout
def test_each_if_all_available(self):
code, stdout, _ = _run_with_plugins(
'''
plugin.load_dir ("__PLUGIN_DIR__")
plugin.each_if ("is_ready", "run")
''',
{
'r1': '''
namespace r1 {
func is_ready (): bool { return true }
func run () { print ("r1 ran") }
}
''',
'r2': '''
namespace r2 {
func is_ready (): bool { return true }
func run () { print ("r2 ran") }
}
''',
},
)
assert code == 0
assert "r1 ran" in stdout
assert "r2 ran" in stdout
def test_each_if_none_available(self):
code, stdout, _ = _run_with_plugins(
'''
plugin.load_dir ("__PLUGIN_DIR__")
plugin.each_if ("is_available", "apply", "x")
print ("ok")
''',
{
'off': '''
namespace off {
func is_available (): bool { return false }
func apply (t: string) { print ("NO") }
}
''',
},
)
assert code == 0
assert "NO" not in stdout
assert "ok" in stdout
class TestPluginThemeSwitcherPattern:
def test_prefix_defaults_apply(self):
code, stdout, _ = _run_with_plugins(
'''
plugin.load_dir ("__PLUGIN_DIR__")
foreach name in plugin.list ().split (" ") {
prefix = plugin.call (name, "prefix")
light = plugin.call (name, "default_light")
dark = plugin.call (name, "default_dark")
print ("{prefix}: {light} / {dark}")
}
suffix = "_DARK_THEME"
foreach name in plugin.list ().split (" ") {
available = plugin.call (name, "is_available")
if available == "true" {
prefix = plugin.call (name, "prefix")
plugin.call (name, "apply", prefix .. suffix)
}
}
''',
{
'kvantum': '''
namespace kvantum {
func prefix (): string { return "KV" }
func default_light (): string { return "KvGnome" }
func default_dark (): string { return "KvGnomeDark" }
func is_available (): bool { return true }
func apply (theme: string) { print ("kvantum -> {theme}") }
}
''',
'gtk3': '''
namespace gtk3 {
func prefix (): string { return "GTK3" }
func default_light (): string { return "adw-gtk3" }
func default_dark (): string { return "adw-gtk3-dark" }
func is_available (): bool { return false }
func apply (theme: string) { print ("gtk3 -> {theme}") }
}
''',
},
)
assert code == 0
assert "KV: KvGnome / KvGnomeDark" in stdout
assert "GTK3: adw-gtk3 / adw-gtk3-dark" in stdout
assert "kvantum -> KV_DARK_THEME" in stdout
assert "gtk3 ->" not in stdout
class TestPluginCompilation:
def test_plugin_module_compiles(self):
rc, script, errs = compile_ct('''
plugin.load_dir ("/some/path")
plugin.each ("test")
''')
assert rc == 0
assert '__ct_plugin_load_dir' in script
assert '__ct_plugin_each' in script
def test_plugin_stdlib_emitted(self):
rc, script, _ = compile_ct('''
plugin.load ("/some/plugin.sh")
result = plugin.list ()
''')
assert rc == 0
assert 'declare -ga __CT_PLUGINS' in script
assert '__ct_plugin_load ()' in script
assert '__ct_plugin_list ()' in script
def test_plugin_not_emitted_when_unused(self):
rc, script, _ = compile_ct('''
print ("no plugins here")
''')
assert rc == 0
assert '__ct_plugin' not in script
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment