Merge pull request #150 from tinygrasshopper/build-fix

Fixing build on travis
diff --git a/gexec/build.go b/gexec/build.go
index 3e9bf9f..220c8c4 100644
--- a/gexec/build.go
+++ b/gexec/build.go
@@ -9,9 +9,13 @@
 	"path"
 	"path/filepath"
 	"runtime"
+	"sync"
 )
 
-var tmpDir string
+var (
+	mu     sync.Mutex
+	tmpDir string
+)
 
 /*
 Build uses go build to compile the package at packagePath.  The resulting binary is saved off in a temporary directory.
@@ -60,13 +64,18 @@
 gexec. In Ginkgo this is typically done in an AfterSuite callback.
 */
 func CleanupBuildArtifacts() {
+	mu.Lock()
+	defer mu.Unlock()
 	if tmpDir != "" {
 		os.RemoveAll(tmpDir)
+		tmpDir = ""
 	}
 }
 
 func temporaryDirectory() (string, error) {
 	var err error
+	mu.Lock()
+	defer mu.Unlock()
 	if tmpDir == "" {
 		tmpDir, err = ioutil.TempDir("", "gexec_artifacts")
 		if err != nil {
diff --git a/gexec/build_test.go b/gexec/build_test.go
new file mode 100644
index 0000000..7bd62fe
--- /dev/null
+++ b/gexec/build_test.go
@@ -0,0 +1,37 @@
+package gexec_test
+
+import (
+	. "github.com/onsi/ginkgo"
+	. "github.com/onsi/gomega"
+	"github.com/onsi/gomega/gexec"
+)
+
+var _ = Describe(".Build", func() {
+	var packagePath = "./_fixture/firefly"
+
+	Context("when there have been previous calls to Build", func() {
+		BeforeEach(func() {
+			_, err := gexec.Build(packagePath)
+			Ω(err).ShouldNot(HaveOccurred())
+		})
+
+		It("compiles the specified package", func() {
+			compiledPath, err := gexec.Build(packagePath)
+			Ω(err).ShouldNot(HaveOccurred())
+			Ω(compiledPath).Should(BeAnExistingFile())
+		})
+
+		Context("and CleanupBuildArtifacts has been called", func() {
+			BeforeEach(func() {
+				gexec.CleanupBuildArtifacts()
+			})
+
+			It("compiles the specified package", func() {
+				var err error
+				fireflyPath, err = gexec.Build(packagePath)
+				Ω(err).ShouldNot(HaveOccurred())
+				Ω(fireflyPath).Should(BeAnExistingFile())
+			})
+		})
+	})
+})
diff --git a/ghttp/test_server.go b/ghttp/test_server.go
index f5f537e..2f76bb2 100644
--- a/ghttp/test_server.go
+++ b/ghttp/test_server.go
@@ -223,7 +223,7 @@
 
 		//If the handler panics GHTTP will silently succeed.  This is bad™.
 		//To catch this case we need to fail the test if the handler has panicked.
-		//However, if the handler is panicking because Ginkgo's causing it to panic (i.e. an asswertion failed)
+		//However, if the handler is panicking because Ginkgo's causing it to panic (i.e. an assertion failed)
 		//then we shouldn't double-report the error as this will confuse people.
 
 		//So: step 1, if this is a Ginkgo panic - do nothing, Ginkgo's aware of the failure
diff --git a/matchers.go b/matchers.go
index 12d2b96..0c30aa1 100644
--- a/matchers.go
+++ b/matchers.go
@@ -214,6 +214,15 @@
 	}
 }
 
+//MatchYAML succeeds if actual is a string or stringer of YAML that matches
+//the expected YAML.  The YAML's are decoded and the resulting objects are compared via
+//reflect.DeepEqual so things like key-ordering and whitespace shouldn't matter.
+func MatchYAML(yaml interface{}) types.GomegaMatcher {
+	return &matchers.MatchYAMLMatcher{
+		YAMLToMatch: yaml,
+	}
+}
+
 //BeEmpty succeeds if actual is empty.  Actual must be of type string, array, map, chan, or slice.
 func BeEmpty() types.GomegaMatcher {
 	return &matchers.BeEmptyMatcher{}
diff --git a/matchers/match_yaml_matcher.go b/matchers/match_yaml_matcher.go
new file mode 100644
index 0000000..69fb51a
--- /dev/null
+++ b/matchers/match_yaml_matcher.go
@@ -0,0 +1,74 @@
+package matchers
+
+import (
+	"fmt"
+	"reflect"
+	"strings"
+
+	"github.com/onsi/gomega/format"
+	"gopkg.in/yaml.v2"
+)
+
+type MatchYAMLMatcher struct {
+	YAMLToMatch interface{}
+}
+
+func (matcher *MatchYAMLMatcher) Match(actual interface{}) (success bool, err error) {
+	actualString, expectedString, err := matcher.toStrings(actual)
+	if err != nil {
+		return false, err
+	}
+
+	var aval interface{}
+	var eval interface{}
+
+	if err := yaml.Unmarshal([]byte(actualString), &aval); err != nil {
+		return false, fmt.Errorf("Actual '%s' should be valid YAML, but it is not.\nUnderlying error:%s", actualString, err)
+	}
+	if err := yaml.Unmarshal([]byte(expectedString), &eval); err != nil {
+		return false, fmt.Errorf("Expected '%s' should be valid YAML, but it is not.\nUnderlying error:%s", expectedString, err)
+	}
+
+	return reflect.DeepEqual(aval, eval), nil
+}
+
+func (matcher *MatchYAMLMatcher) FailureMessage(actual interface{}) (message string) {
+	actualString, expectedString, _ := matcher.toNormalisedStrings(actual)
+	return format.Message(actualString, "to match YAML of", expectedString)
+}
+
+func (matcher *MatchYAMLMatcher) NegatedFailureMessage(actual interface{}) (message string) {
+	actualString, expectedString, _ := matcher.toNormalisedStrings(actual)
+	return format.Message(actualString, "not to match YAML of", expectedString)
+}
+
+func (matcher *MatchYAMLMatcher) toNormalisedStrings(actual interface{}) (actualFormatted, expectedFormatted string, err error) {
+	actualString, expectedString, err := matcher.toStrings(actual)
+	return normalise(actualString), normalise(expectedString), err
+}
+
+func normalise(input string) string {
+	var val interface{}
+	err := yaml.Unmarshal([]byte(input), &val)
+	if err != nil {
+		panic(err) // guarded by Match
+	}
+	output, err := yaml.Marshal(val)
+	if err != nil {
+		panic(err) // guarded by Unmarshal
+	}
+	return strings.TrimSpace(string(output))
+}
+
+func (matcher *MatchYAMLMatcher) toStrings(actual interface{}) (actualFormatted, expectedFormatted string, err error) {
+	actualString, ok := toString(actual)
+	if !ok {
+		return "", "", fmt.Errorf("MatchYAMLMatcher matcher requires a string, stringer, or []byte.  Got actual:\n%s", format.Object(actual, 1))
+	}
+	expectedString, ok := toString(matcher.YAMLToMatch)
+	if !ok {
+		return "", "", fmt.Errorf("MatchYAMLMatcher matcher requires a string, stringer, or []byte.  Got expected:\n%s", format.Object(matcher.YAMLToMatch, 1))
+	}
+
+	return actualString, expectedString, nil
+}
diff --git a/matchers/match_yaml_matcher_test.go b/matchers/match_yaml_matcher_test.go
new file mode 100644
index 0000000..8e63de1
--- /dev/null
+++ b/matchers/match_yaml_matcher_test.go
@@ -0,0 +1,94 @@
+package matchers_test
+
+import (
+	. "github.com/onsi/ginkgo"
+	. "github.com/onsi/gomega"
+
+	. "github.com/onsi/gomega/matchers"
+)
+
+var _ = Describe("MatchYAMLMatcher", func() {
+	Context("When passed stringifiables", func() {
+		It("should succeed if the YAML matches", func() {
+			Expect("---").Should(MatchYAML(""))
+			Expect("a: 1").Should(MatchYAML(`{"a":1}`))
+			Expect("a: 1\nb: 2").Should(MatchYAML(`{"b":2, "a":1}`))
+		})
+
+		It("should explain if the YAML does not match when it should", func() {
+			message := (&MatchYAMLMatcher{YAMLToMatch: "a: 1"}).FailureMessage("b: 2")
+			Expect(message).To(MatchRegexp(`Expected\s+<string>: b: 2\s+to match YAML of\s+<string>: a: 1`))
+		})
+
+		It("should normalise the expected and actual when explaining if the YAML does not match when it should", func() {
+			message := (&MatchYAMLMatcher{YAMLToMatch: "a: 'one'"}).FailureMessage("{b: two}")
+			Expect(message).To(MatchRegexp(`Expected\s+<string>: b: two\s+to match YAML of\s+<string>: a: one`))
+		})
+
+		It("should explain if the YAML matches when it should not", func() {
+			message := (&MatchYAMLMatcher{YAMLToMatch: "a: 1"}).NegatedFailureMessage("a: 1")
+			Expect(message).To(MatchRegexp(`Expected\s+<string>: a: 1\s+not to match YAML of\s+<string>: a: 1`))
+		})
+
+		It("should normalise the expected and actual when explaining if the YAML matches when it should not", func() {
+			message := (&MatchYAMLMatcher{YAMLToMatch: "a: 'one'"}).NegatedFailureMessage("{a: one}")
+			Expect(message).To(MatchRegexp(`Expected\s+<string>: a: one\s+not to match YAML of\s+<string>: a: one`))
+		})
+
+		It("should fail if the YAML does not match", func() {
+			Expect("a: 1").ShouldNot(MatchYAML(`{"b":2, "a":1}`))
+		})
+
+		It("should work with byte arrays", func() {
+			Expect([]byte("a: 1")).Should(MatchYAML([]byte("a: 1")))
+			Expect("a: 1").Should(MatchYAML([]byte("a: 1")))
+			Expect([]byte("a: 1")).Should(MatchYAML("a: 1"))
+		})
+	})
+
+	Context("when the expected is not valid YAML", func() {
+		It("should error and explain why", func() {
+			success, err := (&MatchYAMLMatcher{YAMLToMatch: ""}).Match("good:\nbad")
+			Expect(success).Should(BeFalse())
+			Expect(err).Should(HaveOccurred())
+			Expect(err.Error()).Should(ContainSubstring("Actual 'good:\nbad' should be valid YAML"))
+		})
+	})
+
+	Context("when the actual is not valid YAML", func() {
+		It("should error and explain why", func() {
+			success, err := (&MatchYAMLMatcher{YAMLToMatch: "good:\nbad"}).Match("")
+			Expect(success).Should(BeFalse())
+			Expect(err).Should(HaveOccurred())
+			Expect(err.Error()).Should(ContainSubstring("Expected 'good:\nbad' should be valid YAML"))
+		})
+	})
+
+	Context("when the expected is neither a string nor a stringer nor a byte array", func() {
+		It("should error", func() {
+			success, err := (&MatchYAMLMatcher{YAMLToMatch: 2}).Match("")
+			Expect(success).Should(BeFalse())
+			Expect(err).Should(HaveOccurred())
+			Expect(err.Error()).Should(ContainSubstring("MatchYAMLMatcher matcher requires a string, stringer, or []byte.  Got expected:\n    <int>: 2"))
+
+			success, err = (&MatchYAMLMatcher{YAMLToMatch: nil}).Match("")
+			Expect(success).Should(BeFalse())
+			Expect(err).Should(HaveOccurred())
+			Expect(err.Error()).Should(ContainSubstring("MatchYAMLMatcher matcher requires a string, stringer, or []byte.  Got expected:\n    <nil>: nil"))
+		})
+	})
+
+	Context("when the actual is neither a string nor a stringer nor a byte array", func() {
+		It("should error", func() {
+			success, err := (&MatchYAMLMatcher{YAMLToMatch: ""}).Match(2)
+			Expect(success).Should(BeFalse())
+			Expect(err).Should(HaveOccurred())
+			Expect(err.Error()).Should(ContainSubstring("MatchYAMLMatcher matcher requires a string, stringer, or []byte.  Got actual:\n    <int>: 2"))
+
+			success, err = (&MatchYAMLMatcher{YAMLToMatch: ""}).Match(nil)
+			Expect(success).Should(BeFalse())
+			Expect(err).Should(HaveOccurred())
+			Expect(err.Error()).Should(ContainSubstring("MatchYAMLMatcher matcher requires a string, stringer, or []byte.  Got actual:\n    <nil>: nil"))
+		})
+	})
+})