github.com/ethanhsieh/snapd@v0.0.0-20210615102523-3db9b8e4edc5/snap/quota/quota_test.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2021 Canonical Ltd
     5   *
     6   * This program is free software: you can redistribute it and/or modify
     7   * it under the terms of the GNU General Public License version 3 as
     8   * published by the Free Software Foundation.
     9   *
    10   * This program is distributed in the hope that it will be useful,
    11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
    12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    13   * GNU General Public License for more details.
    14   *
    15   * You should have received a copy of the GNU General Public License
    16   * along with this program.  If not, see <http://www.gnu.org/licenses/>.
    17   *
    18   */
    19  
    20  package quota_test
    21  
    22  import (
    23  	"fmt"
    24  	"math"
    25  	"testing"
    26  
    27  	. "gopkg.in/check.v1"
    28  
    29  	"github.com/snapcore/snapd/gadget/quantity"
    30  	"github.com/snapcore/snapd/snap/quota"
    31  	"github.com/snapcore/snapd/systemd"
    32  )
    33  
    34  // Hook up check.v1 into the "go test" runner
    35  func Test(t *testing.T) { TestingT(t) }
    36  
    37  type quotaTestSuite struct{}
    38  
    39  var _ = Suite(&quotaTestSuite{})
    40  
    41  func (ts *quotaTestSuite) TestNewGroup(c *C) {
    42  
    43  	tt := []struct {
    44  		name          string
    45  		sliceFileName string
    46  		limit         quantity.Size
    47  		err           string
    48  		comment       string
    49  	}{
    50  		{
    51  			name:    "group1",
    52  			limit:   quantity.SizeMiB,
    53  			comment: "basic happy",
    54  		},
    55  		{
    56  			name:    "biglimit",
    57  			limit:   quantity.Size(math.MaxUint64),
    58  			comment: "huge limit happy",
    59  		},
    60  		{
    61  			name:    "zero",
    62  			limit:   0,
    63  			err:     `group memory limit must be non-zero`,
    64  			comment: "group with zero memory limit",
    65  		},
    66  		{
    67  			name:    "group1-unsupported chars",
    68  			limit:   quantity.SizeMiB,
    69  			err:     `invalid quota group name: contains invalid characters.*`,
    70  			comment: "unsupported characters in group name",
    71  		},
    72  		{
    73  			name:    "group%%%",
    74  			limit:   quantity.SizeMiB,
    75  			err:     `invalid quota group name: contains invalid characters.*`,
    76  			comment: "more invalid characters in name",
    77  		},
    78  		{
    79  			name:    "CAPITALIZED",
    80  			limit:   quantity.SizeMiB,
    81  			err:     `invalid quota group name: contains invalid characters.*`,
    82  			comment: "capitalized letters",
    83  		},
    84  		{
    85  			name:    "g1",
    86  			limit:   quantity.SizeMiB,
    87  			comment: "small group name",
    88  		},
    89  		{
    90  			name:          "name-with-dashes",
    91  			sliceFileName: `name\x2dwith\x2ddashes`,
    92  			limit:         quantity.SizeMiB,
    93  			comment:       "name with dashes",
    94  		},
    95  		{
    96  			name:    "",
    97  			limit:   quantity.SizeMiB,
    98  			err:     `invalid quota group name: must not be empty`,
    99  			comment: "empty group name",
   100  		},
   101  		{
   102  			name:    "g",
   103  			limit:   quantity.SizeMiB,
   104  			err:     `invalid quota group name: must be between 2 and 40 characters long.*`,
   105  			comment: "too small group name",
   106  		},
   107  		{
   108  			name:    "root",
   109  			limit:   quantity.SizeMiB,
   110  			err:     `group name "root" reserved`,
   111  			comment: "reserved root name",
   112  		},
   113  		{
   114  			name:    "snapd",
   115  			limit:   quantity.SizeMiB,
   116  			err:     `group name "snapd" reserved`,
   117  			comment: "reserved snapd name",
   118  		},
   119  		{
   120  			name:    "system",
   121  			limit:   quantity.SizeMiB,
   122  			err:     `group name "system" reserved`,
   123  			comment: "reserved system name",
   124  		},
   125  		{
   126  			name:    "user",
   127  			limit:   quantity.SizeMiB,
   128  			err:     `group name "user" reserved`,
   129  			comment: "reserved user name",
   130  		},
   131  	}
   132  
   133  	for _, t := range tt {
   134  		comment := Commentf(t.comment)
   135  		grp, err := quota.NewGroup(t.name, t.limit)
   136  		if t.err != "" {
   137  			c.Assert(err, ErrorMatches, t.err, comment)
   138  			continue
   139  		}
   140  		c.Assert(err, IsNil, comment)
   141  
   142  		if t.sliceFileName != "" {
   143  			c.Assert(grp.SliceFileName(), Equals, "snap."+t.sliceFileName+".slice", comment)
   144  		} else {
   145  			c.Assert(grp.SliceFileName(), Equals, "snap."+t.name+".slice", comment)
   146  		}
   147  	}
   148  }
   149  
   150  func (ts *quotaTestSuite) TestSimpleSubGroupVerification(c *C) {
   151  	tt := []struct {
   152  		rootname      string
   153  		rootlimit     quantity.Size
   154  		subname       string
   155  		sliceFileName string
   156  		sublimit      quantity.Size
   157  		err           string
   158  		comment       string
   159  	}{
   160  		{
   161  			rootlimit: quantity.SizeMiB,
   162  			subname:   "sub",
   163  			sublimit:  quantity.SizeMiB,
   164  			comment:   "basic sub group with same quota as parent happy",
   165  		},
   166  		{
   167  			rootlimit: quantity.SizeMiB,
   168  			subname:   "sub",
   169  			sublimit:  quantity.SizeMiB / 2,
   170  			comment:   "basic sub group with smaller quota than parent happy",
   171  		},
   172  		{
   173  			rootlimit:     quantity.SizeMiB,
   174  			subname:       "sub-with-dashes",
   175  			sliceFileName: `myroot-sub\x2dwith\x2ddashes`,
   176  			sublimit:      quantity.SizeMiB / 2,
   177  			comment:       "basic sub group with dashes in the name",
   178  		},
   179  		{
   180  			rootname:      "my-root",
   181  			rootlimit:     quantity.SizeMiB,
   182  			subname:       "sub-with-dashes",
   183  			sliceFileName: `my\x2droot-sub\x2dwith\x2ddashes`,
   184  			sublimit:      quantity.SizeMiB / 2,
   185  			comment:       "parent and sub group have dashes in name",
   186  		},
   187  		{
   188  			rootlimit: quantity.SizeMiB,
   189  			subname:   "sub",
   190  			sublimit:  quantity.SizeMiB * 2,
   191  			err:       "sub-group memory limit of 2 MiB is too large to fit inside remaining quota space 1 MiB for parent group myroot",
   192  			comment:   "sub group with larger quota than parent unhappy",
   193  		},
   194  		{
   195  			rootlimit: quantity.SizeMiB,
   196  			subname:   "sub invalid chars",
   197  			sublimit:  quantity.SizeMiB,
   198  			err:       `invalid quota group name: contains invalid characters.*`,
   199  			comment:   "sub group with invalid name",
   200  		},
   201  		{
   202  			rootlimit: quantity.SizeMiB,
   203  			subname:   "myroot",
   204  			sublimit:  quantity.SizeMiB,
   205  			err:       `cannot use same name "myroot" for sub group as parent group`,
   206  			comment:   "sub group with same name as parent group",
   207  		},
   208  		{
   209  			rootlimit: quantity.SizeMiB,
   210  			subname:   "snapd",
   211  			sublimit:  quantity.SizeMiB,
   212  			err:       `group name "snapd" reserved`,
   213  			comment:   "sub group with reserved name",
   214  		},
   215  		{
   216  			rootlimit: quantity.SizeMiB,
   217  			subname:   "zero",
   218  			sublimit:  0,
   219  			err:       `group memory limit must be non-zero`,
   220  			comment:   "sub group with zero memory limit",
   221  		},
   222  	}
   223  
   224  	for _, t := range tt {
   225  		comment := Commentf(t.comment)
   226  		// make a root group
   227  		rootname := t.rootname
   228  		if rootname == "" {
   229  			rootname = "myroot"
   230  		}
   231  		rootGrp, err := quota.NewGroup(rootname, t.rootlimit)
   232  		c.Assert(err, IsNil, comment)
   233  
   234  		// make a sub-group under the root group
   235  		subGrp, err := rootGrp.NewSubGroup(t.subname, t.sublimit)
   236  		if t.err != "" {
   237  			c.Assert(err, ErrorMatches, t.err, comment)
   238  			continue
   239  		}
   240  		c.Assert(err, IsNil, comment)
   241  
   242  		if t.sliceFileName != "" {
   243  			c.Assert(subGrp.SliceFileName(), Equals, "snap."+t.sliceFileName+".slice")
   244  		} else {
   245  			c.Assert(subGrp.SliceFileName(), Equals, "snap.myroot-"+t.subname+".slice")
   246  		}
   247  	}
   248  }
   249  
   250  func (ts *quotaTestSuite) TestComplexSubGroups(c *C) {
   251  	rootGrp, err := quota.NewGroup("myroot", quantity.SizeMiB)
   252  	c.Assert(err, IsNil)
   253  
   254  	// try adding 2 sub-groups with total quota split exactly equally
   255  	sub1, err := rootGrp.NewSubGroup("sub1", quantity.SizeMiB/2)
   256  	c.Assert(err, IsNil)
   257  	c.Assert(sub1.SliceFileName(), Equals, "snap.myroot-sub1.slice")
   258  
   259  	sub2, err := rootGrp.NewSubGroup("sub2", quantity.SizeMiB/2)
   260  	c.Assert(err, IsNil)
   261  	c.Assert(sub2.SliceFileName(), Equals, "snap.myroot-sub2.slice")
   262  
   263  	// adding another sub-group to this group fails
   264  	_, err = rootGrp.NewSubGroup("sub3", 1)
   265  	c.Assert(err, ErrorMatches, "sub-group memory limit of 1 B is too large to fit inside remaining quota space 0 B for parent group myroot")
   266  
   267  	// we can however add a sub-group to one of the sub-groups with the exact
   268  	// size of the parent sub-group
   269  	subsub1, err := sub1.NewSubGroup("subsub1", quantity.SizeMiB/2)
   270  	c.Assert(err, IsNil)
   271  	c.Assert(subsub1.SliceFileName(), Equals, "snap.myroot-sub1-subsub1.slice")
   272  
   273  	// and we can even add a smaller sub-sub-sub-group to the sub-group
   274  	subsubsub1, err := subsub1.NewSubGroup("subsubsub1", quantity.SizeMiB/4)
   275  	c.Assert(err, IsNil)
   276  	c.Assert(subsubsub1.SliceFileName(), Equals, "snap.myroot-sub1-subsub1-subsubsub1.slice")
   277  }
   278  
   279  func (ts *quotaTestSuite) TestResolveCrossReferences(c *C) {
   280  	tt := []struct {
   281  		grps    map[string]*quota.Group
   282  		err     string
   283  		comment string
   284  	}{
   285  		{
   286  			grps: map[string]*quota.Group{
   287  				"foogroup": {
   288  					Name:        "foogroup",
   289  					MemoryLimit: quantity.SizeMiB,
   290  				},
   291  			},
   292  			comment: "single group",
   293  		},
   294  		{
   295  			grps: map[string]*quota.Group{
   296  				"foogroup": {
   297  					Name:        "foogroup",
   298  					MemoryLimit: quantity.SizeMiB,
   299  					ParentGroup: "foogroup",
   300  				},
   301  			},
   302  			err:     `group "foogroup" is invalid: group has circular parent reference to itself`,
   303  			comment: "parent group self-reference group",
   304  		},
   305  		{
   306  			grps: map[string]*quota.Group{
   307  				"foogroup": {
   308  					Name:        "foogroup",
   309  					MemoryLimit: quantity.SizeMiB,
   310  					SubGroups:   []string{"foogroup"},
   311  				},
   312  			},
   313  			err:     `group "foogroup" is invalid: group has circular sub-group reference to itself`,
   314  			comment: "parent group self-reference group",
   315  		},
   316  		{
   317  			grps: map[string]*quota.Group{
   318  				"foogroup": {
   319  					Name:        "foogroup",
   320  					MemoryLimit: 0,
   321  				},
   322  			},
   323  			err:     `group "foogroup" is invalid: group memory limit must be non-zero`,
   324  			comment: "invalid group",
   325  		},
   326  		{
   327  			grps: map[string]*quota.Group{
   328  				"foogroup": {
   329  					Name:        "foogroup",
   330  					MemoryLimit: quantity.SizeMiB,
   331  				},
   332  				"foogroup2": {
   333  					Name:        "foogroup2",
   334  					MemoryLimit: quantity.SizeMiB,
   335  				},
   336  			},
   337  			comment: "multiple root groups",
   338  		},
   339  		{
   340  			grps: map[string]*quota.Group{
   341  				"foogroup": {
   342  					Name:        "foogroup",
   343  					MemoryLimit: quantity.SizeMiB,
   344  				},
   345  				"subgroup": {
   346  					Name:        "subgroup",
   347  					MemoryLimit: quantity.SizeMiB,
   348  					ParentGroup: "foogroup",
   349  				},
   350  			},
   351  			err:     `group "foogroup" does not reference necessary child group "subgroup"`,
   352  			comment: "incomplete references in parent group to child group",
   353  		},
   354  		{
   355  			grps: map[string]*quota.Group{
   356  				"foogroup": {
   357  					Name:        "foogroup",
   358  					MemoryLimit: quantity.SizeMiB,
   359  					SubGroups:   []string{"subgroup"},
   360  				},
   361  				"subgroup": {
   362  					Name:        "subgroup",
   363  					MemoryLimit: quantity.SizeMiB,
   364  				},
   365  			},
   366  			err:     `group "subgroup" does not reference necessary parent group "foogroup"`,
   367  			comment: "incomplete references in sub-group to parent group",
   368  		},
   369  		{
   370  			grps: map[string]*quota.Group{
   371  				"foogroup": {
   372  					Name:        "foogroup",
   373  					MemoryLimit: quantity.SizeMiB,
   374  					SubGroups:   []string{"subgroup"},
   375  				},
   376  				"subgroup": {
   377  					Name:        "subgroup",
   378  					MemoryLimit: quantity.SizeMiB,
   379  					ParentGroup: "foogroup",
   380  				},
   381  			},
   382  			comment: "valid fully specified sub-group",
   383  		},
   384  		{
   385  			grps: map[string]*quota.Group{
   386  				"foogroup": {
   387  					Name:        "foogroup",
   388  					MemoryLimit: quantity.SizeMiB,
   389  					SubGroups:   []string{"subgroup1", "subgroup2"},
   390  				},
   391  				"subgroup1": {
   392  					Name:        "subgroup1",
   393  					MemoryLimit: quantity.SizeMiB / 2,
   394  					ParentGroup: "foogroup",
   395  				},
   396  				"subgroup2": {
   397  					Name:        "subgroup2",
   398  					MemoryLimit: quantity.SizeMiB / 2,
   399  					ParentGroup: "foogroup",
   400  				},
   401  			},
   402  			comment: "multiple valid fully specified sub-groups",
   403  		},
   404  		{
   405  			grps: map[string]*quota.Group{
   406  				"foogroup": {
   407  					Name:        "foogroup",
   408  					MemoryLimit: quantity.SizeMiB,
   409  					SubGroups:   []string{"subgroup1"},
   410  				},
   411  				"subgroup1": {
   412  					Name:        "subgroup1",
   413  					MemoryLimit: quantity.SizeMiB,
   414  					ParentGroup: "foogroup",
   415  					SubGroups:   []string{"subgroup2"},
   416  				},
   417  				"subgroup2": {
   418  					Name:        "subgroup2",
   419  					MemoryLimit: quantity.SizeMiB,
   420  					ParentGroup: "subgroup1",
   421  				},
   422  			},
   423  			comment: "deeply nested valid fully specified sub-groups",
   424  		},
   425  		{
   426  			grps: map[string]*quota.Group{
   427  				"foogroup": {
   428  					Name:        "foogroup",
   429  					MemoryLimit: quantity.SizeMiB,
   430  					SubGroups:   []string{"subgroup1"},
   431  				},
   432  				"subgroup1": {
   433  					Name:        "subgroup1",
   434  					MemoryLimit: quantity.SizeMiB,
   435  					ParentGroup: "foogroup",
   436  					SubGroups:   []string{"subgroup2"},
   437  				},
   438  				"subgroup2": {
   439  					Name:        "subgroup2",
   440  					MemoryLimit: quantity.SizeMiB,
   441  					// missing parent reference
   442  				},
   443  			},
   444  			err:     `group "subgroup2" does not reference necessary parent group "subgroup1"`,
   445  			comment: "deeply nested invalid fully specified sub-groups",
   446  		},
   447  		{
   448  			grps: map[string]*quota.Group{
   449  				"not-foogroup": {
   450  					Name:        "foogroup",
   451  					MemoryLimit: quantity.SizeMiB,
   452  				},
   453  			},
   454  			err:     `group has name "foogroup", but is referenced as "not-foogroup"`,
   455  			comment: "group misname",
   456  		},
   457  		{
   458  			grps: map[string]*quota.Group{
   459  				"foogroup": {
   460  					Name:        "foogroup",
   461  					MemoryLimit: quantity.SizeMiB,
   462  					SubGroups:   []string{"other-missing"},
   463  				},
   464  			},
   465  			err:     `missing group "other-missing" referenced as the sub-group of group "foogroup"`,
   466  			comment: "missing sub-group name",
   467  		},
   468  		{
   469  			grps: map[string]*quota.Group{
   470  				"foogroup": {
   471  					Name:        "foogroup",
   472  					MemoryLimit: quantity.SizeMiB,
   473  					ParentGroup: "other-missing",
   474  				},
   475  			},
   476  			err:     `missing group "other-missing" referenced as the parent of group "foogroup"`,
   477  			comment: "missing sub-group name",
   478  		},
   479  	}
   480  
   481  	for _, t := range tt {
   482  		comment := Commentf(t.comment)
   483  		err := quota.ResolveCrossReferences(t.grps)
   484  		if t.err != "" {
   485  			c.Assert(err, ErrorMatches, t.err, comment)
   486  		} else {
   487  			c.Assert(err, IsNil, comment)
   488  		}
   489  	}
   490  }
   491  
   492  func (ts *quotaTestSuite) TestAddAllNecessaryGroupsAvoidsInfiniteRecursion(c *C) {
   493  	grp, err := quota.NewGroup("infinite-group", quantity.SizeGiB)
   494  	c.Assert(err, IsNil)
   495  
   496  	grp2, err := grp.NewSubGroup("infinite-group2", quantity.SizeGiB)
   497  	c.Assert(err, IsNil)
   498  
   499  	// create a cycle artificially to the same group
   500  	grp2.SetInternalSubGroups([]*quota.Group{grp2})
   501  
   502  	// now we fail to add this to a quota set
   503  	qs := &quota.QuotaGroupSet{}
   504  	err = qs.AddAllNecessaryGroups(grp)
   505  	c.Assert(err, ErrorMatches, "internal error: circular reference found")
   506  
   507  	// create a more difficult to detect cycle going from the child to the
   508  	// parent
   509  	grp2.SetInternalSubGroups([]*quota.Group{grp})
   510  	err = qs.AddAllNecessaryGroups(grp)
   511  	c.Assert(err, ErrorMatches, "internal error: circular reference found")
   512  
   513  	// make a real sub-group and try one more level of indirection going back
   514  	// to the parent
   515  	grp2.SetInternalSubGroups(nil)
   516  	grp3, err := grp2.NewSubGroup("infinite-group3", quantity.SizeGiB)
   517  	c.Assert(err, IsNil)
   518  	grp3.SetInternalSubGroups([]*quota.Group{grp})
   519  
   520  	err = qs.AddAllNecessaryGroups(grp)
   521  	c.Assert(err, ErrorMatches, "internal error: circular reference found")
   522  }
   523  
   524  func (ts *quotaTestSuite) TestAddAllNecessaryGroups(c *C) {
   525  	qs := &quota.QuotaGroupSet{}
   526  
   527  	// it should initially be empty
   528  	c.Assert(qs.AllQuotaGroups(), HasLen, 0)
   529  
   530  	grp1, err := quota.NewGroup("myroot", quantity.SizeGiB)
   531  	c.Assert(err, IsNil)
   532  
   533  	// add the group and make sure it is in the set
   534  	err = qs.AddAllNecessaryGroups(grp1)
   535  	c.Assert(err, IsNil)
   536  	c.Assert(qs.AllQuotaGroups(), DeepEquals, []*quota.Group{grp1})
   537  
   538  	// adding multiple times doesn't change the set
   539  	err = qs.AddAllNecessaryGroups(grp1)
   540  	c.Assert(err, IsNil)
   541  	err = qs.AddAllNecessaryGroups(grp1)
   542  	c.Assert(err, IsNil)
   543  	c.Assert(qs.AllQuotaGroups(), DeepEquals, []*quota.Group{grp1})
   544  
   545  	// add a new group and make sure it is in the set now
   546  	grp2, err := quota.NewGroup("myroot2", quantity.SizeGiB)
   547  	c.Assert(err, IsNil)
   548  	err = qs.AddAllNecessaryGroups(grp2)
   549  	c.Assert(err, IsNil)
   550  	c.Assert(qs.AllQuotaGroups(), DeepEquals, []*quota.Group{grp1, grp2})
   551  
   552  	// start again
   553  	qs = &quota.QuotaGroupSet{}
   554  
   555  	// make a sub-group and add the root group - it will automatically add
   556  	// the sub-group without us needing to explicitly add the sub-group
   557  	subgrp1, err := grp1.NewSubGroup("mysub1", quantity.SizeGiB)
   558  	c.Assert(err, IsNil)
   559  	// add grp2 as well
   560  	err = qs.AddAllNecessaryGroups(grp2)
   561  	c.Assert(err, IsNil)
   562  
   563  	err = qs.AddAllNecessaryGroups(grp1)
   564  	c.Assert(err, IsNil)
   565  	c.Assert(qs.AllQuotaGroups(), DeepEquals, []*quota.Group{grp1, grp2, subgrp1})
   566  
   567  	// we can explicitly add the sub-group and still have the same set too
   568  	err = qs.AddAllNecessaryGroups(subgrp1)
   569  	c.Assert(err, IsNil)
   570  	c.Assert(qs.AllQuotaGroups(), DeepEquals, []*quota.Group{grp1, grp2, subgrp1})
   571  
   572  	// create a new set of group and sub-groups to add the deepest child group
   573  	// and add that, and notice that the root groups are also added
   574  	grp3, err := quota.NewGroup("myroot3", quantity.SizeGiB)
   575  	c.Assert(err, IsNil)
   576  
   577  	subgrp3, err := grp3.NewSubGroup("mysub3", quantity.SizeGiB)
   578  	c.Assert(err, IsNil)
   579  
   580  	subsubgrp3, err := subgrp3.NewSubGroup("mysubsub3", quantity.SizeGiB)
   581  	c.Assert(err, IsNil)
   582  
   583  	err = qs.AddAllNecessaryGroups(subsubgrp3)
   584  	c.Assert(err, IsNil)
   585  	c.Assert(qs.AllQuotaGroups(), DeepEquals, []*quota.Group{grp1, grp2, grp3, subgrp1, subgrp3, subsubgrp3})
   586  
   587  	// finally create a tree with multiple branches and ensure that adding just
   588  	// a single deepest child will add all the other deepest children from other
   589  	// branches
   590  	grp4, err := quota.NewGroup("myroot4", quantity.SizeGiB)
   591  	c.Assert(err, IsNil)
   592  
   593  	subgrp4, err := grp4.NewSubGroup("mysub4", quantity.SizeGiB/2)
   594  	c.Assert(err, IsNil)
   595  
   596  	subgrp5, err := grp4.NewSubGroup("mysub5", quantity.SizeGiB/2)
   597  	c.Assert(err, IsNil)
   598  
   599  	// adding just subgrp5 to a quota set will automatically add the other sub
   600  	// group, subgrp4
   601  	qs2 := &quota.QuotaGroupSet{}
   602  	err = qs2.AddAllNecessaryGroups(subgrp4)
   603  	c.Assert(err, IsNil)
   604  	c.Assert(qs2.AllQuotaGroups(), DeepEquals, []*quota.Group{grp4, subgrp4, subgrp5})
   605  }
   606  
   607  func (ts *quotaTestSuite) TestResolveCrossReferencesLimitCheckSkipsSelf(c *C) {
   608  	grp1, err := quota.NewGroup("myroot", quantity.SizeGiB)
   609  	c.Assert(err, IsNil)
   610  
   611  	subgrp1, err := grp1.NewSubGroup("mysub1", quantity.SizeGiB)
   612  	c.Assert(err, IsNil)
   613  
   614  	subgrp2, err := subgrp1.NewSubGroup("mysub2", quantity.SizeGiB)
   615  	c.Assert(err, IsNil)
   616  
   617  	all := map[string]*quota.Group{
   618  		"myroot": grp1,
   619  		"mysub1": subgrp1,
   620  		"mysub2": subgrp2,
   621  	}
   622  	err = quota.ResolveCrossReferences(all)
   623  	c.Assert(err, IsNil)
   624  }
   625  
   626  func (ts *quotaTestSuite) TestResolveCrossReferencesCircular(c *C) {
   627  	grp1, err := quota.NewGroup("myroot", quantity.SizeGiB)
   628  	c.Assert(err, IsNil)
   629  
   630  	subgrp1, err := grp1.NewSubGroup("mysub1", quantity.SizeGiB)
   631  	c.Assert(err, IsNil)
   632  
   633  	subgrp2, err := subgrp1.NewSubGroup("mysub2", quantity.SizeGiB)
   634  	c.Assert(err, IsNil)
   635  
   636  	all := map[string]*quota.Group{
   637  		"myroot": grp1,
   638  		"mysub1": subgrp1,
   639  		"mysub2": subgrp2,
   640  	}
   641  	// try to set up circular ref
   642  	subgrp2.SubGroups = append(subgrp2.SubGroups, "mysub1")
   643  	err = quota.ResolveCrossReferences(all)
   644  	c.Assert(err, ErrorMatches, `.*reference necessary parent.*`)
   645  }
   646  
   647  type systemctlInactiveServiceError struct{}
   648  
   649  func (s systemctlInactiveServiceError) Msg() []byte   { return []byte("inactive") }
   650  func (s systemctlInactiveServiceError) ExitCode() int { return 0 }
   651  func (s systemctlInactiveServiceError) Error() string { return "inactive" }
   652  
   653  func (ts *quotaTestSuite) TestCurrentMemoryUsage(c *C) {
   654  	systemctlCalls := 0
   655  	r := systemd.MockSystemctl(func(args ...string) ([]byte, error) {
   656  		systemctlCalls++
   657  		switch systemctlCalls {
   658  
   659  		// inactive case, memory is 0
   660  		case 1:
   661  			// first time pretend the service is inactive
   662  			c.Assert(args, DeepEquals, []string{"is-active", "snap.group.slice"})
   663  			return []byte("inactive"), systemctlInactiveServiceError{}
   664  
   665  		// active but no tasks, but we still return the memory usage because it
   666  		// can be valid on some systems to have non-zero memory usage for a
   667  		// group without any tasks in it, such as on hirsute, arch, fedora 33+,
   668  		// and debian sid
   669  		case 2:
   670  			// now pretend it is active
   671  			c.Assert(args, DeepEquals, []string{"is-active", "snap.group.slice"})
   672  			return []byte("active"), nil
   673  		case 3:
   674  			// since it is active, we will query the tasks count too
   675  			c.Assert(args, DeepEquals, []string{"show", "--property", "TasksCurrent", "snap.group.slice"})
   676  			return []byte("TasksCurrent=0"), nil
   677  		case 4:
   678  			// and the memory count can be non-zero like
   679  			c.Assert(args, DeepEquals, []string{"show", "--property", "MemoryCurrent", "snap.group.slice"})
   680  			return []byte("MemoryCurrent=4096"), nil
   681  
   682  		// normal case, has tasks + memory usage
   683  		case 5:
   684  			// now pretend it is active again - third time is the charm
   685  			c.Assert(args, DeepEquals, []string{"is-active", "snap.group.slice"})
   686  			return []byte("active"), nil
   687  		case 6:
   688  			// since it is active, we will query the tasks count too
   689  			c.Assert(args, DeepEquals, []string{"show", "--property", "TasksCurrent", "snap.group.slice"})
   690  			return []byte("TasksCurrent=1"), nil
   691  		case 7:
   692  			// since it is active, we will query the current memory usage
   693  			c.Assert(args, DeepEquals, []string{"show", "--property", "MemoryCurrent", "snap.group.slice"})
   694  			return []byte("MemoryCurrent=1024"), nil
   695  
   696  		// bug case where 16 exb is erroneous
   697  		case 8:
   698  			// the cgroup is active, has no tasks and has 16 exb usage
   699  			c.Assert(args, DeepEquals, []string{"is-active", "snap.group.slice"})
   700  			return []byte("active"), nil
   701  		case 9:
   702  			// since it is active, we will query the tasks count too
   703  			c.Assert(args, DeepEquals, []string{"show", "--property", "TasksCurrent", "snap.group.slice"})
   704  			return []byte("TasksCurrent=0"), nil
   705  		case 10:
   706  			// since it is active, we will query the current memory usage,
   707  			// this time return an obviously wrong number
   708  			c.Assert(args, DeepEquals, []string{"show", "--property", "MemoryCurrent", "snap.group.slice"})
   709  			return []byte("MemoryCurrent=18446744073709551615"), nil
   710  
   711  		// not bug case where 16 exb is real usage
   712  		case 11:
   713  			// not the bug case, the cgroup is active, has tasks and has 16 exb
   714  			// usage but we treat it as normal and report 16 exb
   715  			c.Assert(args, DeepEquals, []string{"is-active", "snap.group.slice"})
   716  			return []byte("active"), nil
   717  		case 12:
   718  			// since it is active, we will query the tasks count too
   719  			c.Assert(args, DeepEquals, []string{"show", "--property", "TasksCurrent", "snap.group.slice"})
   720  			return []byte("TasksCurrent=1"), nil
   721  		case 13:
   722  			// since it is active, we will query the current memory usage
   723  			c.Assert(args, DeepEquals, []string{"show", "--property", "MemoryCurrent", "snap.group.slice"})
   724  			return []byte("MemoryCurrent=18446744073709551615"), nil
   725  		default:
   726  			c.Errorf("too many systemctl calls (%d) (current call is %+v)", systemctlCalls, args)
   727  			return []byte("broken test"), fmt.Errorf("broken test")
   728  		}
   729  	})
   730  	defer r()
   731  
   732  	grp1, err := quota.NewGroup("group", quantity.SizeGiB)
   733  	c.Assert(err, IsNil)
   734  
   735  	// group initially is inactive, so it has no current memory usage
   736  	currentMem, err := grp1.CurrentMemoryUsage()
   737  	c.Assert(err, IsNil)
   738  	c.Assert(currentMem, Equals, quantity.Size(0))
   739  
   740  	// now with systemctl mocked as active but no tasks in the group, it still
   741  	// can have memory usage
   742  	currentMem, err = grp1.CurrentMemoryUsage()
   743  	c.Assert(err, IsNil)
   744  	c.Assert(currentMem, Equals, 4*quantity.SizeKiB)
   745  
   746  	// with tasks in the group and a normal memory value, we just report back
   747  	// the memory value
   748  	currentMem, err = grp1.CurrentMemoryUsage()
   749  	c.Assert(err, IsNil)
   750  	c.Assert(currentMem, Equals, quantity.SizeKiB)
   751  
   752  	// with no tasks and the special 16 exb value reported by systemd we get 0
   753  	currentMem, err = grp1.CurrentMemoryUsage()
   754  	c.Assert(err, IsNil)
   755  	c.Assert(currentMem, Equals, quantity.Size(0))
   756  
   757  	// but the special value with tasks in the group reports the huge value
   758  	currentMem, err = grp1.CurrentMemoryUsage()
   759  	c.Assert(err, IsNil)
   760  	const sixteenExb = quantity.Size(1<<64 - 1)
   761  	c.Assert(currentMem, Equals, sixteenExb)
   762  }