Package manager enhancements

* marked icon
* multiple selection
* hidden flag for repository
* background color of package list black always
* consolidation of package list - identical packages are
  reduced to the latest one
This commit is contained in:
Matthias Koefferlein 2017-10-19 00:16:34 +02:00
parent 7455a8151d
commit 4ca24df814
13 changed files with 416 additions and 41 deletions

View File

@ -17,7 +17,7 @@
<item>
<widget class="QTabWidget" name="mode_tab">
<property name="currentIndex">
<number>2</number>
<number>0</number>
</property>
<widget class="QWidget" name="tab_2">
<attribute name="title">
@ -143,9 +143,15 @@
</property>
<item>
<widget class="QListView" name="salt_mine_view_new">
<property name="focusPolicy">
<enum>Qt::WheelFocus</enum>
</property>
<property name="alternatingRowColors">
<bool>true</bool>
</property>
<property name="selectionMode">
<enum>QAbstractItemView::ExtendedSelection</enum>
</property>
<property name="iconSize">
<size>
<width>64</width>
@ -436,9 +442,15 @@
</property>
<item>
<widget class="QListView" name="salt_mine_view_update">
<property name="focusPolicy">
<enum>Qt::WheelFocus</enum>
</property>
<property name="alternatingRowColors">
<bool>true</bool>
</property>
<property name="selectionMode">
<enum>QAbstractItemView::ExtendedSelection</enum>
</property>
<property name="iconSize">
<size>
<width>64</width>
@ -722,9 +734,15 @@
</property>
<item>
<widget class="QListView" name="salt_view">
<property name="focusPolicy">
<enum>Qt::WheelFocus</enum>
</property>
<property name="alternatingRowColors">
<bool>true</bool>
</property>
<property name="selectionMode">
<enum>QAbstractItemView::ExtendedSelection</enum>
</property>
<property name="iconSize">
<size>
<width>64</width>
@ -786,8 +804,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>343</width>
<height>207</height>
<width>537</width>
<height>284</height>
</rect>
</property>
<layout class="QGridLayout" name="gridLayout_2">

Binary file not shown.

Before

Width:  |  Height:  |  Size: 424 B

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1001 B

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -172,7 +172,12 @@ struct NameAndTopoIndexCompare
}
}
// TODO: UTF-8 support?
// The hidden after non-hidden
if (a->is_hidden () != b->is_hidden ()) {
return a->is_hidden () < b->is_hidden ();
}
// Finally the name (TODO: UTF-8 support?)
return a->name () < b->name ();
}
@ -240,6 +245,12 @@ Salt::invalidate ()
emit collections_changed ();
}
void
Salt::consolidate ()
{
m_root.consolidate ();
invalidate ();
}
static
bool remove_from_collection (SaltGrains &collection, const std::string &name)

View File

@ -185,6 +185,13 @@ public:
*/
bool create_grain (const SaltGrain &templ, SaltGrain &target);
/**
* @brief Removes redundant entries with same names
*
* This method will keep the first entry or the one with the higher version.
*/
void consolidate ();
/**
* @brief Gets the root collection
*

View File

@ -41,6 +41,7 @@ namespace lay
static const std::string grain_filename = "grain.xml";
SaltGrain::SaltGrain ()
: m_hidden (false)
{
// .. nothing yet ..
}
@ -62,6 +63,7 @@ SaltGrain::operator== (const SaltGrain &other) const
m_author == other.m_author &&
m_author_contact == other.m_author_contact &&
m_license == other.m_license &&
m_hidden == other.m_hidden &&
m_authored_time == other.m_authored_time &&
m_installed_time == other.m_installed_time;
}
@ -78,6 +80,12 @@ SaltGrain::set_token (const std::string &t)
m_token = t;
}
void
SaltGrain::set_hidden (bool f)
{
m_hidden = f;
}
void
SaltGrain::set_version (const std::string &v)
{
@ -374,6 +382,7 @@ SaltGrain::xml_elements ()
sp_xml_elements = new tl::XMLElementList (
tl::make_member (&SaltGrain::name, &SaltGrain::set_name, "name") +
tl::make_member (&SaltGrain::token, &SaltGrain::set_token, "token") +
tl::make_member (&SaltGrain::is_hidden, &SaltGrain::set_hidden, "hidden") +
tl::make_member (&SaltGrain::version, &SaltGrain::set_version, "version") +
tl::make_member (&SaltGrain::api_version, &SaltGrain::set_api_version, "api-version") +
tl::make_member (&SaltGrain::title, &SaltGrain::set_title, "title") +

View File

@ -326,6 +326,22 @@ public:
*/
void set_url (const std::string &u);
/**
* @brief Gets a value indicating whether the grain is hidden
* A grain can be hidden (in Salt.Mine) if it's a pure dependency package
* which is only there because others need it. Such packages are listed
* as dependencies, but they are not shown by default.
*/
bool is_hidden () const
{
return m_hidden;
}
/**
* @brief Sets a value indicating whether the grain is hidden
*/
void set_hidden (bool f);
/**
* @brief Gets the dependencies of the grain
* Grains this grain depends on are installed automatically when the grain
@ -455,6 +471,7 @@ private:
std::string m_author;
std::string m_author_contact;
std::string m_license;
bool m_hidden;
QDateTime m_authored_time, m_installed_time;
QImage m_icon, m_screenshot;
std::vector<Dependency> m_dependencies;

View File

@ -235,6 +235,71 @@ SaltGrains::from_path (const std::string &path, const std::string &prefix)
return grains;
}
void
SaltGrains::merge_with (const lay::SaltGrains &other)
{
for (lay::SaltGrains::collection_iterator c = other.begin_collections (); c != other.end_collections (); ++c) {
add_collection (*c);
}
for (lay::SaltGrains::grain_iterator g = other.begin_grains (); g != other.end_grains (); ++g) {
add_grain (*g);
}
consolidate ();
}
void
SaltGrains::consolidate ()
{
std::vector<collections_type::iterator> collection_to_delete;
std::map<std::string, collections_type::iterator> collection_by_name;
for (collections_type::iterator c = m_collections.begin (); c != m_collections.end (); ++c) {
std::map<std::string, collections_type::iterator>::iterator cn = collection_by_name.find (c->name ());
if (cn != collection_by_name.end ()) {
cn->second->merge_with (*c);
collection_to_delete.push_back (c);
} else {
c->consolidate ();
collection_by_name.insert (std::make_pair (c->name (), c));
}
}
// actually delete the additional collections
for (std::vector<collections_type::iterator>::reverse_iterator i = collection_to_delete.rbegin (); i != collection_to_delete.rend (); ++i) {
remove_collection (*i);
}
std::vector<lay::SaltGrains::grain_iterator> to_delete;
std::map<std::string, lay::SaltGrains::grain_iterator> grain_by_name;
for (lay::SaltGrains::grain_iterator g = begin_grains (); g != end_grains (); ++g) {
std::map<std::string, lay::SaltGrains::grain_iterator>::iterator gn = grain_by_name.find (g->name ());
if (gn != grain_by_name.end ()) {
// take the one with the higher version. On equal version use the first one.
if (lay::SaltGrain::compare_versions (gn->second->version (), g->version ()) < 0) {
to_delete.push_back (gn->second);
gn->second = g;
} else {
to_delete.push_back (g);
}
} else {
grain_by_name.insert (std::make_pair (g->name (), g));
}
}
// actually delete the additional elements
for (std::vector<lay::SaltGrains::grain_iterator>::reverse_iterator i = to_delete.rbegin (); i != to_delete.rend (); ++i) {
remove_grain (*i);
}
}
static tl::XMLElementList s_group_struct =
tl::make_member (&SaltGrains::name, &SaltGrains::set_name, "name") +
tl::make_member (&SaltGrains::include, "include") +

View File

@ -171,6 +171,19 @@ public:
*/
bool remove_grain (grain_iterator iter, bool with_files = false);
/**
* @brief Merges the other collection into this one
* This method will apply the rules of "consolidate" for grains and will merge
* grain collections with the same name into one.
*/
void merge_with (const lay::SaltGrains &other);
/**
* @brief Removes redundant entries with same names
* This method will keep the first entry or the one with the higher version.
*/
void consolidate ();
/**
* @brief Gets a value indicating whether the collection is empty
*/

View File

@ -132,6 +132,7 @@ SaltManagerDialog::SaltManagerDialog (QWidget *parent, lay::Salt *salt, const st
tl::log << tl::to_string (tr ("Downloading package repository from %1").arg (tl::to_qstring (m_salt_mine_url)));
m_salt_mine.load (m_salt_mine_url);
}
m_salt_mine.consolidate ();
} catch (tl::Exception &ex) {
tl::error << ex.msg ();
}
@ -168,10 +169,10 @@ SaltManagerDialog::SaltManagerDialog (QWidget *parent, lay::Salt *salt, const st
update_models ();
connect (salt_view->selectionModel (), SIGNAL (currentChanged (const QModelIndex &, const QModelIndex &)), this, SLOT (current_changed ()));
connect (salt_view->selectionModel (), SIGNAL (selectionChanged (const QItemSelection &, const QItemSelection &)), this, SLOT (selected_changed ()));
connect (salt_view, SIGNAL (doubleClicked (const QModelIndex &)), this, SLOT (edit_properties ()));
connect (salt_mine_view_new->selectionModel (), SIGNAL (currentChanged (const QModelIndex &, const QModelIndex &)), this, SLOT (mine_new_current_changed ()), Qt::QueuedConnection);
connect (salt_mine_view_update->selectionModel (), SIGNAL (currentChanged (const QModelIndex &, const QModelIndex &)), this, SLOT (mine_update_current_changed ()), Qt::QueuedConnection);
connect (salt_mine_view_new->selectionModel (), SIGNAL (selectionChanged (const QItemSelection &, const QItemSelection &)), this, SLOT (mine_new_selected_changed ()), Qt::QueuedConnection);
connect (salt_mine_view_update->selectionModel (), SIGNAL (selectionChanged (const QItemSelection &, const QItemSelection &)), this, SLOT (mine_update_selected_changed ()), Qt::QueuedConnection);
connect (salt_mine_view_new, SIGNAL (doubleClicked (const QModelIndex &)), this, SLOT (mark_clicked ()));
connect (salt_mine_view_update, SIGNAL (doubleClicked (const QModelIndex &)), this, SLOT (mark_clicked ()));
@ -235,9 +236,9 @@ SaltManagerDialog::SaltManagerDialog (QWidget *parent, lay::Salt *salt, const st
connect (actionMarkForUpdate, SIGNAL (triggered ()), this, SLOT (mark_clicked ()));
connect (actionUnmarkForUpdate, SIGNAL (triggered ()), this, SLOT (mark_clicked ()));
mine_update_current_changed ();
mine_new_current_changed ();
current_changed ();
mine_update_selected_changed ();
mine_new_selected_changed ();
selected_changed ();
}
void
@ -285,13 +286,13 @@ SaltManagerDialog::show_marked_only_new ()
return;
}
salt_mine_view_new->setCurrentIndex (QModelIndex ());
salt_mine_view_new->clearSelection ();
for (int i = model->rowCount (QModelIndex ()); i > 0; ) {
--i;
SaltGrain *g = model->grain_from_index (model->index (i, 0, QModelIndex ()));
salt_mine_view_new->setRowHidden (i, show_marked_only && !(g && model->is_marked (g->name ())));
mine_new_current_changed ();
mine_new_selected_changed ();
}
}
@ -307,13 +308,13 @@ SaltManagerDialog::show_marked_only_update ()
return;
}
salt_mine_view_update->setCurrentIndex (QModelIndex ());
salt_mine_view_new->clearSelection ();
for (int i = model->rowCount (QModelIndex ()); i > 0; ) {
--i;
SaltGrain *g = model->grain_from_index (model->index (i, 0, QModelIndex ()));
salt_mine_view_update->setRowHidden (i, show_marked_only && !(g && model->is_marked (g->name ())));
mine_update_current_changed ();
mine_update_selected_changed ();
}
}
@ -424,11 +425,15 @@ SaltManagerDialog::mark_clicked ()
return;
}
SaltGrain *g = model->grain_from_index (view->currentIndex ());
if (g) {
model->set_marked (g->name (), toggle ? ! model->is_marked (g->name ()) : set);
update_apply_state ();
QModelIndexList indexes = view->selectionModel ()->selectedIndexes ();
for (QModelIndexList::const_iterator i = indexes.begin (); i != indexes.end (); ++i) {
SaltGrain *g = model->grain_from_index (*i);
if (g) {
model->set_marked (g->name (), toggle ? ! model->is_marked (g->name ()) : set);
}
}
update_apply_state ();
}
void
@ -461,7 +466,7 @@ SaltManagerDialog::update_apply_state ()
}
model = dynamic_cast <SaltModel *> (salt_mine_view_update->model ());
model = dynamic_cast <SaltModel *> (salt_mine_view_update->model ());
if (model) {
int marked = 0;
@ -548,7 +553,7 @@ SaltManagerDialog::edit_properties ()
QMessageBox::critical (this, tr ("Package is not Editable"),
tr ("This package cannot be edited.\n\nEither you don't have write permissions on the directory or the package was installed from a repository."));
} else if (mp_properties_dialog->exec_dialog (g, mp_salt)) {
current_changed ();
selected_changed ();
}
}
}
@ -566,6 +571,7 @@ SaltManagerDialog::set_current_grain_by_name (const std::string &current)
QModelIndex index = model->index (i, 0, QModelIndex ());
SaltGrain *g = model->grain_from_index (index);
if (g && g->name () == current) {
salt_view->clearSelection ();
salt_view->setCurrentIndex (index);
break;
}
@ -611,13 +617,22 @@ SaltManagerDialog::delete_grain ()
{
BEGIN_PROTECTED
SaltGrain *g = current_grain ();
if (! g) {
std::vector<SaltGrain *> gg = current_grains ();
if (gg.empty ()) {
throw tl::Exception (tl::to_string (tr ("No package selected to delete")));
}
if (QMessageBox::question (this, tr ("Delete Package"), tr ("Are you sure to delete package '%1'?").arg (tl::to_qstring (g->name ())), QMessageBox::Yes, QMessageBox::No) == QMessageBox::Yes) {
mp_salt->remove_grain (*g);
if (gg.size () == 1) {
SaltGrain *g = gg.front ();
if (QMessageBox::question (this, tr ("Delete Package"), tr ("Are you sure to delete package '%1'?").arg (tl::to_qstring (g->name ())), QMessageBox::Yes, QMessageBox::No) == QMessageBox::Yes) {
mp_salt->remove_grain (*g);
}
} else {
if (QMessageBox::question (this, tr ("Delete Packages"), tr ("Are you sure to delete the selected %1 packages?").arg (int (gg.size ())), QMessageBox::Yes, QMessageBox::No) == QMessageBox::Yes) {
for (std::vector<SaltGrain *>::const_iterator i = gg.begin (); i != gg.end (); ++i) {
mp_salt->remove_grain (**i);
}
}
}
END_PROTECTED
@ -782,6 +797,7 @@ SaltManagerDialog::update_models ()
// select the first grain
if (mine_model->rowCount (QModelIndex ()) > 0) {
salt_mine_view_update->clearSelection ();
salt_mine_view_update->setCurrentIndex (mine_model->index (0, 0, QModelIndex ()));
}
@ -804,17 +820,18 @@ SaltManagerDialog::update_models ()
// select the first grain
if (mine_model->rowCount (QModelIndex ()) > 0) {
salt_mine_view_new->clearSelection ();
salt_mine_view_new->setCurrentIndex (mine_model->index (0, 0, QModelIndex ()));
}
mine_new_current_changed ();
mine_update_current_changed ();
current_changed ();
mine_new_selected_changed ();
mine_update_selected_changed ();
selected_changed ();
update_apply_state ();
}
void
SaltManagerDialog::current_changed ()
SaltManagerDialog::selected_changed ()
{
SaltGrain *g = current_grain ();
details_text->set_grain (g);
@ -832,17 +849,49 @@ lay::SaltGrain *
SaltManagerDialog::current_grain ()
{
SaltModel *model = dynamic_cast <SaltModel *> (salt_view->model ());
return model ? model->grain_from_index (salt_view->currentIndex ()) : 0;
QModelIndexList indexes = salt_view->selectionModel ()->selectedIndexes ();
if (indexes.size () == 1 && model) {
return model->grain_from_index (indexes.front ());
} else {
return 0;
}
}
std::vector<lay::SaltGrain *>
SaltManagerDialog::current_grains ()
{
std::vector<lay::SaltGrain *> res;
SaltModel *model = dynamic_cast <SaltModel *> (salt_view->model ());
if (model) {
QModelIndexList indexes = salt_view->selectionModel ()->selectedIndexes ();
for (QModelIndexList::const_iterator i = indexes.begin (); i != indexes.end (); ++i) {
lay::SaltGrain *g = model->grain_from_index (*i);
if (g) {
res.push_back (g);
}
}
}
return res;
}
void
SaltManagerDialog::mine_update_current_changed ()
SaltManagerDialog::mine_update_selected_changed ()
{
BEGIN_PROTECTED
SaltModel *model = dynamic_cast <SaltModel *> (salt_mine_view_update->model ());
tl_assert (model != 0);
SaltGrain *g = model->grain_from_index (salt_mine_view_update->currentIndex ());
SaltGrain *g = 0;
QModelIndexList indexes = salt_mine_view_update->selectionModel ()->selectedIndexes();
if (indexes.size () == 1) {
g = model->grain_from_index (indexes.front ());
}
details_update_frame->setEnabled (g != 0);
@ -852,13 +901,18 @@ END_PROTECTED
}
void
SaltManagerDialog::mine_new_current_changed ()
SaltManagerDialog::mine_new_selected_changed ()
{
BEGIN_PROTECTED
SaltModel *model = dynamic_cast <SaltModel *> (salt_mine_view_new->model ());
tl_assert (model != 0);
SaltGrain *g = model->grain_from_index (salt_mine_view_new->currentIndex ());
SaltGrain *g = 0;
QModelIndexList indexes = salt_mine_view_new->selectionModel ()->selectedIndexes();
if (indexes.size () == 1) {
g = model->grain_from_index (indexes.front ());
}
details_new_frame->setEnabled (g != 0);

View File

@ -29,6 +29,7 @@
#include <QDialog>
#include <memory>
#include <vector>
namespace lay
{
@ -81,17 +82,17 @@ private slots:
/**
* @brief Called when the currently selected package (grain) has changed
*/
void current_changed ();
void selected_changed ();
/**
* @brief Called when the currently selected package from the update page has changed
*/
void mine_update_current_changed ();
void mine_update_selected_changed ();
/**
* @brief Called when the currently selected package from the new installation page has changed
*/
void mine_new_current_changed ();
void mine_new_selected_changed ();
/**
* @brief Called when the "edit" button is pressed
@ -172,10 +173,12 @@ private:
int m_current_tab;
SaltGrain *current_grain ();
std::vector<lay::SaltGrain *> current_grains ();
void set_current_grain_by_name (const std::string &current);
void update_models ();
void update_apply_state ();
void get_remote_grain_info (lay::SaltGrain *g, SaltGrainDetailsTextWidget *details);
void consolidate_salt_mine_entries ();
};
}

View File

@ -135,10 +135,16 @@ SaltModel::data (const QModelIndex &index, int role) const
}
bool en = is_enabled (g->name ());
bool hidden = g->is_hidden ();
std::string text = "<html><body>";
if (! en) {
if (! en || hidden) {
text += "<font color=\"#c0c0c0\">";
} else {
text += "<font color=\"#303030\">";
}
if (hidden) {
text += "<i>";
}
text += "<h4>";
text += tl::escaped_to_html (g->name ());
@ -168,9 +174,12 @@ SaltModel::data (const QModelIndex &index, int role) const
}
}
if (! en) {
text += "</font>";
if (hidden) {
text += "<p>";
text += tl::to_string (tr ("This package is an auxiliary package for use with other packages."));
text += "</p></i>";
}
text += "</font>";
text += "</body></html>";
return tl::to_qstring (text);

View File

@ -30,7 +30,7 @@
#include <QDir>
#include <QSignalSpy>
static std::string grains_to_string (const lay::SaltGrains &gg)
static std::string grains_to_string (const lay::SaltGrains &gg, bool with_version = false)
{
std::string res;
res += "[";
@ -41,6 +41,15 @@ static std::string grains_to_string (const lay::SaltGrains &gg)
}
first = false;
res += g->name ();
if (with_version) {
res += "(";
res += g->version ();
if (!g->url ().empty ()) {
res += ":";
res += g->url ();
}
res += ")";
}
}
for (lay::SaltGrains::collection_iterator gc = gg.begin_collections (); gc != gg.end_collections (); ++gc) {
if (! first) {
@ -48,7 +57,7 @@ static std::string grains_to_string (const lay::SaltGrains &gg)
}
first = false;
res += gc->name ();
res += grains_to_string (*gc);
res += grains_to_string (*gc, with_version);
}
res += "]";
return res;
@ -403,3 +412,163 @@ TEST (5)
EXPECT_EQ (tl::join (names, ","), "g3,g2,g1,g4");
}
TEST (6)
{
lay::SaltGrains gg1;
lay::SaltGrains gg2;
lay::SaltGrain ga1;
ga1.set_name ("a");
ga1.set_url ("url1");
ga1.set_version ("1.0");
lay::SaltGrain ga2;
ga2.set_name ("a");
ga2.set_url ("url2");
ga2.set_version ("1.1");
lay::SaltGrain gb;
gb.set_name ("b");
lay::SaltGrain gc;
gc.set_name ("c");
gg1.add_grain (ga1);
gg1.add_grain (gb);
gg2.add_grain (gc);
gg2.add_grain (ga2);
// higher version wins
gg1.merge_with (gg2);
EXPECT_EQ (grains_to_string (gg1, true), "[b(),c(),a(1.1:url2)]");
gg1 = lay::SaltGrains ();
gg2 = lay::SaltGrains ();
gg2.add_grain (gc);
gg1.add_grain (ga2);
gg1.add_grain (gb);
gg2.add_grain (ga1);
// higher version wins - also in different order
gg1.merge_with (gg2);
EXPECT_EQ (grains_to_string (gg1, true), "[a(1.1:url2),b(),c()]");
gg1 = lay::SaltGrains ();
gg2 = lay::SaltGrains ();
gg2.add_grain (gc);
ga2.set_version ("1.0");
gg1.add_grain (ga2);
gg1.add_grain (gb);
gg2.add_grain (ga1);
// first one wins on same version
gg1.merge_with (gg2);
EXPECT_EQ (grains_to_string (gg1, true), "[a(1.0:url2),b(),c()]");
gg1 = lay::SaltGrains ();
gg1.add_grain (gc);
gg1.add_grain (ga2);
gg1.add_grain (ga1);
gg1.add_grain (gb);
// consolidate does the same on one list
gg1.consolidate ();
EXPECT_EQ (grains_to_string (gg1, true), "[c(),a(1.0:url2),b()]");
gg1 = lay::SaltGrains ();
gg1.add_grain (ga1);
gg1.add_grain (ga2);
gg1.add_grain (gb);
gg1.add_grain (gc);
// consolidate does the same on one list
gg1.consolidate ();
EXPECT_EQ (grains_to_string (gg1, true), "[a(1.0:url1),b(),c()]");
gg1 = lay::SaltGrains ();
ga1.set_version ("1.1");
gg1.add_grain (ga1);
gg1.add_grain (ga2);
gg1.add_grain (gb);
// consolidate does the same on one list
gg1.consolidate ();
EXPECT_EQ (grains_to_string (gg1, true), "[a(1.1:url1),b()]");
// merging of sub-collections
gg1 = lay::SaltGrains ();
gg2 = lay::SaltGrains ();
lay::SaltGrains gga1;
gga1.set_name ("a");
{
lay::SaltGrain g;
g.set_name ("a");
g.set_version ("1.0");
g.set_url ("url1");
gga1.add_grain (g);
}
{
lay::SaltGrain g;
g.set_name ("b");
gga1.add_grain (g);
}
lay::SaltGrains ggb;
ggb.set_name ("b");
{
lay::SaltGrain g;
g.set_name ("x");
ggb.add_grain (g);
}
gg1.add_collection (gga1);
gg1.add_collection (ggb);
lay::SaltGrains gga2;
gga2.set_name ("a");
{
lay::SaltGrain g;
g.set_name ("a");
g.set_version ("1.1");
g.set_url ("url2");
gga2.add_grain (g);
}
{
lay::SaltGrain g;
g.set_name ("c");
gga2.add_grain (g);
}
lay::SaltGrains ggc;
ggc.set_name ("c");
{
lay::SaltGrain g;
g.set_name ("y");
ggc.add_grain (g);
}
gg2.add_collection (gga2);
gg2.add_collection (ggc);
// gg2:a collection is merged into gg1:a, gg2:c is copied.
gg1.merge_with (gg2);
EXPECT_EQ (grains_to_string (gg1, true), "[a[b(),a(1.1:url2),c()],b[x()],c[y()]]");
}