github.com/mattyw/juju@v0.0.0-20140610034352-732aecd63861/charm/testing/charm.go (about) 1 // Copyright 2012, 2013 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package testing 5 6 import ( 7 "fmt" 8 "go/build" 9 "os" 10 "os/exec" 11 "path/filepath" 12 "sync" 13 14 "github.com/juju/juju/charm" 15 ) 16 17 func check(err error) { 18 if err != nil { 19 panic(err) 20 } 21 } 22 23 // Repo represents a charm repository used for testing. 24 type Repo struct { 25 once sync.Once 26 path string 27 } 28 29 func (r *Repo) Path() string { 30 r.once.Do(r.init) 31 return r.path 32 } 33 34 // init is called once when r.Path() is called for the first time, and 35 // it initializes r.path to the location of the local testing 36 // repository. 37 func (r *Repo) init() { 38 p, err := build.Import("github.com/juju/juju/testing", "", build.FindOnly) 39 check(err) 40 r.path = filepath.Join(p.Dir, "repo") 41 } 42 43 // Charms represents the specific charm repository stored in this package and 44 // used by the Juju unit tests. The series name is "quantal". 45 var Charms = &Repo{} 46 47 func clone(dst, src string) string { 48 check(exec.Command("/bin/cp", "-r", src, dst).Run()) 49 return filepath.Join(dst, filepath.Base(src)) 50 } 51 52 // DirPath returns the path to a charm directory with the given name in the 53 // default series 54 func (r *Repo) DirPath(name string) string { 55 return filepath.Join(r.Path(), "quantal", name) 56 } 57 58 // Dir returns the actual charm.Dir named name. 59 func (r *Repo) Dir(name string) *charm.Dir { 60 ch, err := charm.ReadDir(r.DirPath(name)) 61 check(err) 62 return ch 63 } 64 65 // ClonedDirPath returns the path to a new copy of the default charm directory 66 // named name. 67 func (r *Repo) ClonedDirPath(dst, name string) string { 68 return clone(dst, r.DirPath(name)) 69 } 70 71 // RenamedClonedDirPath returns the path to a new copy of the default 72 // charm directory named name, but renames it to newName. 73 func (r *Repo) RenamedClonedDirPath(dst, name, newName string) string { 74 newDst := clone(dst, r.DirPath(name)) 75 renamedDst := filepath.Join(filepath.Dir(newDst), newName) 76 check(os.Rename(newDst, renamedDst)) 77 return renamedDst 78 } 79 80 // ClonedDir returns an actual charm.Dir based on a new copy of the charm directory 81 // named name, in the directory dst. 82 func (r *Repo) ClonedDir(dst, name string) *charm.Dir { 83 ch, err := charm.ReadDir(r.ClonedDirPath(dst, name)) 84 check(err) 85 return ch 86 } 87 88 // ClonedURL makes a copy of the charm directory. It will create a directory 89 // with the series name if it does not exist, and then clone the charm named 90 // name into that directory. The return value is a URL pointing at the local 91 // charm. 92 func (r *Repo) ClonedURL(dst, series, name string) *charm.URL { 93 dst = filepath.Join(dst, series) 94 if err := os.MkdirAll(dst, os.FileMode(0777)); err != nil { 95 panic(fmt.Errorf("cannot make destination directory: %v", err)) 96 } 97 clone(dst, r.DirPath(name)) 98 return &charm.URL{ 99 Reference: charm.Reference{ 100 Schema: "local", 101 Name: name, 102 Revision: -1, 103 }, 104 Series: series, 105 } 106 } 107 108 // BundlePath returns the path to a new charm bundle file created from the 109 // charm directory named name, in the directory dst. 110 func (r *Repo) BundlePath(dst, name string) string { 111 dir := r.Dir(name) 112 path := filepath.Join(dst, "bundle.charm") 113 file, err := os.Create(path) 114 check(err) 115 defer file.Close() 116 check(dir.BundleTo(file)) 117 return path 118 } 119 120 // Bundle returns an actual charm.Bundle created from a new charm bundle file 121 // created from the charm directory named name, in the directory dst. 122 func (r *Repo) Bundle(dst, name string) *charm.Bundle { 123 ch, err := charm.ReadBundle(r.BundlePath(dst, name)) 124 check(err) 125 return ch 126 } 127 128 // MockCharmStore implements charm.Repository and is used to isolate tests 129 // that would otherwise need to hit the real charm store. 130 type MockCharmStore struct { 131 charms map[string]map[int]*charm.Bundle 132 AuthAttrs string 133 TestMode bool 134 DefaultSeries string 135 } 136 137 func NewMockCharmStore() *MockCharmStore { 138 return &MockCharmStore{charms: map[string]map[int]*charm.Bundle{}} 139 } 140 141 func (s *MockCharmStore) WithAuthAttrs(auth string) charm.Repository { 142 s.AuthAttrs = auth 143 return s 144 } 145 146 func (s *MockCharmStore) WithTestMode(testMode bool) charm.Repository { 147 s.TestMode = testMode 148 return s 149 } 150 151 func (s *MockCharmStore) WithDefaultSeries(series string) charm.Repository { 152 s.DefaultSeries = series 153 return s 154 } 155 156 func (s *MockCharmStore) Resolve(ref charm.Reference) (*charm.URL, error) { 157 if s.DefaultSeries == "" { 158 return nil, fmt.Errorf("missing default series, cannot resolve charm url: %q", ref) 159 } 160 return &charm.URL{Reference: ref, Series: s.DefaultSeries}, nil 161 } 162 163 // SetCharm adds and removes charms in s. The affected charm is identified by 164 // charmURL, which must be revisioned. If bundle is nil, the charm will be 165 // removed; otherwise, it will be stored. It is an error to store a bundle 166 // under a charmURL that does not share its name and revision. 167 func (s *MockCharmStore) SetCharm(charmURL *charm.URL, bundle *charm.Bundle) error { 168 base := charmURL.WithRevision(-1).String() 169 if charmURL.Revision < 0 { 170 return fmt.Errorf("bad charm url revision") 171 } 172 if bundle == nil { 173 delete(s.charms[base], charmURL.Revision) 174 return nil 175 } 176 bundleRev := bundle.Revision() 177 bundleName := bundle.Meta().Name 178 if bundleName != charmURL.Name || bundleRev != charmURL.Revision { 179 return fmt.Errorf("charm url %s mismatch with bundle %s-%d", charmURL, bundleName, bundleRev) 180 } 181 if _, found := s.charms[base]; !found { 182 s.charms[base] = map[int]*charm.Bundle{} 183 } 184 s.charms[base][charmURL.Revision] = bundle 185 return nil 186 } 187 188 // interpret extracts from charmURL information relevant to both Latest and 189 // Get. The returned "base" is always the string representation of the 190 // unrevisioned part of charmURL; the "rev" wil be taken from the charmURL if 191 // available, and will otherwise be the revision of the latest charm in the 192 // store with the same "base". 193 func (s *MockCharmStore) interpret(charmURL *charm.URL) (base string, rev int) { 194 base, rev = charmURL.WithRevision(-1).String(), charmURL.Revision 195 if rev == -1 { 196 for candidate := range s.charms[base] { 197 if candidate > rev { 198 rev = candidate 199 } 200 } 201 } 202 return 203 } 204 205 // Get implements charm.Repository.Get. 206 func (s *MockCharmStore) Get(charmURL *charm.URL) (charm.Charm, error) { 207 base, rev := s.interpret(charmURL) 208 charm, found := s.charms[base][rev] 209 if !found { 210 return nil, fmt.Errorf("charm not found in mock store: %s", charmURL) 211 } 212 return charm, nil 213 } 214 215 // Latest implements charm.Repository.Latest. 216 func (s *MockCharmStore) Latest(charmURLs ...*charm.URL) ([]charm.CharmRevision, error) { 217 result := make([]charm.CharmRevision, len(charmURLs)) 218 for i, curl := range charmURLs { 219 charmURL := curl.WithRevision(-1) 220 base, rev := s.interpret(charmURL) 221 if _, found := s.charms[base][rev]; !found { 222 result[i].Err = fmt.Errorf("charm not found in mock store: %s", charmURL) 223 } else { 224 result[i].Revision = rev 225 } 226 } 227 return result, nil 228 }