github.com/rigado/snapd@v2.42.5-go-mod+incompatible/wrappers/services_gen_test.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2014-2016 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 wrappers_test
    21  
    22  import (
    23  	"fmt"
    24  	"os"
    25  	"os/exec"
    26  	"path/filepath"
    27  	"strings"
    28  	"time"
    29  
    30  	. "gopkg.in/check.v1"
    31  
    32  	"github.com/snapcore/snapd/dirs"
    33  	"github.com/snapcore/snapd/snap"
    34  	"github.com/snapcore/snapd/snap/snaptest"
    35  	"github.com/snapcore/snapd/testutil"
    36  	"github.com/snapcore/snapd/timeout"
    37  	"github.com/snapcore/snapd/timeutil"
    38  	"github.com/snapcore/snapd/wrappers"
    39  )
    40  
    41  type servicesWrapperGenSuite struct {
    42  	testutil.BaseTest
    43  }
    44  
    45  var _ = Suite(&servicesWrapperGenSuite{})
    46  
    47  const expectedServiceFmt = `[Unit]
    48  # Auto-generated, DO NOT EDIT
    49  Description=Service for snap application snap.app
    50  Requires=%s-snap-44.mount
    51  Wants=network.target
    52  After=%s-snap-44.mount network.target
    53  X-Snappy=yes
    54  
    55  [Service]
    56  ExecStart=/usr/bin/snap run snap.app
    57  SyslogIdentifier=snap.app
    58  Restart=%s
    59  WorkingDirectory=/var/snap/snap/44
    60  ExecStop=/usr/bin/snap run --command=stop snap.app
    61  ExecReload=/usr/bin/snap run --command=reload snap.app
    62  ExecStopPost=/usr/bin/snap run --command=post-stop snap.app
    63  TimeoutStopSec=10
    64  Type=%s
    65  
    66  [Install]
    67  WantedBy=multi-user.target
    68  `
    69  
    70  var (
    71  	mountUnitPrefix = strings.Replace(dirs.SnapMountDir[1:], "/", "-", -1)
    72  )
    73  
    74  var (
    75  	expectedAppService     = fmt.Sprintf(expectedServiceFmt, mountUnitPrefix, mountUnitPrefix, "on-failure", "simple")
    76  	expectedDbusService    = fmt.Sprintf(expectedServiceFmt, mountUnitPrefix, mountUnitPrefix, "on-failure", "dbus\nBusName=foo.bar.baz")
    77  	expectedOneshotService = fmt.Sprintf(expectedServiceFmt, mountUnitPrefix, mountUnitPrefix, "no", "oneshot\nRemainAfterExit=yes")
    78  )
    79  
    80  var (
    81  	expectedServiceWrapperFmt = `[Unit]
    82  # Auto-generated, DO NOT EDIT
    83  Description=Service for snap application xkcd-webserver.xkcd-webserver
    84  Requires=%s-xkcd\x2dwebserver-44.mount
    85  Wants=network.target
    86  After=%s-xkcd\x2dwebserver-44.mount network.target
    87  X-Snappy=yes
    88  
    89  [Service]
    90  ExecStart=/usr/bin/snap run xkcd-webserver
    91  SyslogIdentifier=xkcd-webserver.xkcd-webserver
    92  Restart=on-failure
    93  WorkingDirectory=/var/snap/xkcd-webserver/44
    94  ExecStop=/usr/bin/snap run --command=stop xkcd-webserver
    95  ExecReload=/usr/bin/snap run --command=reload xkcd-webserver
    96  ExecStopPost=/usr/bin/snap run --command=post-stop xkcd-webserver
    97  TimeoutStopSec=30
    98  Type=%s
    99  %s`
   100  	expectedTypeForkingWrapper = fmt.Sprintf(expectedServiceWrapperFmt, mountUnitPrefix, mountUnitPrefix, "forking", "\n[Install]\nWantedBy=multi-user.target\n")
   101  )
   102  
   103  func (s *servicesWrapperGenSuite) SetUpTest(c *C) {
   104  	s.BaseTest.SetUpTest(c)
   105  	s.BaseTest.AddCleanup(snap.MockSanitizePlugsSlots(func(snapInfo *snap.Info) {}))
   106  }
   107  
   108  func (s *servicesWrapperGenSuite) TearDownTest(c *C) {
   109  	s.BaseTest.TearDownTest(c)
   110  }
   111  
   112  func (s *servicesWrapperGenSuite) TestGenerateSnapServiceFile(c *C) {
   113  	yamlText := `
   114  name: snap
   115  version: 1.0
   116  apps:
   117      app:
   118          command: bin/start
   119          stop-command: bin/stop
   120          reload-command: bin/reload
   121          post-stop-command: bin/stop --post
   122          stop-timeout: 10s
   123          daemon: simple
   124  `
   125  	info, err := snap.InfoFromSnapYaml([]byte(yamlText))
   126  	c.Assert(err, IsNil)
   127  	info.Revision = snap.R(44)
   128  	app := info.Apps["app"]
   129  
   130  	generatedWrapper, err := wrappers.GenerateSnapServiceFile(app)
   131  	c.Assert(err, IsNil)
   132  	c.Check(string(generatedWrapper), Equals, expectedAppService)
   133  }
   134  
   135  func (s *servicesWrapperGenSuite) TestGenerateSnapServiceFileWithStartTimeout(c *C) {
   136  	yamlText := `
   137  name: snap
   138  version: 1.0
   139  apps:
   140      app:
   141          command: bin/start
   142          start-timeout: 10m
   143          daemon: simple
   144  `
   145  	info, err := snap.InfoFromSnapYaml([]byte(yamlText))
   146  	c.Assert(err, IsNil)
   147  	info.Revision = snap.R(44)
   148  	app := info.Apps["app"]
   149  
   150  	generatedWrapper, err := wrappers.GenerateSnapServiceFile(app)
   151  	c.Assert(err, IsNil)
   152  	c.Check(string(generatedWrapper), testutil.Contains, "\nTimeoutStartSec=600\n")
   153  }
   154  
   155  func (s *servicesWrapperGenSuite) TestGenerateSnapServiceFileRestart(c *C) {
   156  	yamlTextTemplate := `
   157  name: snap
   158  apps:
   159      app:
   160          daemon: simple
   161          restart-condition: %s
   162  `
   163  	for name, cond := range snap.RestartMap {
   164  		yamlText := fmt.Sprintf(yamlTextTemplate, cond)
   165  
   166  		info, err := snap.InfoFromSnapYaml([]byte(yamlText))
   167  		c.Assert(err, IsNil)
   168  		info.Revision = snap.R(44)
   169  		app := info.Apps["app"]
   170  
   171  		generatedWrapper, err := wrappers.GenerateSnapServiceFile(app)
   172  		c.Assert(err, IsNil)
   173  		wrapperText := string(generatedWrapper)
   174  		if cond == snap.RestartNever {
   175  			c.Check(wrapperText, Matches,
   176  				`(?ms).*^Restart=no$.*`, Commentf(name))
   177  		} else {
   178  			c.Check(wrapperText, Matches,
   179  				`(?ms).*^Restart=`+name+`$.*`, Commentf(name))
   180  		}
   181  	}
   182  }
   183  
   184  func (s *servicesWrapperGenSuite) TestGenerateSnapServiceFileTypeForking(c *C) {
   185  	service := &snap.AppInfo{
   186  		Snap: &snap.Info{
   187  			SuggestedName: "xkcd-webserver",
   188  			Version:       "0.3.4",
   189  			SideInfo:      snap.SideInfo{Revision: snap.R(44)},
   190  		},
   191  		Name:            "xkcd-webserver",
   192  		Command:         "bin/foo start",
   193  		StopCommand:     "bin/foo stop",
   194  		ReloadCommand:   "bin/foo reload",
   195  		PostStopCommand: "bin/foo post-stop",
   196  		StopTimeout:     timeout.DefaultTimeout,
   197  		Daemon:          "forking",
   198  	}
   199  
   200  	generatedWrapper, err := wrappers.GenerateSnapServiceFile(service)
   201  	c.Assert(err, IsNil)
   202  	c.Assert(string(generatedWrapper), Equals, expectedTypeForkingWrapper)
   203  }
   204  
   205  func (s *servicesWrapperGenSuite) TestGenerateSnapServiceFileIllegalChars(c *C) {
   206  	service := &snap.AppInfo{
   207  		Snap: &snap.Info{
   208  			SuggestedName: "xkcd-webserver",
   209  			Version:       "0.3.4",
   210  			SideInfo:      snap.SideInfo{Revision: snap.R(44)},
   211  		},
   212  		Name:            "xkcd-webserver",
   213  		Command:         "bin/foo start\n",
   214  		StopCommand:     "bin/foo stop",
   215  		ReloadCommand:   "bin/foo reload",
   216  		PostStopCommand: "bin/foo post-stop",
   217  		StopTimeout:     timeout.DefaultTimeout,
   218  		Daemon:          "simple",
   219  	}
   220  
   221  	_, err := wrappers.GenerateSnapServiceFile(service)
   222  	c.Assert(err, NotNil)
   223  }
   224  
   225  func (s *servicesWrapperGenSuite) TestGenServiceFileWithBusName(c *C) {
   226  
   227  	yamlText := `
   228  name: snap
   229  version: 1.0
   230  apps:
   231      app:
   232          command: bin/start
   233          stop-command: bin/stop
   234          reload-command: bin/reload
   235          post-stop-command: bin/stop --post
   236          stop-timeout: 10s
   237          bus-name: foo.bar.baz
   238          daemon: dbus
   239  `
   240  
   241  	info, err := snap.InfoFromSnapYaml([]byte(yamlText))
   242  	c.Assert(err, IsNil)
   243  	info.Revision = snap.R(44)
   244  	app := info.Apps["app"]
   245  
   246  	generatedWrapper, err := wrappers.GenerateSnapServiceFile(app)
   247  	c.Assert(err, IsNil)
   248  
   249  	c.Assert(string(generatedWrapper), Equals, expectedDbusService)
   250  }
   251  
   252  func (s *servicesWrapperGenSuite) TestGenOneshotServiceFile(c *C) {
   253  
   254  	info := snaptest.MockInfo(c, `
   255  name: snap
   256  version: 1.0
   257  apps:
   258      app:
   259          command: bin/start
   260          stop-command: bin/stop
   261          reload-command: bin/reload
   262          post-stop-command: bin/stop --post
   263          stop-timeout: 10s
   264          daemon: oneshot
   265  `, &snap.SideInfo{Revision: snap.R(44)})
   266  
   267  	app := info.Apps["app"]
   268  
   269  	generatedWrapper, err := wrappers.GenerateSnapServiceFile(app)
   270  	c.Assert(err, IsNil)
   271  
   272  	c.Assert(string(generatedWrapper), Equals, expectedOneshotService)
   273  }
   274  
   275  func (s *servicesWrapperGenSuite) TestGenerateSnapServiceWithSockets(c *C) {
   276  	const sock1ExpectedFmt = `[Unit]
   277  # Auto-generated, DO NOT EDIT
   278  Description=Socket sock1 for snap application some-snap.app
   279  Requires=%s-some\x2dsnap-44.mount
   280  After=%s-some\x2dsnap-44.mount
   281  X-Snappy=yes
   282  
   283  [Socket]
   284  Service=snap.some-snap.app.service
   285  FileDescriptorName=sock1
   286  ListenStream=%s/sock1.socket
   287  SocketMode=0666
   288  
   289  [Install]
   290  WantedBy=sockets.target
   291  `
   292  	const sock2ExpectedFmt = `[Unit]
   293  # Auto-generated, DO NOT EDIT
   294  Description=Socket sock2 for snap application some-snap.app
   295  Requires=%s-some\x2dsnap-44.mount
   296  After=%s-some\x2dsnap-44.mount
   297  X-Snappy=yes
   298  
   299  [Socket]
   300  Service=snap.some-snap.app.service
   301  FileDescriptorName=sock2
   302  ListenStream=%s/sock2.socket
   303  
   304  [Install]
   305  WantedBy=sockets.target
   306  `
   307  
   308  	si := &snap.Info{
   309  		SuggestedName: "some-snap",
   310  		Version:       "1.0",
   311  		SideInfo:      snap.SideInfo{Revision: snap.R(44)},
   312  	}
   313  	service := &snap.AppInfo{
   314  		Snap:    si,
   315  		Name:    "app",
   316  		Command: "bin/foo start",
   317  		Daemon:  "simple",
   318  		Plugs:   map[string]*snap.PlugInfo{"network-bind": {}},
   319  		Sockets: map[string]*snap.SocketInfo{
   320  			"sock1": {
   321  				Name:         "sock1",
   322  				ListenStream: "$SNAP_DATA/sock1.socket",
   323  				SocketMode:   0666,
   324  			},
   325  			"sock2": {
   326  				Name:         "sock2",
   327  				ListenStream: "$SNAP_DATA/sock2.socket",
   328  			},
   329  		},
   330  	}
   331  	service.Sockets["sock1"].App = service
   332  	service.Sockets["sock2"].App = service
   333  
   334  	sock1Path := filepath.Join(dirs.SnapServicesDir, "snap.some-snap.app.sock1.socket")
   335  	sock2Path := filepath.Join(dirs.SnapServicesDir, "snap.some-snap.app.sock2.socket")
   336  	sock1Expected := fmt.Sprintf(sock1ExpectedFmt, mountUnitPrefix, mountUnitPrefix, si.DataDir())
   337  	sock2Expected := fmt.Sprintf(sock2ExpectedFmt, mountUnitPrefix, mountUnitPrefix, si.DataDir())
   338  
   339  	generatedWrapper, err := wrappers.GenerateSnapServiceFile(service)
   340  	c.Assert(err, IsNil)
   341  	c.Assert(strings.Contains(string(generatedWrapper), "[Install]"), Equals, false)
   342  	c.Assert(strings.Contains(string(generatedWrapper), "WantedBy=multi-user.target"), Equals, false)
   343  
   344  	generatedSockets, err := wrappers.GenerateSnapSocketFiles(service)
   345  	c.Assert(err, IsNil)
   346  	c.Assert(generatedSockets, Not(IsNil))
   347  	c.Assert(*generatedSockets, HasLen, 2)
   348  	c.Assert(*generatedSockets, DeepEquals, map[string][]byte{
   349  		sock1Path: []byte(sock1Expected),
   350  		sock2Path: []byte(sock2Expected),
   351  	})
   352  }
   353  
   354  func (s *servicesWrapperGenSuite) TestServiceAfterBefore(c *C) {
   355  	const expectedServiceFmt = `[Unit]
   356  # Auto-generated, DO NOT EDIT
   357  Description=Service for snap application snap.app
   358  Requires=%s-snap-44.mount
   359  Wants=network.target
   360  After=%s-snap-44.mount network.target %s
   361  Before=%s
   362  X-Snappy=yes
   363  
   364  [Service]
   365  ExecStart=/usr/bin/snap run snap.app
   366  SyslogIdentifier=snap.app
   367  Restart=%s
   368  WorkingDirectory=/var/snap/snap/44
   369  TimeoutStopSec=30
   370  Type=%s
   371  
   372  [Install]
   373  WantedBy=multi-user.target
   374  `
   375  
   376  	service := &snap.AppInfo{
   377  		Snap: &snap.Info{
   378  			SuggestedName: "snap",
   379  			Version:       "0.3.4",
   380  			SideInfo:      snap.SideInfo{Revision: snap.R(44)},
   381  			Apps: map[string]*snap.AppInfo{
   382  				"foo": {
   383  					Name:   "foo",
   384  					Snap:   &snap.Info{SuggestedName: "snap"},
   385  					Daemon: "forking",
   386  				},
   387  				"bar": {
   388  					Name:   "bar",
   389  					Snap:   &snap.Info{SuggestedName: "snap"},
   390  					Daemon: "forking",
   391  				},
   392  				"zed": {
   393  					Name:   "zed",
   394  					Snap:   &snap.Info{SuggestedName: "snap"},
   395  					Daemon: "forking",
   396  				},
   397  				"baz": {
   398  					Name:   "baz",
   399  					Snap:   &snap.Info{SuggestedName: "snap"},
   400  					Daemon: "forking",
   401  				},
   402  			},
   403  		},
   404  		Name:        "app",
   405  		Command:     "bin/foo start",
   406  		Daemon:      "simple",
   407  		StopTimeout: timeout.DefaultTimeout,
   408  	}
   409  
   410  	for _, tc := range []struct {
   411  		after           []string
   412  		before          []string
   413  		generatedAfter  string
   414  		generatedBefore string
   415  	}{{
   416  		after:           []string{"bar", "zed"},
   417  		generatedAfter:  "snap.snap.bar.service snap.snap.zed.service",
   418  		before:          []string{"foo", "baz"},
   419  		generatedBefore: "snap.snap.foo.service snap.snap.baz.service",
   420  	}, {
   421  		after:           []string{"bar"},
   422  		generatedAfter:  "snap.snap.bar.service",
   423  		before:          []string{"foo"},
   424  		generatedBefore: "snap.snap.foo.service",
   425  	},
   426  	} {
   427  		c.Logf("tc: %v", tc)
   428  		service.After = tc.after
   429  		service.Before = tc.before
   430  		generatedWrapper, err := wrappers.GenerateSnapServiceFile(service)
   431  		c.Assert(err, IsNil)
   432  
   433  		expectedService := fmt.Sprintf(expectedServiceFmt, mountUnitPrefix, mountUnitPrefix,
   434  			tc.generatedAfter, tc.generatedBefore, "on-failure", "simple")
   435  		c.Assert(string(generatedWrapper), Equals, expectedService)
   436  	}
   437  }
   438  
   439  func (s *servicesWrapperGenSuite) TestServiceTimerUnit(c *C) {
   440  	const expectedServiceFmt = `[Unit]
   441  # Auto-generated, DO NOT EDIT
   442  Description=Timer app for snap application snap.app
   443  Requires=%s-snap-44.mount
   444  After=%s-snap-44.mount
   445  X-Snappy=yes
   446  
   447  [Timer]
   448  Unit=snap.snap.app.service
   449  OnCalendar=*-*-* 10:00
   450  OnCalendar=*-*-* 11:00
   451  
   452  [Install]
   453  WantedBy=timers.target
   454  `
   455  
   456  	expectedService := fmt.Sprintf(expectedServiceFmt, mountUnitPrefix, mountUnitPrefix)
   457  	service := &snap.AppInfo{
   458  		Snap: &snap.Info{
   459  			SuggestedName: "snap",
   460  			Version:       "0.3.4",
   461  			SideInfo:      snap.SideInfo{Revision: snap.R(44)},
   462  		},
   463  		Name:        "app",
   464  		Command:     "bin/foo start",
   465  		Daemon:      "simple",
   466  		StopTimeout: timeout.DefaultTimeout,
   467  		Timer: &snap.TimerInfo{
   468  			Timer: "10:00-12:00/2",
   469  		},
   470  	}
   471  	service.Timer.App = service
   472  
   473  	generatedWrapper, err := wrappers.GenerateSnapTimerFile(service)
   474  	c.Assert(err, IsNil)
   475  
   476  	c.Logf("timer: \n%v\n", string(generatedWrapper))
   477  	c.Assert(string(generatedWrapper), Equals, expectedService)
   478  }
   479  
   480  func (s *servicesWrapperGenSuite) TestServiceTimerUnitBadTimer(c *C) {
   481  	service := &snap.AppInfo{
   482  		Snap: &snap.Info{
   483  			SuggestedName: "snap",
   484  			Version:       "0.3.4",
   485  			SideInfo:      snap.SideInfo{Revision: snap.R(44)},
   486  		},
   487  		Name:        "app",
   488  		Command:     "bin/foo start",
   489  		Daemon:      "simple",
   490  		StopTimeout: timeout.DefaultTimeout,
   491  		Timer: &snap.TimerInfo{
   492  			Timer: "bad-timer",
   493  		},
   494  	}
   495  	service.Timer.App = service
   496  
   497  	generatedWrapper, err := wrappers.GenerateSnapTimerFile(service)
   498  	c.Assert(err, ErrorMatches, `cannot parse "bad-timer": "bad" is not a valid weekday`)
   499  	c.Assert(generatedWrapper, IsNil)
   500  }
   501  
   502  func (s *servicesWrapperGenSuite) TestServiceTimerServiceUnit(c *C) {
   503  	const expectedServiceFmt = `[Unit]
   504  # Auto-generated, DO NOT EDIT
   505  Description=Service for snap application snap.app
   506  Requires=%s-snap-44.mount
   507  Wants=network.target
   508  After=%s-snap-44.mount network.target
   509  X-Snappy=yes
   510  
   511  [Service]
   512  ExecStart=/usr/bin/snap run --timer="10:00-12:00,,mon,23:00~01:00/2" snap.app
   513  SyslogIdentifier=snap.app
   514  Restart=%s
   515  WorkingDirectory=/var/snap/snap/44
   516  TimeoutStopSec=30
   517  Type=%s
   518  
   519  [Install]
   520  WantedBy=multi-user.target
   521  `
   522  
   523  	expectedService := fmt.Sprintf(expectedServiceFmt, mountUnitPrefix, mountUnitPrefix, "on-failure", "simple")
   524  	service := &snap.AppInfo{
   525  		Snap: &snap.Info{
   526  			SuggestedName: "snap",
   527  			Version:       "0.3.4",
   528  			SideInfo:      snap.SideInfo{Revision: snap.R(44)},
   529  		},
   530  		Name:        "app",
   531  		Command:     "bin/foo start",
   532  		Daemon:      "simple",
   533  		StopTimeout: timeout.DefaultTimeout,
   534  		Timer: &snap.TimerInfo{
   535  			Timer: "10:00-12:00,,mon,23:00~01:00/2",
   536  		},
   537  	}
   538  
   539  	generatedWrapper, err := wrappers.GenerateSnapServiceFile(service)
   540  	c.Assert(err, IsNil)
   541  
   542  	c.Logf("service: \n%v\n", string(generatedWrapper))
   543  	c.Assert(string(generatedWrapper), Equals, expectedService)
   544  }
   545  
   546  func (s *servicesWrapperGenSuite) TestTimerGenerateSchedules(c *C) {
   547  	systemdAnalyzePath, _ := exec.LookPath("systemd-analyze")
   548  	if systemdAnalyzePath != "" {
   549  		// systemd-analyze is in the path, but it will fail if the
   550  		// daemon is not running (as it happens in LP builds) and writes
   551  		// the following to stderr:
   552  		//   Failed to create bus connection: No such file or directory
   553  		cmd := exec.Command(systemdAnalyzePath, "calendar", "12:00")
   554  		err := cmd.Run()
   555  		if err != nil {
   556  			// turns out it's not usable, disable extra verification
   557  			fmt.Fprintln(os.Stderr, `WARNING: systemd-analyze not usable, cannot validate a known schedule "12:00"`)
   558  			systemdAnalyzePath = ""
   559  		}
   560  	}
   561  
   562  	if systemdAnalyzePath == "" {
   563  		fmt.Fprintln(os.Stderr, "WARNING: generated schedules will not be validated by systemd-analyze")
   564  	}
   565  
   566  	for _, t := range []struct {
   567  		in         string
   568  		expected   []string
   569  		randomized bool
   570  	}{{
   571  		in:       "9:00-11:00,,20:00-22:00",
   572  		expected: []string{"*-*-* 09:00", "*-*-* 20:00"},
   573  	}, {
   574  		in:       "9:00-11:00/2,,20:00",
   575  		expected: []string{"*-*-* 09:00", "*-*-* 10:00", "*-*-* 20:00"},
   576  	}, {
   577  		in:         "9:00~11:00/2,,20:00",
   578  		expected:   []string{`\*-\*-\* 09:[0-5][0-9]`, `\*-\*-\* 10:[0-5][0-9]`, `\*-\*-\* 20:00`},
   579  		randomized: true,
   580  	}, {
   581  		in:       "mon,10:00,,fri,15:00",
   582  		expected: []string{"Mon *-*-* 10:00", "Fri *-*-* 15:00"},
   583  	}, {
   584  		in:       "mon-fri,10:00-11:00",
   585  		expected: []string{"Mon,Tue,Wed,Thu,Fri *-*-* 10:00"},
   586  	}, {
   587  		in:       "fri-mon,10:00-11:00",
   588  		expected: []string{"Fri,Sat,Sun,Mon *-*-* 10:00"},
   589  	}, {
   590  		in:       "mon5,10:00",
   591  		expected: []string{"Mon *-*~7/1 10:00"},
   592  	}, {
   593  		in:       "mon2,10:00",
   594  		expected: []string{"Mon *-*-8..14/1 10:00"},
   595  	}, {
   596  		in:       "mon2,mon1,10:00",
   597  		expected: []string{"Mon *-*-8..14/1 10:00", "Mon *-*-1..7/1 10:00"},
   598  	}, {
   599  		// NOTE: non-representable, assumes that service runner does the
   600  		// filtering of when to run the timer
   601  		in:       "mon1-mon3,10:00",
   602  		expected: []string{"*-*-1..21/1 10:00"},
   603  	}, {
   604  		in:         "mon,10:00~12:00,,fri,15:00",
   605  		expected:   []string{`Mon \*-\*-\* 1[01]:[0-5][0-9]`, `Fri \*-\*-\* 15:00`},
   606  		randomized: true,
   607  	}, {
   608  		in:         "23:00~24:00/4",
   609  		expected:   []string{`\*-\*-\* 23:[01][0-9]`, `\*-\*-\* 23:[12][0-9]`, `\*-\*-\* 23:[34][0-9]`, `*-*-* 23:[45][0-9]`},
   610  		randomized: true,
   611  	}, {
   612  		in:         "23:00~01:00/4",
   613  		expected:   []string{`\*-\*-\* 23:[0-2][0-9]`, `\*-\*-\* 23:[3-5][0-9]`, `\*-\*-\* 00:[0-2][0-9]`, `\*-\*-\* 00:[3-5][0-9]`},
   614  		randomized: true,
   615  	}, {
   616  		in:       "23:00-01:00/4",
   617  		expected: []string{`*-*-* 23:00`, `*-*-* 23:30`, `*-*-* 00:00`, `*-*-* 00:30`},
   618  	}, {
   619  		in:       "24:00",
   620  		expected: []string{`*-*-* 00:00`},
   621  	}} {
   622  		c.Logf("trying %+v", t)
   623  
   624  		schedule, err := timeutil.ParseSchedule(t.in)
   625  		c.Check(err, IsNil)
   626  
   627  		timer := wrappers.GenerateOnCalendarSchedules(schedule)
   628  		c.Check(timer, Not(IsNil))
   629  		if !t.randomized {
   630  			c.Check(timer, DeepEquals, t.expected)
   631  		} else {
   632  			c.Assert(timer, HasLen, len(t.expected))
   633  			for i := range timer {
   634  				c.Check(timer[i], Matches, t.expected[i])
   635  			}
   636  		}
   637  
   638  		if systemdAnalyzePath != "" {
   639  			cmd := exec.Command(systemdAnalyzePath, append([]string{"calendar"}, timer...)...)
   640  			out, err := cmd.CombinedOutput()
   641  			c.Check(err, IsNil, Commentf("systemd-analyze failed with output:\n%s", string(out)))
   642  		}
   643  	}
   644  }
   645  
   646  func (s *servicesWrapperGenSuite) TestKillModeSig(c *C) {
   647  	for _, rm := range []string{"sigterm", "sighup", "sigusr1", "sigusr2"} {
   648  		service := &snap.AppInfo{
   649  			Snap: &snap.Info{
   650  				SuggestedName: "snap",
   651  				Version:       "0.3.4",
   652  				SideInfo:      snap.SideInfo{Revision: snap.R(44)},
   653  			},
   654  			Name:     "app",
   655  			Command:  "bin/foo start",
   656  			Daemon:   "simple",
   657  			StopMode: snap.StopModeType(rm),
   658  		}
   659  
   660  		generatedWrapper, err := wrappers.GenerateSnapServiceFile(service)
   661  		c.Assert(err, IsNil)
   662  
   663  		c.Check(string(generatedWrapper), Equals, fmt.Sprintf(`[Unit]
   664  # Auto-generated, DO NOT EDIT
   665  Description=Service for snap application snap.app
   666  Requires=%s-snap-44.mount
   667  Wants=network.target
   668  After=%s-snap-44.mount network.target
   669  X-Snappy=yes
   670  
   671  [Service]
   672  ExecStart=/usr/bin/snap run snap.app
   673  SyslogIdentifier=snap.app
   674  Restart=on-failure
   675  WorkingDirectory=/var/snap/snap/44
   676  TimeoutStopSec=30
   677  Type=simple
   678  KillMode=process
   679  KillSignal=%s
   680  
   681  [Install]
   682  WantedBy=multi-user.target
   683  `, mountUnitPrefix, mountUnitPrefix, strings.ToUpper(rm)))
   684  	}
   685  }
   686  
   687  func (s *servicesWrapperGenSuite) TestRestartDelay(c *C) {
   688  	service := &snap.AppInfo{
   689  		Snap: &snap.Info{
   690  			SuggestedName: "snap",
   691  			Version:       "0.3.4",
   692  			SideInfo:      snap.SideInfo{Revision: snap.R(44)},
   693  		},
   694  		Name:         "app",
   695  		Command:      "bin/foo start",
   696  		Daemon:       "simple",
   697  		RestartDelay: timeout.Timeout(20 * time.Second),
   698  	}
   699  
   700  	generatedWrapper, err := wrappers.GenerateSnapServiceFile(service)
   701  	c.Assert(err, IsNil)
   702  
   703  	c.Check(string(generatedWrapper), Equals, fmt.Sprintf(`[Unit]
   704  # Auto-generated, DO NOT EDIT
   705  Description=Service for snap application snap.app
   706  Requires=%s-snap-44.mount
   707  Wants=network.target
   708  After=%s-snap-44.mount network.target
   709  X-Snappy=yes
   710  
   711  [Service]
   712  ExecStart=/usr/bin/snap run snap.app
   713  SyslogIdentifier=snap.app
   714  Restart=on-failure
   715  RestartSec=20
   716  WorkingDirectory=/var/snap/snap/44
   717  TimeoutStopSec=30
   718  Type=simple
   719  
   720  [Install]
   721  WantedBy=multi-user.target
   722  `, mountUnitPrefix, mountUnitPrefix))
   723  }