assert: ensure message is always displayed & fix under bazel (#276)


Co-authored-by: Daniel Nephin <dnephin@gmail.com>
diff --git a/internal/assert/assert.go b/internal/assert/assert.go
index 2dd8025..1e87276 100644
--- a/internal/assert/assert.go
+++ b/internal/assert/assert.go
@@ -87,19 +87,18 @@
 	args, err := source.CallExprArgs(stackIndex)
 	if err != nil {
 		t.Log(err.Error())
-		return
 	}
 
+	var msg string
 	const comparisonArgIndex = 1 // Assert(t, comparison)
 	if len(args) <= comparisonArgIndex {
-		t.Log(failureMessage + "but assert failed to find the expression to print")
-		return
-	}
-
-	msg, err := boolFailureMessage(args[comparisonArgIndex])
-	if err != nil {
-		t.Log(err.Error())
-		msg = "expression is false"
+		msg = "but assert failed to find the expression to print"
+	} else {
+		msg, err = boolFailureMessage(args[comparisonArgIndex])
+		if err != nil {
+			t.Log(err.Error())
+			msg = "expression is false"
+		}
 	}
 
 	t.Log(format.WithCustomMessage(failureMessage+msg, msgAndArgs...))
diff --git a/internal/source/bazel.go b/internal/source/bazel.go
new file mode 100644
index 0000000..1f5197d
--- /dev/null
+++ b/internal/source/bazel.go
@@ -0,0 +1,51 @@
+package source
+
+import (
+	"fmt"
+	"os"
+	"path/filepath"
+)
+
+// These Bazel env vars are documented here:
+// https://bazel.build/reference/test-encyclopedia
+
+// Signifies test executable is being driven by `bazel test`.
+//
+// Due to Bazel's compilation and sandboxing strategy,
+// some care is required to handle resolving the original *.go source file.
+var inBazelTest = os.Getenv("BAZEL_TEST") == "1"
+
+// The name of the target being tested (ex: //some_package:some_package_test)
+var bazelTestTarget = os.Getenv("TEST_TARGET")
+
+// Absolute path to the base of the runfiles tree
+var bazelTestSrcdir = os.Getenv("TEST_SRCDIR")
+
+// The local repository's workspace name (ex: __main__)
+var bazelTestWorkspace = os.Getenv("TEST_WORKSPACE")
+
+func bazelSourcePath(filename string) (string, error) {
+	// Use the env vars to resolve the test source files,
+	// which must be provided as test data in the respective go_test target.
+	filename = filepath.Join(bazelTestSrcdir, bazelTestWorkspace, filename)
+
+	_, err := os.Stat(filename)
+	if os.IsNotExist(err) {
+		return "", fmt.Errorf(bazelMissingSourceMsg, filename, bazelTestTarget)
+	}
+	return filename, nil
+}
+
+var bazelMissingSourceMsg = `
+the test source file does not exist: %s
+It appears that you are running this test under Bazel (target: %s).
+Check that your test source files are added as test data in your go_test targets.
+
+Example:
+    go_test(
+        name = "your_package_test",
+        srcs = ["your_test.go"],
+        deps = ["@tools_gotest_v3//assert"],
+        data = glob(["*_test.go"])
+    )"
+`
diff --git a/internal/source/source.go b/internal/source/source.go
index a4fc24e..df61234 100644
--- a/internal/source/source.go
+++ b/internal/source/source.go
@@ -10,6 +10,7 @@
 	"go/parser"
 	"go/token"
 	"os"
+	"path/filepath"
 	"runtime"
 )
 
@@ -35,6 +36,19 @@
 	}
 	debug("call stack position: %s:%d", filename, line)
 
+	// Normally, `go` will compile programs with absolute paths in
+	// the debug metadata. However, in the name of reproducibility,
+	// Bazel uses a compilation strategy that results in relative paths
+	// (otherwise, since Bazel uses a random tmp dir for compile and sandboxing,
+	// the resulting binaries would change across compiles/test runs).
+	if inBazelTest && !filepath.IsAbs(filename) {
+		var err error
+		filename, err = bazelSourcePath(filename)
+		if err != nil {
+			return nil, err
+		}
+	}
+
 	fileset := token.NewFileSet()
 	astFile, err := parser.ParseFile(fileset, filename, nil, parser.AllErrors)
 	if err != nil {