Permalink
Browse files

Initial commit.

  • Loading branch information...
0 parents commit 4affe71c31d95afccbab71112f48255f2aa42c9d @AlekSi committed Dec 7, 2012
Showing with 1,566 additions and 0 deletions.
  1. +3 −0 .gitignore
  2. 0 CHANGELOG
  3. +2 −0 CONTRIBUTING.md
  4. +56 −0 LICENSE
  5. +27 −0 Makefile
  6. +2 −0 README.md
  7. +2 −0 doc.go
  8. +9 −0 hook_for_test.go
  9. +155 −0 nut.go
  10. +15 −0 nut.json
  11. +239 −0 nut/base.go
  12. +69 −0 nut/check.go
  13. +94 −0 nut/generate.go
  14. +116 −0 nut/get.go
  15. +21 −0 nut/get_test.go
  16. +10 −0 nut/hook_for_test.go
  17. +60 −0 nut/install.go
  18. +119 −0 nut/main.go
  19. +85 −0 nut/pack.go
  20. +89 −0 nut/publish.go
  21. +54 −0 nut/unpack.go
  22. +50 −0 nut_test.go
  23. +94 −0 spec.go
  24. +50 −0 spec_test.go
  25. +80 −0 version.go
  26. +65 −0 version_test.go
3 .gitignore
@@ -0,0 +1,3 @@
+gonut
+*.nut
+gopath/
0 CHANGELOG
No changes.
2 CONTRIBUTING.md
@@ -0,0 +1,2 @@
+Filling issues and contributing to nut tool
+===========================================
56 LICENSE
@@ -0,0 +1,56 @@
+Copyright (c) 2012 Alexey Palazhchenko. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+--
+
+Some files contain code taken from Go distribution.
+
+Copyright (c) 2012 The Go Authors. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+ * Neither the name of Google Inc. nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27 Makefile
@@ -0,0 +1,27 @@
+all: test
+
+prepare:
+ go get -u launchpad.net/gocheck
+ go get -u github.com/AlekSi/test_nut1
+
+# format, vet, build
+fvb:
+ gofmt -e -s -w .
+ go tool vet .
+ go build -v -o gonut ./nut
+
+test: fvb
+ cd ../test_nut1 && rm -f *.nut
+ cd ../test_nut1 && ../nut/gonut generate -v
+ cd ../test_nut1 && ../nut/gonut check -v
+ cd ../test_nut1 && ../nut/gonut pack -v
+ cd ../test_nut1 && ../nut/gonut check -v test_nut1-0.0.1.nut
+ cd ../test_nut1 && ../nut/gonut unpack -v test_nut1-0.0.1.nut
+
+ go test -v ./...
+
+ cd ../test_nut1 && ../nut/gonut install -v test_nut1-0.0.1.nut
+
+test_server: test
+ cd ../test_nut1 && ../nut/gonut publish -v -server localhost:8080 test_nut1-0.0.1.nut
+ cd ../test_nut1 && ../nut/gonut get -v -server localhost:8080 test_nut1/0.0.1
2 README.md
@@ -0,0 +1,2 @@
+nut
+===
2 doc.go
@@ -0,0 +1,2 @@
+// Package nut provides API for managing versioned Go source code packages, called "nuts".
+package nut
9 hook_for_test.go
@@ -0,0 +1,9 @@
+package nut_test
+
+import (
+ . "launchpad.net/gocheck"
+ "testing"
+)
+
+// Global gocheck hook.
+func TestNut(t *testing.T) { TestingT(t) }
155 nut.go
@@ -0,0 +1,155 @@
+package nut
+
+import (
+ "archive/zip"
+ "bytes"
+ "fmt"
+ "go/build"
+ "io"
+ "io/ioutil"
+ "os"
+ "regexp"
+ "sort"
+ "strings"
+)
+
+// Check package for errors and return them.
+func CheckPackage(pack *build.Package) (errors []string) {
+ // check name
+ if strings.HasPrefix(pack.Name, "_") {
+ errors = append(errors, `Package name should not starts with "_".`)
+ }
+ if strings.HasSuffix(pack.Name, "_test") {
+ errors = append(errors, `Package name should not ends with "_test".`)
+ }
+
+ // check doc summary
+ r := regexp.MustCompile(fmt.Sprintf(`Package %s .+\.`, pack.Name))
+ if !r.MatchString(pack.Doc) {
+ errors = append(errors, fmt.Sprintf(`Package summary in code should be in form "Package %s ... ."`, pack.Name))
+ }
+
+ return
+}
+
+// Describes nut – a Go package with associated meta-information.
+type Nut struct {
+ Spec
+ build.Package
+}
+
+// Check nut for errors and return them. Calls Spec.Check() and CheckPackage().
+func (nut *Nut) Check() (errors []string) {
+ errors = nut.Spec.Check()
+ errors = append(errors, CheckPackage(&nut.Package)...)
+ return
+}
+
+// Returns canonical filename in format <name>-<version>.nut
+func (nut *Nut) FileName() string {
+ return fmt.Sprintf("%s-%s.nut", nut.Name, nut.Version)
+}
+
+// Code "Nut.ReadFrom()" will call Nut.Spec.ReadFrom(), while programmer likely wanted to call NutFile.ReadFrom().
+// This method (with weird incompatible signature) is defined to prevent this typical error.
+func (nut *Nut) ReadFrom(Do, Not, Call bool) (do, not, call bool) {
+ panic("Nut.ReadFrom() called: call Nut.Spec.ReadFrom() or NutFile.ReadFrom()")
+}
+
+// Describes .nut file (a ZIP archive).
+type NutFile struct {
+ Nut
+ Reader *zip.Reader
+}
+
+// ReadFrom reads nut from r until EOF.
+// The return value n is the number of bytes read.
+// Any error except io.EOF encountered during the read is also returned.
+// Implements io.ReaderFrom.
+func (nf *NutFile) ReadFrom(r io.Reader) (n int64, err error) {
+ var b []byte
+ b, err = ioutil.ReadAll(r)
+ n = int64(len(b))
+ if err != nil {
+ return
+ }
+
+ nf.Reader, err = zip.NewReader(bytes.NewReader(b), n)
+ if err != nil {
+ return
+ }
+
+ // read spec (typically the last file)
+ var specReader io.ReadCloser
+ for i := len(nf.Reader.File) - 1; i >= 0; i-- {
+ file := nf.Reader.File[i]
+ if file.Name == SpecFileName {
+ specReader, err = file.Open()
+ if err != nil {
+ return
+ }
+ defer func() {
+ e := specReader.Close()
+ if err == nil { // don't hide original error
+ err = e
+ }
+ }()
+ break
+ }
+ }
+ if specReader == nil {
+ err = fmt.Errorf("NutFile.ReadFrom: %q not found", SpecFileName)
+ return
+ }
+ spec := &nf.Spec
+ _, err = spec.ReadFrom(specReader)
+ if err != nil {
+ return
+ }
+
+ // read package
+ pack, err := nf.context().ImportDir(".", 0)
+ nf.Package = *pack
+ return
+}
+
+// byName implements sort.Interface.
+type byName []os.FileInfo
+
+func (f byName) Len() int { return len(f) }
+func (f byName) Less(i, j int) bool { return f[i].Name() < f[j].Name() }
+func (f byName) Swap(i, j int) { f[i], f[j] = f[j], f[i] }
+
+// Returns build.Context for given nut.
+// Returned value may be used to call normal context methods Import and ImportDir
+// to extract information about package in nut without unpacking it: name, doc, dependencies.
+func (nf *NutFile) context() (ctxt *build.Context) {
+ ctxt = new(build.Context)
+ *ctxt = build.Default
+
+ // FIXME path is ignored (multi-package nuts are not supported yet)
+ ctxt.ReadDir = func(path string) (fi []os.FileInfo, err error) {
+ // log.Printf("nf.ReadDir %q", path)
+
+ fi = make([]os.FileInfo, len(nf.Reader.File))
+ for i, f := range nf.Reader.File {
+ fi[i] = f.FileInfo()
+ }
+ sort.Sort(byName(fi))
+ return fi, nil
+ }
+
+ ctxt.OpenFile = func(path string) (io.ReadCloser, error) {
+ // log.Printf("nf.OpenFile %q", path)
+
+ for _, f := range nf.Reader.File {
+ if f.Name == path {
+ return f.Open()
+ }
+ }
+
+ return nil, fmt.Errorf("NutFile.Context.OpenFile: %q not found", path)
+ }
+
+ return
+}
15 nut.json
@@ -0,0 +1,15 @@
+{
+ "Version": "0.1.0",
+ "Authors": [
+ {
+ "FullName": "Alexey Palazhchenko",
+ "Email": "alexey.palazhchenko@gmail.com"
+ }
+ ],
+ "ExtraFiles": [
+ "CHANGELOG",
+ "CONTRIBUTING.md",
+ "LICENSE",
+ "README.md"
+ ]
+}
239 nut/base.go
@@ -0,0 +1,239 @@
+// Package main implements 'nut' command. Code there is not considered to be a public API.
+package main
+
+import (
+ "archive/zip"
+ "bytes"
+ "encoding/json"
+ "go/build"
+ "io/ioutil"
+ "log"
+ "os"
+ "os/exec"
+ "os/user"
+ "path/filepath"
+ "strings"
+
+ . "github.com/AlekSi/nut"
+)
+
+type Config struct {
+ Token string
+}
+
+const (
+ ConfigFileName = ".nut.json"
+)
+
+var (
+ WorkspaceDir string // current workspace (first path in GOPATH)
+ SrcDir string // src directory in current workspace
+ NutDir string // nut directory in current workspace
+ config Config
+)
+
+func init() {
+ log.SetFlags(0)
+
+ srcDirs := build.Default.SrcDirs()[1:]
+ if len(srcDirs) == 0 {
+ env := os.Getenv("GOPATH")
+ if env == "" {
+ log.Fatal("GOPATH environment variable is empty.")
+ } else {
+ log.Fatalf("Workspaces in GOPATH environment variable (%s), or their src subpaths don't exist.", env)
+ }
+ }
+
+ SrcDir = srcDirs[0]
+ WorkspaceDir = filepath.Join(SrcDir, "..")
+ NutDir = filepath.Join(WorkspaceDir, "nut")
+
+ u, err := user.Current()
+ if err != nil {
+ _, err = os.Stat(u.HomeDir)
+ }
+ if err != nil {
+ log.Printf("Warning: Can't detect current user home directory: %s", err)
+ return
+ }
+
+ path := filepath.Join(u.HomeDir, ConfigFileName)
+ b, err := ioutil.ReadFile(path)
+ if err == nil {
+ err = json.Unmarshal(b, &config)
+ }
+ if err != nil && !os.IsNotExist(err) {
+ log.Printf("Warning: Can't load config from %s: %s\n", path, err)
+ config = Config{}
+ }
+}
+
+func PanicIfErr(err error) {
+ if err != nil {
+ log.Panic(err)
+ }
+}
+
+// TODO common functions there are mess for now
+
+// Read spec from SpecFileName in current directory.
+func ReadSpec() (spec *Spec) {
+ f, err := os.Open(SpecFileName)
+ PanicIfErr(err)
+ defer f.Close()
+ spec = new(Spec)
+ _, err = spec.ReadFrom(f)
+ PanicIfErr(err)
+ return
+}
+
+// Read nut file.
+func ReadNut(fileName string) (b []byte, nf *NutFile) {
+ var err error
+ b, err = ioutil.ReadFile(fileName)
+ PanicIfErr(err)
+ nf = new(NutFile)
+ _, err = nf.ReadFrom(bytes.NewReader(b))
+ PanicIfErr(err)
+ return
+}
+
+// Write nut to GOPATH/nut/<prefix>/<name>-<version>.nut
+func WriteNut(b []byte, prefix string, verbose bool) string {
+ nf := new(NutFile)
+ _, err := nf.ReadFrom(bytes.NewReader(b))
+ PanicIfErr(err)
+
+ // create GOPATH/nut/<prefix>
+ dir := filepath.Join(NutDir, prefix)
+ PanicIfErr(os.MkdirAll(dir, WorkspaceDirPerm))
+
+ // write file
+ dstFilepath := filepath.Join(dir, nf.FileName())
+ if verbose {
+ log.Printf("Writing %s ...", dstFilepath)
+ }
+ PanicIfErr(ioutil.WriteFile(dstFilepath, b, NutFilePerm))
+ return dstFilepath
+}
+
+// Copy nut to GOPATH/nut/<prefix>/<name>-<version>.nut
+func CopyNut(nutFilepath string, prefix string, verbose bool) {
+ b, nf := ReadNut(nutFilepath)
+
+ // create GOPATH/nut/<prefix>
+ dir := filepath.Join(NutDir, prefix)
+ PanicIfErr(os.MkdirAll(dir, WorkspaceDirPerm))
+
+ // write file
+ dstFilepath := filepath.Join(dir, nf.FileName())
+ if verbose {
+ log.Printf("Copying %s to %s ...", nutFilepath, dstFilepath)
+ }
+ PanicIfErr(ioutil.WriteFile(dstFilepath, b, NutFilePerm))
+}
+
+// Pack files into nut file with given fileName.
+func PackNut(fileName string, files []string, verbose bool) {
+ // write nut to temporary file first
+ nutFile, err := ioutil.TempFile("", "nut-")
+ PanicIfErr(err)
+ defer func() {
+ if nutFile != nil {
+ PanicIfErr(os.Remove(nutFile.Name()))
+ }
+ }()
+
+ nutWriter := zip.NewWriter(nutFile)
+ defer func() {
+ if nutWriter != nil {
+ PanicIfErr(nutWriter.Close())
+ }
+ }()
+
+ // add files to nut with all meta information
+ for _, file := range files {
+ if verbose {
+ log.Printf("Packing %s ...", file)
+ }
+
+ fi, err := os.Stat(file)
+ PanicIfErr(err)
+
+ fh, err := zip.FileInfoHeader(fi)
+ PanicIfErr(err)
+ fh.Name = file
+
+ f, err := nutWriter.CreateHeader(fh)
+ PanicIfErr(err)
+
+ b, err := ioutil.ReadFile(file)
+ PanicIfErr(err)
+
+ _, err = f.Write(b)
+ PanicIfErr(err)
+ }
+
+ err = nutWriter.Close()
+ nutWriter = nil
+ PanicIfErr(err)
+
+ PanicIfErr(nutFile.Close())
+
+ // move file to specified location and fix permissions
+ if verbose {
+ log.Printf("Creating %s ...", fileName)
+ }
+ PanicIfErr(os.Rename(nutFile.Name(), fileName))
+ nutFile = nil
+ PanicIfErr(os.Chmod(fileName, NutFilePerm))
+}
+
+// Unpack nut file with given fileName into dir. Creates dir if needed. Removes dir first if asked.
+func UnpackNut(fileName string, dir string, removeDir, verbose bool) {
+ // check dir
+ _, err := os.Stat(dir)
+ if err == nil && removeDir {
+ if verbose {
+ log.Printf("Removing existing directory %s ...", dir)
+ }
+ os.RemoveAll(dir)
+ }
+ PanicIfErr(os.MkdirAll(dir, WorkspaceDirPerm))
+
+ _, nf := ReadNut(fileName)
+
+ for _, file := range nf.Reader.File {
+ if verbose {
+ log.Printf("Unpacking %s ...", file.Name)
+ }
+
+ rc, err := file.Open()
+ PanicIfErr(err)
+ defer rc.Close()
+
+ b, err := ioutil.ReadAll(rc)
+ PanicIfErr(err)
+
+ PanicIfErr(ioutil.WriteFile(filepath.Join(dir, file.Name), b, file.Mode()))
+ }
+}
+
+// Call 'go install <path>'.
+func InstallPackage(path string, verbose bool) {
+ args := []string{"install"}
+ if verbose {
+ args = append(args, "-v")
+ }
+ args = append(args, path)
+ c := exec.Command("go", args...)
+ if verbose {
+ log.Printf("Running %q", strings.Join(c.Args, " "))
+ }
+ out, err := c.CombinedOutput()
+ if verbose || err != nil {
+ log.Print(string(out))
+ }
+ PanicIfErr(err)
+}
69 nut/check.go
@@ -0,0 +1,69 @@
+package main
+
+import (
+ "go/build"
+ "log"
+ "os"
+ "strings"
+
+ . "github.com/AlekSi/nut"
+)
+
+var (
+ cmdCheck = &Command{
+ Run: runCheck,
+ UsageLine: "check [-v] [filenames]",
+ Short: "check specs and nuts for errors",
+ }
+
+ checkV bool
+)
+
+func init() {
+ cmdCheck.Long = `
+Checks given spec (.json) or nut (.nut) files.
+If no filenames are given, checks spec nut.json in current directory.
+ `
+
+ cmdCheck.Flag.BoolVar(&checkV, "v", false, "be verbose")
+}
+
+func runCheck(cmd *Command) {
+ args := cmd.Flag.Args()
+ if len(args) == 0 {
+ args = []string{SpecFileName}
+ }
+
+ for _, arg := range args {
+ var errors []string
+
+ parts := strings.Split(arg, ".")
+ switch parts[len(parts)-1] {
+ case "json":
+ spec := ReadSpec()
+ pack, err := build.ImportDir(".", 0)
+ PanicIfErr(err)
+ errors = spec.Check()
+ errors = append(errors, CheckPackage(pack)...)
+
+ case "nut":
+ _, nf := ReadNut(arg)
+ errors = nf.Check()
+
+ default:
+ log.Fatalf("%q doesn't end with .json or .nut", arg)
+ }
+
+ if len(errors) != 0 {
+ log.Printf("Found issues in %s:", arg)
+ for _, e := range errors {
+ log.Printf(" %s", e)
+ }
+ os.Exit(1)
+ }
+
+ if checkV {
+ log.Printf("%s looks good.", arg)
+ }
+ }
+}
94 nut/generate.go
@@ -0,0 +1,94 @@
+package main
+
+import (
+ "go/build"
+ "log"
+ "os"
+ "path/filepath"
+ "strings"
+
+ . "github.com/AlekSi/nut"
+)
+
+const (
+ SpecFilePerm = 0644
+)
+
+var (
+ cmdGenerate = &Command{
+ Run: runGenerate,
+ UsageLine: "generate [-v]",
+ Short: "generate or update spec in current directory",
+ }
+
+ generateV bool
+)
+
+func init() {
+ cmdGenerate.Long = `
+Generates or updates spec nut.json in for package in current directory.
+ `
+
+ cmdGenerate.Flag.BoolVar(&generateV, "v", false, "be verbose")
+}
+
+func runGenerate(cmd *Command) {
+ action := "updated"
+ var err error
+ var spec *Spec
+ var pack *build.Package
+
+ // read spec
+ if _, err = os.Stat(SpecFileName); os.IsNotExist(err) {
+ action = "generated"
+ spec = new(Spec)
+ } else {
+ spec = ReadSpec()
+ }
+
+ // read package
+ pack, err = build.ImportDir(".", 0)
+ PanicIfErr(err)
+
+ // add example author
+ if len(spec.Authors) == 0 {
+ spec.Authors = []Person{{FullName: ExampleFullName, Email: ExampleEmail}}
+ }
+
+ // some extra files
+ if len(spec.ExtraFiles) == 0 {
+ var globs []string
+ for _, glob := range []string{"read*", "licen?e*", "copying*", "contrib*", "author*",
+ "thank*", "news*", "change*", "install*", "bug*", "todo*"} {
+ globs = append(globs, glob, strings.ToUpper(glob), strings.Title(glob))
+ }
+
+ for _, glob := range globs {
+ files, err := filepath.Glob(glob)
+ PanicIfErr(err)
+ spec.ExtraFiles = append(spec.ExtraFiles, files...)
+ }
+ }
+
+ // write spec
+ f, err := os.OpenFile(SpecFileName, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, SpecFilePerm)
+ PanicIfErr(err)
+ defer f.Close()
+ _, err = spec.WriteTo(f)
+ PanicIfErr(err)
+
+ if generateV {
+ log.Printf("%s %s.", SpecFileName, action)
+ }
+
+ // check spec and package
+ errors := spec.Check()
+ errors = append(errors, CheckPackage(pack)...)
+ if len(errors) != 0 {
+ log.Print("\nYou should fix following issues:")
+ for _, e := range errors {
+ log.Printf(" %s", e)
+ }
+ log.Print("\nAfter that run 'nut check' to check spec again.")
+ }
+}
116 nut/get.go
@@ -0,0 +1,116 @@
+package main
+
+import (
+ "bytes"
+ "fmt"
+ "io/ioutil"
+ "log"
+ "net"
+ "net/http"
+ "net/url"
+ "path/filepath"
+ "strings"
+
+ . "github.com/AlekSi/nut"
+)
+
+const (
+ maxDownloads = 4
+)
+
+var (
+ cmdGet = &Command{
+ Run: runGet,
+ UsageLine: "get [-p prefix] [-server server] [-v] [name or URL]",
+ Short: "download and install nut",
+ }
+
+ getP string
+ getS string
+ getV bool
+)
+
+func init() {
+ cmdGet.Long = `
+Download and install nut from http://gonuts.io/ or specified URL.
+ `
+
+ cmdGet.Flag.StringVar(&getP, "p", "", "install prefix in workspace, uses hostname if omitted")
+ cmdGet.Flag.StringVar(&getS, "server", "www.gonuts.io", "server to use")
+ cmdGet.Flag.BoolVar(&getV, "v", false, "be verbose")
+}
+
+func ArgToURL(s string) (url *url.URL) {
+ var err error
+
+ if strings.HasPrefix(s, "http://") || strings.HasPrefix(s, "https://") {
+ url, err = url.Parse(s)
+ PanicIfErr(err)
+ return
+ }
+
+ switch strings.Count(s, "/") {
+ case 0, 1:
+ url, err = url.Parse(fmt.Sprintf("http://%s/%s", getS, s))
+ case 2:
+ p := strings.Split(s, "/")
+ url, err = url.Parse(fmt.Sprintf("http://%s/%s-%s.nut", p[0], p[1], p[2]))
+ default:
+ log.Panicf("Failed to parse argument %q", s)
+ }
+
+ PanicIfErr(err)
+ return
+}
+
+func get(url *url.URL) (b []byte, err error) {
+ if getV {
+ log.Printf("Getting %s ...", url)
+ }
+
+ req, err := http.NewRequest("GET", url.String(), nil)
+ if err != nil {
+ return
+ }
+ req.Header.Set("Accept", "application/zip")
+
+ res, err := http.DefaultClient.Do(req)
+ defer res.Body.Close()
+ if err != nil {
+ return
+ }
+
+ b, err = ioutil.ReadAll(res.Body)
+ if err != nil {
+ return
+ }
+
+ if res.StatusCode/100 != 2 {
+ err = fmt.Errorf("Status code %d", res.StatusCode)
+ return
+ }
+
+ return
+}
+
+func runGet(cmd *Command) {
+ for _, arg := range cmd.Flag.Args() {
+ url := ArgToURL(arg)
+ b, err := get(url)
+ PanicIfErr(err)
+
+ nf := new(NutFile)
+ nf.ReadFrom(bytes.NewReader(b))
+
+ p := getP
+ if p == "" {
+ p, _, err = net.SplitHostPort(url.Host)
+ PanicIfErr(err)
+ }
+ fileName := WriteNut(b, p, getV)
+ path := filepath.Join(p, nf.Name, nf.Version.String())
+
+ UnpackNut(fileName, filepath.Join(SrcDir, path), true, getV)
+ InstallPackage(path, getV)
+ }
+}
21 nut/get_test.go
@@ -0,0 +1,21 @@
+package main_test
+
+import (
+ . "."
+ . "launchpad.net/gocheck"
+)
+
+type G struct{}
+
+var _ = Suite(&G{})
+
+func (*G) TestArgToURL(c *C) {
+ c.Check(ArgToURL("test_nut1").String(), Equals, "http://www.gonuts.io/test_nut1")
+ c.Check(ArgToURL("test_nut1/0.0.1").String(), Equals, "http://www.gonuts.io/test_nut1/0.0.1")
+ c.Check(ArgToURL("http://gonuts.io/test_nut1").String(), Equals, "http://gonuts.io/test_nut1")
+ c.Check(ArgToURL("http://gonuts.io/test_nut1/0.0.1").String(), Equals, "http://gonuts.io/test_nut1/0.0.1")
+
+ c.Check(ArgToURL("localhost/test_nut1/0.0.1").String(), Equals, "http://localhost/test_nut1-0.0.1.nut")
+
+ c.Check(ArgToURL("http://localhost/test_nut1-0.0.1.nut").String(), Equals, "http://localhost/test_nut1-0.0.1.nut")
+}
10 nut/hook_for_test.go
@@ -0,0 +1,10 @@
+package main_test
+
+import (
+ "testing"
+
+ . "launchpad.net/gocheck"
+)
+
+// Global gocheck hook.
+func TestMain(t *testing.T) { TestingT(t) }
60 nut/install.go
@@ -0,0 +1,60 @@
+package main
+
+import (
+ "log"
+ "path/filepath"
+)
+
+const (
+ WorkspaceDirPerm = 0755
+)
+
+var (
+ cmdInstall = &Command{
+ Run: runInstall,
+ UsageLine: "install [-nc] [-p prefix] [-v] [filenames]",
+ Short: "unpack, compile and install nut",
+ }
+
+ installNC bool
+ installP string
+ installV bool
+)
+
+func init() {
+ cmdInstall.Long = `
+Copies nuts into GOPATH/nut/<prefix>, unpacks them into GOPATH/src/<prefix>/<name>/<version>
+and installs using 'go install'.
+ `
+
+ cmdInstall.Flag.BoolVar(&installNC, "nc", false, "no check (not recommended)")
+ cmdInstall.Flag.StringVar(&installP, "p", "localhost", "install prefix in workspace")
+ cmdInstall.Flag.BoolVar(&installV, "v", false, "be verbose")
+}
+
+func runInstall(cmd *Command) {
+ for _, arg := range cmd.Flag.Args() {
+ _, nf := ReadNut(arg)
+
+ if nf.Name == "main" {
+ log.Fatal(`Binaries (package "main") are not supported yet.`)
+ }
+
+ // check nut
+ if !installNC {
+ errors := nf.Check()
+ if len(errors) != 0 {
+ log.Print("Found errors:")
+ for _, e := range errors {
+ log.Printf(" %s", e)
+ }
+ log.Fatalf("Please contact nut author.")
+ }
+ }
+
+ CopyNut(arg, installP, installV)
+ path := filepath.Join(installP, nf.Name, nf.Version.String())
+ UnpackNut(arg, filepath.Join(SrcDir, path), true, installV)
+ InstallPackage(path, installV)
+ }
+}
119 nut/main.go
@@ -0,0 +1,119 @@
+// This package implements nut commands.
+// It is not a public API, and may change without notice.
+package main
+
+import (
+ "flag"
+ "log"
+ "os"
+ "strings"
+ "text/template"
+)
+
+// A Command is an implementation of a nut command like nut get or nut install.
+type Command struct {
+ // Run runs the command.
+ // To access args use cmd.Flag.Args().
+ Run func(cmd *Command)
+
+ // UsageLine is the one-line usage message.
+ // The first word in the line is taken to be the command name.
+ UsageLine string
+
+ // Short is the short description shown in the 'nut help' output.
+ Short string
+
+ // Long is the long message shown in the 'nut help <this-command>' output.
+ Long string
+
+ // Flag is a set of flags specific to this command.
+ Flag flag.FlagSet
+}
+
+// Name returns the command's name: the first word in the usage line.
+func (c *Command) Name() string {
+ name := c.UsageLine
+ i := strings.Index(name, " ")
+ if i >= 0 {
+ name = name[:i]
+ }
+ return name
+}
+
+func (c *Command) Usage() {
+ log.Printf("usage: %s", c.UsageLine)
+ log.Printf("\n%s\n\n", strings.TrimSpace(c.Long))
+ log.Print("Flags:")
+ c.Flag.PrintDefaults()
+}
+
+// Commands lists the available commands.
+// The order here is the order in which they are printed by 'nut help'.
+var Commands = []*Command{cmdCheck, cmdGenerate, cmdGet, cmdInstall, cmdPack, cmdPublish, cmdUnpack}
+
+var usageTemplate = template.Must(template.New("top").Parse(`Nut is a tool for managing versioned Go source code packages.
+
+Usage:
+
+ nut command [arguments]
+
+The commands are:
+{{range .}}
+ {{.Name | printf "%-11s"}} {{.Short}}{{end}}
+
+Use "nut help [command]" for more information about a command.
+
+`))
+
+// help implements the 'help' command.
+func help(args ...string) {
+ if len(args) == 0 {
+ flag.Usage()
+ os.Exit(0)
+ }
+ if len(args) != 1 {
+ log.Print("usage: nut help [command]\n\nToo many arguments given.")
+ os.Exit(2)
+ }
+
+ arg := args[0]
+ for _, cmd := range Commands {
+ if cmd.Name() == arg {
+ cmd.Usage()
+ os.Exit(0)
+ }
+ }
+
+ log.Printf("Unknown help topic %#q. Run 'nut help'.", arg)
+ os.Exit(2)
+}
+
+func main() {
+ flag.Usage = func() {
+ usageTemplate.Execute(os.Stderr, Commands)
+ flag.PrintDefaults()
+ }
+
+ flag.Parse()
+ args := flag.Args()
+ if len(args) == 0 {
+ help()
+ panic("not reached")
+ }
+ if args[0] == "help" {
+ help(args[1:]...)
+ panic("not reached")
+ }
+
+ for _, cmd := range Commands {
+ if cmd.Name() == args[0] {
+ cmd.Flag.Usage = func() { cmd.Usage() }
+ cmd.Flag.Parse(args[1:])
+ cmd.Run(cmd)
+ os.Exit(0)
+ }
+ }
+
+ log.Printf("nut: unknown subcommand %q\nRun 'nut help' for usage.", args[0])
+ os.Exit(2)
+}
85 nut/pack.go
@@ -0,0 +1,85 @@
+package main
+
+import (
+ "go/build"
+ "log"
+
+ . "github.com/AlekSi/nut"
+)
+
+const (
+ NutFilePerm = 0644
+)
+
+var (
+ cmdPack = &Command{
+ Run: runPack,
+ UsageLine: "pack [-nc] [-o filename] [-v]",
+ Short: "pack package in current directory into nut",
+ }
+
+ packNC bool
+ packO string
+ packV bool
+)
+
+func init() {
+ cmdPack.Long = `
+Packs package in current directory into nut.
+ `
+
+ cmdPack.Flag.BoolVar(&packNC, "nc", false, "no check (not recommended)")
+ cmdPack.Flag.StringVar(&packO, "o", "", "output filename")
+ cmdPack.Flag.BoolVar(&packV, "v", false, "be verbose")
+}
+
+func runPack(cmd *Command) {
+ /*
+ packages := build.ImportDir(".", 0)
+ if len(packages) > 1 {
+ panic(fmt.Errorf("Multi-package nuts are not supported yet\n%#v", packages))
+ // implementation will require import overwrites on install to prevent error like
+ // "local import "./dir/subdir" in non-local package"
+ }
+ */
+
+ pack, err := build.ImportDir(".", 0)
+ PanicIfErr(err)
+
+ if pack.Name == "main" {
+ log.Fatal(`Binaries (package "main") are not supported yet.`)
+ }
+
+ var fileName string
+ spec := ReadSpec()
+ nut := Nut{Spec: *spec, Package: *pack}
+ if packO == "" {
+ fileName = nut.FileName()
+ } else {
+ fileName = packO
+ }
+
+ if !packNC {
+ errors := nut.Check()
+ if len(errors) != 0 {
+ log.Print("Found errors:")
+ for _, e := range errors {
+ log.Printf(" %s", e)
+ }
+ log.Fatalf("Hint: use 'nut check'.")
+ }
+ }
+
+ var files []string
+ files = append(files, pack.GoFiles...)
+ files = append(files, pack.CgoFiles...)
+ files = append(files, pack.TestGoFiles...)
+ files = append(files, pack.XTestGoFiles...)
+ files = append(files, spec.ExtraFiles...)
+ files = append(files, SpecFileName)
+
+ PackNut(fileName, files, packV)
+ if packV {
+ log.Printf("%s created.", fileName)
+ }
+}
89 nut/publish.go
@@ -0,0 +1,89 @@
+package main
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "io/ioutil"
+ "log"
+ "net/http"
+ "net/url"
+ "strings"
+)
+
+var (
+ cmdPublish = &Command{
+ Run: runPublish,
+ UsageLine: "publish [-server server] [-token token] [-v] [filename]",
+ Short: "publish nut on gonuts.io",
+ }
+
+ publishServer string
+ publishToken string
+ publishV bool
+)
+
+func init() {
+ cmdPublish.Long = `
+Publish nut on http://gonuts.io/ (requires registration with Google account).
+ `
+
+ tokenHelp := fmt.Sprintf("access token (see http://gonuts.io/-/me), may be read from ~/%s", ConfigFileName)
+ cmdPublish.Flag.StringVar(&publishServer, "server", "www.gonuts.io", "server to use")
+ cmdPublish.Flag.StringVar(&publishToken, "token", "", tokenHelp)
+ cmdPublish.Flag.BoolVar(&publishV, "v", false, "be verbose")
+}
+
+func runPublish(cmd *Command) {
+ if publishToken == "" {
+ publishToken = config.Token
+ }
+
+ // otherwise localhost:8080 will be parsed as url.URL{Scheme:"localhost", Opaque:"8080"}
+ if !(strings.HasPrefix(publishServer, "http://") || strings.HasPrefix(publishServer, "https://")) {
+ publishServer = "http://" + publishServer
+ }
+ url, err := url.Parse(publishServer)
+ PanicIfErr(err)
+
+ url.RawQuery = "token=" + publishToken
+
+ for _, arg := range cmd.Flag.Args() {
+ b, nf := ReadNut(arg)
+ url.Path = fmt.Sprintf("/%s/%s", nf.Name, nf.Version)
+
+ if publishV {
+ log.Printf("Putting %s to %s ...", arg, url)
+ }
+ req, err := http.NewRequest("PUT", url.String(), bytes.NewReader(b))
+ PanicIfErr(err)
+ req.Header.Set("Content-Type", "application/zip")
+ req.ContentLength = int64(len(b)) // set Content-Length explicitly: dev_appserver.py doesn't support chunked encoding
+
+ res, err := http.DefaultClient.Do(req)
+ PanicIfErr(err)
+
+ b, err = ioutil.ReadAll(res.Body)
+ PanicIfErr(err)
+ res.Body.Close()
+
+ var body map[string]interface{}
+ err = json.Unmarshal(b, &body)
+ if err != nil {
+ log.Print(err)
+ }
+
+ m, ok := body["Message"]
+ if ok {
+ ok = res.StatusCode/100 == 2
+ } else {
+ m = string(b)
+ }
+ if !ok {
+ log.Fatal(m)
+ }
+ if publishV {
+ log.Print(m)
+ }
+ }
+}
54 nut/unpack.go
@@ -0,0 +1,54 @@
+package main
+
+import (
+ "log"
+ "os"
+)
+
+var (
+ cmdUnpack = &Command{
+ Run: runUnpack,
+ UsageLine: "unpack [-nc] [-v] [filename]",
+ Short: "unpack nut into current directory",
+ }
+
+ unpackNC bool
+ unpackV bool
+)
+
+func init() {
+ cmdUnpack.Long = `
+Unpack nut into current directory.
+ `
+
+ cmdUnpack.Flag.BoolVar(&unpackNC, "nc", false, "no check (not recommended)")
+ cmdUnpack.Flag.BoolVar(&unpackV, "v", false, "be verbose")
+}
+
+func runUnpack(cmd *Command) {
+ if len(cmd.Flag.Args()) != 1 {
+ log.Fatalf("Expected exactly one filename, got %s", cmd.Flag.Args())
+ }
+ fileName := cmd.Flag.Args()[0]
+
+ // check nut
+ if !unpackNC {
+ _, nf := ReadNut(fileName)
+ errors := nf.Check()
+ if len(errors) != 0 {
+ log.Print("Found errors:")
+ for _, e := range errors {
+ log.Printf(" %s", e)
+ }
+ log.Fatalf("Please contact nut author.")
+ }
+ }
+
+ // unpack nut
+ dir, err := os.Getwd()
+ PanicIfErr(err)
+ UnpackNut(fileName, dir, false, unpackV)
+ if unpackV {
+ log.Printf("%s unpacked.", fileName)
+ }
+}
50 nut_test.go
@@ -0,0 +1,50 @@
+package nut_test
+
+import (
+ . "."
+ "bytes"
+ "io/ioutil"
+ . "launchpad.net/gocheck"
+ "os"
+)
+
+type N struct {
+ f *os.File
+ b *bytes.Buffer
+}
+
+var _ = Suite(&N{})
+
+func (f *N) SetUpTest(c *C) {
+ file, err := os.Open("../test_nut1/test_nut1-0.0.1.nut")
+ c.Assert(err, Equals, nil)
+ f.f = file
+
+ b, err := ioutil.ReadAll(f.f)
+ c.Assert(err, Equals, nil)
+ f.b = bytes.NewBuffer(b)
+
+ _, err = file.Seek(0, 0)
+ c.Assert(err, Equals, nil)
+}
+
+func (f *N) TestNutFile(c *C) {
+ nf := new(NutFile)
+ _, err := nf.ReadFrom(f.f)
+ c.Assert(err, Equals, nil)
+
+ c.Check(nf.Spec.Version.String(), Equals, "0.0.1")
+ c.Check(nf.Version.String(), Equals, "0.0.1")
+ c.Check(nf.Package.Name, Equals, "test_nut1")
+ c.Check(nf.Name, Equals, "test_nut1")
+ c.Check(nf.FileName(), Equals, "test_nut1-0.0.1.nut")
+ c.Check(nf.Doc, Equals, "Package test_nut1 is used to test nut.")
+ c.Check(nf.GoFiles, DeepEquals, []string{"test_nut1.go", "test_nut11.go"})
+
+ c.Check(len(nf.Reader.File), Equals, 5)
+ names := make([]string, 0, 4)
+ for _, f := range nf.Reader.File {
+ names = append(names, f.Name)
+ }
+ c.Check([]string{"test_nut1.go", "test_nut11.go", "README", "LICENSE", "nut.json"}, DeepEquals, names)
+}
94 spec.go
@@ -0,0 +1,94 @@
+package nut
+
+import (
+ "encoding/json"
+ "fmt"
+ "io"
+ "io/ioutil"
+ "strings"
+)
+
+// Describes part of nut meta-information, stored in file nut.json.
+type Spec struct {
+ Version Version
+ Authors []Person
+ ExtraFiles []string `json:",omitempty"`
+}
+
+// Describes nut author.
+type Person struct {
+ FullName string
+ Email string `json:",omitempty"`
+}
+
+const (
+ ExampleFullName = "Crazy Nutter"
+ ExampleEmail = "crazy.nutter@gonuts.io"
+ SpecFileName = "nut.json"
+)
+
+// ReadFrom reads spec from r until EOF.
+// The return value n is the number of bytes read.
+// Any error except io.EOF encountered during the read is also returned.
+// Implements io.ReaderFrom.
+func (spec *Spec) ReadFrom(r io.Reader) (n int64, err error) {
+ var b []byte
+ b, err = ioutil.ReadAll(r)
+ n = int64(len(b))
+ if err != nil {
+ return
+ }
+
+ err = json.Unmarshal(b, spec)
+ return
+}
+
+// WriteTo writes spec to w.
+// The return value n is the number of bytes written.
+// Any error encountered during the write is also returned.
+// Implements io.WriterTo.
+func (spec *Spec) WriteTo(w io.Writer) (n int64, err error) {
+ var b []byte
+ b, err = json.MarshalIndent(spec, "", " ")
+ if err != nil {
+ return
+ }
+
+ b = append(b, '\n')
+ n1, err := w.Write(b)
+ n = int64(n1)
+ return
+}
+
+// Check spec for errors and return them.
+func (spec *Spec) Check() (errors []string) {
+ // check version
+ if spec.Version.String() == "0.0.0" {
+ errors = append(errors, fmt.Sprintf("Version %q is invalid.", spec.Version))
+ }
+
+ // author should be specified
+ if len(spec.Authors) == 0 {
+ errors = append(errors, "No authors given.")
+ } else {
+ for _, a := range spec.Authors {
+ if a.FullName == ExampleFullName {
+ errors = append(errors, fmt.Sprintf("%q is not a real person.", a.FullName))
+ }
+ }
+ }
+
+ // check license
+ licenseFound := false
+ for _, f := range spec.ExtraFiles {
+ f = strings.ToLower(f)
+ if strings.HasPrefix(f, "license") || strings.HasPrefix(f, "licence") || strings.HasPrefix(f, "copying") {
+ licenseFound = true
+ }
+ }
+ if !licenseFound {
+ errors = append(errors, "Spec should include license file in ExtraFiles.")
+ }
+
+ return
+}
50 spec_test.go
@@ -0,0 +1,50 @@
+package nut_test
+
+import (
+ . "."
+ "bytes"
+ "io/ioutil"
+ . "launchpad.net/gocheck"
+ "os"
+)
+
+type S struct {
+ f *os.File
+ b *bytes.Buffer
+}
+
+var _ = Suite(&S{})
+
+func (f *S) SetUpTest(c *C) {
+ file, err := os.Open("../test_nut1/nut.json")
+ c.Assert(err, Equals, nil)
+ f.f = file
+
+ b, err := ioutil.ReadAll(f.f)
+ c.Assert(err, Equals, nil)
+ f.b = bytes.NewBuffer(b)
+
+ _, err = file.Seek(0, 0)
+ c.Assert(err, Equals, nil)
+}
+
+func (f *S) TestReadWrite(c *C) {
+ spec := new(Spec)
+
+ n, err := spec.ReadFrom(f.f)
+ c.Check(n, Equals, int64(f.b.Len()))
+ c.Assert(err, Equals, nil)
+
+ c.Check(spec.Version.String(), Equals, "0.0.1")
+ c.Check(len(spec.Authors), Equals, 1)
+ c.Check(spec.Authors[0], Equals, Person{FullName: "Alexey Palazhchenko", Email: "alexey.palazhchenko@gmail.com"})
+ c.Check(len(spec.ExtraFiles), Equals, 2)
+ c.Check(spec.ExtraFiles[0], Equals, "README")
+ c.Check(spec.ExtraFiles[1], Equals, "LICENSE")
+
+ buf := new(bytes.Buffer)
+ n2, err := spec.WriteTo(buf)
+ c.Check(n, Equals, n2)
+ c.Check(buf.String(), Equals, f.b.String())
+ c.Assert(err, Equals, nil)
+}
80 version.go
@@ -0,0 +1,80 @@
+package nut
+
+import (
+ "fmt"
+ "regexp"
+ "strconv"
+)
+
+// Current format for nut version.
+var VersionRegexp = regexp.MustCompile(`^(\d+).(\d+).(\d+)$`)
+
+// Describes nut version. See http://gonuts.io/XXX for explanation of version specification.
+type Version struct {
+ Major int
+ Minor int
+ Patch int
+}
+
+// Parse and set version.
+func NewVersion(version string) (v *Version, err error) {
+ v = new(Version)
+ err = v.setVersion(version)
+ return
+}
+
+// Return version as string in current format.
+func (v Version) String() string {
+ res := fmt.Sprintf("%d.%d.%d", v.Major, v.Minor, v.Patch)
+ if !VersionRegexp.MatchString(res) { // sanity check
+ panic(fmt.Sprintf("%s not matches %s", res, VersionRegexp))
+ }
+ return res
+}
+
+// Returns true if left < right, false otherwise.
+func (left *Version) Less(right *Version) bool {
+ if left.Major < right.Major {
+ return true
+ } else if left.Major > right.Major {
+ return false
+ }
+
+ if left.Minor < right.Minor {
+ return true
+ } else if left.Minor > right.Minor {
+ return false
+ }
+
+ if left.Patch < right.Patch {
+ return true
+ } else if left.Patch > right.Patch {
+ return false
+ }
+
+ // left == right => "left < right" is false
+ return false
+}
+
+// Marshal to JSON.
+func (v *Version) MarshalJSON() ([]byte, error) {
+ return []byte(fmt.Sprintf(`"%s"`, v)), nil
+}
+
+// Unmarshal from JSON.
+func (v *Version) UnmarshalJSON(b []byte) error {
+ return v.setVersion(string(b[1 : len(b)-1]))
+}
+
+func (v *Version) setVersion(version string) (err error) {
+ parsed := VersionRegexp.FindAllStringSubmatch(version, -1)
+ if (parsed == nil) || (len(parsed[0]) != 4) {
+ err = fmt.Errorf("Bad format for version %q: parsed as %#v", version, parsed)
+ return
+ }
+
+ v.Major, _ = strconv.Atoi(parsed[0][1])
+ v.Minor, _ = strconv.Atoi(parsed[0][2])
+ v.Patch, _ = strconv.Atoi(parsed[0][3])
+ return
+}
65 version_test.go
@@ -0,0 +1,65 @@
+package nut_test
+
+import (
+ . "."
+ "encoding/json"
+ . "launchpad.net/gocheck"
+)
+
+type V struct {
+ versions []string
+}
+
+var _ = Suite(&V{})
+
+func (f *V) SetUpTest(c *C) {
+ f.versions = []string{
+ "0.0.0", "0.0.1", "0.0.2",
+ "0.1.0", "0.1.1", "0.1.2",
+ "1.0.0", "1.0.1", "1.0.2",
+ "1.1.0", "1.1.1", "1.1.2",
+ "1.1.10", "1.10.1", "10.1.1", "10.10.10",
+ }
+}
+
+func (f *V) TestNew(c *C) {
+ for _, vs := range f.versions {
+ v, err := NewVersion(vs)
+ c.Check(err, Equals, nil)
+ c.Check(v.String(), Equals, vs)
+ }
+}
+
+func (f *V) TestLess(c *C) {
+ for i, vi := range f.versions {
+ left, err := NewVersion(vi)
+ c.Assert(err, Equals, nil)
+
+ for _, vj := range f.versions[:i] {
+ right, err := NewVersion(vj)
+ c.Assert(err, Equals, nil)
+ c.Check(left.Less(right), Equals, false, Commentf("Expected %s >= %s", left, right))
+ }
+ for _, vj := range f.versions[i+1:] {
+ right, err := NewVersion(vj)
+ c.Assert(err, Equals, nil)
+ c.Check(left.Less(right), Equals, true, Commentf("Expected %s < %s", left, right))
+ }
+ }
+}
+
+func (f *V) TestJSON(c *C) {
+ for _, vs := range f.versions {
+ v, err := NewVersion(vs)
+ c.Assert(err, Equals, nil)
+
+ b, err := json.Marshal(v)
+ c.Check(string(b), Equals, `"`+vs+`"`)
+ c.Assert(err, Equals, nil)
+
+ v2 := new(Version)
+ err = json.Unmarshal(b, v2)
+ c.Check(v2, DeepEquals, v)
+ c.Assert(err, Equals, nil)
+ }
+}

0 comments on commit 4affe71

Please sign in to comment.