github.com/juju/charmrepo/v7@v7.0.1/testing/testcharm.go (about)

     1  // Copyright 2012, 2013 Canonical Ltd.
     2  // Licensed under the LGPLv3, see LICENCE file for details.
     3  
     4  package testing
     5  
     6  import (
     7  	"archive/zip"
     8  	"bytes"
     9  	"fmt"
    10  	"io"
    11  	"os"
    12  	"path"
    13  	"strings"
    14  	"sync"
    15  
    16  	"github.com/juju/charm/v9"
    17  	"github.com/juju/charm/v9/resource"
    18  	"github.com/juju/testing/filetesting"
    19  	gc "gopkg.in/check.v1"
    20  	"gopkg.in/errgo.v1"
    21  	"gopkg.in/yaml.v2"
    22  )
    23  
    24  // Charm holds a charm for testing. It does not
    25  // have a representation on disk by default, but
    26  // can be written to disk using Archive and its ExpandTo
    27  // method. It implements the charm.Charm interface.
    28  //
    29  // All methods on Charm may be called concurrently.
    30  type Charm struct {
    31  	meta     *charm.Meta
    32  	config   *charm.Config
    33  	actions  *charm.Actions
    34  	metrics  *charm.Metrics
    35  	revision int
    36  
    37  	files filetesting.Entries
    38  
    39  	makeArchiveOnce sync.Once
    40  	archiveBytes    []byte
    41  	archive         *charm.CharmArchive
    42  }
    43  
    44  // CharmSpec holds the specification for a charm. The fields
    45  // hold data in YAML format.
    46  type CharmSpec struct {
    47  	// Meta holds the contents of metadata.yaml.
    48  	Meta string
    49  
    50  	// Config holds the contents of config.yaml.
    51  	Config string
    52  
    53  	// Actions holds the contents of actions.yaml.
    54  	Actions string
    55  
    56  	// Metrics holds the contents of metrics.yaml.
    57  	Metrics string
    58  
    59  	// Files holds any additional files that should be
    60  	// added to the charm. If this is nil, a minimal set
    61  	// of files will be added to ensure the charm is readable.
    62  	Files []filetesting.Entry
    63  
    64  	// Revision specifies the revision of the charm.
    65  	Revision int
    66  }
    67  
    68  type file struct {
    69  	path string
    70  	data []byte
    71  	perm os.FileMode
    72  }
    73  
    74  // NewCharm returns a charm following the given specification.
    75  func NewCharm(c *gc.C, spec CharmSpec) *Charm {
    76  	return newCharm(spec)
    77  }
    78  
    79  // newCharm is the internal version of NewCharm that
    80  // doesn't take a *gc.C so it can be used in NewCharmWithMeta.
    81  func newCharm(spec CharmSpec) *Charm {
    82  	ch := &Charm{
    83  		revision: spec.Revision,
    84  	}
    85  	var err error
    86  	ch.meta, err = charm.ReadMeta(strings.NewReader(spec.Meta))
    87  	if err != nil {
    88  		panic(err)
    89  	}
    90  
    91  	ch.files = append(ch.files, filetesting.File{
    92  		Path: "metadata.yaml",
    93  		Data: spec.Meta,
    94  		Perm: 0644,
    95  	})
    96  
    97  	if spec.Config != "" {
    98  		ch.config, err = charm.ReadConfig(strings.NewReader(spec.Config))
    99  		if err != nil {
   100  			panic(err)
   101  		}
   102  		ch.files = append(ch.files, filetesting.File{
   103  			Path: "config.yaml",
   104  			Data: spec.Config,
   105  			Perm: 0644,
   106  		})
   107  	}
   108  	if spec.Actions != "" {
   109  		ch.actions, err = charm.ReadActionsYaml(ch.meta.Name, strings.NewReader(spec.Actions))
   110  		if err != nil {
   111  			panic(err)
   112  		}
   113  		ch.files = append(ch.files, filetesting.File{
   114  			Path: "actions.yaml",
   115  			Data: spec.Actions,
   116  			Perm: 0644,
   117  		})
   118  	}
   119  	if spec.Metrics != "" {
   120  		ch.metrics, err = charm.ReadMetrics(strings.NewReader(spec.Metrics))
   121  		if err != nil {
   122  			panic(err)
   123  		}
   124  		ch.files = append(ch.files, filetesting.File{
   125  			Path: "metrics.yaml",
   126  			Data: spec.Metrics,
   127  			Perm: 0644,
   128  		})
   129  	}
   130  	if spec.Files == nil {
   131  		ch.files = append(ch.files, filetesting.File{
   132  			Path: "hooks/install",
   133  			Data: "#!/bin/sh\n",
   134  			Perm: 0755,
   135  		}, filetesting.File{
   136  			Path: "hooks/start",
   137  			Data: "#!/bin/sh\n",
   138  			Perm: 0755,
   139  		})
   140  	} else {
   141  		ch.files = append(ch.files, spec.Files...)
   142  		// Check for duplicates.
   143  		names := make(map[string]bool)
   144  		for _, f := range ch.files {
   145  			name := path.Clean(f.GetPath())
   146  			if names[name] {
   147  				panic(fmt.Errorf("duplicate file entry %q", f.GetPath()))
   148  			}
   149  			names[name] = true
   150  		}
   151  	}
   152  	return ch
   153  }
   154  
   155  // NewCharmMeta returns a charm with the given metadata.
   156  // It doesn't take a *gc.C, so it can be used at init time,
   157  // for example in table-driven tests.
   158  func NewCharmMeta(meta *charm.Meta) *Charm {
   159  	if meta == nil {
   160  		meta = new(charm.Meta)
   161  	}
   162  	metaYAML, err := yaml.Marshal(meta)
   163  	if err != nil {
   164  		panic(err)
   165  	}
   166  	return newCharm(CharmSpec{
   167  		Meta: string(metaYAML),
   168  	})
   169  }
   170  
   171  // Meta implements charm.Charm.Meta.
   172  func (ch *Charm) Meta() *charm.Meta {
   173  	return ch.meta
   174  }
   175  
   176  // Manifest implements charm.Charm.Manifest.
   177  func (ch *Charm) Manifest() *charm.Manifest {
   178  	return nil
   179  }
   180  
   181  // Config implements charm.Charm.Config.
   182  func (ch *Charm) Config() *charm.Config {
   183  	if ch.config == nil {
   184  		return &charm.Config{
   185  			Options: map[string]charm.Option{},
   186  		}
   187  	}
   188  	return ch.config
   189  }
   190  
   191  // Metrics implements charm.Charm.Metrics.
   192  func (ch *Charm) Metrics() *charm.Metrics {
   193  	return ch.metrics
   194  }
   195  
   196  // Actions implements charm.Charm.Actions.
   197  func (ch *Charm) Actions() *charm.Actions {
   198  	if ch.actions == nil {
   199  		return &charm.Actions{}
   200  	}
   201  	return ch.actions
   202  }
   203  
   204  // Revision implements charm.Charm.Revision.
   205  func (ch *Charm) Revision() int {
   206  	return ch.revision
   207  }
   208  
   209  // Archive returns a charm archive holding the charm.
   210  func (ch *Charm) Archive() *charm.CharmArchive {
   211  	ch.makeArchiveOnce.Do(ch.makeArchive)
   212  	return ch.archive
   213  }
   214  
   215  // ArchiveBytes returns the contents of the charm archive
   216  // holding the charm.
   217  func (ch *Charm) ArchiveBytes() []byte {
   218  	ch.makeArchiveOnce.Do(ch.makeArchive)
   219  	return ch.archiveBytes
   220  }
   221  
   222  // ArchiveTo implements ArchiveTo as implemented
   223  // by *charm.Dir, enabling the charm to be used in some APIs
   224  // that check for that method.
   225  func (c *Charm) ArchiveTo(w io.Writer) error {
   226  	_, err := w.Write(c.ArchiveBytes())
   227  	return err
   228  }
   229  
   230  // Size returns the size of the charm's archive blob.
   231  func (c *Charm) Size() int64 {
   232  	return int64(len(c.ArchiveBytes()))
   233  }
   234  
   235  func (ch *Charm) makeArchive() {
   236  	var buf bytes.Buffer
   237  	zw := zip.NewWriter(&buf)
   238  
   239  	for _, f := range ch.files {
   240  		addZipEntry(zw, f)
   241  	}
   242  	if err := zw.Close(); err != nil {
   243  		panic(err)
   244  	}
   245  	// ReadCharmArchiveFromReader requires a ReaderAt, so make one.
   246  	r := bytes.NewReader(buf.Bytes())
   247  
   248  	// Actually make the charm archive.
   249  	archive, err := charm.ReadCharmArchiveFromReader(r, int64(buf.Len()))
   250  	if err != nil {
   251  		panic(err)
   252  	}
   253  	ch.archiveBytes = buf.Bytes()
   254  	ch.archive = archive
   255  	ch.archive.SetRevision(ch.revision)
   256  }
   257  
   258  func addZipEntry(zw *zip.Writer, f filetesting.Entry) {
   259  	h := &zip.FileHeader{
   260  		Name: f.GetPath(),
   261  		// Don't bother compressing - the contents are so small that
   262  		// it will just slow things down for no particular benefit.
   263  		Method: zip.Store,
   264  	}
   265  	contents := ""
   266  	switch f := f.(type) {
   267  	case filetesting.Dir:
   268  		h.SetMode(os.ModeDir | 0755)
   269  	case filetesting.File:
   270  		h.SetMode(f.Perm)
   271  		contents = f.Data
   272  	case filetesting.Symlink:
   273  		h.SetMode(os.ModeSymlink | 0777)
   274  		contents = f.Link
   275  	}
   276  	w, err := zw.CreateHeader(h)
   277  	if err != nil {
   278  		panic(err)
   279  	}
   280  	if contents != "" {
   281  		if _, err := w.Write([]byte(contents)); err != nil {
   282  			panic(err)
   283  		}
   284  	}
   285  }
   286  
   287  // MetaWithSupportedSeries returns m with Series
   288  // set to series. If m is nil, new(charm.Meta)
   289  // will be used instead.
   290  func MetaWithSupportedSeries(m *charm.Meta, series ...string) *charm.Meta {
   291  	if m == nil {
   292  		m = new(charm.Meta)
   293  	}
   294  	m.Series = series
   295  	return m
   296  }
   297  
   298  // MetaWithRelations returns m with Provides and Requires set
   299  // to the given relations, where each relation
   300  // is specified as a white-space-separated
   301  // triple:
   302  //	role name interface
   303  // where role specifies the role of the interface
   304  // ("provides" or "requires"), name holds the relation
   305  // name and interface holds the interface relation type.
   306  //
   307  // If m is nil, new(charm.Meta) will be used instead.
   308  func MetaWithRelations(m *charm.Meta, relations ...string) *charm.Meta {
   309  	if m == nil {
   310  		m = new(charm.Meta)
   311  	}
   312  	provides := make(map[string]charm.Relation)
   313  	requires := make(map[string]charm.Relation)
   314  	for _, rel := range relations {
   315  		r, err := parseRelation(rel)
   316  		if err != nil {
   317  			panic(fmt.Errorf("bad relation %q", err))
   318  		}
   319  		if r.Role == charm.RoleProvider {
   320  			provides[r.Name] = r
   321  		} else {
   322  			requires[r.Name] = r
   323  		}
   324  	}
   325  	m.Provides = provides
   326  	m.Requires = requires
   327  	return m
   328  }
   329  
   330  func parseRelation(s string) (charm.Relation, error) {
   331  	fields := strings.Fields(s)
   332  	if len(fields) != 3 {
   333  		return charm.Relation{}, errgo.Newf("wrong field count")
   334  	}
   335  	r := charm.Relation{
   336  		Scope:     charm.ScopeGlobal,
   337  		Name:      fields[1],
   338  		Interface: fields[2],
   339  	}
   340  	switch fields[0] {
   341  	case "provides":
   342  		r.Role = charm.RoleProvider
   343  	case "requires":
   344  		r.Role = charm.RoleRequirer
   345  	default:
   346  		return charm.Relation{}, errgo.Newf("unknown role")
   347  	}
   348  	return r, nil
   349  }
   350  
   351  // MetaWithResources returns m with Resources set to a set of resources
   352  // with the given names. If m is nil, new(charm.Meta) will be used
   353  // instead.
   354  //
   355  // The path and description of the resources are derived from
   356  // the resource name by adding a "-file" and a " description"
   357  // suffix respectively.
   358  func MetaWithResources(m *charm.Meta, resources ...string) *charm.Meta {
   359  	if m == nil {
   360  		m = new(charm.Meta)
   361  	}
   362  	m.Resources = make(map[string]resource.Meta)
   363  	for _, name := range resources {
   364  		m.Resources[name] = resource.Meta{
   365  			Name:        name,
   366  			Type:        resource.TypeFile,
   367  			Path:        name + "-file",
   368  			Description: name + " description",
   369  		}
   370  	}
   371  	return m
   372  }