diff --git a/CHANGES b/CHANGES index 09d0aac22..270aa3191 100644 --- a/CHANGES +++ b/CHANGES @@ -34,9 +34,15 @@ This release contains contributions from (alphabetically by first name): ## Modules ## - *packages* now reports more details in the installation progress-bar. - - *netinstall* module supports and `expanded` key, which will pre-expand + - *netinstall* module supports an `expanded` key, which will pre-expand a group (as if the user had pressed the arrow-button in the tree-view). - This only affects the UI. + This only affects the UI, and only the **outermost** level of groups. + - *netinstall* module now supports a special value for *groupsUrl*. + Setting this to *local* will read the groups directly from the + configuration file. + - *netinstall* groups now support a new key `immutable` which prevents + their check-state from being changed (they are shown, or hidden, + as usual and can be expanded). - Modules that use QML need a new import line. The QML file for the module is configured through new keys *qmlSearch* and *qmlFilename* (previously those were without the `qml` prefix, which invites name diff --git a/CMakeModules/CalamaresAddTest.cmake b/CMakeModules/CalamaresAddTest.cmake index 36be0f03e..65f9389e8 100644 --- a/CMakeModules/CalamaresAddTest.cmake +++ b/CMakeModules/CalamaresAddTest.cmake @@ -50,7 +50,7 @@ function( calamares_add_test ) Qt5::Test ) calamares_automoc( ${TEST_NAME} ) - target_compile_definitions( ${TEST_NAME} PRIVATE -DBUILD_AS_TEST ${TEST_DEFINITIONS} ) + target_compile_definitions( ${TEST_NAME} PRIVATE -DBUILD_AS_TEST="${CMAKE_CURRENT_SOURCE_DIR}" ${TEST_DEFINITIONS} ) if( TEST_GUI ) target_link_libraries( ${TEST_NAME} calamaresui Qt5::Gui ) endif() diff --git a/src/modules/netinstall/CMakeLists.txt b/src/modules/netinstall/CMakeLists.txt index c5eddd32b..3e6ac3cb5 100644 --- a/src/modules/netinstall/CMakeLists.txt +++ b/src/modules/netinstall/CMakeLists.txt @@ -2,6 +2,7 @@ calamares_add_plugin( netinstall TYPE viewmodule EXPORT_MACRO PLUGINDLLEXPORT_PRO SOURCES + Config.cpp NetInstallViewStep.cpp NetInstallPage.cpp PackageTreeItem.cpp @@ -16,3 +17,14 @@ calamares_add_plugin( netinstall yamlcpp SHARED_LIB ) + +calamares_add_test( + netinstalltest + SOURCES + Tests.cpp + PackageTreeItem.cpp + PackageModel.cpp + LIBRARIES + Qt5::Gui +) + diff --git a/src/modules/netinstall/Config.cpp b/src/modules/netinstall/Config.cpp new file mode 100644 index 000000000..78718add1 --- /dev/null +++ b/src/modules/netinstall/Config.cpp @@ -0,0 +1,156 @@ +/* + * Copyright 2016, Luca Giambonini + * Copyright 2016, Lisa Vitolo + * Copyright 2017, Kyle Robbertze + * Copyright 2017-2018, 2020, Adriaan de Groot + * Copyright 2017, Gabriel Craciunescu + * + * Calamares is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Calamares is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Calamares. If not, see . + */ + +#include "Config.h" + +#include "network/Manager.h" +#include "utils/Logger.h" +#include "utils/Yaml.h" + +#include + +Config::Config( QObject* parent ) + : QObject( parent ) + , m_model( new PackageModel( this ) ) +{ +} + +Config::~Config() {} + +QString +Config::status() const +{ + switch ( m_status ) + { + case Status::Ok: + return QString(); + case Status::FailedBadConfiguration: + return tr( "Network Installation. (Disabled: Incorrect configuration)" ); + case Status::FailedBadData: + return tr( "Network Installation. (Disabled: Received invalid groups data)" ); + case Status::FailedInternalError: + return tr( "Network Installation. (Disabled: internal error)" ); + case Status::FailedNetworkError: + return tr( "Network Installation. (Disabled: Unable to fetch package lists, check your network connection)" ); + } + NOTREACHED return QString(); +} + + +void +Config::setStatus( Status s ) +{ + m_status = s; + emit statusChanged( status() ); +} + +void +Config::loadGroupList( const QVariantList& groupData ) +{ + m_model->setupModelData( groupData ); + emit statusReady(); +} + +void +Config::loadGroupList( const QUrl& url ) +{ + if ( !url.isValid() ) + { + setStatus( Status::FailedBadConfiguration ); + } + + using namespace CalamaresUtils::Network; + + cDebug() << "NetInstall loading groups from" << url; + QNetworkReply* reply = Manager::instance().asynchronousGet( + url, + RequestOptions( RequestOptions::FakeUserAgent | RequestOptions::FollowRedirect, std::chrono::seconds( 30 ) ) ); + + if ( !reply ) + { + cDebug() << Logger::Continuation << "request failed immediately."; + setStatus( Status::FailedBadConfiguration ); + } + else + { + m_reply = reply; + connect( reply, &QNetworkReply::finished, this, &Config::receivedGroupData ); + } +} + +/// @brief Convenience to zero out and deleteLater on the reply, used in dataIsHere +struct ReplyDeleter +{ + QNetworkReply*& p; + + ~ReplyDeleter() + { + if ( p ) + { + p->deleteLater(); + } + p = nullptr; + } +}; + +void +Config::receivedGroupData() +{ + if ( !m_reply || !m_reply->isFinished() ) + { + cWarning() << "NetInstall data called too early."; + setStatus( Status::FailedInternalError ); + return; + } + + cDebug() << "NetInstall group data received" << m_reply->size() << "bytes from" << m_reply->url(); + + ReplyDeleter d { m_reply }; + + // If m_required is *false* then we still say we're ready + // even if the reply is corrupt or missing. + if ( m_reply->error() != QNetworkReply::NoError ) + { + cWarning() << "unable to fetch netinstall package lists."; + cDebug() << Logger::SubEntry << "Netinstall reply error: " << m_reply->error(); + cDebug() << Logger::SubEntry << "Request for url: " << m_reply->url().toString() + << " failed with: " << m_reply->errorString(); + setStatus( Status::FailedNetworkError ); + return; + } + + QByteArray yamlData = m_reply->readAll(); + try + { + YAML::Node groups = YAML::Load( yamlData.constData() ); + + if ( !groups.IsSequence() ) + { + cWarning() << "NetInstall groups data does not form a sequence."; + } + loadGroupList( CalamaresUtils::yamlSequenceToVariant( groups ) ); + } + catch ( YAML::Exception& e ) + { + CalamaresUtils::explainYamlException( e, yamlData, "netinstall groups data" ); + setStatus( Status::FailedBadData ); + } +} diff --git a/src/modules/netinstall/Config.h b/src/modules/netinstall/Config.h new file mode 100644 index 000000000..781c9be5d --- /dev/null +++ b/src/modules/netinstall/Config.h @@ -0,0 +1,87 @@ +/* + * Copyright 2016, Luca Giambonini + * Copyright 2016, Lisa Vitolo + * Copyright 2017, Kyle Robbertze + * Copyright 2017-2018, 2020, Adriaan de Groot + * + * Calamares is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Calamares is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Calamares. If not, see . + */ + +#ifndef NETINSTALL_CONFIG_H +#define NETINSTALL_CONFIG_H + +#include "PackageModel.h" + +#include +#include + +class QNetworkReply; + +class Config : public QObject +{ + Q_OBJECT + + Q_PROPERTY( PackageModel* packageModel MEMBER m_model FINAL ) + Q_PROPERTY( QString status READ status NOTIFY statusChanged FINAL ) + +public: + Config( QObject* parent = nullptr ); + virtual ~Config(); + + enum class Status + { + Ok, + FailedBadConfiguration, + FailedInternalError, + FailedNetworkError, + FailedBadData + }; + + QString status() const; + void setStatus( Status s ); + + bool required() const { return m_required; } + void setRequired( bool r ) { m_required = r; } + + /** @brief Retrieves the groups, with name, description and packages + * + * Loads data from the given URL. Once done, the data is parsed + * and passed on to the other loadGroupList() method. + */ + void loadGroupList( const QUrl& url ); + + /** @brief Fill model from parsed data. + * + * Fills the model with a list of groups -- which can contain + * subgroups and packages -- from @p groupData. + */ + void loadGroupList( const QVariantList& groupData ); + + PackageModel* model() const { return m_model; } + +signals: + void statusChanged( QString status ); ///< Something changed + void statusReady(); ///< Loading groups is complete + +private slots: + void receivedGroupData(); ///< From async-loading group data + +private: + PackageModel* m_model = nullptr; + QNetworkReply* m_reply = nullptr; // For fetching data + Status m_status = Status::Ok; + bool m_required = false; +}; + +#endif diff --git a/src/modules/netinstall/NetInstallPage.cpp b/src/modules/netinstall/NetInstallPage.cpp index e21814caa..688e99b09 100644 --- a/src/modules/netinstall/NetInstallPage.cpp +++ b/src/modules/netinstall/NetInstallPage.cpp @@ -34,22 +34,21 @@ #include #include -NetInstallPage::NetInstallPage( QWidget* parent ) +NetInstallPage::NetInstallPage( Config* c, QWidget* parent ) : QWidget( parent ) + , m_config( c ) , ui( new Ui::Page_NetInst ) - , m_reply( nullptr ) - , m_groups( nullptr ) { ui->setupUi( this ); + ui->groupswidget->setModel( c->model() ); + connect( c, &Config::statusChanged, this, &NetInstallPage::setStatus ); + connect( c, &Config::statusReady, this, &NetInstallPage::expandGroups ); + setPageTitle( nullptr ); CALAMARES_RETRANSLATE_SLOT( &NetInstallPage::retranslate ); } -NetInstallPage::~NetInstallPage() -{ - delete m_groups; - delete m_reply; -} +NetInstallPage::~NetInstallPage() {} void NetInstallPage::setPageTitle( CalamaresUtils::Locale::TranslatedString* t ) @@ -69,147 +68,34 @@ NetInstallPage::setPageTitle( CalamaresUtils::Locale::TranslatedString* t ) void NetInstallPage::retranslate() { - if ( ui && m_title ) + if ( m_title ) { ui->label->setText( m_title->get() ); // That's get() on the TranslatedString } + ui->netinst_status->setText( m_config->status() ); } -bool -NetInstallPage::readGroups( const QByteArray& yamlData ) -{ - try - { - YAML::Node groups = YAML::Load( yamlData.constData() ); - - if ( !groups.IsSequence() ) - { - cWarning() << "netinstall groups data does not form a sequence."; - } - Q_ASSERT( groups.IsSequence() ); - m_groups = new PackageModel( groups ); - return true; - } - catch ( YAML::Exception& e ) - { - CalamaresUtils::explainYamlException( e, yamlData, "netinstall groups data" ); - return false; - } -} - -/// @brief Convenience to zero out and deleteLater on the reply, used in dataIsHere -struct ReplyDeleter -{ - QNetworkReply*& p; - - ~ReplyDeleter() - { - if ( p ) - { - p->deleteLater(); - } - p = nullptr; - } -}; - void -NetInstallPage::dataIsHere() +NetInstallPage::expandGroups() { - if ( !m_reply || !m_reply->isFinished() ) - { - cWarning() << "NetInstall data called too early."; - return; - } - - cDebug() << "NetInstall group data received" << m_reply->url(); - - ReplyDeleter d { m_reply }; - - // If m_required is *false* then we still say we're ready - // even if the reply is corrupt or missing. - if ( m_reply->error() != QNetworkReply::NoError ) - { - cWarning() << "unable to fetch netinstall package lists."; - cDebug() << Logger::SubEntry << "Netinstall reply error: " << m_reply->error(); - cDebug() << Logger::SubEntry << "Request for url: " << m_reply->url().toString() - << " failed with: " << m_reply->errorString(); - ui->netinst_status->setText( - tr( "Network Installation. (Disabled: Unable to fetch package lists, check your network connection)" ) ); - emit checkReady( !m_required ); - return; - } - - if ( !readGroups( m_reply->readAll() ) ) - { - cWarning() << "netinstall groups data was received, but invalid."; - cDebug() << Logger::SubEntry << "Url: " << m_reply->url().toString(); - cDebug() << Logger::SubEntry << "Headers: " << m_reply->rawHeaderList(); - ui->netinst_status->setText( tr( "Network Installation. (Disabled: Received invalid groups data)" ) ); - emit checkReady( !m_required ); - return; - } - - retranslate(); // For changed model - ui->groupswidget->setModel( m_groups ); - ui->groupswidget->header()->setSectionResizeMode( 0, QHeaderView::ResizeToContents ); - ui->groupswidget->header()->setSectionResizeMode( 1, QHeaderView::Stretch ); - + auto* model = m_config->model(); // Go backwards because expanding a group may cause rows to appear below it - for ( int i = m_groups->rowCount() - 1; i >= 0; --i ) + for ( int i = model->rowCount() - 1; i >= 0; --i ) { - auto index = m_groups->index(i,0); - if ( m_groups->data(index, PackageModel::MetaExpandRole).toBool() ) + auto index = model->index( i, 0 ); + if ( model->data( index, PackageModel::MetaExpandRole ).toBool() ) { - ui->groupswidget->setExpanded(index, true); + ui->groupswidget->setExpanded( index, true ); } } - - emit checkReady( true ); -} - -PackageModel::PackageItemDataList -NetInstallPage::selectedPackages() const -{ - if ( m_groups ) - { - return m_groups->getPackages(); - } - else - { - cWarning() << "no netinstall groups are available."; - return PackageModel::PackageItemDataList(); - } } void -NetInstallPage::loadGroupList( const QString& confUrl ) +NetInstallPage::setStatus( QString s ) { - using namespace CalamaresUtils::Network; - - cDebug() << "NetInstall loading groups from" << confUrl; - QNetworkReply* reply = Manager::instance().asynchronousGet( - QUrl( confUrl ), - RequestOptions( RequestOptions::FakeUserAgent | RequestOptions::FollowRedirect, std::chrono::seconds( 30 ) ) ); - - if ( !reply ) - { - cDebug() << Logger::Continuation << "request failed immediately."; - ui->netinst_status->setText( tr( "Network Installation. (Disabled: Incorrect configuration)" ) ); - } - else - { - m_reply = reply; - connect( reply, &QNetworkReply::finished, this, &NetInstallPage::dataIsHere ); - } + ui->netinst_status->setText( s ); } -void -NetInstallPage::setRequired( bool b ) -{ - m_required = b; -} - - void NetInstallPage::onActivate() { diff --git a/src/modules/netinstall/NetInstallPage.h b/src/modules/netinstall/NetInstallPage.h index 12633c6b9..13a106eaf 100644 --- a/src/modules/netinstall/NetInstallPage.h +++ b/src/modules/netinstall/NetInstallPage.h @@ -21,6 +21,7 @@ #ifndef NETINSTALLPAGE_H #define NETINSTALLPAGE_H +#include "Config.h" #include "PackageModel.h" #include "PackageTreeItem.h" @@ -42,7 +43,7 @@ class NetInstallPage : public QWidget { Q_OBJECT public: - NetInstallPage( QWidget* parent = nullptr ); + NetInstallPage( Config* config, QWidget* parent = nullptr ); virtual ~NetInstallPage(); /** @brief Sets the page title @@ -58,45 +59,22 @@ public: void onActivate(); - /** @brief Retrieves the groups, with name, description and packages - * - * Loads data from the given URL. This should be called before - * displaying the page. - */ - void loadGroupList( const QString& url ); - - // Sets the "required" state of netinstall data. Influences whether - // corrupt or unavailable data causes checkReady() to be emitted - // true (not-required) or false. - void setRequired( bool ); - bool getRequired() const { return m_required; } - - // Returns the list of packages belonging to groups that are - // selected in the view in this given moment. No data is cached here, so - // this function does not have constant time. - PackageModel::PackageItemDataList selectedPackages() const; - public slots: - void dataIsHere(); - void retranslate(); + void setStatus( QString s ); -signals: - void checkReady( bool ); + /** @brief Expand entries that should be pre-expanded. + * + * Follows the *expanded* key / the startExpanded field in the + * group entries of the model. Call this after filling up the model. + */ + void expandGroups(); private: - // Takes the YAML data representing the groups and reads them into the - // m_groups and m_groupOrder internal structures. See the README.md - // of this module to know the format expected of the YAML files. - bool readGroups( const QByteArray& yamlData ); - + Config* m_config; Ui::Page_NetInst* ui; std::unique_ptr< CalamaresUtils::Locale::TranslatedString > m_title; // Above the treeview - - QNetworkReply* m_reply; - PackageModel* m_groups; - bool m_required; }; #endif // NETINSTALLPAGE_H diff --git a/src/modules/netinstall/NetInstallViewStep.cpp b/src/modules/netinstall/NetInstallViewStep.cpp index cb79982f9..3eba788db 100644 --- a/src/modules/netinstall/NetInstallViewStep.cpp +++ b/src/modules/netinstall/NetInstallViewStep.cpp @@ -32,12 +32,11 @@ CALAMARES_PLUGIN_FACTORY_DEFINITION( NetInstallViewStepFactory, registerPlugin< NetInstallViewStep::NetInstallViewStep( QObject* parent ) : Calamares::ViewStep( parent ) - , m_widget( new NetInstallPage() ) - , m_nextEnabled( false ) + , m_widget( new NetInstallPage( &m_config ) ) , m_sidebarLabel( nullptr ) + , m_nextEnabled( false ) { - emit nextStatusChanged( true ); - connect( m_widget, &NetInstallPage::checkReady, this, &NetInstallViewStep::nextIsReady ); + connect( &m_config, &Config::statusReady, this, &NetInstallViewStep::nextIsReady ); } @@ -56,7 +55,7 @@ NetInstallViewStep::prettyName() const { return m_sidebarLabel ? m_sidebarLabel->get() : tr( "Package selection" ); -#if defined(TABLE_OF_TRANSLATIONS) +#if defined( TABLE_OF_TRANSLATIONS ) NOTREACHED // This is a table of "standard" labels for this module. If you use them // in the label: sidebar: section of the config file, the existing @@ -86,7 +85,7 @@ NetInstallViewStep::widget() bool NetInstallViewStep::isNextEnabled() const { - return m_nextEnabled; + return !m_config.required() || m_nextEnabled; } @@ -111,10 +110,10 @@ NetInstallViewStep::isAtEnd() const } -QList< Calamares::job_ptr > +Calamares::JobList NetInstallViewStep::jobs() const { - return m_jobs; + return Calamares::JobList(); } @@ -127,7 +126,7 @@ NetInstallViewStep::onActivate() void NetInstallViewStep::onLeave() { - PackageModel::PackageItemDataList packages = m_widget->selectedPackages(); + auto packages = m_config.model()->getPackages(); cDebug() << "Netinstall: Processing" << packages.length() << "packages."; static const char PACKAGEOP[] = "packageOperations"; @@ -158,13 +157,13 @@ NetInstallViewStep::onLeave() for ( const auto& package : packages ) { - if ( package.isCritical ) + if ( package->isCritical() ) { - installPackages.append( package.toOperation() ); + installPackages.append( package->toOperation() ); } else { - tryInstallPackages.append( package.toOperation() ); + tryInstallPackages.append( package->toOperation() ); } } @@ -192,16 +191,16 @@ NetInstallViewStep::onLeave() } void -NetInstallViewStep::nextIsReady( bool b ) +NetInstallViewStep::nextIsReady() { - m_nextEnabled = b; - emit nextStatusChanged( b ); + m_nextEnabled = true; + emit nextStatusChanged( true ); } void NetInstallViewStep::setConfigurationMap( const QVariantMap& configurationMap ) { - m_widget->setRequired( CalamaresUtils::getBool( configurationMap, "required", false ) ); + m_config.setRequired( CalamaresUtils::getBool( configurationMap, "required", false ) ); QString groupsUrl = CalamaresUtils::getString( configurationMap, "groupsUrl" ); if ( !groupsUrl.isEmpty() ) @@ -209,7 +208,15 @@ NetInstallViewStep::setConfigurationMap( const QVariantMap& configurationMap ) // Keep putting groupsUrl into the global storage, // even though it's no longer used for in-module data-passing. Calamares::JobQueue::instance()->globalStorage()->insert( "groupsUrl", groupsUrl ); - m_widget->loadGroupList( groupsUrl ); + if ( groupsUrl == QStringLiteral( "local" ) ) + { + QVariantList l = configurationMap.value( "groups" ).toList(); + m_config.loadGroupList( l ); + } + else + { + m_config.loadGroupList( groupsUrl ); + } } bool bogus = false; diff --git a/src/modules/netinstall/NetInstallViewStep.h b/src/modules/netinstall/NetInstallViewStep.h index ad796b8b2..d2114a346 100644 --- a/src/modules/netinstall/NetInstallViewStep.h +++ b/src/modules/netinstall/NetInstallViewStep.h @@ -20,6 +20,8 @@ #ifndef NETINSTALLVIEWSTEP_H #define NETINSTALLVIEWSTEP_H +#include "Config.h" + #include "DllMacro.h" #include "locale/TranslatableConfiguration.h" #include "utils/PluginFactory.h" @@ -47,7 +49,7 @@ public: bool isAtBeginning() const override; bool isAtEnd() const override; - QList< Calamares::job_ptr > jobs() const override; + Calamares::JobList jobs() const override; void onActivate() override; @@ -57,13 +59,14 @@ public: void setConfigurationMap( const QVariantMap& configurationMap ) override; public slots: - void nextIsReady( bool ); + void nextIsReady(); private: + Config m_config; + NetInstallPage* m_widget; - bool m_nextEnabled; CalamaresUtils::Locale::TranslatedString* m_sidebarLabel; // As it appears in the sidebar - QList< Calamares::job_ptr > m_jobs; + bool m_nextEnabled = false; }; CALAMARES_PLUGIN_FACTORY_DECLARATION( NetInstallViewStepFactory ) diff --git a/src/modules/netinstall/PackageModel.cpp b/src/modules/netinstall/PackageModel.cpp index 215ac2912..dad2207b2 100644 --- a/src/modules/netinstall/PackageModel.cpp +++ b/src/modules/netinstall/PackageModel.cpp @@ -1,7 +1,7 @@ /* === This file is part of Calamares - === * * Copyright (c) 2017, Kyle Robbertze - * Copyright 2017-2018, Adriaan de Groot + * Copyright 2017-2018, 2020, Adriaan de Groot * * Calamares is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -19,13 +19,12 @@ #include "PackageModel.h" +#include "utils/Variant.h" #include "utils/Yaml.h" -PackageModel::PackageModel( const YAML::Node& data, QObject* parent ) +PackageModel::PackageModel( QObject* parent ) : QAbstractItemModel( parent ) { - m_rootItem = new PackageTreeItem(); - setupModelData( data, m_rootItem ); } PackageModel::~PackageModel() @@ -36,7 +35,7 @@ PackageModel::~PackageModel() QModelIndex PackageModel::index( int row, int column, const QModelIndex& parent ) const { - if ( !hasIndex( row, column, parent ) ) + if ( !m_rootItem || !hasIndex( row, column, parent ) ) { return QModelIndex(); } @@ -66,7 +65,7 @@ PackageModel::index( int row, int column, const QModelIndex& parent ) const QModelIndex PackageModel::parent( const QModelIndex& index ) const { - if ( !index.isValid() ) + if ( !m_rootItem || !index.isValid() ) { return QModelIndex(); } @@ -84,7 +83,7 @@ PackageModel::parent( const QModelIndex& index ) const int PackageModel::rowCount( const QModelIndex& parent ) const { - if ( parent.column() > 0 ) + if ( !m_rootItem || ( parent.column() > 0 ) ) { return 0; } @@ -111,7 +110,7 @@ PackageModel::columnCount( const QModelIndex& ) const QVariant PackageModel::data( const QModelIndex& index, int role ) const { - if ( !index.isValid() ) + if ( !m_rootItem || !index.isValid() ) { return QVariant(); } @@ -120,7 +119,7 @@ PackageModel::data( const QModelIndex& index, int role ) const switch ( role ) { case Qt::CheckStateRole: - return index.column() == NameColumn ? item->isSelected() : QVariant(); + return index.column() == NameColumn ? ( item->isImmutable() ? QVariant() : item->isSelected() ) : QVariant(); case Qt::DisplayRole: return item->isHidden() ? QVariant() : item->data( index.column() ); case MetaExpandRole: @@ -133,6 +132,11 @@ PackageModel::data( const QModelIndex& index, int role ) const bool PackageModel::setData( const QModelIndex& index, const QVariant& value, int role ) { + if ( !m_rootItem ) + { + return false; + } + if ( role == Qt::CheckStateRole && index.isValid() ) { PackageTreeItem* item = static_cast< PackageTreeItem* >( index.internalPointer() ); @@ -148,12 +152,17 @@ PackageModel::setData( const QModelIndex& index, const QVariant& value, int role Qt::ItemFlags PackageModel::flags( const QModelIndex& index ) const { - if ( !index.isValid() ) + if ( !m_rootItem || !index.isValid() ) { return Qt::ItemFlags(); } if ( index.column() == NameColumn ) { + PackageTreeItem* item = static_cast< PackageTreeItem* >( index.internalPointer() ); + if ( item->isImmutable() ) + { + return QAbstractItemModel::flags( index ); //Qt::NoItemFlags; + } return Qt::ItemIsUserCheckable | QAbstractItemModel::flags( index ); } return QAbstractItemModel::flags( index ); @@ -169,109 +178,84 @@ PackageModel::headerData( int section, Qt::Orientation orientation, int role ) c return QVariant(); } -QList< PackageTreeItem::ItemData > +PackageTreeItem::List PackageModel::getPackages() const { - QList< PackageTreeItem* > items = getItemPackages( m_rootItem ); + if ( !m_rootItem ) + { + return PackageTreeItem::List(); + } + + auto items = getItemPackages( m_rootItem ); for ( auto package : m_hiddenItems ) + { if ( package->hiddenSelected() ) { items.append( getItemPackages( package ) ); } - QList< PackageTreeItem::ItemData > packages; - for ( auto item : items ) - { - PackageTreeItem::ItemData itemData; - itemData.preScript = item->parentItem()->preScript(); // Only groups have hooks - itemData.packageName = item->packageName(); // this seg faults - itemData.postScript = item->parentItem()->postScript(); // Only groups have hooks - itemData.isCritical = item->parentItem()->isCritical(); // Only groups are critical - packages.append( itemData ); } - return packages; + return items; } -QList< PackageTreeItem* > +PackageTreeItem::List PackageModel::getItemPackages( PackageTreeItem* item ) const { - QList< PackageTreeItem* > selectedPackages; + PackageTreeItem::List selectedPackages; for ( int i = 0; i < item->childCount(); i++ ) { - if ( item->child( i )->isSelected() == Qt::Unchecked ) + auto* child = item->child( i ); + if ( child->isSelected() == Qt::Unchecked ) { continue; } - if ( !item->child( i )->childCount() ) // package + if ( child->isPackage() ) // package { - selectedPackages.append( item->child( i ) ); + selectedPackages.append( child ); } else { - selectedPackages.append( getItemPackages( item->child( i ) ) ); + selectedPackages.append( getItemPackages( child ) ); } } return selectedPackages; } -static QString -getString( const YAML::Node& itemDefinition, const char* key ) -{ - return itemDefinition[ key ] ? CalamaresUtils::yamlToVariant( itemDefinition[ key ] ).toString() : QString(); -} - -static bool -getBool( const YAML::Node& itemDefinition, const char* key ) -{ - return itemDefinition[ key ] ? CalamaresUtils::yamlToVariant( itemDefinition[ key ] ).toBool() : false; -} - void -PackageModel::setupModelData( const YAML::Node& data, PackageTreeItem* parent ) +PackageModel::setupModelData( const QVariantList& groupList, PackageTreeItem* parent ) { - for ( YAML::const_iterator it = data.begin(); it != data.end(); ++it ) + for ( const auto& group : groupList ) { - const YAML::Node itemDefinition = *it; - - QString name( tr( CalamaresUtils::yamlToVariant( itemDefinition[ "name" ] ).toByteArray() ) ); - QString description( tr( CalamaresUtils::yamlToVariant( itemDefinition[ "description" ] ).toByteArray() ) ); - - PackageTreeItem::ItemData itemData; - itemData.name = name; - itemData.description = description; - - itemData.preScript = getString( itemDefinition, "pre-install" ); - itemData.postScript = getString( itemDefinition, "post-install" ); - itemData.isCritical = getBool( itemDefinition, "critical" ); - itemData.isHidden = getBool( itemDefinition, "hidden" ); - itemData.startExpanded = getBool( itemDefinition, "expanded" ); - - PackageTreeItem* item = new PackageTreeItem( itemData, parent ); - - if ( itemDefinition[ "selected" ] ) + QVariantMap groupMap = group.toMap(); + if ( groupMap.isEmpty() ) { - item->setSelected( getBool( itemDefinition, "selected" ) ? Qt::Checked : Qt::Unchecked ); - } - else - { - item->setSelected( parent->isSelected() ); // Inherit from it's parent + continue; } - if ( itemDefinition[ "packages" ] ) + PackageTreeItem* item = new PackageTreeItem( groupMap, parent ); + if ( groupMap.contains( "selected" ) ) { - for ( YAML::const_iterator packageIt = itemDefinition[ "packages" ].begin(); - packageIt != itemDefinition[ "packages" ].end(); - ++packageIt ) + item->setSelected( CalamaresUtils::getBool( groupMap, "selected", false ) ? Qt::Checked : Qt::Unchecked ); + } + if ( groupMap.contains( "packages" ) ) + { + for ( const auto& packageName : groupMap.value( "packages" ).toStringList() ) { - item->appendChild( - new PackageTreeItem( CalamaresUtils::yamlToVariant( *packageIt ).toString(), item ) ); + item->appendChild( new PackageTreeItem( packageName, item ) ); } } - if ( itemDefinition[ "subgroups" ] ) + if ( groupMap.contains( "subgroups" ) ) { - setupModelData( itemDefinition[ "subgroups" ], item ); + QVariantList subgroups = groupMap.value( "subgroups" ).toList(); + if ( !subgroups.isEmpty() ) + { + setupModelData( subgroups, item ); + // The children might be checked while the parent isn't (yet). + // Children are added to their parent (below) without affecting + // the checked-state -- do it manually. + item->updateSelected(); + } } - if ( item->isHidden() ) { m_hiddenItems.append( item ); @@ -283,3 +267,13 @@ PackageModel::setupModelData( const YAML::Node& data, PackageTreeItem* parent ) } } } + +void +PackageModel::setupModelData( const QVariantList& l ) +{ + emit beginResetModel(); + delete m_rootItem; + m_rootItem = new PackageTreeItem(); + setupModelData( l, m_rootItem ); + emit endResetModel(); +} diff --git a/src/modules/netinstall/PackageModel.h b/src/modules/netinstall/PackageModel.h index b76a58a42..b4e8fc102 100644 --- a/src/modules/netinstall/PackageModel.h +++ b/src/modules/netinstall/PackageModel.h @@ -37,8 +37,6 @@ class PackageModel : public QAbstractItemModel Q_OBJECT public: - using PackageItemDataList = QList< PackageTreeItem::ItemData >; - // Names for columns (unused in the code) static constexpr const int NameColumn = 0; static constexpr const int DescriptionColumn = 1; @@ -49,9 +47,11 @@ public: */ static constexpr const int MetaExpandRole = Qt::UserRole + 1; - explicit PackageModel( const YAML::Node& data, QObject* parent = nullptr ); + explicit PackageModel( QObject* parent = nullptr ); ~PackageModel() override; + void setupModelData( const QVariantList& l ); + QVariant data( const QModelIndex& index, int role ) const override; bool setData( const QModelIndex& index, const QVariant& value, int role = Qt::EditRole ) override; Qt::ItemFlags flags( const QModelIndex& index ) const override; @@ -63,14 +63,16 @@ public: int rowCount( const QModelIndex& parent = QModelIndex() ) const override; int columnCount( const QModelIndex& parent = QModelIndex() ) const override; - PackageItemDataList getPackages() const; - QList< PackageTreeItem* > getItemPackages( PackageTreeItem* item ) const; + PackageTreeItem::List getPackages() const; + PackageTreeItem::List getItemPackages( PackageTreeItem* item ) const; private: - void setupModelData( const YAML::Node& data, PackageTreeItem* parent ); + friend class ItemTests; - PackageTreeItem* m_rootItem; - QList< PackageTreeItem* > m_hiddenItems; + void setupModelData( const QVariantList& l, PackageTreeItem* parent ); + + PackageTreeItem* m_rootItem = nullptr; + PackageTreeItem::List m_hiddenItems; }; #endif // PACKAGEMODEL_H diff --git a/src/modules/netinstall/PackageTreeItem.cpp b/src/modules/netinstall/PackageTreeItem.cpp index 7e20d63e1..edc89c536 100644 --- a/src/modules/netinstall/PackageTreeItem.cpp +++ b/src/modules/netinstall/PackageTreeItem.cpp @@ -20,56 +20,68 @@ #include "PackageTreeItem.h" #include "utils/Logger.h" +#include "utils/Variant.h" -QVariant -PackageTreeItem::ItemData::toOperation() const +/** @brief Should a package be selected, given its parent's state? */ +static Qt::CheckState +parentCheckState( PackageTreeItem* parent ) { - // If it's a package with a pre- or post-script, replace - // with the more complicated datastructure. - if ( !preScript.isEmpty() || !postScript.isEmpty() ) + if ( parent ) { - QMap< QString, QVariant > sdetails; - sdetails.insert( "pre-script", preScript ); - sdetails.insert( "package", packageName ); - sdetails.insert( "post-script", postScript ); - return sdetails; + // Avoid partially-checked .. a package can't be partial + return parent->isSelected() == Qt::Unchecked ? Qt::Unchecked : Qt::Checked; } else { - return packageName; + return Qt::Unchecked; } } -PackageTreeItem::PackageTreeItem( const ItemData& data, PackageTreeItem* parent ) +/** @brief Should a subgroup be marked critical? + * + * If set explicitly, then use that, otherwise use the parent's critical-ness. + */ +static bool +parentCriticality( const QVariantMap& groupData, PackageTreeItem* parent ) +{ + if ( groupData.contains( "critical" ) ) + { + return CalamaresUtils::getBool( groupData, "critical", false ); + } + return parent ? parent->isCritical() : false; +} + +PackageTreeItem::PackageTreeItem( const QString& packageName, PackageTreeItem* parent ) : m_parentItem( parent ) - , m_data( data ) + , m_packageName( packageName ) + , m_selected( parentCheckState( parent ) ) + , m_isGroup( false ) + , m_isCritical( parent ? parent->isCritical() : false ) + , m_showReadOnly( parent ? parent->isImmutable() : false ) { } -PackageTreeItem::PackageTreeItem( const QString packageName, PackageTreeItem* parent ) - : m_parentItem( parent ) -{ - m_data.packageName = packageName; - if ( parent != nullptr ) - { - m_data.selected = parent->isSelected(); - } - else - { - m_data.selected = Qt::Unchecked; - } -} - -PackageTreeItem::PackageTreeItem( PackageTreeItem* parent ) +PackageTreeItem::PackageTreeItem( const QVariantMap& groupData, PackageTreeItem* parent ) : m_parentItem( parent ) + , m_name( CalamaresUtils::getString( groupData, "name" ) ) + , m_selected( parentCheckState( parent ) ) + , m_description( CalamaresUtils::getString( groupData, "description" ) ) + , m_preScript( CalamaresUtils::getString( groupData, "pre-install" ) ) + , m_postScript( CalamaresUtils::getString( groupData, "post-install" ) ) + , m_isGroup( true ) + , m_isCritical( parentCriticality( groupData, parent ) ) + , m_isHidden( CalamaresUtils::getBool( groupData, "hidden", false ) ) + , m_showReadOnly( CalamaresUtils::getBool( groupData, "immutable", false ) ) + , m_startExpanded( CalamaresUtils::getBool( groupData, "expanded", false ) ) { } PackageTreeItem::PackageTreeItem::PackageTreeItem() - : PackageTreeItem( QString(), nullptr ) + : m_parentItem( nullptr ) + , m_name( QStringLiteral( "" ) ) + , m_selected( Qt::Checked ) + , m_isGroup( true ) { - m_data.selected = Qt::Checked; - m_data.name = QLatin1String( "" ); } PackageTreeItem::~PackageTreeItem() @@ -108,7 +120,7 @@ PackageTreeItem::row() const QVariant PackageTreeItem::data( int column ) const { - if ( !packageName().isEmpty() ) // packages have a packagename, groups don't + if ( isPackage() ) // packages have a packagename, groups don't { switch ( column ) { @@ -123,7 +135,7 @@ PackageTreeItem::data( int column ) const switch ( column ) // group { case 0: - return QVariant( prettyName() ); + return QVariant( name() ); case 1: return QVariant( description() ); default: @@ -145,47 +157,15 @@ PackageTreeItem::parentItem() const } -QString -PackageTreeItem::prettyName() const -{ - return m_data.name; -} - -QString -PackageTreeItem::description() const -{ - return m_data.description; -} - -QString -PackageTreeItem::preScript() const -{ - return m_data.preScript; -} - -QString -PackageTreeItem::packageName() const -{ - return m_data.packageName; -} - -QString -PackageTreeItem::postScript() const -{ - return m_data.postScript; -} - -bool -PackageTreeItem::isHidden() const -{ - return m_data.isHidden; -} - bool PackageTreeItem::hiddenSelected() const { - Q_ASSERT( m_data.isHidden ); - if ( !m_data.selected ) + if ( !m_isHidden ) + { + return m_selected != Qt::Unchecked; + } + + if ( m_selected == Qt::Unchecked ) { return false; } @@ -200,33 +180,21 @@ PackageTreeItem::hiddenSelected() const currentItem = currentItem->parentItem(); } - /* Has no non-hiddent parents */ - return m_data.selected; + /* Has no non-hidden parents */ + return m_selected != Qt::Unchecked; } -bool -PackageTreeItem::isCritical() const -{ - return m_data.isCritical; -} - -Qt::CheckState -PackageTreeItem::isSelected() const -{ - return m_data.selected; -} - void PackageTreeItem::setSelected( Qt::CheckState isSelected ) { if ( parentItem() == nullptr ) - // This is the root, it is always checked so don't change state { + // This is the root, it is always checked so don't change state return; } - m_data.selected = isSelected; + m_selected = isSelected; setChildrenSelected( isSelected ); // Look for suitable parent item which may change checked-state @@ -237,39 +205,46 @@ PackageTreeItem::setSelected( Qt::CheckState isSelected ) currentItem = currentItem->parentItem(); } if ( currentItem == nullptr ) - // Reached the root .. don't bother { + // Reached the root .. don't bother return; } + currentItem->updateSelected(); +} + +void +PackageTreeItem::updateSelected() +{ // Figure out checked-state based on the children int childrenSelected = 0; int childrenPartiallySelected = 0; - for ( int i = 0; i < currentItem->childCount(); i++ ) + for ( int i = 0; i < childCount(); i++ ) { - if ( currentItem->child( i )->isSelected() == Qt::Checked ) + if ( child( i )->isSelected() == Qt::Checked ) { childrenSelected++; } - if ( currentItem->child( i )->isSelected() == Qt::PartiallyChecked ) + if ( child( i )->isSelected() == Qt::PartiallyChecked ) { childrenPartiallySelected++; } } if ( !childrenSelected && !childrenPartiallySelected ) { - currentItem->setSelected( Qt::Unchecked ); + setSelected( Qt::Unchecked ); } - else if ( childrenSelected == currentItem->childCount() ) + else if ( childrenSelected == childCount() ) { - currentItem->setSelected( Qt::Checked ); + setSelected( Qt::Checked ); } else { - currentItem->setSelected( Qt::PartiallyChecked ); + setSelected( Qt::PartiallyChecked ); } } + void PackageTreeItem::setChildrenSelected( Qt::CheckState isSelected ) { @@ -277,7 +252,7 @@ PackageTreeItem::setChildrenSelected( Qt::CheckState isSelected ) // Children are never root; don't need to use setSelected on them. for ( auto child : m_childItems ) { - child->m_data.selected = isSelected; + child->m_selected = isSelected; child->setChildrenSelected( isSelected ); } } @@ -287,3 +262,43 @@ PackageTreeItem::type() const { return QStandardItem::UserType; } + +QVariant +PackageTreeItem::toOperation() const +{ + // If it's a package with a pre- or post-script, replace + // with the more complicated datastructure. + if ( !m_preScript.isEmpty() || !m_postScript.isEmpty() ) + { + QMap< QString, QVariant > sdetails; + sdetails.insert( "pre-script", m_preScript ); + sdetails.insert( "package", m_packageName ); + sdetails.insert( "post-script", m_postScript ); + return sdetails; + } + else + { + return m_packageName; + } +} + +bool +PackageTreeItem::operator==( const PackageTreeItem& rhs ) const +{ + if ( isGroup() != rhs.isGroup() ) + { + // Different kinds + return false; + } + + if ( isGroup() ) + { + return name() == rhs.name() && description() == rhs.description() && preScript() == rhs.preScript() + && postScript() == rhs.postScript() && isCritical() == rhs.isCritical() && isHidden() == rhs.isHidden() + && m_showReadOnly == rhs.m_showReadOnly && expandOnStart() == rhs.expandOnStart(); + } + else + { + return packageName() == rhs.packageName(); + } +} diff --git a/src/modules/netinstall/PackageTreeItem.h b/src/modules/netinstall/PackageTreeItem.h index d9c1f9ec2..d443bcdc6 100644 --- a/src/modules/netinstall/PackageTreeItem.h +++ b/src/modules/netinstall/PackageTreeItem.h @@ -27,29 +27,14 @@ class PackageTreeItem : public QStandardItem { public: - struct ItemData - { - QString name; - QString description; - QString preScript; - QString packageName; - QString postScript; - bool isCritical = false; - bool isHidden = false; - bool startExpanded = false; // Only for groups - Qt::CheckState selected = Qt::Unchecked; + using List = QList< PackageTreeItem* >; - /** @brief Turns this item into a variant for PackageOperations use - * - * For "plain" items, this is just the package name; items with - * scripts return a map. See the package module for how it's interpreted. - */ - QVariant toOperation() const; - }; - explicit PackageTreeItem( const ItemData& data, PackageTreeItem* parent = nullptr ); - explicit PackageTreeItem( const QString packageName, PackageTreeItem* parent = nullptr ); - explicit PackageTreeItem( PackageTreeItem* parent ); - explicit PackageTreeItem(); // The root of the tree; always selected, named + ///@brief A package (individual package) + explicit PackageTreeItem( const QString& packageName, PackageTreeItem* parent = nullptr ); + ///@brief A group (sub-items and sub-groups are ignored) + explicit PackageTreeItem( const QVariantMap& groupData, PackageTreeItem* parent = nullptr ); + ///@brief A root item, always selected, named "" + explicit PackageTreeItem(); ~PackageTreeItem() override; void appendChild( PackageTreeItem* child ); @@ -61,18 +46,33 @@ public: PackageTreeItem* parentItem(); const PackageTreeItem* parentItem() const; - QString prettyName() const; - QString description() const; - QString preScript() const; - QString packageName() const; - QString postScript() const; + QString name() const { return m_name; } + QString packageName() const { return m_packageName; } + + QString description() const { return m_description; } + QString preScript() const { return m_preScript; } + QString postScript() const { return m_postScript; } + + /** @brief Is this item a group-item? + * + * Groups have a (possibly empty) list of packages, and a + * (possibly empty) list of sub-groups, and can be marked + * critical, hidden, etc. Packages, on the other hand, only + * have a meaningful packageName() and selection status. + * + * Root is a group. + */ + bool isGroup() const { return m_isGroup; } + + /// @brief Is this item a single package? + bool isPackage() const { return !isGroup(); } /** @brief Is this item hidden? * * Hidden items (generally only groups) are maintained separately, * not shown to the user, but do enter into the package-installation process. */ - bool isHidden() const; + bool isHidden() const { return m_isHidden; } /** @brief Is this hidden item, considered "selected"? * @@ -87,7 +87,7 @@ public: * A critical group must be successfully installed, for the Calamares * installation to continue. */ - bool isCritical() const; + bool isCritical() const { return m_isCritical; } /** @brief Is this group expanded on start? * @@ -95,17 +95,69 @@ public: * that expands on start is shown expanded (not collapsed) * in the treeview when the page is loaded. */ - bool expandOnStart() const { return m_data.startExpanded; } + bool expandOnStart() const { return m_startExpanded; } + + /** @brief Is this an immutable item? + * + * Groups can be immutable: then you can't toggle the selected + * state of any of its items. + */ + bool isImmutable() const { return m_showReadOnly; } + + /** @brief is this item selected? + * + * Groups may be partially selected; packages are only on or off. + */ + Qt::CheckState isSelected() const { return m_selected; } + + /** @brief Turns this item into a variant for PackageOperations use + * + * For "plain" items, this is just the package name; items with + * scripts return a map. See the package module for how it's interpreted. + */ + QVariant toOperation() const; - Qt::CheckState isSelected() const; void setSelected( Qt::CheckState isSelected ); void setChildrenSelected( Qt::CheckState isSelected ); + + /** @brief Update selectedness based on the children's states + * + * This only makes sense for groups, which might have packages + * or subgroups; it checks only direct children. + */ + void updateSelected(); + + // QStandardItem methods int type() const override; + /** @brief Are two items equal + * + * This **disregards** parent-item and the child-items, and compares + * only the fields for the items-proper (name, .. expanded). Note + * also that *isSelected()* is a run-time state, and is **not** + * compared either. + */ + bool operator==( const PackageTreeItem& rhs ) const; + bool operator!=( const PackageTreeItem& rhs ) const { return !( *this == rhs ); } + private: PackageTreeItem* m_parentItem; - QList< PackageTreeItem* > m_childItems; - ItemData m_data; + List m_childItems; + + // An entry can be a package, or a group. + QString m_name; + QString m_packageName; + Qt::CheckState m_selected = Qt::Unchecked; + + // These are only useful for groups + QString m_description; + QString m_preScript; + QString m_postScript; + bool m_isGroup = false; + bool m_isCritical = false; + bool m_isHidden = false; + bool m_showReadOnly = false; + bool m_startExpanded = false; }; #endif // PACKAGETREEITEM_H diff --git a/src/modules/netinstall/README.md b/src/modules/netinstall/README.md index a8803edd5..cda4b6c88 100644 --- a/src/modules/netinstall/README.md +++ b/src/modules/netinstall/README.md @@ -48,8 +48,12 @@ More keys (per group) are supported: - *critical*: if true, make the installation process fail if installing any of the packages in the group fails. Otherwise, just log a warning. Defaults to false. + - *immutable*: if true, the state of the group (and all its subgroups) + cannot be changed; it really only makes sense in combination + with *selected* set to true. This only affects the user-interface. - *expanded*: if true, the group is shown in an expanded form (that is, - not-collapsed) in the treeview on start. + not-collapsed) in the treeview on start. This only affects the user- + interface. - *subgroups*: if present this follows the same structure as the top level of the YAML file, allowing there to be sub-groups of packages to an arbitary depth diff --git a/src/modules/netinstall/Tests.cpp b/src/modules/netinstall/Tests.cpp new file mode 100644 index 000000000..cfaf20efa --- /dev/null +++ b/src/modules/netinstall/Tests.cpp @@ -0,0 +1,310 @@ +/* === This file is part of Calamares - === + * + * Copyright 2020, Adriaan de Groot + * + * Calamares is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Calamares is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Calamares. If not, see . + */ + +#include "PackageModel.h" +#include "PackageTreeItem.h" + +#include "utils/Logger.h" +#include "utils/Variant.h" +#include "utils/Yaml.h" + +#include + +class ItemTests : public QObject +{ + Q_OBJECT +public: + ItemTests(); + virtual ~ItemTests() {} + +private: + void checkAllSelected( PackageTreeItem* p ); + void recursiveCompare( PackageTreeItem*, PackageTreeItem* ); + void recursiveCompare( PackageModel&, PackageModel& ); + +private Q_SLOTS: + void initTestCase(); + + void testRoot(); + void testPackage(); + void testGroup(); + void testCompare(); + void testModel(); + void testExampleFiles(); +}; + +ItemTests::ItemTests() {} + +void +ItemTests::initTestCase() +{ + Logger::setupLogLevel( Logger::LOGDEBUG ); +} + +void +ItemTests::testRoot() +{ + PackageTreeItem r; + + QCOMPARE( r.isSelected(), Qt::Checked ); + QCOMPARE( r.name(), QStringLiteral( "" ) ); + QCOMPARE( r.parentItem(), nullptr ); + QVERIFY( r.isGroup() ); + + QVERIFY( r == r ); +} + +void +ItemTests::testPackage() +{ + PackageTreeItem p( "bash", nullptr ); + + QCOMPARE( p.isSelected(), Qt::Unchecked ); + QCOMPARE( p.packageName(), QStringLiteral( "bash" ) ); + QVERIFY( p.name().isEmpty() ); // not a group + QCOMPARE( p.parentItem(), nullptr ); + QCOMPARE( p.childCount(), 0 ); + QVERIFY( !p.isHidden() ); + QVERIFY( !p.isCritical() ); + QVERIFY( !p.isGroup() ); + QVERIFY( p.isPackage() ); + QVERIFY( p == p ); + + // This doesn't happen in normal constructions, + // because a package can't have children. + PackageTreeItem c( "zsh", &p ); + QCOMPARE( c.isSelected(), Qt::Unchecked ); + QCOMPARE( c.packageName(), QStringLiteral( "zsh" ) ); + QVERIFY( c.name().isEmpty() ); // not a group + QCOMPARE( c.parentItem(), &p ); + QVERIFY( !c.isGroup() ); + QVERIFY( c.isPackage() ); + QVERIFY( c == c ); + QVERIFY( c != p ); + + QCOMPARE( p.childCount(), 0 ); // not noticed it has a child +} + +// *INDENT-OFF* +// clang-format off +static const char doc[] = +"- name: \"CCR\"\n" +" description: \"Tools for the Chakra Community Repository\"\n" +" packages:\n" +" - ccr\n" +" - base-devel\n" +" - bash\n"; + +static const char doc_no_packages[] = +"- name: \"CCR\"\n" +" description: \"Tools for the Chakra Community Repository\"\n" +" packages: []\n"; + +static const char doc_with_expanded[] = +"- name: \"CCR\"\n" +" description: \"Tools for the Chakra Community Repository\"\n" +" expanded: true\n" +" packages:\n" +" - ccr\n" +" - base-devel\n" +" - bash\n"; +// *INDENT-ON* +// clang-format on + +void +ItemTests::testGroup() +{ + YAML::Node yamldoc = YAML::Load( doc ); + QVariantList yamlContents = CalamaresUtils::yamlSequenceToVariant( yamldoc ); + + QCOMPARE( yamlContents.length(), 1 ); + + PackageTreeItem p( yamlContents[ 0 ].toMap(), nullptr ); + QCOMPARE( p.name(), QStringLiteral( "CCR" ) ); + QVERIFY( p.packageName().isEmpty() ); + QVERIFY( p.description().startsWith( QStringLiteral( "Tools " ) ) ); + QCOMPARE( p.parentItem(), nullptr ); + QVERIFY( !p.isHidden() ); + QVERIFY( !p.isCritical() ); + // The item-constructor doesn't consider the packages: list + QCOMPARE( p.childCount(), 0 ); + QVERIFY( p.isGroup() ); + QVERIFY( !p.isPackage() ); + QVERIFY( p == p ); + + PackageTreeItem c( "zsh", nullptr ); + QVERIFY( p != c ); +} + +void +ItemTests::testCompare() +{ + PackageTreeItem p0( "bash", nullptr ); + PackageTreeItem p1( "bash", &p0 ); + PackageTreeItem p2( "bash", nullptr ); + + QVERIFY( p0 == p1 ); // Parent doesn't matter + QVERIFY( p0 == p2 ); + + p2.setSelected( Qt::Checked ); + p1.setSelected( Qt::Unchecked ); + QVERIFY( p0 == p1 ); // Neither does selected state + QVERIFY( p0 == p2 ); + + PackageTreeItem r0( nullptr ); + QVERIFY( p0 != r0 ); + QVERIFY( p1 != r0 ); + QVERIFY( r0 == r0 ); + PackageTreeItem r1( nullptr ); + QVERIFY( r0 == r1 ); // Different roots are still equal + + PackageTreeItem r2( "", nullptr ); // Fake root + QVERIFY( r0 != r2 ); + QVERIFY( r1 != r2 ); + QVERIFY( p0 != r2 ); + PackageTreeItem r3( "", nullptr ); + QVERIFY( r3 == r2 ); + + YAML::Node yamldoc = YAML::Load( doc ); // See testGroup() + QVariantList yamlContents = CalamaresUtils::yamlSequenceToVariant( yamldoc ); + QCOMPARE( yamlContents.length(), 1 ); + + PackageTreeItem p3( yamlContents[ 0 ].toMap(), nullptr ); + QVERIFY( p3 == p3 ); + QVERIFY( p3 != p1 ); + QVERIFY( p1 != p3 ); + QCOMPARE( p3.childCount(), 0 ); // Doesn't load the packages: list + + PackageTreeItem p4( CalamaresUtils::yamlSequenceToVariant( YAML::Load( doc ) )[ 0 ].toMap(), nullptr ); + QVERIFY( p3 == p4 ); + PackageTreeItem p5( CalamaresUtils::yamlSequenceToVariant( YAML::Load( doc_no_packages ) )[ 0 ].toMap(), nullptr ); + QVERIFY( p3 == p5 ); +} + +void +ItemTests::checkAllSelected( PackageTreeItem* p ) +{ + QVERIFY( p->isSelected() ); + for ( int i = 0; i < p->childCount(); ++i ) + { + checkAllSelected( p->child( i ) ); + } +} + +void +ItemTests::recursiveCompare( PackageTreeItem* l, PackageTreeItem* r ) +{ + QVERIFY( l && r ); + QVERIFY( *l == *r ); + QCOMPARE( l->childCount(), r->childCount() ); + + for ( int i = 0; i < l->childCount(); ++i ) + { + QCOMPARE( l->childCount(), r->childCount() ); + recursiveCompare( l->child( i ), r->child( i ) ); + } +} + +void +ItemTests::recursiveCompare( PackageModel& l, PackageModel& r ) +{ + return recursiveCompare( l.m_rootItem, r.m_rootItem ); +} + + +void +ItemTests::testModel() +{ + YAML::Node yamldoc = YAML::Load( doc ); // See testGroup() + QVariantList yamlContents = CalamaresUtils::yamlSequenceToVariant( yamldoc ); + QCOMPARE( yamlContents.length(), 1 ); + + PackageModel m0( nullptr ); + m0.setupModelData( yamlContents ); + + QCOMPARE( m0.m_hiddenItems.count(), 0 ); // Nothing hidden + QCOMPARE( m0.rowCount(), 1 ); // Group, the packages are invisible + QCOMPARE( m0.rowCount( m0.index( 0, 0 ) ), 3 ); // The packages + + checkAllSelected( m0.m_rootItem ); + + PackageModel m2( nullptr ); + m2.setupModelData( CalamaresUtils::yamlSequenceToVariant( YAML::Load( doc_with_expanded ) ) ); + QCOMPARE( m2.m_hiddenItems.count(), 0 ); + QCOMPARE( m2.rowCount(), 1 ); // Group, now the packages expanded but not counted + QCOMPARE( m2.rowCount( m2.index( 0, 0 ) ), 3 ); // The packages + checkAllSelected( m2.m_rootItem ); + + PackageTreeItem r; + QVERIFY( r == *m0.m_rootItem ); + + QCOMPARE( m0.m_rootItem->childCount(), 1 ); + + PackageTreeItem* group = m0.m_rootItem->child( 0 ); + QVERIFY( group->isGroup() ); + QCOMPARE( group->name(), QStringLiteral( "CCR" ) ); + QCOMPARE( group->childCount(), 3 ); + + PackageTreeItem bash( "bash", nullptr ); + // Check that the sub-packages loaded correctly + bool found_one_bash = false; + for ( int i = 0; i < group->childCount(); ++i ) + { + QVERIFY( group->child( i )->isPackage() ); + if ( bash == *( group->child( i ) ) ) + { + found_one_bash = true; + } + } + QVERIFY( found_one_bash ); + + // But m2 has "expanded" set which the others do no + QVERIFY( *( m2.m_rootItem->child( 0 ) ) != *group ); +} + +void +ItemTests::testExampleFiles() +{ + QVERIFY( QStringLiteral( BUILD_AS_TEST ).endsWith( "/netinstall" ) ); + + QDir d( BUILD_AS_TEST ); + + for ( const QString& filename : QStringList { "netinstall.yaml" } ) + { + QFile f( d.filePath( filename ) ); + QVERIFY( f.exists() ); + QVERIFY( f.open( QIODevice::ReadOnly ) ); + QByteArray contents = f.readAll(); + QVERIFY( !contents.isEmpty() ); + + YAML::Node yamldoc = YAML::Load( contents.constData() ); + QVariantList yamlContents = CalamaresUtils::yamlSequenceToVariant( yamldoc ); + + PackageModel m1( nullptr ); + m1.setupModelData( yamlContents ); + + // TODO: should test *something* about this file :/ + } +} + + +QTEST_GUILESS_MAIN( ItemTests ) + +#include "utils/moc-warnings.h" + +#include "Tests.moc" diff --git a/src/modules/netinstall/netinstall.conf b/src/modules/netinstall/netinstall.conf index 5f90fec76..82e12d558 100644 --- a/src/modules/netinstall/netinstall.conf +++ b/src/modules/netinstall/netinstall.conf @@ -11,7 +11,11 @@ # # The format of the groups file is documented in `README.md`. # -# groupsUrl: file:///usr/share/calamares/netinstall.yaml +# As a special case, setting *groupsUrl* to the literal string +# `local` means that the data is obtained from **this** config +# file, under the key *groups*. +# +groupsUrl: local # If the installation can proceed without netinstall (e.g. the Live CD # can create a working installed system, but netinstall is preferred @@ -46,3 +50,46 @@ label: # sidebar[nl]: "Pakketkeuze" # title: "Office Package" # title[nl]: "Kantoorsoftware" + +# If, and only if, *groupsUrl* is set to the literal string `local`, +# groups data is read from this file. The value of *groups* must be +# a list, with the same format as the regular `netinstall.yaml` file. +# +# This is recommended only for small static package lists. +groups: + - name: "Default" + description: "Default group" + hidden: true + selected: true + critical: false + packages: + - base + - chakra-live-skel + - name: "Shells" + description: "Shells" + hidden: false + selected: false + critical: true + subgroups: + - name: "Bash" + description: "Bourne Again Shell" + selected: true + packages: + - bash + - bash-completion + - name: "Zsh" + description: "Zee shell, boss" + packages: + - zsh + - zsh-completion + - zsh-extensions + - name: "Kernel" + description: "Kernel bits" + hidden: false + selected: true + critical: true + immutable: true + packages: + - kernel + - kernel-debugsym + - kernel-nvidia