Skip to content

Commit

Permalink
Allow scheduling a (example) product via the web UI
Browse files Browse the repository at this point in the history
  • Loading branch information
Martchus committed Sep 18, 2024
1 parent 4b4ed38 commit ac5e408
Show file tree
Hide file tree
Showing 11 changed files with 307 additions and 7 deletions.
4 changes: 4 additions & 0 deletions assets/assetpack.def
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@
< ../node_modules/ace-builds/src-min/mode-perl.js
< ../node_modules/ace-builds/src-min/mode-yaml.js
< ../node_modules/ace-builds/src-min/mode-diff.js
< ../node_modules/ace-builds/src-min/mode-ini.js

! step_edit.js
< javascripts/needleeditor.js
Expand Down Expand Up @@ -161,6 +162,9 @@
< javascripts/running.js
< javascripts/disable_status_updates.js [mode==test]

! create_tests.js
< javascripts/create_tests.js

! job_next_previous.js
< javascripts/job_next_previous.js

Expand Down
63 changes: 63 additions & 0 deletions assets/javascripts/create_tests.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
function getNonEmptyFormParams(form) {
const formData = new FormData(form);
const queryParams = new URLSearchParams();
for (const [key, value] of formData) {
if (value.length > 0) {
queryParams.append(key, value);
}
}
return queryParams;
}

function setupAceEditor(elementID, mode) {
const element = document.getElementById(elementID);
const initialValue = element.textContent;
const editor = ace.edit(element, {
mode: mode,
maxLines: Infinity,
tabSize: 2,
useSoftTabs: true
});
editor.session.setUseWrapMode(true);
editor.initialValue = initialValue;
return editor;
}

function setupCreateTestsForm() {
window.scenarioDefinitionsEditor = setupAceEditor('create-tests-scenario-definitions', 'ace/mode/yaml');
window.settingsEditor = setupAceEditor('create-tests-settings', 'ace/mode/ini');
}

function resetCreateTestsForm() {
window.scenarioDefinitionsEditor.setValue(window.scenarioDefinitionsEditor.initialValue, -1);
window.settingsEditor.setValue(window.settingsEditor.initialValue, -1);
}

function createTests(form) {
event.preventDefault();

const scenarioDefinitions = window.scenarioDefinitionsEditor.getValue();
const queryParams = getNonEmptyFormParams(form);
window.settingsEditor
.getValue()
.split('\n')
.map(line => line.split('=', 2))
.forEach(setting => queryParams.append(setting[0].trim(), (setting[1] ?? '').trim()));
queryParams.append('async', true);
if (scenarioDefinitions.length > 0) {
queryParams.append('SCENARIO_DEFINITIONS_YAML', scenarioDefinitions);
}
$.ajax({
url: form.dataset.postUrl,
method: form.method,
data: queryParams.toString(),
success: function (response) {
const id = response.scheduled_product_id;
const url = `${form.dataset.productlogUrl}?id=${id}`;
addFlash('info', `Tests have been scheduled, checkout the <a href="${url}">product log</a> for details.`);
},
error: function (xhr, ajaxOptions, thrownError) {
addFlash('danger', 'Unable to create tests: ' + (xhr.responseJSON?.error ?? xhr.responseText ?? thrownError));
}
});
}
11 changes: 9 additions & 2 deletions lib/OpenQA/Setup.pm
Original file line number Diff line number Diff line change
Expand Up @@ -198,8 +198,15 @@ sub read_config ($app) {
influxdb => {
ignored_failed_minion_jobs => '',
},
carry_over => \%CARRY_OVER_DEFAULTS
);
carry_over => \%CARRY_OVER_DEFAULTS,
'test_presets/example' => {
title => 'Create example test',
info => 'Parameters to create an example test have been pre-filled in the following form. '
. 'You can simply submit the form as-is to test your openQA setup.',
casedir => 'https://github.com/os-autoinst/os-autoinst-distri-example.git',
distri => 'example',
build => 'openqa',
});

# in development mode we use fake auth and log to stderr
my %mode_defaults = (
Expand Down
1 change: 1 addition & 0 deletions lib/OpenQA/WebAPI.pm
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ sub startup ($self) {
$r->get('/search')->name('search')->to(template => 'search/search');

$r->get('/tests')->name('tests')->to('test#list');
$r->get('/tests/create')->name('tests_create')->to('test#create');
# we have to set this and some later routes up differently on Mojo
# < 9 and Mojo >= 9.11
if ($Mojolicious::VERSION > 9.10) {
Expand Down
48 changes: 47 additions & 1 deletion lib/OpenQA/WebAPI/Controller/Test.pm
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ use OpenQA::Utils;
use OpenQA::Jobs::Constants;
use OpenQA::Schema::Result::Jobs;
use OpenQA::Schema::Result::JobDependencies;
use OpenQA::Utils qw(determine_web_ui_web_socket_url get_ws_status_only_url);
use OpenQA::YAML qw(load_yaml);
use OpenQA::Utils qw(determine_web_ui_web_socket_url get_ws_status_only_url testcasedir);
use Mojo::ByteStream;
use Mojo::Util 'xml_escape';
use Mojo::File 'path';
Expand Down Expand Up @@ -116,6 +117,51 @@ sub list {
my ($self) = @_;
}

sub _load_test_preset ($self, $preset_key) {
return undef unless defined $preset_key;
# avoid reading INI file again on subsequent calls
state %presets;
return $presets{$preset_key} if exists $presets{$preset_key};
$presets{$preset_key} = undef;
# read preset from an INI section [test_presets/…] or fallback to defaults assigned on setup
my $config = $self->app->config;
return undef unless my $ini_config = $config->{ini_config};
my $ini_key = "test_presets/$preset_key";
return $presets{$preset_key}
= $ini_config->SectionExists($ini_key)
? {map { ($_ => $ini_config->val($ini_key, $_)) } $ini_config->Parameters($ini_key)}

Check warning on line 132 in lib/OpenQA/WebAPI/Controller/Test.pm

View check run for this annotation

Codecov / codecov/patch

lib/OpenQA/WebAPI/Controller/Test.pm#L132

Added line #L132 was not covered by tests
: $config->{$ini_key};
}

sub _load_scenario_definitions ($self, $preset) {
return undef if exists $preset->{scenario_definitions};
return undef unless my $casedir = testcasedir($preset->{distri}, $preset->{version});
my $defs_yaml = eval { path($casedir, 'scenario-definitions.yaml')->slurp('UTF-8') };
$preset->{scenario_definitions} = $defs_yaml;
return $self->stash(flash_error => "Unable to read scenario definitions for the specified preset: $@") if $@;
my $defs = eval { load_yaml(string => $defs_yaml) };
return $self->stash(flash_error => "Unable to parse scenario definitions for the specified preset: $@") if $@;
my $e = join("\n", @{$self->app->validate_yaml($defs, 'JobScenarios-01.yaml')});
return $self->stash(flash_error => "Unable to validate scenarios definitions of the specified preset:\n$e") if $e;
return undef unless my @products = values %{$defs->{products}};
return undef unless my @job_templates = keys %{$defs->{job_templates}};
$preset->{$_} //= $products[0]->{$_} for qw(distri version flavor arch);
$preset->{test} //= $job_templates[0];
}

sub create ($self) {
my $preset_key = $self->param('preset');
my $preset = $self->_load_test_preset($preset_key);
if (defined $preset) {
$self->stash(flash_info => $preset->{info});
$self->_load_scenario_definitions($preset);
}
elsif (defined $preset_key) {
$self->stash(flash_error => "The specified preset '$preset_key' does not exist.");
}
$self->stash(preset => ($preset // {}));
}

sub get_match_param {
my ($self) = @_;

Expand Down
3 changes: 3 additions & 0 deletions t/config.t
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,7 @@ subtest 'Test configuration default modes' => sub {
$test_config->{logging}->{level} = "debug";
$test_config->{global}->{service_port_delta} = 2;
is ref delete $config->{global}->{auto_clone_regex}, 'Regexp', 'auto_clone_regex parsed as regex';
ok delete $config->{'test_presets/example'}, 'default values for example tests assigned';
is_deeply $config, $test_config, '"test" configuration';

# Test configuration generation with "development" mode
Expand All @@ -193,6 +194,7 @@ subtest 'Test configuration default modes' => sub {
$test_config->{_openid_secret} = $config->{_openid_secret};
$test_config->{global}->{service_port_delta} = 2;
delete $config->{global}->{auto_clone_regex};
delete $config->{'test_presets/example'};
is_deeply $config, $test_config, 'right "development" configuration';

# Test configuration generation with an unknown mode (should fallback to default)
Expand All @@ -203,6 +205,7 @@ subtest 'Test configuration default modes' => sub {
$test_config->{auth}->{method} = "OpenID";
$test_config->{global}->{service_port_delta} = 2;
delete $config->{global}->{auto_clone_regex};
delete $config->{'test_presets/example'};
delete $test_config->{logging};
is_deeply $config, $test_config, 'right default configuration';
};
Expand Down
10 changes: 10 additions & 0 deletions t/data/openqa/share/tests/example/scenario-definitions.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
products:
example:
distri: "example"
flavor: "DVD"
arch: "x86_64"
version: '0'
job_templates:
simple_boot:
product: "example"
77 changes: 77 additions & 0 deletions t/ui/29-create_tests.t
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
#!/usr/bin/env perl
# Copyright 2024 SUSE LLC
# SPDX-License-Identifier: GPL-2.0-or-later

use Test::Most;

use FindBin;
use Test::Mojo;
use Test::Warnings ':report_warnings';
use lib "$FindBin::Bin/../lib", "$FindBin::Bin/../../external/os-autoinst-common/lib";
use OpenQA::Test::TimeLimit '15';
use OpenQA::Test::Case;
use OpenQA::SeleniumTest;

my $schema = OpenQA::Test::Case->new->init_data;
my $scheduled_products = $schema->resultset('ScheduledProducts');

driver_missing unless my $driver = call_driver;
my $url = 'http://localhost:' . OpenQA::SeleniumTest::get_mojoport;

subtest 'navigation to form' => sub {
$driver->get("$url/login");
$driver->find_element_by_id('create-tests-action')->click;
$driver->find_element_by_link_text('Example test')->click;
$driver->title_is('openQA: Create example test', 'on page to create example test');
};

subtest 'form is pre-filled' => sub {
my $flash_messages = $driver->find_element_by_id('flash-messages');
like $flash_messages->get_text, qr/create.*example test.*pre-filled/i, 'note about example present';
$driver->find_element('#flash-messages button')->click; # dismiss
my %expected_values = (
'create-tests-distri' => 'example',
'create-tests-version' => '0',
'create-tests-flavor' => 'DVD',
'create-tests-arch' => 'x86_64',
'create-tests-build' => 'openqa',
'create-tests-test' => 'simple_boot',
'create-tests-casedir' => 'https://github.com/os-autoinst/os-autoinst-distri-example.git',
'create-tests-needlesdir' => '',
);
is element_prop($_), $expected_values{$_}, "$_ is pre-filled" for keys %expected_values;
};

subtest 'form can be submitted' => sub {
$driver->find_element('#create-tests-settings-container textarea')->send_keys("_PRIORITY=42\nISO=foo.iso");
$driver->find_element('#create-tests-form button[type="submit"]')->click;
wait_for_ajax msg => 'test creation';
my $flash_messages = $driver->find_element_by_id('flash-messages');
like $flash_messages->get_text, qr/scheduled.*product log/i, 'note about success';
};

subtest 'settings shown in product log' => sub {
$driver->find_element_by_link_text('product log')->click;
$driver->title_is('openQA: Scheduled products log', 'on product log details page');

my $settings = $driver->find_element('.settings-table')->get_text;
like $settings, qr/ARCH x86_64/, 'ARCH present';
like $settings, qr/BUILD openqa/, 'BUILD present';
like $settings, qr/CASEDIR http.*\.git/, 'CASEDIR present';
like $settings, qr/DISTRI example/, 'DISTRI present';
like $settings, qr/FLAVOR DVD/, 'FLAVOR present';
like $settings, qr/SCENARIO_DEFINITIONS_YAML ---.*products:.*job_templates:/s, 'SCENARIO_DEFINITIONS_YAML present';
like $settings, qr/TEST simple_boot/, 'TEST present';
like $settings, qr/VERSION 0/, 'VERSION present';
like $settings, qr/_PRIORITY 42/, '_PRIORITY present';
like $settings, qr/ISO foo.iso/, 'ISO present';
};

subtest 'preset not found' => sub {
$driver->get("$url/tests/create?preset=foo");
my $flash_messages = $driver->find_element_by_id('flash-messages');
like $flash_messages->get_text, qr/'foo' does not exist/i, 'error if preset does not exist';
};

kill_driver;
done_testing;
2 changes: 1 addition & 1 deletion templates/webapi/layouts/flash_messages.html.ep
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
% if (my $msg = flash('info')) {
% if (my $msg = flash('info') || stash('flash_info')) {
<div class="alert alert-primary alert-dismissible fade show" role="alert">
<span><%= $msg %></span>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
Expand Down
17 changes: 14 additions & 3 deletions templates/webapi/layouts/navbar.html.ep
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
% my $current_user = current_user;
<nav class="navbar navbar-expand-lg navbar-light">
<div class="container-fluid">
<a class="navbar-brand" href="/"><img src="<%= icon_url 'logo.svg'%>" alt="openQA"></a>
Expand All @@ -9,7 +10,17 @@
<li class='nav-item' id="all_tests">
%= link_to 'All Tests' => url_for('tests') => class => 'nav-link', title => 'Lists all tests grouped by state'
</li>

% if ($current_user && $current_user->is_operator) {
<li class="nav-item dropdown">
<a href="#" class="nav-link dropdown-toggle" data-bs-toggle="dropdown" role="button"
id="create-tests-action" title="Creates one or multiple tests"
aria-haspopup="true" aria-expanded="false">Create …</a>
<div class="dropdown-menu">
%= link_to 'Example test' => url_for('tests_create')->query({preset => 'example'}) => class => 'dropdown-item'
%= link_to 'Tests from scenario definitions' => url_for('tests_create') => class => 'dropdown-item'
</div>
</li>
% }
<li class="nav-item dropdown" id="job_groups">
<a href="#" class="nav-link dropdown-toggle" data-bs-toggle="dropdown" role="button"
aria-haspopup="true" aria-expanded="false" data-submenu
Expand Down Expand Up @@ -42,11 +53,11 @@
<input type="search" name="q" id="global-search" class="form-control navbar-input" value="<%= $self->param('q') %>" placeholder="Type to search" aria-label="Global search input">
</form>
</li>
% if (current_user) {
% if ($current_user) {
<li class="nav-item dropdown" id="user-action">
<a href="#" class="nav-link dropdown-toggle" data-bs-toggle="dropdown" role="button"
aria-haspopup="true" aria-expanded="false"
title="Contains the Activity View and various configuration pages">Logged in as <%= current_user->name %></a>
title="Contains the Activity View and various configuration pages">Logged in as <%= $current_user->name %></a>
<div class="dropdown-menu">
%= tag 'h3' => class => 'dropdown-header' => 'Operators Menu'
%= link_to 'Activity View' => url_for('activity_view') => class => 'dropdown-item' => id => 'activity_view', title => 'Gives you an overview of your current jobs'
Expand Down
Loading

0 comments on commit ac5e408

Please sign in to comment.