Commit 34bd58f8 authored by Ivan Mazhukin's avatar Ivan Mazhukin

add vscode extension for epm docker tests

parent f88ade56
......@@ -25,6 +25,21 @@
./epm-docker-test.sh ayugram fedora
```
## VS Code
В каталоге `vscode-extension/` есть локальное расширение для запуска тестов кнопками из VS Code.
Быстрый запуск для разработки:
```text
1. Открыть vscode-extension/ в VS Code.
2. Нажать F5.
3. В открывшемся Extension Development Host открыть рабочий каталог с eepm.
4. Использовать панель EPM Test или кнопку EPM Test в статус-баре.
```
Расширение запускает `epm-docker-test.sh` в обычном терминале VS Code. Путь к скрипту, рабочий каталог, режим запуска, `--latest`, `-j`, remote/builder-настройки и избранные команды настраиваются через параметры `epmDockerTest.*`.
Запуск одного приложения сразу на нескольких системах:
```bash
......
{
"version": "0.2.0",
"configurations": [
{
"name": "Run Extension",
"type": "extensionHost",
"request": "launch",
"args": [
"--extensionDevelopmentPath=${workspaceFolder}"
]
}
]
}
# EPM Docker Test Runner
VS Code extension with buttons for `epm-docker-test.sh`.
## Use
1. Open this folder in VS Code: `vscode-extension`.
2. Press `F5` to start an Extension Development Host.
3. Open the repository with `epm-docker-test.sh` in that host.
4. Use the `EPM Test` activity bar view or the `$(beaker) EPM Test` status bar button.
The extension runs commands in a normal VS Code terminal, so the same output and prompts remain visible.
## Actions
- `Run app on systems`: asks for an app and target systems.
- `Run app preset`: asks for an app and `main`, `russian`, or `all`.
- `Run exec command`: asks for a shell command and target systems or preset.
- `Rerun last command`: repeats the last generated command.
- `Open log folder`: opens `${XDG_STATE_HOME:-$HOME/.local/state}/epm-docker-test` or the configured log root.
## Favorites
Pinned commands can be added to workspace settings:
```json
{
"epmDockerTest.favorites": [
{
"label": "ayugram main",
"app": "ayugram",
"preset": "main"
},
{
"label": "os-release fedora",
"exec": "cat /etc/os-release | head -3",
"systems": ["fedora"]
}
]
}
```
## Settings
Important settings:
- `epmDockerTest.scriptPath`: path to `epm-docker-test.sh`; empty means auto-detect.
- `epmDockerTest.workingDirectory`: command working directory.
- `epmDockerTest.defaultSystems`: default systems for app/exec runs.
- `epmDockerTest.defaultPreset`: default preset for preset runs.
- `epmDockerTest.defaultMode`: `auto`, `local`, or `remote`.
- `epmDockerTest.latest`: pass `--latest`.
- `epmDockerTest.parallelJobs`: pass `-j N` when greater than `1`.
- `epmDockerTest.eepmDir`, `epmDockerTest.eepmSource`, `epmDockerTest.remoteHost`, `epmDockerTest.remoteUser`, `epmDockerTest.builderUser`, `epmDockerTest.builderPath`, `epmDockerTest.logRoot`: map to the same script options.
const fs = require('fs');
const os = require('os');
const path = require('path');
const vscode = require('vscode');
const EXTENSION_ID = 'epmDockerTest';
const TERMINAL_NAME = 'EPM Docker Test';
let lastRun = undefined;
let terminal = undefined;
let extensionContext = undefined;
function activate(context) {
extensionContext = context;
const provider = new ActionsProvider();
vscode.window.registerTreeDataProvider('epmDockerTest.actions', provider);
const status = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 100);
status.name = 'EPM Docker Test';
status.text = '$(beaker) EPM Test';
status.tooltip = 'Run epm-docker-test.sh';
status.command = 'epmDockerTest.runQuick';
status.show();
context.subscriptions.push(
status,
vscode.commands.registerCommand('epmDockerTest.runQuick', runQuick),
vscode.commands.registerCommand('epmDockerTest.runApp', runApp),
vscode.commands.registerCommand('epmDockerTest.runPreset', runPreset),
vscode.commands.registerCommand('epmDockerTest.runExec', runExec),
vscode.commands.registerCommand('epmDockerTest.rerunLast', rerunLast),
vscode.commands.registerCommand('epmDockerTest.openLogs', openLogs),
vscode.commands.registerCommand('epmDockerTest.refresh', () => provider.refresh()),
vscode.commands.registerCommand('epmDockerTest.configure', () => {
vscode.commands.executeCommand('workbench.action.openSettings', '@ext:eter.epm-docker-test-runner');
}),
vscode.commands.registerCommand('epmDockerTest.runFavorite', (favorite) => runFavorite(favorite)),
vscode.workspace.onDidChangeConfiguration((event) => {
if (event.affectsConfiguration(EXTENSION_ID)) {
provider.refresh();
}
})
);
}
function deactivate() {}
class ActionsProvider {
constructor() {
this.emitter = new vscode.EventEmitter();
this.onDidChangeTreeData = this.emitter.event;
}
refresh() {
this.emitter.fire();
}
getTreeItem(item) {
return item;
}
getChildren(item) {
if (item) {
return [];
}
const favorites = getConfig().get('favorites', []);
const favoriteItems = favorites
.filter((favorite) => favorite && favorite.label)
.map((favorite) => commandItem(
favorite.label,
'epmDockerTest.runFavorite',
favorite,
new vscode.ThemeIcon('star-full')
));
const builtIns = [
commandItem('Run app on systems', 'epmDockerTest.runApp', undefined, new vscode.ThemeIcon('run')),
commandItem('Run app preset', 'epmDockerTest.runPreset', undefined, new vscode.ThemeIcon('list-selection')),
commandItem('Run exec command', 'epmDockerTest.runExec', undefined, new vscode.ThemeIcon('terminal')),
commandItem('Rerun last command', 'epmDockerTest.rerunLast', undefined, new vscode.ThemeIcon('debug-restart')),
commandItem('Open log folder', 'epmDockerTest.openLogs', undefined, new vscode.ThemeIcon('folder-opened')),
commandItem('Configure', 'epmDockerTest.configure', undefined, new vscode.ThemeIcon('settings-gear'))
];
return [...favoriteItems, ...builtIns];
}
}
function commandItem(label, command, argument, iconPath) {
const item = new vscode.TreeItem(label, vscode.TreeItemCollapsibleState.None);
item.iconPath = iconPath;
item.command = {
command,
title: label,
arguments: argument === undefined ? [] : [argument]
};
return item;
}
async function runQuick() {
const choice = await vscode.window.showQuickPick([
{ label: '$(run) Run app on systems', command: 'epmDockerTest.runApp' },
{ label: '$(list-selection) Run app preset', command: 'epmDockerTest.runPreset' },
{ label: '$(terminal) Run exec command', command: 'epmDockerTest.runExec' },
{ label: '$(debug-restart) Rerun last command', command: 'epmDockerTest.rerunLast' },
{ label: '$(folder-opened) Open log folder', command: 'epmDockerTest.openLogs' }
], {
placeHolder: 'Choose an epm-docker-test action'
});
if (choice) {
await vscode.commands.executeCommand(choice.command);
}
}
async function runApp() {
const app = await vscode.window.showInputBox({
title: 'EPM Docker Test',
prompt: 'Application name for epm play',
placeHolder: 'telegram',
value: getWorkspaceState('lastApp', '')
});
if (!app) {
return;
}
const systems = await vscode.window.showInputBox({
title: 'EPM Docker Test',
prompt: 'Target systems separated by spaces',
value: getWorkspaceState('lastSystems', getConfig().get('defaultSystems', 'fedora'))
});
if (!systems) {
return;
}
await setWorkspaceState('lastApp', app);
await setWorkspaceState('lastSystems', systems);
await executeTest({ app, systems: splitArgs(systems) });
}
async function runPreset() {
const app = await vscode.window.showInputBox({
title: 'EPM Docker Test',
prompt: 'Application name for epm play',
placeHolder: 'telegram',
value: getWorkspaceState('lastApp', '')
});
if (!app) {
return;
}
const preset = await pickPreset(getConfig().get('defaultPreset', 'main'));
if (!preset) {
return;
}
await setWorkspaceState('lastApp', app);
await executeTest({ app, preset });
}
async function runExec() {
const exec = await vscode.window.showInputBox({
title: 'EPM Docker Test',
prompt: 'Shell command to run in the container',
placeHolder: 'cat /etc/os-release | head -3',
value: getWorkspaceState('lastExec', '')
});
if (!exec) {
return;
}
const targetKind = await vscode.window.showQuickPick([
{ label: 'Systems', description: 'Type one or more target systems' },
{ label: 'Preset', description: 'Use main, russian, or all' }
], {
title: 'EPM Docker Test',
placeHolder: 'Choose target type'
});
if (!targetKind) {
return;
}
await setWorkspaceState('lastExec', exec);
if (targetKind.label === 'Preset') {
const preset = await pickPreset(getConfig().get('defaultPreset', 'main'));
if (preset) {
await executeTest({ exec, preset });
}
return;
}
const systems = await vscode.window.showInputBox({
title: 'EPM Docker Test',
prompt: 'Target systems separated by spaces',
value: getWorkspaceState('lastSystems', getConfig().get('defaultSystems', 'fedora'))
});
if (systems) {
await setWorkspaceState('lastSystems', systems);
await executeTest({ exec, systems: splitArgs(systems) });
}
}
async function rerunLast() {
const command = getWorkspaceState('lastCommand', lastRun);
if (!command) {
vscode.window.showWarningMessage('No epm-docker-test command has been run yet.');
return;
}
sendToTerminal(command);
}
async function runFavorite(favorite) {
if (!favorite) {
return;
}
await executeTest({
label: favorite.label,
app: favorite.app,
systems: favorite.systems,
preset: favorite.preset,
exec: favorite.exec,
favoriteArgs: favorite.args
});
}
async function executeTest(request) {
const resolved = resolvePaths();
if (!resolved) {
return;
}
if (!request.exec && !request.app) {
vscode.window.showErrorMessage('Favorite must define either app or exec.');
return;
}
if (!request.preset && (!request.systems || request.systems.length === 0)) {
vscode.window.showErrorMessage('Test command must define systems or preset.');
return;
}
const args = buildCommonArgs();
if (Array.isArray(request.favoriteArgs)) {
args.push(...request.favoriteArgs);
}
if (request.exec) {
args.push('--exec', request.exec);
if (request.preset) {
args.push('--preset', request.preset);
} else {
args.push(...request.systems);
}
} else {
args.push(request.app);
if (request.preset) {
args.push('--preset', request.preset);
} else {
args.push(...request.systems);
}
}
return finishExecution(resolved, args);
}
async function finishExecution(resolved, args) {
const command = [
'cd',
shellQuote(resolved.cwd),
'&&',
shellQuote(resolved.script),
...args.map(shellQuote)
].join(' ');
lastRun = command;
await setWorkspaceState('lastCommand', command);
sendToTerminal(command);
}
function buildCommonArgs() {
const config = getConfig();
const args = [];
const mode = config.get('defaultMode', 'auto');
const eepmSource = config.get('eepmSource', 'local');
const parallelJobs = Number(config.get('parallelJobs', 0));
if (mode && mode !== 'auto') {
args.push(`--${mode}`);
}
if (eepmSource && eepmSource !== 'local') {
args.push('--eepm-source', eepmSource);
}
addStringArg(args, '--eepm-dir', config.get('eepmDir', ''));
addStringArg(args, '--remote-host', config.get('remoteHost', ''));
addStringArg(args, '--remote-user', config.get('remoteUser', ''));
addStringArg(args, '--builder-user', config.get('builderUser', ''));
addStringArg(args, '--builder-path', config.get('builderPath', ''));
addStringArg(args, '--log-root', config.get('logRoot', ''));
if (config.get('latest', false)) {
args.push('--latest');
}
if (parallelJobs > 1) {
args.push('-j', String(parallelJobs));
}
const additionalArgs = config.get('additionalArgs', []);
if (Array.isArray(additionalArgs)) {
args.push(...additionalArgs.filter((value) => typeof value === 'string' && value.length > 0));
}
return args;
}
function addStringArg(args, flag, value) {
if (typeof value === 'string' && value.trim()) {
args.push(flag, value.trim());
}
}
async function pickPreset(defaultPreset) {
return vscode.window.showQuickPick(['main', 'russian', 'all'], {
title: 'EPM Docker Test',
placeHolder: `Choose a preset (${defaultPreset})`
});
}
async function openLogs() {
const logRoot = configuredLogRoot();
if (!fs.existsSync(logRoot)) {
const answer = await vscode.window.showInformationMessage(
`Log folder does not exist yet: ${logRoot}`,
'Open Parent'
);
if (answer !== 'Open Parent') {
return;
}
await vscode.commands.executeCommand('vscode.openFolder', vscode.Uri.file(path.dirname(logRoot)), {
forceNewWindow: true
});
return;
}
await vscode.commands.executeCommand('vscode.openFolder', vscode.Uri.file(logRoot), {
forceNewWindow: true
});
}
function configuredLogRoot() {
const configured = getConfig().get('logRoot', '');
if (configured && configured.trim()) {
return expandHome(configured.trim());
}
return path.join(process.env.XDG_STATE_HOME || path.join(os.homedir(), '.local', 'state'), 'epm-docker-test');
}
function resolvePaths() {
const config = getConfig();
const workspaceFolder = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
const configuredScript = config.get('scriptPath', '').trim();
const script = configuredScript ? resolveWorkspacePath(configuredScript) : findScript(workspaceFolder);
if (!script || !fs.existsSync(script)) {
vscode.window.showErrorMessage('Could not find epm-docker-test.sh. Set epmDockerTest.scriptPath.');
return undefined;
}
const configuredCwd = config.get('workingDirectory', '').trim();
const cwd = configuredCwd
? resolveWorkspacePath(configuredCwd)
: workspaceFolder || path.dirname(script);
return { script, cwd };
}
function findScript(workspaceFolder) {
const candidates = [];
if (workspaceFolder) {
candidates.push(
path.join(workspaceFolder, 'epm-docker-test.sh'),
path.join(workspaceFolder, 'epm-docker-test', 'epm-docker-test.sh')
);
}
candidates.push(path.resolve(__dirname, '..', 'epm-docker-test.sh'));
return candidates.find((candidate) => fs.existsSync(candidate));
}
function resolveWorkspacePath(value) {
const expanded = expandHome(value);
if (path.isAbsolute(expanded)) {
return expanded;
}
const workspaceFolder = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath || process.cwd();
return path.join(workspaceFolder, expanded);
}
function expandHome(value) {
if (value === '~') {
return os.homedir();
}
if (value.startsWith('~/')) {
return path.join(os.homedir(), value.slice(2));
}
return value;
}
function sendToTerminal(command) {
if (!terminal || terminal.exitStatus) {
terminal = vscode.window.createTerminal(TERMINAL_NAME);
}
terminal.show();
terminal.sendText(command, true);
}
function shellQuote(value) {
const stringValue = String(value);
if (/^[A-Za-z0-9_/:=.,@%+-]+$/.test(stringValue)) {
return stringValue;
}
return `'${stringValue.replace(/'/g, `'\\''`)}'`;
}
function splitArgs(value) {
return value.split(/\s+/).map((item) => item.trim()).filter(Boolean);
}
function getConfig() {
return vscode.workspace.getConfiguration(EXTENSION_ID);
}
function getWorkspaceState(key, fallback) {
return extensionContext?.workspaceState.get(key, fallback) ?? fallback;
}
async function setWorkspaceState(key, value) {
await extensionContext?.workspaceState.update(key, value);
}
module.exports = {
activate,
deactivate
};
{
"name": "epm-docker-test-runner",
"displayName": "EPM Docker Test Runner",
"description": "VS Code buttons for running epm-docker-test.sh test commands.",
"version": "0.1.0",
"publisher": "eter",
"engines": {
"vscode": "^1.85.0"
},
"categories": [
"Other"
],
"activationEvents": [
"onStartupFinished",
"onView:epmDockerTest.actions",
"onCommand:epmDockerTest.runQuick",
"onCommand:epmDockerTest.runApp",
"onCommand:epmDockerTest.runPreset",
"onCommand:epmDockerTest.runExec",
"onCommand:epmDockerTest.rerunLast"
],
"main": "./extension.js",
"contributes": {
"commands": [
{
"command": "epmDockerTest.runQuick",
"title": "EPM Docker Test: Run..."
},
{
"command": "epmDockerTest.runApp",
"title": "EPM Docker Test: Run App on Systems"
},
{
"command": "epmDockerTest.runPreset",
"title": "EPM Docker Test: Run App Preset"
},
{
"command": "epmDockerTest.runExec",
"title": "EPM Docker Test: Run Exec Command"
},
{
"command": "epmDockerTest.rerunLast",
"title": "EPM Docker Test: Rerun Last Command"
},
{
"command": "epmDockerTest.openLogs",
"title": "EPM Docker Test: Open Log Folder"
},
{
"command": "epmDockerTest.refresh",
"title": "EPM Docker Test: Refresh"
},
{
"command": "epmDockerTest.configure",
"title": "EPM Docker Test: Configure"
}
],
"configuration": {
"title": "EPM Docker Test",
"properties": {
"epmDockerTest.scriptPath": {
"type": "string",
"default": "",
"description": "Path to epm-docker-test.sh. Empty means auto-detect in the workspace."
},
"epmDockerTest.workingDirectory": {
"type": "string",
"default": "",
"description": "Working directory for test commands. Empty means the workspace folder or script directory."
},
"epmDockerTest.defaultSystems": {
"type": "string",
"default": "fedora",
"description": "Default systems used by Run App on Systems."
},
"epmDockerTest.defaultPreset": {
"type": "string",
"enum": [
"main",
"russian",
"all"
],
"default": "main",
"description": "Default preset used by Run App Preset."
},
"epmDockerTest.defaultMode": {
"type": "string",
"enum": [
"auto",
"local",
"remote"
],
"default": "auto",
"description": "Default runner mode."
},
"epmDockerTest.latest": {
"type": "boolean",
"default": false,
"description": "Pass --latest to epm play."
},
"epmDockerTest.parallelJobs": {
"type": "number",
"default": 0,
"minimum": 0,
"description": "Parallel jobs. 0 disables -j/--parallel, 1 runs serially, values greater than 1 pass -j N."
},
"epmDockerTest.eepmDir": {
"type": "string",
"default": "",
"description": "Optional --eepm-dir value."
},
"epmDockerTest.eepmSource": {
"type": "string",
"enum": [
"local",
"builder64"
],
"default": "local",
"description": "Value for --eepm-source."
},
"epmDockerTest.remoteHost": {
"type": "string",
"default": "",
"description": "Optional --remote-host value."
},
"epmDockerTest.remoteUser": {
"type": "string",
"default": "",
"description": "Optional --remote-user value."
},
"epmDockerTest.builderUser": {
"type": "string",
"default": "",
"description": "Optional --builder-user value."
},
"epmDockerTest.builderPath": {
"type": "string",
"default": "",
"description": "Optional --builder-path value."
},
"epmDockerTest.logRoot": {
"type": "string",
"default": "",
"description": "Optional --log-root value."
},
"epmDockerTest.additionalArgs": {
"type": "array",
"default": [],
"items": {
"type": "string"
},
"description": "Extra arguments passed before the app/exec target."
},
"epmDockerTest.favorites": {
"type": "array",
"default": [],
"description": "Pinned commands shown as buttons in the EPM Test activity view.",
"items": {
"type": "object",
"properties": {
"label": {
"type": "string"
},
"app": {
"type": "string"
},
"systems": {
"type": "array",
"items": {
"type": "string"
}
},
"preset": {
"type": "string",
"enum": [
"main",
"russian",
"all"
]
},
"exec": {
"type": "string"
},
"args": {
"type": "array",
"items": {
"type": "string"
}
}
}
}
}
}
},
"viewsContainers": {
"activitybar": [
{
"id": "epmDockerTest",
"title": "EPM Test",
"icon": "resources/epm-test.svg"
}
]
},
"views": {
"epmDockerTest": [
{
"id": "epmDockerTest.actions",
"name": "Actions"
}
]
},
"menus": {
"view/title": [
{
"command": "epmDockerTest.refresh",
"when": "view == epmDockerTest.actions",
"group": "navigation"
}
]
}
}
}
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#c5c5c5" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
<path d="M9 3h6"/>
<path d="M10 3v5l-5.2 8.4A3 3 0 0 0 7.4 21h9.2a3 3 0 0 0 2.6-4.6L14 8V3"/>
<path d="M7.5 15h9"/>
<path d="M9 18h6"/>
</svg>
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