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 }