compiler_wrapper: dump tricium clang-tidy crash information

This CL has us dump extra info when we observe a crash while using
`WITH_TIDY=tricium`. In particular:
- We'll now try to generate a -E'ed source file, and stash that in a
  subdirectory of our general crash_diagnostics directory.
- Our JSON object that represents clang-tidy output has metadata about
  the aforementioned crash file, if it was generated.

BUG=chromium:1113442
TEST=`go test`; ran locally with a crashy clang-tidy

Change-Id: I577e5ec8e990af3b7cf1d8a9fea347582d5b227b
Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/third_party/toolchain-utils/+/2343974
Reviewed-by: Manoj Gupta <manojgupta@chromium.org>
Tested-by: George Burgess <gbiv@chromium.org>
diff --git a/compiler_wrapper/clang_tidy_flag.go b/compiler_wrapper/clang_tidy_flag.go
index 2d97ddd..01387fd 100644
--- a/compiler_wrapper/clang_tidy_flag.go
+++ b/compiler_wrapper/clang_tidy_flag.go
@@ -9,12 +9,15 @@
 	"fmt"
 	"io/ioutil"
 	"os"
+	"path"
 	"path/filepath"
 	"strings"
 )
 
 type useTidyMode int
 
+const clangTidyCrashSubstring = "PLEASE submit a bug report"
+
 const (
 	tidyModeNone useTidyMode = iota
 	tidyModeAll
@@ -95,7 +98,7 @@
 	}, nil
 }
 
-func runClangTidyForTricium(env env, clangCmd *command, cSrcFile, fixesDir string, extraTidyFlags []string) error {
+func runClangTidyForTricium(env env, clangCmd *command, cSrcFile, fixesDir string, extraTidyFlags []string, crashArtifactsDir string) error {
 	if err := os.MkdirAll(fixesDir, 0777); err != nil {
 		return fmt.Errorf("creating fixes directory at %q: %v", fixesDir, err)
 	}
@@ -128,13 +131,58 @@
 		return err
 	}
 
+	type crashOutput struct {
+		CrashReproducerPath string `json:"crash_reproducer_path"`
+		Stdstreams          string `json:"stdstreams"`
+	}
+
 	type metadata struct {
-		Args       []string `json:"args"`
-		Executable string   `json:"executable"`
-		ExitCode   int      `json:"exit_code"`
-		LintTarget string   `json:"lint_target"`
-		Stdstreams string   `json:"stdstreams"`
-		Wd         string   `json:"wd"`
+		Args        []string     `json:"args"`
+		CrashOutput *crashOutput `json:"crash_output"`
+		Executable  string       `json:"executable"`
+		ExitCode    int          `json:"exit_code"`
+		LintTarget  string       `json:"lint_target"`
+		Stdstreams  string       `json:"stdstreams"`
+		Wd          string       `json:"wd"`
+	}
+
+	meta := &metadata{
+		Args:        clangTidyCmd.Args,
+		CrashOutput: nil,
+		Executable:  clangTidyCmd.Path,
+		ExitCode:    exitCode,
+		LintTarget:  cSrcFile,
+		Stdstreams:  stdstreams.String(),
+		Wd:          env.getwd(),
+	}
+
+	// Sometimes, clang-tidy crashes. Unfortunately, these don't get funnelled through the
+	// standard clang crash machinery. :(. Try to work with our own.
+	if crashArtifactsDir != "" && strings.Contains(meta.Stdstreams, clangTidyCrashSubstring) {
+		tidyCrashArtifacts := path.Join(crashArtifactsDir, "clang-tidy")
+		if err := os.MkdirAll(tidyCrashArtifacts, 0777); err != nil {
+			return fmt.Errorf("creating crash artifacts directory at %q: %v", tidyCrashArtifacts, err)
+		}
+
+		f, err := ioutil.TempFile(tidyCrashArtifacts, "crash-")
+		if err != nil {
+			return fmt.Errorf("making tempfile for crash output: %v", err)
+		}
+		f.Close()
+
+		reproCmd := &command{}
+		*reproCmd = *clangCmd
+		reproCmd.Args = append(reproCmd.Args, "-E", "-o", f.Name())
+
+		reproOut := &strings.Builder{}
+		_, err = wrapSubprocessErrorWithSourceLoc(reproCmd, env.run(reproCmd, nil, reproOut, reproOut))
+		if err != nil {
+			return fmt.Errorf("attempting to produce a clang-tidy crash reproducer: %v", err)
+		}
+		meta.CrashOutput = &crashOutput{
+			CrashReproducerPath: f.Name(),
+			Stdstreams:          reproOut.String(),
+		}
 	}
 
 	f, err = os.Create(fixesMetadataPath)
@@ -142,14 +190,6 @@
 		return fmt.Errorf("creating fixes metadata: %v", err)
 	}
 
-	meta := &metadata{
-		Args:       clangTidyCmd.Args,
-		Executable: clangTidyCmd.Path,
-		ExitCode:   exitCode,
-		LintTarget: cSrcFile,
-		Stdstreams: stdstreams.String(),
-		Wd:         env.getwd(),
-	}
 	if err := json.NewEncoder(f).Encode(meta); err != nil {
 		return fmt.Errorf("writing fixes metadata: %v", err)
 	}
diff --git a/compiler_wrapper/clang_tidy_flag_test.go b/compiler_wrapper/clang_tidy_flag_test.go
index 54159cd..4293bb2 100644
--- a/compiler_wrapper/clang_tidy_flag_test.go
+++ b/compiler_wrapper/clang_tidy_flag_test.go
@@ -386,6 +386,63 @@
 	})
 }
 
+func TestTriciumReportsClangTidyCrashesGracefully(t *testing.T) {
+	withClangTidyTestContext(t, func(ctx *testContext) {
+		ctx.env = []string{"WITH_TIDY=tricium"}
+		ctx.cmdMock = func(cmd *command, stdin io.Reader, stdout io.Writer, stderr io.Writer) error {
+			switch ctx.cmdCount {
+			case 1:
+				if err := verifyPath(cmd, "usr/bin/clang"); err != nil {
+					t.Error(err)
+				}
+				return nil
+			case 2:
+				if err := verifyPath(cmd, "usr/bin/clang-tidy"); err != nil {
+					return err
+				}
+
+				if _, err := io.WriteString(stdout, clangTidyCrashSubstring); err != nil {
+					return err
+				}
+				return nil
+			case 3:
+				if err := verifyPath(cmd, "usr/bin/clang"); err != nil {
+					t.Error(err)
+				}
+
+				args := cmd.Args
+				if len(args) < 3 {
+					t.Errorf("insufficient number of args provided; got %d; want at least 3", len(args))
+					return nil
+				}
+
+				lastArgs := args[len(args)-3:]
+				eArg, oArg, outFileArg := lastArgs[0], lastArgs[1], lastArgs[2]
+				if eArg != "-E" {
+					t.Errorf("got eArg=%q; wanted -E", eArg)
+				}
+
+				if oArg != "-o" {
+					t.Errorf("got oArg=%q; wanted -o", oArg)
+				}
+
+				wantPrefix := path.Join(ctx.cfg.crashArtifactsDir, "clang-tidy")
+				if !strings.HasPrefix(outFileArg, wantPrefix) {
+					t.Errorf("got out file %q; wanted one starting with %q", outFileArg, wantPrefix)
+				}
+
+				return nil
+			default:
+				return nil
+			}
+		}
+		ctx.must(callCompiler(ctx, ctx.cfg, ctx.newCommand(clangX86_64, mainCc)))
+		if ctx.cmdCount != 4 {
+			t.Errorf("expected 3 calls. Got: %d", ctx.cmdCount)
+		}
+	})
+}
+
 func withClangTidyTestContext(t *testing.T, work func(ctx *testContext)) {
 	withTestContext(t, func(ctx *testContext) {
 		ctx.env = []string{"WITH_TIDY=1"}
diff --git a/compiler_wrapper/compiler_wrapper.go b/compiler_wrapper/compiler_wrapper.go
index a5d35a2..da712a3 100644
--- a/compiler_wrapper/compiler_wrapper.go
+++ b/compiler_wrapper/compiler_wrapper.go
@@ -106,7 +106,7 @@
 					if cfg.triciumNitsDir == "" {
 						return 0, newErrorwithSourceLocf("tricium linting was requested, but no nits directory is configured")
 					}
-					err = runClangTidyForTricium(env, clangCmdWithoutGomaAndCCache, cSrcFile, cfg.triciumNitsDir, tidyFlags)
+					err = runClangTidyForTricium(env, clangCmdWithoutGomaAndCCache, cSrcFile, cfg.triciumNitsDir, tidyFlags, cfg.crashArtifactsDir)
 				case tidyModeAll:
 					err = runClangTidy(env, clangCmdWithoutGomaAndCCache, cSrcFile, tidyFlags)
 				default:
diff --git a/compiler_wrapper/config.go b/compiler_wrapper/config.go
index 546a0e1..e87c7ba 100644
--- a/compiler_wrapper/config.go
+++ b/compiler_wrapper/config.go
@@ -31,6 +31,8 @@
 	newWarningsDir string
 	// Directory to store nits in when using `WITH_TIDY=tricium`.
 	triciumNitsDir string
+	// Directory to store crash artifacts in.
+	crashArtifactsDir string
 	// Version. Only used for printing via -print-cmd.
 	version string
 }
@@ -143,6 +145,8 @@
 	},
 	newWarningsDir: "/tmp/fatal_clang_warnings",
 	triciumNitsDir: "/tmp/linting_output/clang-tidy",
+	// FIXME(gbiv): apply -fcrash-diagnostics-dir based on this.
+	crashArtifactsDir: "/tmp/clang_crash_diagnostics",
 }
 
 // Flags to be added to non-hardened toolchain.
@@ -173,8 +177,9 @@
 	clangPostFlags: []string{
 		"-Wno-implicit-int-float-conversion",
 	},
-	newWarningsDir: "/tmp/fatal_clang_warnings",
-	triciumNitsDir: "/tmp/linting_output/clang-tidy",
+	newWarningsDir:    "/tmp/fatal_clang_warnings",
+	triciumNitsDir:    "/tmp/linting_output/clang-tidy",
+	crashArtifactsDir: "/tmp/clang_crash_diagnostics",
 }
 
 // Flags to be added to host toolchain.
@@ -211,18 +216,20 @@
 	clangPostFlags: []string{
 		"-Wno-implicit-int-float-conversion",
 	},
-	newWarningsDir: "/tmp/fatal_clang_warnings",
-	triciumNitsDir: "/tmp/linting_output/clang-tidy",
+	newWarningsDir:    "/tmp/fatal_clang_warnings",
+	triciumNitsDir:    "/tmp/linting_output/clang-tidy",
+	crashArtifactsDir: "/tmp/clang_crash_diagnostics",
 }
 
 var androidConfig = &config{
-	isHostWrapper:    false,
-	isAndroidWrapper: true,
-	rootRelPath:      "./",
-	commonFlags:      []string{},
-	gccFlags:         []string{},
-	clangFlags:       []string{},
-	clangPostFlags:   []string{},
-	newWarningsDir:   "",
-	triciumNitsDir:   "",
+	isHostWrapper:     false,
+	isAndroidWrapper:  true,
+	rootRelPath:       "./",
+	commonFlags:       []string{},
+	gccFlags:          []string{},
+	clangFlags:        []string{},
+	clangPostFlags:    []string{},
+	newWarningsDir:    "",
+	triciumNitsDir:    "",
+	crashArtifactsDir: "",
 }
diff --git a/compiler_wrapper/testutil_test.go b/compiler_wrapper/testutil_test.go
index d23a843..21b7169 100644
--- a/compiler_wrapper/testutil_test.go
+++ b/compiler_wrapper/testutil_test.go
@@ -142,6 +142,7 @@
 	*ctx.cfg = *cfg
 	ctx.cfg.newWarningsDir = filepath.Join(ctx.tempDir, "fatal_clang_warnings")
 	ctx.cfg.triciumNitsDir = filepath.Join(ctx.tempDir, "tricium_nits")
+	ctx.cfg.crashArtifactsDir = filepath.Join(ctx.tempDir, "clang_crash_diagnostics")
 }
 
 func (ctx *testContext) newCommand(path string, args ...string) *command {