From b041f48e56f0ac0670d44cd98e968da62a5ea9be Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 20 Feb 2015 13:39:49 -0800 Subject: [PATCH] terraform: State.Equal --- terraform/state.go | 158 +++++++++++++++++++++++++++++++ terraform/state_test.go | 201 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 359 insertions(+) diff --git a/terraform/state.go b/terraform/state.go index 22a0935b7..530d01c08 100644 --- a/terraform/state.go +++ b/terraform/state.go @@ -134,6 +134,30 @@ func (s *State) RootModule() *ModuleState { return root } +// Equal tests if one state is equal to another. +func (s *State) Equal(other *State) bool { + // If the versions are different, they're certainly not equal + if s.Version != other.Version { + return false + } + + // If any of the modules are not equal, then this state isn't equal + for _, m := range s.Modules { + // This isn't very optimal currently but works. + otherM := other.ModuleByPath(m.Path) + if otherM == nil { + return false + } + + // If they're not equal, then we're not equal! + if !m.Equal(otherM) { + return false + } + } + + return true +} + func (s *State) init() { if s.Version == 0 { s.Version = StateVersion @@ -289,6 +313,54 @@ type ModuleState struct { Dependencies []string `json:"depends_on,omitempty"` } +// Equal tests whether one module state is equal to another. +func (m *ModuleState) Equal(other *ModuleState) bool { + // Paths must be equal + if !reflect.DeepEqual(m.Path, other.Path) { + return false + } + + // Outputs must be equal + if len(m.Outputs) != len(other.Outputs) { + return false + } + for k, v := range m.Outputs { + if other.Outputs[k] != v { + return false + } + } + + // Dependencies must be equal. This sorts these in place but + // this shouldn't cause any problems. + sort.Strings(m.Dependencies) + sort.Strings(other.Dependencies) + if len(m.Dependencies) != len(other.Dependencies) { + return false + } + for i, d := range m.Dependencies { + if other.Dependencies[i] != d { + return false + } + } + + // Resources must be equal + if len(m.Resources) != len(other.Resources) { + return false + } + for k, r := range m.Resources { + otherR, ok := other.Resources[k] + if !ok { + return false + } + + if !r.Equal(otherR) { + return false + } + } + + return true +} + // IsRoot says whether or not this module diff is for the root module. func (m *ModuleState) IsRoot() bool { return reflect.DeepEqual(m.Path, rootModulePath) @@ -522,6 +594,63 @@ type ResourceState struct { Tainted []*InstanceState `json:"tainted,omitempty"` } +// Equal tests whether two ResourceStates are equal. +func (s *ResourceState) Equal(other *ResourceState) bool { + if s.Type != other.Type { + return false + } + + // Dependencies must be equal + sort.Strings(s.Dependencies) + sort.Strings(other.Dependencies) + if len(s.Dependencies) != len(other.Dependencies) { + return false + } + for i, d := range s.Dependencies { + if other.Dependencies[i] != d { + return false + } + } + + // States must be equal + if !s.Primary.Equal(other.Primary) { + return false + } + + // Tainted + taints := make(map[string]*InstanceState) + for _, t := range other.Tainted { + if t == nil { + continue + } + + taints[t.ID] = t + } + for _, t := range s.Tainted { + if t == nil { + continue + } + + otherT, ok := taints[t.ID] + if !ok { + return false + } + delete(taints, t.ID) + + if !t.Equal(otherT) { + return false + } + } + + // This means that we have stuff in other tainted that we don't + // have, so it is not equal. + if len(taints) > 0 { + return false + } + + return true +} + func (r *ResourceState) init() { if r.Primary == nil { r.Primary = &InstanceState{} @@ -618,6 +747,35 @@ func (i *InstanceState) deepcopy() *InstanceState { return n } +func (s *InstanceState) Equal(other *InstanceState) bool { + // Short circuit some nil checks + if s == nil || other == nil { + return s == other + } + + // IDs must be equal + if s.ID != other.ID { + return false + } + + // Attributes must be equal + if len(s.Attributes) != len(other.Attributes) { + return false + } + for k, v := range s.Attributes { + otherV, ok := other.Attributes[k] + if !ok { + return false + } + + if v != otherV { + return false + } + } + + return true +} + // MergeDiff takes a ResourceDiff and merges the attributes into // this resource state in order to generate a new state. This new // state can be used to provide updated attribute lookups for diff --git a/terraform/state_test.go b/terraform/state_test.go index 8211a35b5..9b4a7d1e3 100644 --- a/terraform/state_test.go +++ b/terraform/state_test.go @@ -111,6 +111,207 @@ func TestStateModuleOrphans_nilConfig(t *testing.T) { } } +func TestStateEqual(t *testing.T) { + cases := []struct { + Result bool + One, Two *State + }{ + // Different versions + { + false, + &State{Version: 5}, + &State{Version: 2}, + }, + + // Different modules + { + false, + &State{ + Modules: []*ModuleState{ + &ModuleState{ + Path: RootModulePath, + }, + }, + }, + &State{}, + }, + + { + true, + &State{ + Modules: []*ModuleState{ + &ModuleState{ + Path: RootModulePath, + }, + }, + }, + &State{ + Modules: []*ModuleState{ + &ModuleState{ + Path: RootModulePath, + }, + }, + }, + }, + } + + for i, tc := range cases { + if tc.One.Equal(tc.Two) != tc.Result { + t.Fatalf("Bad: %d\n\n%s\n\n%s", i, tc.One.String(), tc.Two.String()) + } + } +} + +func TestResourceStateEqual(t *testing.T) { + cases := []struct { + Result bool + One, Two *ResourceState + }{ + // Different types + { + false, + &ResourceState{Type: "foo"}, + &ResourceState{Type: "bar"}, + }, + + // Different dependencies + { + false, + &ResourceState{Dependencies: []string{"foo"}}, + &ResourceState{Dependencies: []string{"bar"}}, + }, + + { + false, + &ResourceState{Dependencies: []string{"foo", "bar"}}, + &ResourceState{Dependencies: []string{"foo"}}, + }, + + { + true, + &ResourceState{Dependencies: []string{"bar", "foo"}}, + &ResourceState{Dependencies: []string{"foo", "bar"}}, + }, + + // Different primaries + { + false, + &ResourceState{Primary: nil}, + &ResourceState{Primary: &InstanceState{ID: "foo"}}, + }, + + { + true, + &ResourceState{Primary: &InstanceState{ID: "foo"}}, + &ResourceState{Primary: &InstanceState{ID: "foo"}}, + }, + + // Different tainted + { + false, + &ResourceState{ + Tainted: nil, + }, + &ResourceState{ + Tainted: []*InstanceState{ + &InstanceState{ID: "foo"}, + }, + }, + }, + + { + true, + &ResourceState{ + Tainted: []*InstanceState{ + &InstanceState{ID: "foo"}, + }, + }, + &ResourceState{ + Tainted: []*InstanceState{ + &InstanceState{ID: "foo"}, + }, + }, + }, + + { + true, + &ResourceState{ + Tainted: []*InstanceState{ + &InstanceState{ID: "foo"}, + nil, + }, + }, + &ResourceState{ + Tainted: []*InstanceState{ + &InstanceState{ID: "foo"}, + }, + }, + }, + } + + for i, tc := range cases { + if tc.One.Equal(tc.Two) != tc.Result { + t.Fatalf("Bad: %d\n\n%s\n\n%s", i, tc.One.String(), tc.Two.String()) + } + if tc.Two.Equal(tc.One) != tc.Result { + t.Fatalf("Bad: %d\n\n%s\n\n%s", i, tc.One.String(), tc.Two.String()) + } + } +} + +func TestInstanceStateEqual(t *testing.T) { + cases := []struct { + Result bool + One, Two *InstanceState + }{ + // Nils + { + false, + nil, + &InstanceState{}, + }, + + { + false, + &InstanceState{}, + nil, + }, + + // Different IDs + { + false, + &InstanceState{ID: "foo"}, + &InstanceState{ID: "bar"}, + }, + + // Different Attributes + { + false, + &InstanceState{Attributes: map[string]string{"foo": "bar"}}, + &InstanceState{Attributes: map[string]string{"foo": "baz"}}, + }, + + // Different Attribute keys + { + false, + &InstanceState{Attributes: map[string]string{"foo": "bar"}}, + &InstanceState{Attributes: map[string]string{"bar": "baz"}}, + }, + + { + false, + &InstanceState{Attributes: map[string]string{"bar": "baz"}}, + &InstanceState{Attributes: map[string]string{"foo": "bar"}}, + }, + } + + for i, tc := range cases { + if tc.One.Equal(tc.Two) != tc.Result { + t.Fatalf("Bad: %d\n\n%s\n\n%s", i, tc.One.String(), tc.Two.String()) + } + } +} + func TestInstanceState_MergeDiff(t *testing.T) { is := InstanceState{ ID: "foo",