diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json index 8b47a1eb..6c268015 100644 --- a/Godeps/Godeps.json +++ b/Godeps/Godeps.json @@ -72,6 +72,14 @@ "ImportPath": "github.com/inconshreveable/mousetrap", "Rev": "76626ae9c91c4f2a10f34cad8ce83ea42c93bb75" }, + { + "ImportPath": "github.com/kylelemons/godebug/diff", + "Rev": "21cb3784d9bda523911b96719efba02b7e983256" + }, + { + "ImportPath": "github.com/kylelemons/godebug/pretty", + "Rev": "21cb3784d9bda523911b96719efba02b7e983256" + }, { "ImportPath": "github.com/spf13/cobra", "Rev": "66816bcd0378e248c613e3c443c020f544c28804" diff --git a/Godeps/_workspace/src/github.com/kylelemons/godebug/diff/diff.go b/Godeps/_workspace/src/github.com/kylelemons/godebug/diff/diff.go new file mode 100644 index 00000000..8d7716b9 --- /dev/null +++ b/Godeps/_workspace/src/github.com/kylelemons/godebug/diff/diff.go @@ -0,0 +1,133 @@ +// Copyright 2013 Google Inc. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package diff implements a linewise diff algorithm. +package diff + +import ( + "bytes" + "fmt" + "strings" +) + +// Chunk represents a piece of the diff. A chunk will not have both added and +// deleted lines. Equal lines are always after any added or deleted lines. +// A Chunk may or may not have any lines in it, especially for the first or last +// chunk in a computation. +type Chunk struct { + Added []string + Deleted []string + Equal []string +} + +// Diff returns a string containing a line-by-line unified diff of the linewise +// changes required to make A into B. Each line is prefixed with '+', '-', or +// ' ' to indicate if it should be added, removed, or is correct respectively. +func Diff(A, B string) string { + aLines := strings.Split(A, "\n") + bLines := strings.Split(B, "\n") + + chunks := DiffChunks(aLines, bLines) + + buf := new(bytes.Buffer) + for _, c := range chunks { + for _, line := range c.Added { + fmt.Fprintf(buf, "+%s\n", line) + } + for _, line := range c.Deleted { + fmt.Fprintf(buf, "-%s\n", line) + } + for _, line := range c.Equal { + fmt.Fprintf(buf, " %s\n", line) + } + } + return strings.TrimRight(buf.String(), "\n") +} + +// DiffChunks uses an O(D(N+M)) shortest-edit-script algorithm +// to compute the edits required from A to B and returns the +// edit chunks. +func DiffChunks(A, B []string) []Chunk { + // algorithm: http://www.xmailserver.org/diff2.pdf + + N, M := len(A), len(B) + MAX := N + M + V := make([]int, 2*MAX+1) + Vs := make([][]int, 0, 8) + + var D int +dLoop: + for D = 0; D <= MAX; D++ { + for k := -D; k <= D; k += 2 { + var x int + if k == -D || (k != D && V[MAX+k-1] < V[MAX+k+1]) { + x = V[MAX+k+1] + } else { + x = V[MAX+k-1] + 1 + } + y := x - k + for x < N && y < M && A[x] == B[y] { + x++ + y++ + } + V[MAX+k] = x + if x >= N && y >= M { + Vs = append(Vs, append(make([]int, 0, len(V)), V...)) + break dLoop + } + } + Vs = append(Vs, append(make([]int, 0, len(V)), V...)) + } + if D == 0 { + return nil + } + chunks := make([]Chunk, D+1) + + x, y := N, M + for d := D; d > 0; d-- { + V := Vs[d] + k := x - y + insert := k == -d || (k != d && V[MAX+k-1] < V[MAX+k+1]) + + x1 := V[MAX+k] + var x0, xM, kk int + if insert { + kk = k + 1 + x0 = V[MAX+kk] + xM = x0 + } else { + kk = k - 1 + x0 = V[MAX+kk] + xM = x0 + 1 + } + y0 := x0 - kk + + var c Chunk + if insert { + c.Added = B[y0:][:1] + } else { + c.Deleted = A[x0:][:1] + } + if xM < x1 { + c.Equal = A[xM:][:x1-xM] + } + + x, y = x0, y0 + chunks[d] = c + } + if x > 0 { + chunks[0].Equal = A[:x] + } + return chunks +} diff --git a/Godeps/_workspace/src/github.com/kylelemons/godebug/diff/diff_test.go b/Godeps/_workspace/src/github.com/kylelemons/godebug/diff/diff_test.go new file mode 100644 index 00000000..ed3cb9ab --- /dev/null +++ b/Godeps/_workspace/src/github.com/kylelemons/godebug/diff/diff_test.go @@ -0,0 +1,120 @@ +// Copyright 2013 Google Inc. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package diff + +import ( + "fmt" + "reflect" + "strings" + "testing" +) + +func TestDiff(t *testing.T) { + tests := []struct { + desc string + A, B []string + chunks []Chunk + }{ + { + desc: "constitution", + A: []string{ + "We the People of the United States, in Order to form a more perfect Union,", + "establish Justice, insure domestic Tranquility, provide for the common defence,", + "and secure the Blessings of Liberty to ourselves", + "and our Posterity, do ordain and establish this Constitution for the United", + "States of America.", + }, + B: []string{ + "We the People of the United States, in Order to form a more perfect Union,", + "establish Justice, insure domestic Tranquility, provide for the common defence,", + "promote the general Welfare, and secure the Blessings of Liberty to ourselves", + "and our Posterity, do ordain and establish this Constitution for the United", + "States of America.", + }, + chunks: []Chunk{ + 0: { + Equal: []string{ + "We the People of the United States, in Order to form a more perfect Union,", + "establish Justice, insure domestic Tranquility, provide for the common defence,", + }, + }, + 1: { + Deleted: []string{ + "and secure the Blessings of Liberty to ourselves", + }, + }, + 2: { + Added: []string{ + "promote the general Welfare, and secure the Blessings of Liberty to ourselves", + }, + Equal: []string{ + "and our Posterity, do ordain and establish this Constitution for the United", + "States of America.", + }, + }, + }, + }, + } + + for _, test := range tests { + got := DiffChunks(test.A, test.B) + if got, want := len(got), len(test.chunks); got != want { + t.Errorf("%s: edit distance = %v, want %v", test.desc, got-1, want-1) + continue + } + for i := range got { + got, want := got[i], test.chunks[i] + if got, want := got.Added, want.Added; !reflect.DeepEqual(got, want) { + t.Errorf("%s[%d]: Added = %v, want %v", test.desc, i, got, want) + } + if got, want := got.Deleted, want.Deleted; !reflect.DeepEqual(got, want) { + t.Errorf("%s[%d]: Deleted = %v, want %v", test.desc, i, got, want) + } + if got, want := got.Equal, want.Equal; !reflect.DeepEqual(got, want) { + t.Errorf("%s[%d]: Equal = %v, want %v", test.desc, i, got, want) + } + } + } +} + +func ExampleDiff() { + constitution := strings.TrimSpace(` +We the People of the United States, in Order to form a more perfect Union, +establish Justice, insure domestic Tranquility, provide for the common defence, +promote the general Welfare, and secure the Blessings of Liberty to ourselves +and our Posterity, do ordain and establish this Constitution for the United +States of America. +`) + + got := strings.TrimSpace(` +:wq +We the People of the United States, in Order to form a more perfect Union, +establish Justice, insure domestic Tranquility, provide for the common defence, +and secure the Blessings of Liberty to ourselves +and our Posterity, do ordain and establish this Constitution for the United +States of America. +`) + + fmt.Println(Diff(got, constitution)) + + // Output: + // -:wq + // We the People of the United States, in Order to form a more perfect Union, + // establish Justice, insure domestic Tranquility, provide for the common defence, + // -and secure the Blessings of Liberty to ourselves + // +promote the general Welfare, and secure the Blessings of Liberty to ourselves + // and our Posterity, do ordain and establish this Constitution for the United + // States of America. +} diff --git a/Godeps/_workspace/src/github.com/kylelemons/godebug/pretty/.gitignore b/Godeps/_workspace/src/github.com/kylelemons/godebug/pretty/.gitignore new file mode 100644 index 00000000..fa9a735d --- /dev/null +++ b/Godeps/_workspace/src/github.com/kylelemons/godebug/pretty/.gitignore @@ -0,0 +1,5 @@ +*.test +*.bench +*.golden +*.txt +*.prof diff --git a/Godeps/_workspace/src/github.com/kylelemons/godebug/pretty/doc.go b/Godeps/_workspace/src/github.com/kylelemons/godebug/pretty/doc.go new file mode 100644 index 00000000..03b5718a --- /dev/null +++ b/Godeps/_workspace/src/github.com/kylelemons/godebug/pretty/doc.go @@ -0,0 +1,25 @@ +// Copyright 2013 Google Inc. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package pretty pretty-prints Go structures. +// +// This package uses reflection to examine a Go value and can +// print out in a nice, aligned fashion. It supports three +// modes (normal, compact, and extended) for advanced use. +// +// See the Reflect and Print examples for what the output looks like. +package pretty + +// TODO: +// - Catch cycles diff --git a/Godeps/_workspace/src/github.com/kylelemons/godebug/pretty/examples_test.go b/Godeps/_workspace/src/github.com/kylelemons/godebug/pretty/examples_test.go new file mode 100644 index 00000000..d0ed19d8 --- /dev/null +++ b/Godeps/_workspace/src/github.com/kylelemons/godebug/pretty/examples_test.go @@ -0,0 +1,158 @@ +// Copyright 2013 Google Inc. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package pretty_test + +import ( + "fmt" + "github.com/appc/acbuild/Godeps/_workspace/src/github.com/kylelemons/godebug/pretty" +) + +func ExampleConfig_Sprint() { + type Pair [2]int + type Map struct { + Name string + Players map[string]Pair + Obstacles map[Pair]string + } + + m := Map{ + Name: "Rock Creek", + Players: map[string]Pair{ + "player1": {1, 3}, + "player2": {0, -1}, + }, + Obstacles: map[Pair]string{ + Pair{0, 0}: "rock", + Pair{2, 1}: "pond", + Pair{1, 1}: "stream", + Pair{0, 1}: "stream", + }, + } + + // Specific output formats + compact := &pretty.Config{ + Compact: true, + } + diffable := &pretty.Config{ + Diffable: true, + } + + // Print out a summary + fmt.Printf("Players: %s\n", compact.Sprint(m.Players)) + + // Print diffable output + fmt.Printf("Map State:\n%s", diffable.Sprint(m)) + + // Output: + // Players: {player1:[1,3],player2:[0,-1]} + // Map State: + // { + // Name: "Rock Creek", + // Players: { + // player1: [ + // 1, + // 3, + // ], + // player2: [ + // 0, + // -1, + // ], + // }, + // Obstacles: { + // [0,0]: "rock", + // [0,1]: "stream", + // [1,1]: "stream", + // [2,1]: "pond", + // }, + // } +} + +func ExamplePrint() { + type ShipManifest struct { + Name string + Crew map[string]string + Androids int + Stolen bool + } + + manifest := &ShipManifest{ + Name: "Spaceship Heart of Gold", + Crew: map[string]string{ + "Zaphod Beeblebrox": "Galactic President", + "Trillian": "Human", + "Ford Prefect": "A Hoopy Frood", + "Arthur Dent": "Along for the Ride", + }, + Androids: 1, + Stolen: true, + } + + pretty.Print(manifest) + + // Output: + // {Name: "Spaceship Heart of Gold", + // Crew: {Arthur Dent: "Along for the Ride", + // Ford Prefect: "A Hoopy Frood", + // Trillian: "Human", + // Zaphod Beeblebrox: "Galactic President"}, + // Androids: 1, + // Stolen: true} +} + +func ExampleCompare() { + type ShipManifest struct { + Name string + Crew map[string]string + Androids int + Stolen bool + } + + reported := &ShipManifest{ + Name: "Spaceship Heart of Gold", + Crew: map[string]string{ + "Zaphod Beeblebrox": "Galactic President", + "Trillian": "Human", + "Ford Prefect": "A Hoopy Frood", + "Arthur Dent": "Along for the Ride", + }, + Androids: 1, + Stolen: true, + } + + expected := &ShipManifest{ + Name: "Spaceship Heart of Gold", + Crew: map[string]string{ + "Rowan Artosok": "Captain", + }, + Androids: 1, + Stolen: false, + } + + fmt.Println(pretty.Compare(reported, expected)) + // Output: + // { + // Name: "Spaceship Heart of Gold", + // Crew: { + // - Arthur Dent: "Along for the Ride", + // - Ford Prefect: "A Hoopy Frood", + // - Trillian: "Human", + // - Zaphod Beeblebrox: "Galactic President", + // + Rowan Artosok: "Captain", + // }, + // Androids: 1, + // - Stolen: true, + // + Stolen: false, + // } +} diff --git a/Godeps/_workspace/src/github.com/kylelemons/godebug/pretty/public.go b/Godeps/_workspace/src/github.com/kylelemons/godebug/pretty/public.go new file mode 100644 index 00000000..cf171a48 --- /dev/null +++ b/Godeps/_workspace/src/github.com/kylelemons/godebug/pretty/public.go @@ -0,0 +1,115 @@ +// Copyright 2013 Google Inc. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package pretty + +import ( + "bytes" + "fmt" + "io" + "reflect" + + "github.com/appc/acbuild/Godeps/_workspace/src/github.com/kylelemons/godebug/diff" +) + +// A Config represents optional configuration parameters for formatting. +// +// Some options, notably ShortList, dramatically increase the overhead +// of pretty-printing a value. +type Config struct { + // Verbosity options + Compact bool // One-line output. Overrides Diffable. + Diffable bool // Adds extra newlines for more easily diffable output. + + // Field and value options + IncludeUnexported bool // Include unexported fields in output + PrintStringers bool // Call String on a fmt.Stringer + SkipZeroFields bool // Skip struct fields that have a zero value. + + // Output transforms + ShortList int // Maximum character length for short lists if nonzero. +} + +// Default Config objects +var ( + // CompareConfig is the default configuration used for Compare. + CompareConfig = &Config{ + Diffable: true, + IncludeUnexported: true, + } + + // DefaultConfig is the default configuration used for all other top-level functions. + DefaultConfig = &Config{} +) + +func (cfg *Config) fprint(buf *bytes.Buffer, vals ...interface{}) { + for i, val := range vals { + if i > 0 { + buf.WriteByte('\n') + } + cfg.val2node(reflect.ValueOf(val)).WriteTo(buf, "", cfg) + } +} + +// Print writes the DefaultConfig representation of the given values to standard output. +func Print(vals ...interface{}) { + DefaultConfig.Print(vals...) +} + +// Print writes the configured presentation of the given values to standard output. +func (cfg *Config) Print(vals ...interface{}) { + fmt.Println(cfg.Sprint(vals...)) +} + +// Sprint returns a string representation of the given value according to the DefaultConfig. +func Sprint(vals ...interface{}) string { + return DefaultConfig.Sprint(vals...) +} + +// Sprint returns a string representation of the given value according to cfg. +func (cfg *Config) Sprint(vals ...interface{}) string { + buf := new(bytes.Buffer) + cfg.fprint(buf, vals...) + return buf.String() +} + +// Fprint writes the representation of the given value to the writer according to the DefaultConfig. +func Fprint(w io.Writer, vals ...interface{}) (n int64, err error) { + return DefaultConfig.Fprint(w, vals...) +} + +// Fprint writes the representation of the given value to the writer according to the cfg. +func (cfg *Config) Fprint(w io.Writer, vals ...interface{}) (n int64, err error) { + buf := new(bytes.Buffer) + cfg.fprint(buf, vals...) + return buf.WriteTo(w) +} + +// Compare returns a string containing a line-by-line unified diff of the +// values in got and want, using the CompareConfig. +// +// Each line in the output is prefixed with '+', '-', or ' ' to indicate if it +// should be added to, removed from, or is correct for the "got" value with +// respect to the "want" value. +func Compare(got, want interface{}) string { + return CompareConfig.Compare(got, want) +} + +// Compare returns a string containing a line-by-line unified diff of the +// values in got and want according to the cfg. +func (cfg *Config) Compare(got, want interface{}) string { + diffCfg := *cfg + diffCfg.Diffable = true + return diff.Diff(cfg.Sprint(got), cfg.Sprint(want)) +} diff --git a/Godeps/_workspace/src/github.com/kylelemons/godebug/pretty/public_test.go b/Godeps/_workspace/src/github.com/kylelemons/godebug/pretty/public_test.go new file mode 100644 index 00000000..957cdebc --- /dev/null +++ b/Godeps/_workspace/src/github.com/kylelemons/godebug/pretty/public_test.go @@ -0,0 +1,128 @@ +// Copyright 2013 Google Inc. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package pretty + +import ( + "testing" +) + +func TestDiff(t *testing.T) { + type example struct { + Name string + Age int + Friends []string + } + + tests := []struct { + desc string + got, want interface{} + diff string + }{ + { + desc: "basic struct", + got: example{ + Name: "Zaphd", + Age: 42, + Friends: []string{ + "Ford Prefect", + "Trillian", + "Marvin", + }, + }, + want: example{ + Name: "Zaphod", + Age: 42, + Friends: []string{ + "Ford Prefect", + "Trillian", + }, + }, + diff: ` { +- Name: "Zaphd", ++ Name: "Zaphod", + Age: 42, + Friends: [ + "Ford Prefect", + "Trillian", +- "Marvin", + ], + }`, + }, + } + + for _, test := range tests { + if got, want := Compare(test.got, test.want), test.diff; got != want { + t.Errorf("%s:", test.desc) + t.Errorf(" got: %q", got) + t.Errorf(" want: %q", want) + } + } +} + +func TestSkipZeroFields(t *testing.T) { + type example struct { + Name string + Species string + Age int + Friends []string + } + + tests := []struct { + desc string + got, want interface{} + diff string + }{ + { + desc: "basic struct", + got: example{ + Name: "Zaphd", + Species: "Betelgeusian", + Age: 42, + }, + want: example{ + Name: "Zaphod", + Species: "Betelgeusian", + Age: 42, + Friends: []string{ + "Ford Prefect", + "Trillian", + "", + }, + }, + diff: ` { +- Name: "Zaphd", ++ Name: "Zaphod", + Species: "Betelgeusian", + Age: 42, ++ Friends: [ ++ "Ford Prefect", ++ "Trillian", ++ "", ++ ], + }`, + }, + } + + cfg := *CompareConfig + cfg.SkipZeroFields = true + + for _, test := range tests { + if got, want := cfg.Compare(test.got, test.want), test.diff; got != want { + t.Errorf("%s:", test.desc) + t.Errorf(" got: %q", got) + t.Errorf(" want: %q", want) + } + } +} diff --git a/Godeps/_workspace/src/github.com/kylelemons/godebug/pretty/reflect.go b/Godeps/_workspace/src/github.com/kylelemons/godebug/pretty/reflect.go new file mode 100644 index 00000000..b64d81ae --- /dev/null +++ b/Godeps/_workspace/src/github.com/kylelemons/godebug/pretty/reflect.go @@ -0,0 +1,104 @@ +// Copyright 2013 Google Inc. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package pretty + +import ( + "fmt" + "reflect" + "sort" +) + +func isZeroVal(val reflect.Value) bool { + if !val.CanInterface() { + return false + } + z := reflect.Zero(val.Type()).Interface() + return reflect.DeepEqual(val.Interface(), z) +} + +func (c *Config) val2node(val reflect.Value) node { + // TODO(kevlar): pointer tracking? + + if c.PrintStringers && val.CanInterface() { + stringer, ok := val.Interface().(fmt.Stringer) + if ok { + return stringVal(stringer.String()) + } + } + + switch kind := val.Kind(); kind { + case reflect.Ptr, reflect.Interface: + if val.IsNil() { + return rawVal("nil") + } + return c.val2node(val.Elem()) + case reflect.String: + return stringVal(val.String()) + case reflect.Slice, reflect.Array: + n := list{} + length := val.Len() + for i := 0; i < length; i++ { + n = append(n, c.val2node(val.Index(i))) + } + return n + case reflect.Map: + n := keyvals{} + keys := val.MapKeys() + for _, key := range keys { + // TODO(kevlar): Support arbitrary type keys? + n = append(n, keyval{compactString(c.val2node(key)), c.val2node(val.MapIndex(key))}) + } + sort.Sort(n) + return n + case reflect.Struct: + n := keyvals{} + typ := val.Type() + fields := typ.NumField() + for i := 0; i < fields; i++ { + sf := typ.Field(i) + if !c.IncludeUnexported && sf.PkgPath != "" { + continue + } + field := val.Field(i) + if c.SkipZeroFields && isZeroVal(field) { + continue + } + n = append(n, keyval{sf.Name, c.val2node(field)}) + } + return n + case reflect.Bool: + if val.Bool() { + return rawVal("true") + } + return rawVal("false") + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return rawVal(fmt.Sprintf("%d", val.Int())) + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + return rawVal(fmt.Sprintf("%d", val.Uint())) + case reflect.Uintptr: + return rawVal(fmt.Sprintf("0x%X", val.Uint())) + case reflect.Float32, reflect.Float64: + return rawVal(fmt.Sprintf("%v", val.Float())) + case reflect.Complex64, reflect.Complex128: + return rawVal(fmt.Sprintf("%v", val.Complex())) + } + + // Fall back to the default %#v if we can + if val.CanInterface() { + return rawVal(fmt.Sprintf("%#v", val.Interface())) + } + + return rawVal(val.String()) +} diff --git a/Godeps/_workspace/src/github.com/kylelemons/godebug/pretty/reflect_test.go b/Godeps/_workspace/src/github.com/kylelemons/godebug/pretty/reflect_test.go new file mode 100644 index 00000000..15c80971 --- /dev/null +++ b/Godeps/_workspace/src/github.com/kylelemons/godebug/pretty/reflect_test.go @@ -0,0 +1,143 @@ +// Copyright 2013 Google Inc. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package pretty + +import ( + "reflect" + "testing" + "time" +) + +func TestVal2nodeDefault(t *testing.T) { + tests := []struct { + desc string + raw interface{} + want node + }{ + { + "nil", + (*int)(nil), + rawVal("nil"), + }, + { + "string", + "zaphod", + stringVal("zaphod"), + }, + { + "slice", + []string{"a", "b"}, + list{stringVal("a"), stringVal("b")}, + }, + { + "map", + map[string]string{ + "zaphod": "beeblebrox", + "ford": "prefect", + }, + keyvals{ + {"ford", stringVal("prefect")}, + {"zaphod", stringVal("beeblebrox")}, + }, + }, + { + "map of [2]int", + map[[2]int]string{ + [2]int{-1, 2}: "school", + [2]int{0, 0}: "origin", + [2]int{1, 3}: "home", + }, + keyvals{ + {"[-1,2]", stringVal("school")}, + {"[0,0]", stringVal("origin")}, + {"[1,3]", stringVal("home")}, + }, + }, + { + "struct", + struct{ Zaphod, Ford string }{"beeblebrox", "prefect"}, + keyvals{ + {"Zaphod", stringVal("beeblebrox")}, + {"Ford", stringVal("prefect")}, + }, + }, + { + "int", + 3, + rawVal("3"), + }, + } + + for _, test := range tests { + if got, want := DefaultConfig.val2node(reflect.ValueOf(test.raw)), test.want; !reflect.DeepEqual(got, want) { + t.Errorf("%s: got %#v, want %#v", test.desc, got, want) + } + } +} + +func TestVal2node(t *testing.T) { + tests := []struct { + desc string + raw interface{} + cfg *Config + want node + }{ + { + "struct default", + struct{ Zaphod, Ford, foo string }{"beeblebrox", "prefect", "BAD"}, + DefaultConfig, + keyvals{ + {"Zaphod", stringVal("beeblebrox")}, + {"Ford", stringVal("prefect")}, + }, + }, + { + "struct w/ IncludeUnexported", + struct{ Zaphod, Ford, foo string }{"beeblebrox", "prefect", "GOOD"}, + &Config{ + IncludeUnexported: true, + }, + keyvals{ + {"Zaphod", stringVal("beeblebrox")}, + {"Ford", stringVal("prefect")}, + {"foo", stringVal("GOOD")}, + }, + }, + { + "time default", + struct{ Date time.Time }{time.Unix(1234567890, 0).UTC()}, + DefaultConfig, + keyvals{ + {"Date", keyvals{}}, // empty struct, it has unexported fields + }, + }, + { + "time w/ PrintStringers", + struct{ Date time.Time }{time.Unix(1234567890, 0).UTC()}, + &Config{ + PrintStringers: true, + }, + keyvals{ + {"Date", stringVal("2009-02-13 23:31:30 +0000 UTC")}, + }, + }, + } + + for _, test := range tests { + if got, want := test.cfg.val2node(reflect.ValueOf(test.raw)), test.want; !reflect.DeepEqual(got, want) { + t.Errorf("%s: got %#v, want %#v", test.desc, got, want) + } + } +} diff --git a/Godeps/_workspace/src/github.com/kylelemons/godebug/pretty/structure.go b/Godeps/_workspace/src/github.com/kylelemons/godebug/pretty/structure.go new file mode 100644 index 00000000..2c963eab --- /dev/null +++ b/Godeps/_workspace/src/github.com/kylelemons/godebug/pretty/structure.go @@ -0,0 +1,128 @@ +// Copyright 2013 Google Inc. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package pretty + +import ( + "bytes" + "strconv" + "strings" +) + +type node interface { + WriteTo(w *bytes.Buffer, indent string, cfg *Config) +} + +func compactString(n node) string { + switch k := n.(type) { + case stringVal: + return string(k) + case rawVal: + return string(k) + } + + buf := new(bytes.Buffer) + n.WriteTo(buf, "", &Config{Compact: true}) + return buf.String() +} + +type stringVal string + +func (str stringVal) WriteTo(w *bytes.Buffer, indent string, cfg *Config) { + w.WriteString(strconv.Quote(string(str))) +} + +type rawVal string + +func (r rawVal) WriteTo(w *bytes.Buffer, indent string, cfg *Config) { + w.WriteString(string(r)) +} + +type keyval struct { + key string + val node +} + +type keyvals []keyval + +func (l keyvals) Len() int { return len(l) } +func (l keyvals) Swap(i, j int) { l[i], l[j] = l[j], l[i] } +func (l keyvals) Less(i, j int) bool { return l[i].key < l[j].key } + +func (l keyvals) WriteTo(w *bytes.Buffer, indent string, cfg *Config) { + keyWidth := 0 + + for _, kv := range l { + if kw := len(kv.key); kw > keyWidth { + keyWidth = kw + } + } + padding := strings.Repeat(" ", keyWidth+1) + + inner := indent + " " + padding + w.WriteByte('{') + for i, kv := range l { + if cfg.Compact { + w.WriteString(kv.key) + w.WriteByte(':') + } else { + if i > 0 || cfg.Diffable { + w.WriteString("\n ") + w.WriteString(indent) + } + w.WriteString(kv.key) + w.WriteByte(':') + w.WriteString(padding[len(kv.key):]) + } + kv.val.WriteTo(w, inner, cfg) + if i+1 < len(l) || cfg.Diffable { + w.WriteByte(',') + } + } + if !cfg.Compact && cfg.Diffable && len(l) > 0 { + w.WriteString("\n") + w.WriteString(indent) + } + w.WriteByte('}') +} + +type list []node + +func (l list) WriteTo(w *bytes.Buffer, indent string, cfg *Config) { + if max := cfg.ShortList; max > 0 { + short := compactString(l) + if len(short) <= max { + w.WriteString(short) + return + } + } + + inner := indent + " " + w.WriteByte('[') + for i, v := range l { + if !cfg.Compact && (i > 0 || cfg.Diffable) { + w.WriteByte('\n') + w.WriteString(inner) + } + v.WriteTo(w, inner, cfg) + if i+1 < len(l) || cfg.Diffable { + w.WriteByte(',') + } + } + if !cfg.Compact && cfg.Diffable && len(l) > 0 { + w.WriteByte('\n') + w.WriteString(indent) + } + w.WriteByte(']') +} diff --git a/Godeps/_workspace/src/github.com/kylelemons/godebug/pretty/structure_test.go b/Godeps/_workspace/src/github.com/kylelemons/godebug/pretty/structure_test.go new file mode 100644 index 00000000..0f0d91de --- /dev/null +++ b/Godeps/_workspace/src/github.com/kylelemons/godebug/pretty/structure_test.go @@ -0,0 +1,262 @@ +// Copyright 2013 Google Inc. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package pretty + +import ( + "bytes" + "testing" +) + +func TestWriteTo(t *testing.T) { + tests := []struct { + desc string + node node + normal string + extended string + }{ + { + desc: "string", + node: stringVal("zaphod"), + normal: `"zaphod"`, + extended: `"zaphod"`, + }, + { + desc: "raw", + node: rawVal("42"), + normal: `42`, + extended: `42`, + }, + { + desc: "keyvals", + node: keyvals{ + {"name", stringVal("zaphod")}, + {"age", rawVal("42")}, + }, + normal: `{name: "zaphod", + age: 42}`, + extended: `{ + name: "zaphod", + age: 42, +}`, + }, + { + desc: "list", + node: list{ + stringVal("zaphod"), + rawVal("42"), + }, + normal: `["zaphod", + 42]`, + extended: `[ + "zaphod", + 42, +]`, + }, + { + desc: "nested", + node: list{ + stringVal("first"), + list{rawVal("1"), rawVal("2"), rawVal("3")}, + keyvals{ + {"trillian", keyvals{ + {"race", stringVal("human")}, + {"age", rawVal("36")}, + }}, + {"zaphod", keyvals{ + {"occupation", stringVal("president of the galaxy")}, + {"features", stringVal("two heads")}, + }}, + }, + keyvals{}, + }, + normal: `["first", + [1, + 2, + 3], + {trillian: {race: "human", + age: 36}, + zaphod: {occupation: "president of the galaxy", + features: "two heads"}}, + {}]`, + extended: `[ + "first", + [ + 1, + 2, + 3, + ], + { + trillian: { + race: "human", + age: 36, + }, + zaphod: { + occupation: "president of the galaxy", + features: "two heads", + }, + }, + {}, +]`, + }, + } + + for _, test := range tests { + buf := new(bytes.Buffer) + test.node.WriteTo(buf, "", &Config{}) + if got, want := buf.String(), test.normal; got != want { + t.Errorf("%s: normal rendendered incorrectly\ngot:\n%s\nwant:\n%s", test.desc, got, want) + } + buf.Reset() + test.node.WriteTo(buf, "", &Config{Diffable: true}) + if got, want := buf.String(), test.extended; got != want { + t.Errorf("%s: extended rendendered incorrectly\ngot:\n%s\nwant:\n%s", test.desc, got, want) + } + } +} + +func TestCompactString(t *testing.T) { + tests := []struct { + node + compact string + }{ + { + stringVal("abc"), + "abc", + }, + { + rawVal("2"), + "2", + }, + { + list{ + rawVal("2"), + rawVal("3"), + }, + "[2,3]", + }, + { + keyvals{ + {"name", stringVal("zaphod")}, + {"age", rawVal("42")}, + }, + `{name:"zaphod",age:42}`, + }, + { + list{ + list{ + rawVal("0"), + rawVal("1"), + rawVal("2"), + rawVal("3"), + }, + list{ + rawVal("1"), + rawVal("2"), + rawVal("3"), + rawVal("0"), + }, + list{ + rawVal("2"), + rawVal("3"), + rawVal("0"), + rawVal("1"), + }, + }, + `[[0,1,2,3],[1,2,3,0],[2,3,0,1]]`, + }, + } + + for _, test := range tests { + if got, want := compactString(test.node), test.compact; got != want { + t.Errorf("%#v: compact = %q, want %q", test.node, got, want) + } + } +} + +func TestShortList(t *testing.T) { + cfg := &Config{ + ShortList: 16, + } + + tests := []struct { + node + want string + }{ + { + list{ + list{ + rawVal("0"), + rawVal("1"), + rawVal("2"), + rawVal("3"), + }, + list{ + rawVal("1"), + rawVal("2"), + rawVal("3"), + rawVal("0"), + }, + list{ + rawVal("2"), + rawVal("3"), + rawVal("0"), + rawVal("1"), + }, + }, + `[[0,1,2,3], + [1,2,3,0], + [2,3,0,1]]`, + }, + } + + for _, test := range tests { + buf := new(bytes.Buffer) + test.node.WriteTo(buf, "", cfg) + if got, want := buf.String(), test.want; got != want { + t.Errorf("%#v: got:\n%s\nwant:\n%s", test.node, got, want) + } + } +} + +var benchNode = keyvals{ + {"list", list{ + rawVal("0"), + rawVal("1"), + rawVal("2"), + rawVal("3"), + }}, + {"keyvals", keyvals{ + {"a", stringVal("b")}, + {"c", stringVal("e")}, + {"d", stringVal("f")}, + }}, +} + +func benchOpts(b *testing.B, cfg *Config) { + buf := new(bytes.Buffer) + benchNode.WriteTo(buf, "", cfg) + b.SetBytes(int64(buf.Len())) + b.ResetTimer() + + for i := 0; i < b.N; i++ { + buf.Reset() + benchNode.WriteTo(buf, "", cfg) + } +} + +func BenchmarkWriteDefault(b *testing.B) { benchOpts(b, DefaultConfig) } +func BenchmarkWriteShortList(b *testing.B) { benchOpts(b, &Config{ShortList: 16}) } +func BenchmarkWriteCompact(b *testing.B) { benchOpts(b, &Config{Compact: true}) } +func BenchmarkWriteDiffable(b *testing.B) { benchOpts(b, &Config{Diffable: true}) } diff --git a/acbuild/acbuild.go b/acbuild/acbuild.go index 99bab115..f72f2ec3 100644 --- a/acbuild/acbuild.go +++ b/acbuild/acbuild.go @@ -99,6 +99,17 @@ func newACBuild() *lib.ACBuild { return lib.NewACBuild(contextpath, debug) } +func getErrorCode(err error) int { + switch err { + case lib.ErrNotFound: + return 2 + case nil: + return 0 + default: + return 1 + } +} + // runWrapper return a func(cmd *cobra.Command, args []string) that internally // will add command function return code and the reinsertion of the "--" flag // terminator. @@ -163,7 +174,7 @@ func runWrapper(cf func(cmd *cobra.Command, args []string) (exit int)) func(cmd err = a.Begin(aciToModify, false) if err != nil { stderr("%v", err) - cmdExitCode = 1 + cmdExitCode = getErrorCode(err) return } @@ -172,14 +183,14 @@ func runWrapper(cf func(cmd *cobra.Command, args []string) (exit int)) func(cmd err = a.Write(aciToModify, true, false, nil) if err != nil { stderr("%v", err) - cmdExitCode = 1 + cmdExitCode = getErrorCode(err) return } err = a.End() if err != nil { stderr("%v", err) - cmdExitCode = 1 + cmdExitCode = getErrorCode(err) return } } diff --git a/acbuild/annotation.go b/acbuild/annotation.go index d5002665..35e8d12d 100644 --- a/acbuild/annotation.go +++ b/acbuild/annotation.go @@ -64,7 +64,7 @@ func runAddAnno(cmd *cobra.Command, args []string) (exit int) { if err != nil { stderr("annotation add: %v", err) - return 1 + return getErrorCode(err) } return 0 @@ -88,7 +88,7 @@ func runRmAnno(cmd *cobra.Command, args []string) (exit int) { if err != nil { stderr("annotation remove: %v", err) - return 1 + return getErrorCode(err) } return 0 diff --git a/acbuild/begin.go b/acbuild/begin.go index cd6ea28e..92981398 100644 --- a/acbuild/begin.go +++ b/acbuild/begin.go @@ -56,7 +56,7 @@ func runBegin(cmd *cobra.Command, args []string) (exit int) { if err != nil { stderr("begin: %v", err) - return 1 + return getErrorCode(err) } return 0 diff --git a/acbuild/copy.go b/acbuild/copy.go index c3d8fd59..5835b1ba 100644 --- a/acbuild/copy.go +++ b/acbuild/copy.go @@ -49,7 +49,7 @@ func runCopy(cmd *cobra.Command, args []string) (exit int) { if err != nil { stderr("copy: %v", err) - return 1 + return getErrorCode(err) } return 0 diff --git a/acbuild/dependency.go b/acbuild/dependency.go index 3cff0cb2..ac8af911 100644 --- a/acbuild/dependency.go +++ b/acbuild/dependency.go @@ -76,7 +76,7 @@ func runAddDep(cmd *cobra.Command, args []string) (exit int) { if err != nil { stderr("dependency add: %v", err) - return 1 + return getErrorCode(err) } return 0 @@ -100,7 +100,7 @@ func runRmDep(cmd *cobra.Command, args []string) (exit int) { if err != nil { stderr("dependency remove: %v", err) - return 1 + return getErrorCode(err) } return 0 diff --git a/acbuild/end.go b/acbuild/end.go index b0767fd7..c3ff2350 100644 --- a/acbuild/end.go +++ b/acbuild/end.go @@ -46,7 +46,7 @@ func runEnd(cmd *cobra.Command, args []string) (exit int) { if err != nil { stderr("end: %v", err) - return 1 + return getErrorCode(err) } return 0 diff --git a/acbuild/environment.go b/acbuild/environment.go index 536e6813..fc466864 100644 --- a/acbuild/environment.go +++ b/acbuild/environment.go @@ -64,7 +64,7 @@ func runAddEnv(cmd *cobra.Command, args []string) (exit int) { if err != nil { stderr("environment add: %v", err) - return 1 + return getErrorCode(err) } return 0 @@ -88,7 +88,7 @@ func runRemoveEnv(cmd *cobra.Command, args []string) (exit int) { if err != nil { stderr("environment remove: %v", err) - return 1 + return getErrorCode(err) } return 0 diff --git a/acbuild/label.go b/acbuild/label.go index ea58c137..1d378bdd 100644 --- a/acbuild/label.go +++ b/acbuild/label.go @@ -64,7 +64,7 @@ func runAddLabel(cmd *cobra.Command, args []string) (exit int) { if err != nil { stderr("label add: %v", err) - return 1 + return getErrorCode(err) } return 0 @@ -88,7 +88,7 @@ func runRemoveLabel(cmd *cobra.Command, args []string) (exit int) { if err != nil { stderr("label remove: %v", err) - return 1 + return getErrorCode(err) } return 0 diff --git a/acbuild/mount.go b/acbuild/mount.go index 23fe9c25..419342bc 100644 --- a/acbuild/mount.go +++ b/acbuild/mount.go @@ -71,7 +71,7 @@ func runAddMount(cmd *cobra.Command, args []string) (exit int) { if err != nil { stderr("mount add: %v", err) - return 1 + return getErrorCode(err) } return 0 @@ -95,7 +95,7 @@ func runRmMount(cmd *cobra.Command, args []string) (exit int) { if err != nil { stderr("mount remove: %v", err) - return 1 + return getErrorCode(err) } return 0 diff --git a/acbuild/port.go b/acbuild/port.go index 199e4924..da4ccb35 100644 --- a/acbuild/port.go +++ b/acbuild/port.go @@ -76,7 +76,7 @@ func runAddPort(cmd *cobra.Command, args []string) (exit int) { if err != nil { stderr("port add: %v", err) - return 1 + return getErrorCode(err) } return 0 @@ -100,7 +100,7 @@ func runRmPort(cmd *cobra.Command, args []string) (exit int) { if err != nil { stderr("port remove: %v", err) - return 1 + return getErrorCode(err) } return 0 diff --git a/acbuild/run.go b/acbuild/run.go index e9eab58d..6d30a38b 100644 --- a/acbuild/run.go +++ b/acbuild/run.go @@ -49,7 +49,7 @@ func runRun(cmd *cobra.Command, args []string) (exit int) { if err != nil { stderr("run: %v", err) - return 1 + return getErrorCode(err) } return 0 diff --git a/acbuild/set-exec.go b/acbuild/set-exec.go index f29dee80..141b05f9 100644 --- a/acbuild/set-exec.go +++ b/acbuild/set-exec.go @@ -46,7 +46,7 @@ func runSetExec(cmd *cobra.Command, args []string) (exit int) { if err != nil { stderr("set-exec: %v", err) - return 1 + return getErrorCode(err) } return 0 diff --git a/acbuild/set-group.go b/acbuild/set-group.go index 119e46a5..6c13480a 100644 --- a/acbuild/set-group.go +++ b/acbuild/set-group.go @@ -50,7 +50,7 @@ func runSetGroup(cmd *cobra.Command, args []string) (exit int) { if err != nil { stderr("set-group: %v", err) - return 1 + return getErrorCode(err) } return 0 diff --git a/acbuild/set-name.go b/acbuild/set-name.go index 3e690443..dcc0af08 100644 --- a/acbuild/set-name.go +++ b/acbuild/set-name.go @@ -50,7 +50,7 @@ func runSetName(cmd *cobra.Command, args []string) (exit int) { if err != nil { stderr("set-name: %v", err) - return 1 + return getErrorCode(err) } return 0 diff --git a/acbuild/set-user.go b/acbuild/set-user.go index bf237312..fb7bbe17 100644 --- a/acbuild/set-user.go +++ b/acbuild/set-user.go @@ -50,7 +50,7 @@ func runSetUser(cmd *cobra.Command, args []string) (exit int) { if err != nil { stderr("set-user: %v", err) - return 1 + return getErrorCode(err) } return 0 diff --git a/acbuild/write.go b/acbuild/write.go index fb0cf12a..910b7fca 100644 --- a/acbuild/write.go +++ b/acbuild/write.go @@ -51,7 +51,7 @@ func runWrite(cmd *cobra.Command, args []string) (exit int) { if err != nil { stderr("write: %v", err) - return 1 + return getErrorCode(err) } return 0 diff --git a/lib/annotations.go b/lib/annotations.go index 7772d630..30e010d5 100644 --- a/lib/annotations.go +++ b/lib/annotations.go @@ -21,15 +21,21 @@ import ( "github.com/appc/acbuild/Godeps/_workspace/src/github.com/appc/spec/schema/types" ) -func removeAnnotation(name types.ACIdentifier) func(*schema.ImageManifest) { - return func(s *schema.ImageManifest) { +func removeAnnotation(name types.ACIdentifier) func(*schema.ImageManifest) error { + return func(s *schema.ImageManifest) error { + foundOne := false for i := len(s.Annotations) - 1; i >= 0; i-- { if s.Annotations[i].Name == name { + foundOne = true s.Annotations = append( s.Annotations[:i], s.Annotations[i+1:]...) } } + if !foundOne { + return ErrNotFound + } + return nil } } @@ -51,8 +57,9 @@ func (a *ACBuild) AddAnnotation(name, value string) (err error) { return err } - fn := func(s *schema.ImageManifest) { + fn := func(s *schema.ImageManifest) error { s.Annotations.Set(*acid, value) + return nil } return util.ModifyManifest(fn, a.CurrentACIPath) } diff --git a/lib/common.go b/lib/common.go index 9b6c3025..4c81a49f 100644 --- a/lib/common.go +++ b/lib/common.go @@ -25,6 +25,10 @@ import ( const defaultWorkPath = ".acbuild" +// ErrNotFound is returned when acbuild is asked to remove an element from +// a list and the element is not present in the list +var ErrNotFound = fmt.Errorf("element to be removed does not exist in this ACI") + // ACBuild contains all the information for a current build. Once an ACBuild // has been created, the functions available on it will perform different // actions in the build, like updating a dependency or writing a finished ACI. diff --git a/lib/dependencies.go b/lib/dependencies.go index d1f277ae..14235ae4 100644 --- a/lib/dependencies.go +++ b/lib/dependencies.go @@ -21,15 +21,21 @@ import ( "github.com/appc/acbuild/Godeps/_workspace/src/github.com/appc/spec/schema/types" ) -func removeDep(imageName types.ACIdentifier) func(*schema.ImageManifest) { - return func(s *schema.ImageManifest) { +func removeDep(imageName types.ACIdentifier) func(*schema.ImageManifest) error { + return func(s *schema.ImageManifest) error { + foundOne := false for i := len(s.Dependencies) - 1; i >= 0; i-- { if s.Dependencies[i].ImageName == imageName { + foundOne = true s.Dependencies = append( s.Dependencies[:i], s.Dependencies[i+1:]...) } } + if !foundOne { + return ErrNotFound + } + return nil } } @@ -60,7 +66,7 @@ func (a *ACBuild) AddDependency(imageName, imageId string, labels types.Labels, } } - fn := func(s *schema.ImageManifest) { + fn := func(s *schema.ImageManifest) error { removeDep(*acid)(s) s.Dependencies = append(s.Dependencies, types.Dependency{ @@ -69,6 +75,7 @@ func (a *ACBuild) AddDependency(imageName, imageId string, labels types.Labels, Labels: labels, Size: size, }) + return nil } return util.ModifyManifest(fn, a.CurrentACIPath) } diff --git a/lib/env.go b/lib/env.go index 4e37301b..a2c8cf7d 100644 --- a/lib/env.go +++ b/lib/env.go @@ -21,18 +21,24 @@ import ( "github.com/appc/acbuild/Godeps/_workspace/src/github.com/appc/spec/schema/types" ) -func removeFromEnv(name string) func(*schema.ImageManifest) { - return func(s *schema.ImageManifest) { +func removeFromEnv(name string) func(*schema.ImageManifest) error { + return func(s *schema.ImageManifest) error { if s.App == nil { - return + return nil } + foundOne := false for i := len(s.App.Environment) - 1; i >= 0; i-- { if s.App.Environment[i].Name == name { + foundOne = true s.App.Environment = append( s.App.Environment[:i], s.App.Environment[i+1:]...) } } + if !foundOne { + return ErrNotFound + } + return nil } } @@ -49,11 +55,12 @@ func (a *ACBuild) AddEnv(name, value string) (err error) { } }() - fn := func(s *schema.ImageManifest) { + fn := func(s *schema.ImageManifest) error { if s.App == nil { s.App = &types.App{} } s.App.Environment.Set(name, value) + return nil } return util.ModifyManifest(fn, a.CurrentACIPath) } diff --git a/lib/isolator.go b/lib/isolator.go index 27a5b232..a2cd0bc7 100644 --- a/lib/isolator.go +++ b/lib/isolator.go @@ -23,18 +23,24 @@ import ( "github.com/appc/acbuild/Godeps/_workspace/src/github.com/appc/spec/schema/types" ) -func removeIsolatorFromMan(name types.ACIdentifier) func(*schema.ImageManifest) { - return func(s *schema.ImageManifest) { +func removeIsolatorFromMan(name types.ACIdentifier) func(*schema.ImageManifest) error { + return func(s *schema.ImageManifest) error { if s.App == nil { - return + return nil } + foundOne := false for i := len(s.App.Isolators) - 1; i >= 0; i-- { if s.App.Isolators[i].Name == name { + foundOne = true s.App.Isolators = append( s.App.Isolators[:i], s.App.Isolators[i+1:]...) } } + if !foundOne { + return ErrNotFound + } + return nil } } @@ -54,13 +60,14 @@ func (a *ACBuild) AddIsolator(name string, value []byte) (err error) { } rawMsg := json.RawMessage(value) - fn := func(s *schema.ImageManifest) { + fn := func(s *schema.ImageManifest) error { removeIsolatorFromMan(*acid)(s) s.App.Isolators = append(s.App.Isolators, types.Isolator{ Name: *acid, ValueRaw: &rawMsg, }) + return nil } return util.ModifyManifest(fn, a.CurrentACIPath) } diff --git a/lib/label.go b/lib/label.go index 639f2539..cedd8980 100644 --- a/lib/label.go +++ b/lib/label.go @@ -21,15 +21,21 @@ import ( "github.com/appc/acbuild/Godeps/_workspace/src/github.com/appc/spec/schema/types" ) -func removeLabelFromMan(name types.ACIdentifier) func(*schema.ImageManifest) { - return func(s *schema.ImageManifest) { +func removeLabelFromMan(name types.ACIdentifier) func(*schema.ImageManifest) error { + return func(s *schema.ImageManifest) error { + foundOne := false for i := len(s.Labels) - 1; i >= 0; i-- { if s.Labels[i].Name == name { + foundOne = true s.Labels = append( s.Labels[:i], s.Labels[i+1:]...) } } + if !foundOne { + return ErrNotFound + } + return nil } } @@ -51,13 +57,14 @@ func (a *ACBuild) AddLabel(name, value string) (err error) { return err } - fn := func(s *schema.ImageManifest) { + fn := func(s *schema.ImageManifest) error { removeLabelFromMan(*acid)(s) s.Labels = append(s.Labels, types.Label{ Name: *acid, Value: value, }) + return nil } return util.ModifyManifest(fn, a.CurrentACIPath) } diff --git a/lib/mounts.go b/lib/mounts.go index b78b8a2a..ecb219a0 100644 --- a/lib/mounts.go +++ b/lib/mounts.go @@ -21,18 +21,24 @@ import ( "github.com/appc/acbuild/Godeps/_workspace/src/github.com/appc/spec/schema/types" ) -func removeMount(name types.ACName) func(*schema.ImageManifest) { - return func(s *schema.ImageManifest) { +func removeMount(name types.ACName) func(*schema.ImageManifest) error { + return func(s *schema.ImageManifest) error { if s.App == nil { - return + return nil } + foundOne := false for i := len(s.App.MountPoints) - 1; i >= 0; i-- { if s.App.MountPoints[i].Name == name { + foundOne = true s.App.MountPoints = append( s.App.MountPoints[:i], s.App.MountPoints[i+1:]...) } } + if !foundOne { + return ErrNotFound + } + return nil } } @@ -55,7 +61,7 @@ func (a *ACBuild) AddMount(name, path string, readOnly bool) (err error) { return err } - fn := func(s *schema.ImageManifest) { + fn := func(s *schema.ImageManifest) error { removeMount(*acn)(s) if s.App == nil { s.App = &types.App{} @@ -66,6 +72,7 @@ func (a *ACBuild) AddMount(name, path string, readOnly bool) (err error) { Path: path, ReadOnly: readOnly, }) + return nil } return util.ModifyManifest(fn, a.CurrentACIPath) } diff --git a/lib/port.go b/lib/port.go index f12af176..7b843cca 100644 --- a/lib/port.go +++ b/lib/port.go @@ -21,18 +21,24 @@ import ( "github.com/appc/acbuild/Godeps/_workspace/src/github.com/appc/spec/schema/types" ) -func removePort(name types.ACName) func(*schema.ImageManifest) { - return func(s *schema.ImageManifest) { +func removePort(name types.ACName) func(*schema.ImageManifest) error { + return func(s *schema.ImageManifest) error { if s.App == nil { - return + return nil } + foundOne := false for i := len(s.App.Ports) - 1; i >= 0; i-- { if s.App.Ports[i].Name == name { + foundOne = true s.App.Ports = append( s.App.Ports[:i], s.App.Ports[i+1:]...) } } + if !foundOne { + return ErrNotFound + } + return nil } } @@ -55,7 +61,7 @@ func (a *ACBuild) AddPort(name, protocol string, port, count uint, socketActivat return err } - fn := func(s *schema.ImageManifest) { + fn := func(s *schema.ImageManifest) error { removePort(*acn)(s) if s.App == nil { s.App = &types.App{} @@ -68,6 +74,7 @@ func (a *ACBuild) AddPort(name, protocol string, port, count uint, socketActivat Count: count, SocketActivated: socketActivated, }) + return nil } return util.ModifyManifest(fn, a.CurrentACIPath) } diff --git a/lib/set-exec.go b/lib/set-exec.go index 4f50f3be..80b64daf 100644 --- a/lib/set-exec.go +++ b/lib/set-exec.go @@ -33,11 +33,12 @@ func (a *ACBuild) SetExec(cmd []string) (err error) { } }() - fn := func(s *schema.ImageManifest) { + fn := func(s *schema.ImageManifest) error { if s.App == nil { s.App = &types.App{} } s.App.Exec = cmd + return nil } return util.ModifyManifest(fn, a.CurrentACIPath) } diff --git a/lib/set-group.go b/lib/set-group.go index 1fd01b4e..a6ad9866 100644 --- a/lib/set-group.go +++ b/lib/set-group.go @@ -38,11 +38,12 @@ func (a *ACBuild) SetGroup(group string) (err error) { if group == "" { return fmt.Errorf("group cannot be empty") } - fn := func(s *schema.ImageManifest) { + fn := func(s *schema.ImageManifest) error { if s.App == nil { s.App = &types.App{} } s.App.Group = group + return nil } return util.ModifyManifest(fn, a.CurrentACIPath) } diff --git a/lib/set-name.go b/lib/set-name.go index bfc87b8c..c6d0f821 100644 --- a/lib/set-name.go +++ b/lib/set-name.go @@ -42,8 +42,9 @@ func (a *ACBuild) SetName(name string) (err error) { return err } - fn := func(s *schema.ImageManifest) { + fn := func(s *schema.ImageManifest) error { s.Name = *acid + return nil } return util.ModifyManifest(fn, a.CurrentACIPath) } diff --git a/lib/set-user.go b/lib/set-user.go index a3132f11..50eaa6bc 100644 --- a/lib/set-user.go +++ b/lib/set-user.go @@ -38,11 +38,12 @@ func (a *ACBuild) SetUser(user string) (err error) { if user == "" { return fmt.Errorf("user cannot be empty") } - fn := func(s *schema.ImageManifest) { + fn := func(s *schema.ImageManifest) error { if s.App == nil { s.App = &types.App{} } s.App.User = user + return nil } return util.ModifyManifest(fn, a.CurrentACIPath) } diff --git a/test b/test new file mode 100755 index 00000000..2153a5db --- /dev/null +++ b/test @@ -0,0 +1,13 @@ +#!/usr/bin/env bash +set -e + +ORG_PATH="github.com/appc" +REPO_PATH="${ORG_PATH}/acbuild" +TESTS_PATH="${REPO_PATH}/tests" + +export ACBUILD_BIN="${PWD}/bin/acbuild" +export GOPATH="${PWD}/gopath" + +source "${GOPATH}/src/${REPO_PATH}/build" + +go test "${TESTS_PATH}" "$@" diff --git a/tests/annotation_test.go b/tests/annotation_test.go new file mode 100644 index 00000000..805d908f --- /dev/null +++ b/tests/annotation_test.go @@ -0,0 +1,137 @@ +// Copyright 2015 The appc Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package tests + +import ( + "testing" + + "github.com/appc/acbuild/Godeps/_workspace/src/github.com/appc/spec/schema" + "github.com/appc/acbuild/Godeps/_workspace/src/github.com/appc/spec/schema/types" +) + +const ( + annoName = "authors" + annoValue = "the acbuild developers" +) + +var manWithOneAnno = schema.ImageManifest{ + ACKind: schema.ImageManifestKind, + ACVersion: schema.AppContainerVersion, + Name: *types.MustACIdentifier("acbuild-unnamed"), + App: &types.App{ + Exec: nil, + User: "0", + Group: "0", + }, + Annotations: types.Annotations{ + types.Annotation{ + Name: *types.MustACIdentifier(annoName), + Value: annoValue, + }, + }, + Labels: systemLabels, +} + +func TestAddAnnotation(t *testing.T) { + workingDir := setUpTest(t) + defer cleanUpTest(workingDir) + + err := runACBuild(workingDir, "annotation", "add", annoName, annoValue) + if err != nil { + t.Fatalf("%v\n", err) + } + + checkManifest(t, workingDir, manWithOneAnno) + checkEmptyRootfs(t, workingDir) +} + +func TestAddRmAnnotation(t *testing.T) { + workingDir := setUpTest(t) + defer cleanUpTest(workingDir) + + err := runACBuild(workingDir, "annotation", "add", annoName, annoValue) + if err != nil { + t.Fatalf("%v\n", err) + } + + err = runACBuild(workingDir, "annotation", "remove", annoName) + if err != nil { + t.Fatalf("%v\n", err) + } + + checkManifest(t, workingDir, emptyManifest) + checkEmptyRootfs(t, workingDir) +} + +func TestAddOverwriteAnnotation(t *testing.T) { + workingDir := setUpTest(t) + defer cleanUpTest(workingDir) + + err := runACBuild(workingDir, "annotation", "add", annoName, annoValue) + if err != nil { + t.Fatalf("%v\n", err) + } + + err = runACBuild(workingDir, "annotation", "add", annoName, annoValue) + if err != nil { + t.Fatalf("%v\n", err) + } + + checkManifest(t, workingDir, manWithOneAnno) + checkEmptyRootfs(t, workingDir) +} + +func TestRmNonexistentAnnotation(t *testing.T) { + workingDir := setUpTest(t) + defer cleanUpTest(workingDir) + + err := runACBuild(workingDir, "annotation", "remove", annoName) + switch { + case err == nil: + t.Fatalf("annotation remove didn't return an error when asked to remove nonexistent annotation") + case err.exitCode == 2: + return + default: + t.Fatalf("error occurred when running annotation remove:\n%v", err) + } + + checkManifest(t, workingDir, emptyManifest) + checkEmptyRootfs(t, workingDir) +} + +func TestAddAddRmAnnotation(t *testing.T) { + workingDir := setUpTest(t) + defer cleanUpTest(workingDir) + + const suffix = "1" + + err := runACBuild(workingDir, "annotation", "add", annoName+suffix, annoValue) + if err != nil { + t.Fatalf("%v\n", err) + } + + err = runACBuild(workingDir, "annotation", "add", annoName, annoValue) + if err != nil { + t.Fatalf("%v\n", err) + } + + err = runACBuild(workingDir, "annotation", "remove", annoName+suffix) + if err != nil { + t.Fatalf("%v\n", err) + } + + checkManifest(t, workingDir, manWithOneAnno) + checkEmptyRootfs(t, workingDir) +} diff --git a/tests/begin_test.go b/tests/begin_test.go index 7aa73961..7a30e1df 100644 --- a/tests/begin_test.go +++ b/tests/begin_test.go @@ -25,43 +25,27 @@ import ( "github.com/appc/acbuild/Godeps/_workspace/src/github.com/appc/spec/aci" "github.com/appc/acbuild/Godeps/_workspace/src/github.com/appc/spec/schema" "github.com/appc/acbuild/Godeps/_workspace/src/github.com/appc/spec/schema/types" - "github.com/kylelemons/godebug/pretty" ) func TestBeginEmpty(t *testing.T) { - tmpdir := mustTempDir() - defer cleanUpTest(tmpdir) + workingDir := mustTempDir() + defer cleanUpTest(workingDir) - err := runAcbuild(tmpdir, "begin") + err := runACBuild(workingDir, "begin") if err != nil { - t.Fatalf("%v", err) + t.Fatalf("%v\n", err) } - wim := schema.ImageManifest{ - ACKind: schema.ImageManifestKind, - ACVersion: schema.AppContainerVersion, - Name: *types.MustACIdentifier("acbuild-unnamed"), - App: &types.App{ - Exec: nil, - User: "0", - Group: "0", - }, - } - - checkMinimalContainer(t, path.Join(tmpdir, ".acbuild", "currentaci"), wim) + checkManifest(t, workingDir, emptyManifest) + checkEmptyRootfs(t, workingDir) } func TestBeginLocalACI(t *testing.T) { - wim := schema.ImageManifest{ + wantedManifest := schema.ImageManifest{ ACKind: schema.ImageManifestKind, ACVersion: schema.AppContainerVersion, Name: *types.MustACIdentifier("acbuild-begin-test"), - Labels: types.Labels{ - types.Label{ - Name: *types.MustACIdentifier("version"), - Value: "9001", - }, - }, + Labels: systemLabels, App: &types.App{ Exec: types.Exec{"/bin/nethack4", "-D", "wizard"}, User: "0", @@ -101,7 +85,7 @@ func TestBeginLocalACI(t *testing.T) { }, } - manblob, err := wim.MarshalJSON() + manblob, err := wantedManifest.MarshalJSON() if err != nil { panic(err) } @@ -125,7 +109,7 @@ func TestBeginLocalACI(t *testing.T) { } defer os.RemoveAll(tmpaci.Name()) - aw := aci.NewImageWriter(wim, tar.NewWriter(tmpaci)) + aw := aci.NewImageWriter(wantedManifest, tar.NewWriter(tmpaci)) err = filepath.Walk(tmpexpandedaci, aci.BuildWalker(tmpexpandedaci, aw, nil)) aw.Close() if err != nil { @@ -133,41 +117,14 @@ func TestBeginLocalACI(t *testing.T) { } tmpaci.Close() - tmpdir := mustTempDir() - defer cleanUpTest(tmpdir) + workingDir := mustTempDir() + defer cleanUpTest(workingDir) - err = runAcbuild(tmpdir, "begin", tmpaci.Name()) - if err != nil { - t.Fatalf("%v", err) + err1 := runACBuild(workingDir, "begin", tmpaci.Name()) + if err1 != nil { + t.Fatalf("%s\n", err1.Error()) } - checkMinimalContainer(t, path.Join(tmpdir, ".acbuild", "currentaci"), wim) -} - -func checkMinimalContainer(t *testing.T, acipath string, expectedManifest schema.ImageManifest) { - // Check that there are no files in the rootfs - files, err := ioutil.ReadDir(path.Join(acipath, aci.RootfsDir)) - if err != nil { - t.Errorf("%v", err) - } - if len(files) != 0 { - t.Errorf("rootfs in aci contains files, should be empty") - } - - // Check that the manifest is no bigger than it needs to be - manblob, err := ioutil.ReadFile(path.Join(acipath, aci.ManifestFile)) - if err != nil { - t.Errorf("%v", err) - } - - var man schema.ImageManifest - - err = man.UnmarshalJSON(manblob) - if err != nil { - t.Errorf("invalid manifest schema: %v", err) - } - - if str := pretty.Compare(man, expectedManifest); str != "" { - t.Errorf("unexpected manifest:\n%s", str) - } + checkManifest(t, workingDir, wantedManifest) + checkEmptyRootfs(t, workingDir) } diff --git a/tests/common.go b/tests/common.go index e184a0ad..87c3f504 100644 --- a/tests/common.go +++ b/tests/common.go @@ -16,13 +16,46 @@ package tests import ( "fmt" + "io" "io/ioutil" "os" "os/exec" + "path" + "runtime" + "syscall" + "testing" + + "github.com/appc/acbuild/Godeps/_workspace/src/github.com/appc/spec/aci" + "github.com/appc/acbuild/Godeps/_workspace/src/github.com/appc/spec/schema" + "github.com/appc/acbuild/Godeps/_workspace/src/github.com/appc/spec/schema/types" + "github.com/appc/acbuild/Godeps/_workspace/src/github.com/kylelemons/godebug/pretty" ) var ( acbuildBinPath string + + systemLabels = types.Labels{ + types.Label{ + *types.MustACIdentifier("arch"), + runtime.GOARCH, + }, + types.Label{ + *types.MustACIdentifier("os"), + runtime.GOOS, + }, + } + + emptyManifest = schema.ImageManifest{ + ACKind: schema.ImageManifestKind, + ACVersion: schema.AppContainerVersion, + Name: *types.MustACIdentifier("acbuild-unnamed"), + App: &types.App{ + Exec: nil, + User: "0", + Group: "0", + }, + Labels: systemLabels, + } ) func init() { @@ -36,20 +69,58 @@ func init() { } } -func runAcbuild(workingDir string, args ...string) error { +type acbuildError struct { + err *exec.ExitError + exitCode int + stdout []byte + stderr []byte +} + +func (ae acbuildError) Error() string { + return fmt.Sprintf("non-zero exit code of %d: %v\nstdout:\n%s\nstderr:\n%s", ae.exitCode, ae.err, string(ae.stdout), string(ae.stderr)) +} + +func runACBuild(workingDir string, args ...string) *acbuildError { cmd := exec.Command(acbuildBinPath, args...) - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr cmd.Dir = workingDir - return cmd.Run() + stdoutpipe, err := cmd.StdoutPipe() + if err != nil { + panic(err) + } + stderrpipe, err := cmd.StderrPipe() + if err != nil { + panic(err) + } + stdoutch := make(chan []byte) + stderrch := make(chan []byte) + readTillClosed := func(in io.ReadCloser, out chan []byte) { + msg, err := ioutil.ReadAll(in) + if err != nil { + panic(err) + } + out <- msg + } + go readTillClosed(stdoutpipe, stdoutch) + go readTillClosed(stderrpipe, stderrch) + err = cmd.Run() + stdout := <-stdoutch + stderr := <-stderrch + if exitErr, ok := err.(*exec.ExitError); ok { + code := exitErr.Sys().(syscall.WaitStatus).ExitStatus() + return &acbuildError{exitErr, code, stdout, stderr} + } + if err != nil { + panic(err) + } + return nil } -func setUpTest() string { +func setUpTest(t *testing.T) string { tmpdir := mustTempDir() - err := runAcbuild(tmpdir, "begin") + err := runACBuild(tmpdir, "begin") if err != nil { - panic(err) + t.Fatalf("%v\n", err) } return tmpdir @@ -66,3 +137,33 @@ func mustTempDir() string { } return dir } + +func checkManifest(t *testing.T, workingDir string, wantedManifest schema.ImageManifest) { + acipath := path.Join(workingDir, ".acbuild", "currentaci") + + manblob, err := ioutil.ReadFile(path.Join(acipath, aci.ManifestFile)) + if err != nil { + panic(err) + } + + var man schema.ImageManifest + + err = man.UnmarshalJSON(manblob) + if err != nil { + t.Errorf("invalid manifest schema: %v", err) + } + + if str := pretty.Compare(man, wantedManifest); str != "" { + t.Errorf("unexpected manifest:\n%s", str) + } +} + +func checkEmptyRootfs(t *testing.T, workingDir string) { + files, err := ioutil.ReadDir(path.Join(workingDir, ".acbuild", "currentaci", aci.RootfsDir)) + if err != nil { + t.Errorf("%v", err) + } + if len(files) != 0 { + t.Errorf("rootfs in aci contains files, should be empty") + } +} diff --git a/tests/dependency_test.go b/tests/dependency_test.go new file mode 100644 index 00000000..9390fe82 --- /dev/null +++ b/tests/dependency_test.go @@ -0,0 +1,298 @@ +// Copyright 2015 The appc Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package tests + +import ( + "strconv" + "testing" + + "github.com/appc/acbuild/Godeps/_workspace/src/github.com/appc/spec/schema" + "github.com/appc/acbuild/Godeps/_workspace/src/github.com/appc/spec/schema/types" +) + +const ( + depName = "example.com/app" + depImageID = "sha512-739d7ae77d9e" + depLabel1Key = "version" + depLabel1Val = "2" + depLabel2Key = "env" + depLabel2Val = "canary" + depSize uint = 9823749 + + depName2 = "example.com/app2" +) + +func newLabel(key, val string) string { + return key + "=" + val +} + +func manWithDeps(deps types.Dependencies) schema.ImageManifest { + return schema.ImageManifest{ + ACKind: schema.ImageManifestKind, + ACVersion: schema.AppContainerVersion, + Name: *types.MustACIdentifier("acbuild-unnamed"), + App: &types.App{ + Exec: nil, + User: "0", + Group: "0", + }, + Dependencies: deps, + Labels: systemLabels, + } +} + +func TestAddDependency(t *testing.T) { + workingDir := setUpTest(t) + defer cleanUpTest(workingDir) + + err := runACBuild(workingDir, "dependency", "add", depName) + if err != nil { + t.Fatalf("%v\n", err) + } + + deps := types.Dependencies{ + types.Dependency{ + ImageName: *types.MustACIdentifier(depName), + }, + } + + checkManifest(t, workingDir, manWithDeps(deps)) + checkEmptyRootfs(t, workingDir) +} + +func TestAddDependencyWithImageID(t *testing.T) { + workingDir := setUpTest(t) + defer cleanUpTest(workingDir) + + err := runACBuild(workingDir, "dependency", "add", depName, "--image-id", depImageID) + if err != nil { + t.Fatalf("%v\n", err) + } + + hash, err1 := types.NewHash(depImageID) + if err1 != nil { + panic(err1) + } + + deps := types.Dependencies{ + types.Dependency{ + ImageName: *types.MustACIdentifier(depName), + ImageID: hash, + }, + } + + checkManifest(t, workingDir, manWithDeps(deps)) + checkEmptyRootfs(t, workingDir) +} + +func TestAddDependencyWithLabels(t *testing.T) { + workingDir := setUpTest(t) + defer cleanUpTest(workingDir) + + err := runACBuild(workingDir, "dependency", "add", depName, + "--label", newLabel(depLabel1Key, depLabel1Val), + "--label", newLabel(depLabel2Key, depLabel2Val)) + if err != nil { + t.Fatalf("%v\n", err) + } + + deps := types.Dependencies{ + types.Dependency{ + ImageName: *types.MustACIdentifier(depName), + Labels: types.Labels{ + types.Label{ + Name: *types.MustACIdentifier(depLabel1Key), + Value: depLabel1Val, + }, + types.Label{ + Name: *types.MustACIdentifier(depLabel2Key), + Value: depLabel2Val, + }, + }, + }, + } + + checkManifest(t, workingDir, manWithDeps(deps)) + checkEmptyRootfs(t, workingDir) +} + +func TestAddDependencyWithSize(t *testing.T) { + workingDir := setUpTest(t) + defer cleanUpTest(workingDir) + + err := runACBuild(workingDir, "dependency", "add", depName, "--size", strconv.Itoa(int(depSize))) + if err != nil { + t.Fatalf("%v\n", err) + } + + deps := types.Dependencies{ + types.Dependency{ + ImageName: *types.MustACIdentifier(depName), + Size: depSize, + }, + } + + checkManifest(t, workingDir, manWithDeps(deps)) + checkEmptyRootfs(t, workingDir) +} + +func TestAdd2Dependencies(t *testing.T) { + workingDir := setUpTest(t) + defer cleanUpTest(workingDir) + + err := runACBuild(workingDir, "dependency", "add", depName, + "--image-id", depImageID, + "--label", newLabel(depLabel1Key, depLabel1Val), + "--label", newLabel(depLabel2Key, depLabel2Val), + "--size", strconv.Itoa(int(depSize))) + if err != nil { + t.Fatalf("%v\n", err) + } + + err = runACBuild(workingDir, "dependency", "add", depName2) + if err != nil { + t.Fatalf("%v\n", err) + } + + hash, err1 := types.NewHash(depImageID) + if err1 != nil { + panic(err1) + } + + deps := types.Dependencies{ + types.Dependency{ + ImageName: *types.MustACIdentifier(depName), + ImageID: hash, + Labels: types.Labels{ + types.Label{ + Name: *types.MustACIdentifier(depLabel1Key), + Value: depLabel1Val, + }, + types.Label{ + Name: *types.MustACIdentifier(depLabel2Key), + Value: depLabel2Val, + }, + }, + Size: depSize, + }, + types.Dependency{ + ImageName: *types.MustACIdentifier(depName2), + }, + } + + checkManifest(t, workingDir, manWithDeps(deps)) + checkEmptyRootfs(t, workingDir) +} + +func TestAddAddRmDependencies(t *testing.T) { + workingDir := setUpTest(t) + defer cleanUpTest(workingDir) + + err := runACBuild(workingDir, "dependency", "add", depName, + "--image-id", depImageID, + "--label", newLabel(depLabel1Key, depLabel1Val), + "--label", newLabel(depLabel2Key, depLabel2Val), + "--size", strconv.Itoa(int(depSize))) + if err != nil { + t.Fatalf("%v\n", err) + } + + err = runACBuild(workingDir, "dependency", "add", depName2) + if err != nil { + t.Fatalf("%v\n", err) + } + + err = runACBuild(workingDir, "dependency", "remove", depName) + if err != nil { + t.Fatalf("%v\n", err) + } + + deps := types.Dependencies{ + types.Dependency{ + ImageName: *types.MustACIdentifier(depName2), + }, + } + + checkManifest(t, workingDir, manWithDeps(deps)) + checkEmptyRootfs(t, workingDir) +} + +func TestAddRmDependencie(t *testing.T) { + workingDir := setUpTest(t) + defer cleanUpTest(workingDir) + + err := runACBuild(workingDir, "dependency", "add", depName, + "--image-id", depImageID, + "--label", newLabel(depLabel1Key, depLabel1Val), + "--label", newLabel(depLabel2Key, depLabel2Val), + "--size", strconv.Itoa(int(depSize))) + if err != nil { + t.Fatalf("%v\n", err) + } + + err = runACBuild(workingDir, "dependency", "remove", depName) + if err != nil { + t.Fatalf("%v\n", err) + } + + checkManifest(t, workingDir, emptyManifest) + checkEmptyRootfs(t, workingDir) +} + +func TestOverwriteDependency(t *testing.T) { + workingDir := setUpTest(t) + defer cleanUpTest(workingDir) + + err := runACBuild(workingDir, "dependency", "add", depName, + "--image-id", depImageID, + "--label", newLabel(depLabel1Key, depLabel1Val), + "--label", newLabel(depLabel2Key, depLabel2Val), + "--size", strconv.Itoa(int(depSize))) + if err != nil { + t.Fatalf("%v\n", err) + } + + err = runACBuild(workingDir, "dependency", "add", depName) + if err != nil { + t.Fatalf("%v\n", err) + } + + deps := types.Dependencies{ + types.Dependency{ + ImageName: *types.MustACIdentifier(depName), + }, + } + + checkManifest(t, workingDir, manWithDeps(deps)) + checkEmptyRootfs(t, workingDir) +} + +func TestRmNonexistentDependency(t *testing.T) { + workingDir := setUpTest(t) + defer cleanUpTest(workingDir) + + err := runACBuild(workingDir, "dependency", "remove", depName) + switch { + case err == nil: + t.Fatalf("dependency remove didn't return an error when asked to remove nonexistent dependency") + case err.exitCode == 2: + return + default: + t.Fatalf("error occurred when running dependency remove:\n%v", err) + } + + checkManifest(t, workingDir, emptyManifest) + checkEmptyRootfs(t, workingDir) +} diff --git a/tests/end_test.go b/tests/end_test.go new file mode 100644 index 00000000..9bf90dea --- /dev/null +++ b/tests/end_test.go @@ -0,0 +1,41 @@ +// Copyright 2015 The appc Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package tests + +import ( + "os" + "path" + "testing" +) + +func TestEnd(t *testing.T) { + workingDir := setUpTest(t) + defer cleanUpTest(workingDir) + + err := runACBuild(workingDir, "end") + if err != nil { + t.Fatalf("%v\n", err) + } + + _, err1 := os.Stat(path.Join(workingDir, ".acbuild")) + switch { + case os.IsNotExist(err1): + return + case err1 != nil: + panic(err1) + default: + t.Fatalf("end failed to remove acbuild working directory") + } +} diff --git a/tests/environment_test.go b/tests/environment_test.go new file mode 100644 index 00000000..aee0ca99 --- /dev/null +++ b/tests/environment_test.go @@ -0,0 +1,185 @@ +// Copyright 2015 The appc Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package tests + +import ( + "testing" + + "github.com/appc/acbuild/Godeps/_workspace/src/github.com/appc/spec/schema" + "github.com/appc/acbuild/Godeps/_workspace/src/github.com/appc/spec/schema/types" +) + +const ( + envName = "FOO" + envVal = "BAR" + + envName2 = "BOO" + envVal2 = "FAR" +) + +func manWithEnv(env types.Environment) schema.ImageManifest { + return schema.ImageManifest{ + ACKind: schema.ImageManifestKind, + ACVersion: schema.AppContainerVersion, + Name: *types.MustACIdentifier("acbuild-unnamed"), + App: &types.App{ + Exec: nil, + User: "0", + Group: "0", + Environment: env, + }, + Labels: systemLabels, + } +} + +func TestAddEnv(t *testing.T) { + workingDir := setUpTest(t) + defer cleanUpTest(workingDir) + + err := runACBuild(workingDir, "environment", "add", envName, envVal) + if err != nil { + t.Fatalf("%v\n", err) + } + + env := types.Environment{ + types.EnvironmentVariable{ + Name: envName, + Value: envVal, + }, + } + + checkManifest(t, workingDir, manWithEnv(env)) + checkEmptyRootfs(t, workingDir) +} + +func TestAddTwoEnv(t *testing.T) { + workingDir := setUpTest(t) + defer cleanUpTest(workingDir) + + err := runACBuild(workingDir, "environment", "add", envName, envVal) + if err != nil { + t.Fatalf("%v\n", err) + } + + err = runACBuild(workingDir, "environment", "add", envName2, envVal2) + if err != nil { + t.Fatalf("%v\n", err) + } + + env := types.Environment{ + types.EnvironmentVariable{ + Name: envName, + Value: envVal, + }, + types.EnvironmentVariable{ + Name: envName2, + Value: envVal2, + }, + } + + checkManifest(t, workingDir, manWithEnv(env)) + checkEmptyRootfs(t, workingDir) +} + +func TestAddAddRmEnv(t *testing.T) { + workingDir := setUpTest(t) + defer cleanUpTest(workingDir) + + err := runACBuild(workingDir, "environment", "add", envName, envVal) + if err != nil { + t.Fatalf("%v\n", err) + } + + err = runACBuild(workingDir, "environment", "add", envName2, envVal2) + if err != nil { + t.Fatalf("%v\n", err) + } + + err = runACBuild(workingDir, "environment", "rm", envName) + if err != nil { + t.Fatalf("%v\n", err) + } + + env := types.Environment{ + types.EnvironmentVariable{ + Name: envName2, + Value: envVal2, + }, + } + + checkManifest(t, workingDir, manWithEnv(env)) + checkEmptyRootfs(t, workingDir) +} + +func TestAddOverwriteEnv(t *testing.T) { + workingDir := setUpTest(t) + defer cleanUpTest(workingDir) + + err := runACBuild(workingDir, "environment", "add", envName, envVal) + if err != nil { + t.Fatalf("%v\n", err) + } + + err = runACBuild(workingDir, "environment", "add", envName, envVal2) + if err != nil { + t.Fatalf("%v\n", err) + } + + env := types.Environment{ + types.EnvironmentVariable{ + Name: envName, + Value: envVal2, + }, + } + + checkManifest(t, workingDir, manWithEnv(env)) + checkEmptyRootfs(t, workingDir) +} + +func TestAddRmEnv(t *testing.T) { + workingDir := setUpTest(t) + defer cleanUpTest(workingDir) + + err := runACBuild(workingDir, "environment", "add", envName, envVal) + if err != nil { + t.Fatalf("%v\n", err) + } + + err = runACBuild(workingDir, "environment", "rm", envName) + if err != nil { + t.Fatalf("%v\n", err) + } + + checkManifest(t, workingDir, emptyManifest) + checkEmptyRootfs(t, workingDir) +} + +func TestRmNonexistentEnv(t *testing.T) { + workingDir := setUpTest(t) + defer cleanUpTest(workingDir) + + err := runACBuild(workingDir, "environment", "rm", envName) + switch { + case err == nil: + t.Fatalf("environment remove didn't return an error when asked to remove nonexistent environment variable") + case err.exitCode == 2: + return + default: + t.Fatalf("error occurred when running environment remove:\n%v", err) + } + + checkManifest(t, workingDir, emptyManifest) + checkEmptyRootfs(t, workingDir) +} diff --git a/tests/label_test.go b/tests/label_test.go new file mode 100644 index 00000000..1ff9d2c2 --- /dev/null +++ b/tests/label_test.go @@ -0,0 +1,184 @@ +// Copyright 2015 The appc Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package tests + +import ( + "testing" + + "github.com/appc/acbuild/Godeps/_workspace/src/github.com/appc/spec/schema" + "github.com/appc/acbuild/Godeps/_workspace/src/github.com/appc/spec/schema/types" +) + +const ( + labelName = "version" + labelVal = "1.0.0" + + labelName2 = "foo" + labelVal2 = "bar" +) + +func manWithLabels(labels types.Labels) schema.ImageManifest { + return schema.ImageManifest{ + ACKind: schema.ImageManifestKind, + ACVersion: schema.AppContainerVersion, + Name: *types.MustACIdentifier("acbuild-unnamed"), + App: &types.App{ + Exec: nil, + User: "0", + Group: "0", + }, + Labels: append(systemLabels, labels...), + } +} + +func TestAddLabel(t *testing.T) { + workingDir := setUpTest(t) + defer cleanUpTest(workingDir) + + err := runACBuild(workingDir, "label", "add", labelName, labelVal) + if err != nil { + t.Fatalf("%v\n", err) + } + + labels := types.Labels{ + types.Label{ + Name: *types.MustACIdentifier(labelName), + Value: labelVal, + }, + } + + checkManifest(t, workingDir, manWithLabels(labels)) + checkEmptyRootfs(t, workingDir) +} + +func TestAddTwoLabels(t *testing.T) { + workingDir := setUpTest(t) + defer cleanUpTest(workingDir) + + err := runACBuild(workingDir, "label", "add", labelName, labelVal) + if err != nil { + t.Fatalf("%v\n", err) + } + + err = runACBuild(workingDir, "label", "add", labelName2, labelVal2) + if err != nil { + t.Fatalf("%v\n", err) + } + + labels := types.Labels{ + types.Label{ + Name: *types.MustACIdentifier(labelName), + Value: labelVal, + }, + types.Label{ + Name: *types.MustACIdentifier(labelName2), + Value: labelVal2, + }, + } + + checkManifest(t, workingDir, manWithLabels(labels)) + checkEmptyRootfs(t, workingDir) +} + +func TestAddAddRmLabels(t *testing.T) { + workingDir := setUpTest(t) + defer cleanUpTest(workingDir) + + err := runACBuild(workingDir, "label", "add", labelName, labelVal) + if err != nil { + t.Fatalf("%v\n", err) + } + + err = runACBuild(workingDir, "label", "add", labelName2, labelVal2) + if err != nil { + t.Fatalf("%v\n", err) + } + + err = runACBuild(workingDir, "label", "rm", labelName) + if err != nil { + t.Fatalf("%v\n", err) + } + + labels := types.Labels{ + types.Label{ + Name: *types.MustACIdentifier(labelName2), + Value: labelVal2, + }, + } + + checkManifest(t, workingDir, manWithLabels(labels)) + checkEmptyRootfs(t, workingDir) +} + +func TestAddOverwriteLabel(t *testing.T) { + workingDir := setUpTest(t) + defer cleanUpTest(workingDir) + + err := runACBuild(workingDir, "label", "add", labelName, labelVal) + if err != nil { + t.Fatalf("%v\n", err) + } + + err = runACBuild(workingDir, "label", "add", labelName, labelVal2) + if err != nil { + t.Fatalf("%v\n", err) + } + + labels := types.Labels{ + types.Label{ + Name: *types.MustACIdentifier(labelName), + Value: labelVal2, + }, + } + + checkManifest(t, workingDir, manWithLabels(labels)) + checkEmptyRootfs(t, workingDir) +} + +func TestAddRmLabel(t *testing.T) { + workingDir := setUpTest(t) + defer cleanUpTest(workingDir) + + err := runACBuild(workingDir, "label", "add", labelName, labelVal) + if err != nil { + t.Fatalf("%v\n", err) + } + + err = runACBuild(workingDir, "label", "rm", labelName) + if err != nil { + t.Fatalf("%v\n", err) + } + + checkManifest(t, workingDir, emptyManifest) + checkEmptyRootfs(t, workingDir) +} + +func TestRmNonexistentLabel(t *testing.T) { + workingDir := setUpTest(t) + defer cleanUpTest(workingDir) + + err := runACBuild(workingDir, "label", "rm", labelName) + switch { + case err == nil: + t.Fatalf("label remove didn't return an error when asked to remove nonexistent label") + case err.exitCode == 2: + return + default: + t.Fatalf("error occurred when running label remove:\n%v", err) + } + + checkManifest(t, workingDir, emptyManifest) + checkEmptyRootfs(t, workingDir) +} diff --git a/tests/mount_test.go b/tests/mount_test.go new file mode 100644 index 00000000..11693967 --- /dev/null +++ b/tests/mount_test.go @@ -0,0 +1,207 @@ +// Copyright 2015 The appc Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package tests + +import ( + "testing" + + "github.com/appc/acbuild/Godeps/_workspace/src/github.com/appc/spec/schema" + "github.com/appc/acbuild/Godeps/_workspace/src/github.com/appc/spec/schema/types" +) + +const ( + mountName = "html" + mountPath = "/usr/html" + + mountName2 = "nethack4-data" + mountPath2 = "/root/nethack4-data" +) + +func manWithMounts(mounts []types.MountPoint) schema.ImageManifest { + return schema.ImageManifest{ + ACKind: schema.ImageManifestKind, + ACVersion: schema.AppContainerVersion, + Name: *types.MustACIdentifier("acbuild-unnamed"), + App: &types.App{ + Exec: nil, + User: "0", + Group: "0", + MountPoints: mounts, + }, + Labels: systemLabels, + } +} + +func TestAddMount(t *testing.T) { + workingDir := setUpTest(t) + defer cleanUpTest(workingDir) + + err := runACBuild(workingDir, "mount", "add", mountName, mountPath) + if err != nil { + t.Fatalf("%v\n", err) + } + + mounts := []types.MountPoint{ + types.MountPoint{ + Name: *types.MustACName(mountName), + Path: mountPath, + }, + } + + checkManifest(t, workingDir, manWithMounts(mounts)) + checkEmptyRootfs(t, workingDir) +} + +func TestAddMountReadOnly(t *testing.T) { + workingDir := setUpTest(t) + defer cleanUpTest(workingDir) + + err := runACBuild(workingDir, "mount", "add", mountName, mountPath, "--read-only") + if err != nil { + t.Fatalf("%v\n", err) + } + + mounts := []types.MountPoint{ + types.MountPoint{ + Name: *types.MustACName(mountName), + Path: mountPath, + ReadOnly: true, + }, + } + + checkManifest(t, workingDir, manWithMounts(mounts)) + checkEmptyRootfs(t, workingDir) +} + +func TestAdd2Mounts(t *testing.T) { + workingDir := setUpTest(t) + defer cleanUpTest(workingDir) + + err := runACBuild(workingDir, "mount", "add", mountName, mountPath, "--read-only") + if err != nil { + t.Fatalf("%v\n", err) + } + + err = runACBuild(workingDir, "mount", "add", mountName2, mountPath2) + if err != nil { + t.Fatalf("%v\n", err) + } + + mounts := []types.MountPoint{ + types.MountPoint{ + Name: *types.MustACName(mountName), + Path: mountPath, + ReadOnly: true, + }, + types.MountPoint{ + Name: *types.MustACName(mountName2), + Path: mountPath2, + }, + } + + checkManifest(t, workingDir, manWithMounts(mounts)) + checkEmptyRootfs(t, workingDir) +} + +func TestAddAddRmMounts(t *testing.T) { + workingDir := setUpTest(t) + defer cleanUpTest(workingDir) + + err := runACBuild(workingDir, "mount", "add", mountName, mountPath, "--read-only") + if err != nil { + t.Fatalf("%v\n", err) + } + + err = runACBuild(workingDir, "mount", "add", mountName2, mountPath2) + if err != nil { + t.Fatalf("%v\n", err) + } + + err = runACBuild(workingDir, "mount", "rm", mountName) + if err != nil { + t.Fatalf("%v\n", err) + } + + mounts := []types.MountPoint{ + types.MountPoint{ + Name: *types.MustACName(mountName2), + Path: mountPath2, + }, + } + + checkManifest(t, workingDir, manWithMounts(mounts)) + checkEmptyRootfs(t, workingDir) +} + +func TestAddRmMount(t *testing.T) { + workingDir := setUpTest(t) + defer cleanUpTest(workingDir) + + err := runACBuild(workingDir, "mount", "add", mountName, mountPath, "--read-only") + if err != nil { + t.Fatalf("%v\n", err) + } + + err = runACBuild(workingDir, "mount", "rm", mountName) + if err != nil { + t.Fatalf("%v\n", err) + } + + checkManifest(t, workingDir, emptyManifest) + checkEmptyRootfs(t, workingDir) +} + +func TestOverwriteMount(t *testing.T) { + workingDir := setUpTest(t) + defer cleanUpTest(workingDir) + + err := runACBuild(workingDir, "mount", "add", mountName, mountPath, "--read-only") + if err != nil { + t.Fatalf("%v\n", err) + } + + err = runACBuild(workingDir, "mount", "add", mountName, mountPath2) + if err != nil { + t.Fatalf("%v\n", err) + } + + mounts := []types.MountPoint{ + types.MountPoint{ + Name: *types.MustACName(mountName), + Path: mountPath2, + }, + } + + checkManifest(t, workingDir, manWithMounts(mounts)) + checkEmptyRootfs(t, workingDir) +} + +func TestRmNonexistentMount(t *testing.T) { + workingDir := setUpTest(t) + defer cleanUpTest(workingDir) + + err := runACBuild(workingDir, "mount", "rm", mountName) + switch { + case err == nil: + t.Fatalf("mount remove didn't return an error when asked to remove nonexistent mount") + case err.exitCode == 2: + return + default: + t.Fatalf("error occurred when running mount remove:\n%v", err) + } + + checkManifest(t, workingDir, emptyManifest) + checkEmptyRootfs(t, workingDir) +} diff --git a/tests/port_test.go b/tests/port_test.go new file mode 100644 index 00000000..77852105 --- /dev/null +++ b/tests/port_test.go @@ -0,0 +1,246 @@ +// Copyright 2015 The appc Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package tests + +import ( + "strconv" + "testing" + + "github.com/appc/acbuild/Godeps/_workspace/src/github.com/appc/spec/schema" + "github.com/appc/acbuild/Godeps/_workspace/src/github.com/appc/spec/schema/types" +) + +const ( + portName = "http" + portProtocol = "tcp" + portNumber uint = 8080 + portCount uint = 2 + + portName2 = "snmp" + portProtocol2 = "udp" + portNumber2 = 161 +) + +func manWithPorts(ports []types.Port) schema.ImageManifest { + return schema.ImageManifest{ + ACKind: schema.ImageManifestKind, + ACVersion: schema.AppContainerVersion, + Name: *types.MustACIdentifier("acbuild-unnamed"), + App: &types.App{ + Exec: nil, + User: "0", + Group: "0", + Ports: ports, + }, + Labels: systemLabels, + } +} + +func TestAddPort(t *testing.T) { + workingDir := setUpTest(t) + defer cleanUpTest(workingDir) + + err := runACBuild(workingDir, "port", "add", portName, portProtocol, strconv.Itoa(int(portNumber))) + if err != nil { + t.Fatalf("%v\n", err) + } + + ports := []types.Port{ + types.Port{ + Name: *types.MustACName(portName), + Protocol: portProtocol, + Port: portNumber, + Count: 1, + }, + } + + checkManifest(t, workingDir, manWithPorts(ports)) + checkEmptyRootfs(t, workingDir) +} + +func TestAddPortWithCount(t *testing.T) { + workingDir := setUpTest(t) + defer cleanUpTest(workingDir) + + err := runACBuild(workingDir, "port", "add", portName, portProtocol, strconv.Itoa(int(portNumber)), + "--count", strconv.Itoa(int(portCount))) + if err != nil { + t.Fatalf("%v\n", err) + } + + ports := []types.Port{ + types.Port{ + Name: *types.MustACName(portName), + Protocol: portProtocol, + Port: portNumber, + Count: portCount, + }, + } + + checkManifest(t, workingDir, manWithPorts(ports)) + checkEmptyRootfs(t, workingDir) +} + +func TestAddPortWithSocketActivated(t *testing.T) { + workingDir := setUpTest(t) + defer cleanUpTest(workingDir) + + err := runACBuild(workingDir, "port", "add", portName, portProtocol, strconv.Itoa(int(portNumber)), + "--socket-activated") + if err != nil { + t.Fatalf("%v\n", err) + } + + ports := []types.Port{ + types.Port{ + Name: *types.MustACName(portName), + Protocol: portProtocol, + Port: portNumber, + Count: 1, + SocketActivated: true, + }, + } + + checkManifest(t, workingDir, manWithPorts(ports)) + checkEmptyRootfs(t, workingDir) +} + +func TestAddNegativePort(t *testing.T) { + workingDir := setUpTest(t) + defer cleanUpTest(workingDir) + + // The "--" is required to prevent cobra from parsing the "-1" as a flag + err := runACBuild(workingDir, "port", "add", portName, portProtocol, "--", "-1") + if err == nil { + t.Fatalf("port add didn't return an error when asked to add a port with a negative number") + } + + checkManifest(t, workingDir, emptyManifest) + checkEmptyRootfs(t, workingDir) +} + +func TestAddPortThatsTooHigh(t *testing.T) { + workingDir := setUpTest(t) + defer cleanUpTest(workingDir) + + err := runACBuild(workingDir, "port", "add", portName, portProtocol, "65536") + if err == nil { + t.Fatalf("port add didn't return an error when asked to add a port with a number > 65535") + } + + checkManifest(t, workingDir, emptyManifest) + checkEmptyRootfs(t, workingDir) +} + +func TestAddTwoPorts(t *testing.T) { + workingDir := setUpTest(t) + defer cleanUpTest(workingDir) + + err := runACBuild(workingDir, "port", "add", portName, portProtocol, strconv.Itoa(int(portNumber))) + if err != nil { + t.Fatalf("%v\n", err) + } + + err = runACBuild(workingDir, "port", "add", portName2, portProtocol2, strconv.Itoa(int(portNumber2))) + if err != nil { + t.Fatalf("%v\n", err) + } + + ports := []types.Port{ + types.Port{ + Name: *types.MustACName(portName), + Protocol: portProtocol, + Port: portNumber, + Count: 1, + }, + types.Port{ + Name: *types.MustACName(portName2), + Protocol: portProtocol2, + Port: portNumber2, + Count: 1, + }, + } + + checkManifest(t, workingDir, manWithPorts(ports)) + checkEmptyRootfs(t, workingDir) +} + +func TestAddRmPorts(t *testing.T) { + workingDir := setUpTest(t) + defer cleanUpTest(workingDir) + + err := runACBuild(workingDir, "port", "add", portName, portProtocol, strconv.Itoa(int(portNumber))) + if err != nil { + t.Fatalf("%v\n", err) + } + + err = runACBuild(workingDir, "port", "rm", portName) + if err != nil { + t.Fatalf("%v\n", err) + } + + checkManifest(t, workingDir, emptyManifest) + checkEmptyRootfs(t, workingDir) +} + +func TestAddAddRmPorts(t *testing.T) { + workingDir := setUpTest(t) + defer cleanUpTest(workingDir) + + err := runACBuild(workingDir, "port", "add", portName, portProtocol, strconv.Itoa(int(portNumber))) + if err != nil { + t.Fatalf("%v\n", err) + } + + err = runACBuild(workingDir, "port", "add", portName2, portProtocol2, strconv.Itoa(int(portNumber2))) + if err != nil { + t.Fatalf("%v\n", err) + } + + err = runACBuild(workingDir, "port", "rm", portName) + if err != nil { + t.Fatalf("%v\n", err) + } + + ports := []types.Port{ + types.Port{ + Name: *types.MustACName(portName2), + Protocol: portProtocol2, + Port: portNumber2, + Count: 1, + }, + } + + checkManifest(t, workingDir, manWithPorts(ports)) + checkEmptyRootfs(t, workingDir) +} + +func TestRmNonexistentPorts(t *testing.T) { + workingDir := setUpTest(t) + defer cleanUpTest(workingDir) + + err := runACBuild(workingDir, "port", "remove", portName) + switch { + case err == nil: + t.Fatalf("port remove didn't return an error when asked to remove nonexistent port") + case err.exitCode == 2: + return + default: + t.Fatalf("error occurred when running port remove:\n%v", err) + } + + checkManifest(t, workingDir, emptyManifest) + checkEmptyRootfs(t, workingDir) +} diff --git a/tests/set-exec_test.go b/tests/set-exec_test.go new file mode 100644 index 00000000..4bb10629 --- /dev/null +++ b/tests/set-exec_test.go @@ -0,0 +1,48 @@ +// Copyright 2015 The appc Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package tests + +import ( + "testing" + + "github.com/appc/acbuild/Godeps/_workspace/src/github.com/appc/spec/schema" + "github.com/appc/acbuild/Godeps/_workspace/src/github.com/appc/spec/schema/types" +) + +func TestSetExec(t *testing.T) { + workingDir := setUpTest(t) + defer cleanUpTest(workingDir) + + var exec = []string{"/bin/nethack4", "-D", "wizard"} + + err := runACBuild(workingDir, append([]string{"set-exec", "--"}, exec...)...) + if err != nil { + t.Fatalf("%v\n", err) + } + man := schema.ImageManifest{ + ACKind: schema.ImageManifestKind, + ACVersion: schema.AppContainerVersion, + Name: *types.MustACIdentifier("acbuild-unnamed"), + App: &types.App{ + Exec: exec, + User: "0", + Group: "0", + }, + Labels: systemLabels, + } + + checkManifest(t, workingDir, man) + checkEmptyRootfs(t, workingDir) +} diff --git a/tests/set-group_test.go b/tests/set-group_test.go new file mode 100644 index 00000000..3d8a4a47 --- /dev/null +++ b/tests/set-group_test.go @@ -0,0 +1,48 @@ +// Copyright 2015 The appc Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package tests + +import ( + "testing" + + "github.com/appc/acbuild/Godeps/_workspace/src/github.com/appc/spec/schema" + "github.com/appc/acbuild/Godeps/_workspace/src/github.com/appc/spec/schema/types" +) + +func TestSetGroup(t *testing.T) { + workingDir := setUpTest(t) + defer cleanUpTest(workingDir) + + const group = "10" + + err := runACBuild(workingDir, "set-group", group) + if err != nil { + t.Fatalf("%v\n", err) + } + man := schema.ImageManifest{ + ACKind: schema.ImageManifestKind, + ACVersion: schema.AppContainerVersion, + Name: *types.MustACIdentifier("acbuild-unnamed"), + App: &types.App{ + Exec: nil, + User: "0", + Group: group, + }, + Labels: systemLabels, + } + + checkManifest(t, workingDir, man) + checkEmptyRootfs(t, workingDir) +} diff --git a/tests/set-name_test.go b/tests/set-name_test.go new file mode 100644 index 00000000..e51960fa --- /dev/null +++ b/tests/set-name_test.go @@ -0,0 +1,48 @@ +// Copyright 2015 The appc Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package tests + +import ( + "testing" + + "github.com/appc/acbuild/Godeps/_workspace/src/github.com/appc/spec/schema" + "github.com/appc/acbuild/Godeps/_workspace/src/github.com/appc/spec/schema/types" +) + +func TestSetName(t *testing.T) { + workingDir := setUpTest(t) + defer cleanUpTest(workingDir) + + const name = "example.com/app" + + err := runACBuild(workingDir, "set-name", name) + if err != nil { + t.Fatalf("%v\n", err) + } + man := schema.ImageManifest{ + ACKind: schema.ImageManifestKind, + ACVersion: schema.AppContainerVersion, + Name: *types.MustACIdentifier(name), + App: &types.App{ + Exec: nil, + User: "0", + Group: "0", + }, + Labels: systemLabels, + } + + checkManifest(t, workingDir, man) + checkEmptyRootfs(t, workingDir) +} diff --git a/tests/set-user_test.go b/tests/set-user_test.go new file mode 100644 index 00000000..7e67721d --- /dev/null +++ b/tests/set-user_test.go @@ -0,0 +1,48 @@ +// Copyright 2015 The appc Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package tests + +import ( + "testing" + + "github.com/appc/acbuild/Godeps/_workspace/src/github.com/appc/spec/schema" + "github.com/appc/acbuild/Godeps/_workspace/src/github.com/appc/spec/schema/types" +) + +func TestSetUser(t *testing.T) { + workingDir := setUpTest(t) + defer cleanUpTest(workingDir) + + const user = "10" + + err := runACBuild(workingDir, "set-user", user) + if err != nil { + t.Fatalf("%v\n", err) + } + man := schema.ImageManifest{ + ACKind: schema.ImageManifestKind, + ACVersion: schema.AppContainerVersion, + Name: *types.MustACIdentifier("acbuild-unnamed"), + App: &types.App{ + Exec: nil, + User: user, + Group: "0", + }, + Labels: systemLabels, + } + + checkManifest(t, workingDir, man) + checkEmptyRootfs(t, workingDir) +} diff --git a/util/manifest.go b/util/manifest.go index 2cd03492..09a2774d 100644 --- a/util/manifest.go +++ b/util/manifest.go @@ -50,13 +50,16 @@ func GetManifest(acipath string) (*schema.ImageManifest, error) { // ModifyManifest will read in the manifest from the untarred ACI stored at // acipath, run the fn function (which is intended to modify the manifest), and // then write the resulting manifest back to the file it was read from. -func ModifyManifest(fn func(*schema.ImageManifest), acipath string) error { +func ModifyManifest(fn func(*schema.ImageManifest) error, acipath string) error { man, err := GetManifest(acipath) if err != nil { return err } - fn(man) + err = fn(man) + if err != nil { + return err + } blob, err := man.MarshalJSON() if err != nil {