Skip to content
This repository has been archived by the owner on Jul 31, 2024. It is now read-only.

Commit

Permalink
hook/hooktest: more useful Runner
Browse files Browse the repository at this point in the history
charmbits/service: first steps towards mocking services
  • Loading branch information
rogpeppe committed Dec 16, 2014
1 parent dab71ef commit 20c1fcd
Show file tree
Hide file tree
Showing 4 changed files with 194 additions and 83 deletions.
52 changes: 7 additions & 45 deletions charmbits/httprelation/provider_test.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package httprelation_test

import (
"encoding/json"
"sort"
"testing"

Expand Down Expand Up @@ -118,56 +117,19 @@ type context struct {
func (ctxt *context) runHook(c *gc.C, hookName string, relId hook.RelationId, relUnit hook.UnitId, register func(*httprelation.Provider, *hook.Registry)) [][]string {
var p httprelation.Provider

r := hook.NewRegistry()
p.Register(r, "foo", ctxt.withHTTPS)
if register != nil {
register(&p, r)
}
hook.RegisterMainHooks(r)
runner := &hooktest.Runner{
RunFunc: func(cmd string, args ...string) ([]byte, error) {
c.Logf("RunFunc %s %q", cmd, args)
if cmd != "config-get" {
return nil, nil
RegisterHooks: func(r *hook.Registry) {
p.Register(r, "foo", ctxt.withHTTPS)
if register != nil {
register(&p, r)
}
var val interface{}
if len(args) < 4 {
val = ctxt.config
} else {
key := args[3]
val = ctxt.config[key]
}
data, err := json.Marshal(val)
c.Assert(err, gc.IsNil)
c.Logf("returning %s", data)
return data, nil
},
Logger: c,
}
hctxt := &hook.Context{
UUID: hooktest.UUID,
Unit: "wordpress/0",
CharmDir: "/nowhere",
HookName: hookName,
Runner: runner,
Relations: ctxt.relations,
RelationIds: ctxt.relationIds,
Config: ctxt.config,
Logger: c,
}
if relId != "" {
hctxt.RelationId = relId
hctxt.RemoteUnit = relUnit
loop:
for name, ids := range ctxt.relationIds {
for _, id := range ids {
if id == hctxt.RelationId {
hctxt.RelationName = name
break loop
}
}
}
c.Assert(hctxt.RelationName, gc.Not(gc.Equals), "")
}
err := hook.Main(r, hctxt, ctxt.state)
err := runner.RunHook(hookName, relId, relUnit)
c.Assert(err, gc.IsNil)
return runner.Record
}
65 changes: 36 additions & 29 deletions charmbits/service/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,23 @@ import (
"path/filepath"
"time"

serviceCommon "github.com/juju/juju/service/common"
"github.com/juju/juju/service/upstart"
"github.com/juju/utils"
"gopkg.in/errgo.v1"

"github.com/juju/gocharm/hook"
)

// OSService defines the interface provided by an
// operating system service. It is implemented by
// *upstart.Service (from github.com/juju/juju/service/upstart).
type OSService interface {
Install() error
StopAndRemove() error
Running() bool
Stop() error
Start() error
}

// Service represents a long running service that runs
// outside of the usual charm hook context.
type Service struct {
Expand Down Expand Up @@ -83,7 +92,7 @@ func (svc *Service) Start(args ...string) error {
return errgo.Notef(err, "cannot create state directory")
}
svc.ctxt.Logf("starting service")
usvc := svc.upstartService(args)
usvc := svc.osService(args)
// Note: Install will restart the service if the configuration
// file has changed.
if err := usvc.Install(); err != nil {
Expand All @@ -102,23 +111,23 @@ func (svc *Service) Start(args ...string) error {

// Stop stops the service running.
func (svc *Service) Stop() error {
if err := svc.upstartService(nil).Stop(); err != nil {
if err := svc.osService(nil).Stop(); err != nil {
return errgo.Mask(err)
}
return nil
}

// Started reports whether the service has been started.
func (svc *Service) Started() bool {
return svc.upstartService(nil).Running()
return svc.osService(nil).Running()
}

// StopAndRemove stops and removes the service completely.
func (svc *Service) StopAndRemove() error {
if !svc.state.Installed {
return nil
}
if err := svc.upstartService(nil).StopAndRemove(); err != nil {
if err := svc.osService(nil).StopAndRemove(); err != nil {
return errgo.Mask(err)
}
svc.state.Installed = false
Expand Down Expand Up @@ -160,19 +169,7 @@ func (svc *Service) Call(method string, args interface{}, reply interface{}) err
return nil
}

func dialRPC(path string) (*rpc.Client, error) {
c, err := net.Dial("unix", path)
if err != nil {
return nil, errgo.Mask(err)
}
return rpc.NewClientWithCodec(jsonrpc.NewClientCodec(c)), nil
}

func (svc *Service) socketPath() string {
return "@" + filepath.Join(svc.ctxt.StateDir(), "service")
}

func (svc *Service) upstartService(args []string) *upstart.Service {
func (svc *Service) osService(args []string) OSService {
exe := filepath.Join(svc.ctxt.CharmDir, "bin", "runhook")
serviceName := svc.serviceName
if serviceName == "" {
Expand All @@ -187,16 +184,26 @@ func (svc *Service) upstartService(args []string) *upstart.Service {
if err != nil {
panic(errgo.Notef(err, "cannot marshal parameters"))
}
cmd := exe + " " +
svc.ctxt.CommandName() + " " +
base64.StdEncoding.EncodeToString(pdata)
return &upstart.Service{
Name: serviceName,
Conf: serviceCommon.Conf{
InitDir: "/etc/init",
Desc: fmt.Sprintf("service for juju unit %q", svc.ctxt.Unit),
Cmd: cmd,
Out: filepath.Join(svc.ctxt.StateDir(), "servicelog.out"),
return NewService(OSServiceParams{
Name: serviceName,
Description: fmt.Sprintf("service for juju unit %q", svc.ctxt.Unit),
Exe: exe,
Args: []string{
svc.ctxt.CommandName(),
base64.StdEncoding.EncodeToString(pdata),
},
Output: filepath.Join(svc.ctxt.StateDir(), "servicelog.out"),
})
}

func dialRPC(path string) (*rpc.Client, error) {
c, err := net.Dial("unix", path)
if err != nil {
return nil, errgo.Mask(err)
}
return rpc.NewClientWithCodec(jsonrpc.NewClientCodec(c)), nil
}

func (svc *Service) socketPath() string {
return "@" + filepath.Join(svc.ctxt.StateDir(), "service")
}
45 changes: 45 additions & 0 deletions charmbits/service/upstart.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package service

import (
"strings"

"github.com/juju/juju/service/common"
"github.com/juju/juju/service/upstart"
)

// OSServiceParams holds the parameters for
// creating a new service.
type OSServiceParams struct {
// Name holds the name of the service.
Name string

// Description holds the description of the service.
Description string

// Output holds the file where output will be put.
Output string

// Exe holds the name of the executable to run.
Exe string

// Args holds any arguments to the executable,
// which should be OK to to pass to the shell
// without quoting.
Args []string
}

// NewService is used to create a new service.
// It is defined as a variable so that it can be
// replaced for testing purposes.
var NewService = func(p OSServiceParams) OSService {
cmd := p.Exe + " " + strings.Join(p.Args, " ")
return &upstart.Service{
Name: p.Name,
Conf: common.Conf{
InitDir: "/etc/init",
Desc: p.Description,
Cmd: cmd,
Out: p.Output,
},
}
}
115 changes: 106 additions & 9 deletions hook/hooktest/hooktest.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,41 @@
package hooktest

import (
"encoding/json"

"github.com/juju/gocharm/hook"
)

// Find out which commands will be generated by which Context methods.

// Runner is implemention of hook.Runner suitable for use in tests.
// It calls the given RunFunc function whenever the Run method
// and records all the calls in the Record field.
// and records all the calls in the Record field, with the
// exception of the calls mentioned below.
//
// Any calls to juju-log are logged using Logger, but otherwise ignored.
// Any calls to config-get or unit-get are passed through to RunFunc but
// not recorded.
// Calls to config-get from the Config field and not invoked through RunFunc.
// Likewise, calls to unit-get will be satisfied from the PublicAddress
// and PrivateAddress fields.
type Runner struct {
RegisterHooks func(r *hook.Registry)
// The following fields hold information that will
// be available through the hook context.
Relations map[hook.RelationId]map[hook.UnitId]map[string]string
RelationIds map[string][]hook.RelationId
Config map[string]interface{}

PublicAddress string
PrivateAddress string

// State holds the persistent state.
// If it is nil, it will be set to a hooktest.MemState
// instance.
State hook.PersistentState

// RunFunc is called when a hook tool runs.
// It may be nil, in which case it will be assumed
// that the hook tool runs successfully with no output.
RunFunc func(string, ...string) ([]byte, error)
Record [][]string
Logger interface {
Expand All @@ -19,21 +46,91 @@ type Runner struct {
Closed bool
}

// RunHook runs a hook in the context of the Runner. If it's a relation
// hook, then relId should hold the current relation id and
// relUnit should hold the unit that the relation hook is running for.
//
// Any hook tools that have been run will be stored in r.Record.
func (runner *Runner) RunHook(hookName string, relId hook.RelationId, relUnit hook.UnitId) error {
if runner.State == nil {
runner.State = make(MemState)
}
r := hook.NewRegistry()
runner.RegisterHooks(r)
hook.RegisterMainHooks(r)
hctxt := &hook.Context{
UUID: UUID,
Unit: "someunit/0",
CharmDir: "/nowhere",
HookName: hookName,
Runner: runner,
Relations: runner.Relations,
RelationIds: runner.RelationIds,
}
if relId != "" {
hctxt.RelationId = relId
hctxt.RemoteUnit = relUnit
loop:
for name, ids := range runner.RelationIds {
for _, id := range ids {
if id == hctxt.RelationId {
hctxt.RelationName = name
break loop
}
}
}
if hctxt.RelationName == "" {
panic("relation id not found")
}
}
return hook.Main(r, hctxt, runner.State)
}

// Run implements hook.Runner.Run.
func (r *Runner) Run(cmd string, args ...string) ([]byte, error) {
if cmd == "juju-log" {
if len(args) != 1 {
panic("expected only one argument to juju-log")
panic("expected exactly one argument to juju-log")
}
r.Logger.Logf("%s", args[0])
return nil, nil
}
if cmd != "config-get" && cmd != "unit-get" {
rec := []string{cmd}
rec = append(rec, args...)
r.Record = append(r.Record, rec)
switch cmd {
case "config-get":
var val interface{}
if len(args) < 4 {
// config-get --format json
val = r.Config
} else {
// config-get --format json -- key
key := args[3]
val = r.Config[key]
}
data, err := json.Marshal(val)
if err != nil {
panic(err)
}
return data, nil
case "unit-get":
if len(args) != 1 {
panic("expected exactly one argument to unit-get")
}
switch args[0] {
case "public-address":
return []byte(r.PublicAddress), nil
case "private-address":
return []byte(r.PrivateAddress), nil
default:
panic("unexpected argument to unit-get")
}
}
rec := []string{cmd}
rec = append(rec, args...)
r.Record = append(r.Record, rec)
if r.RunFunc != nil {
return r.RunFunc(cmd, args...)
}
return r.RunFunc(cmd, args...)
return nil, nil
}

// Run implements hook.Runner.Close.
Expand Down

0 comments on commit 20c1fcd

Please sign in to comment.