[infra][ftx] Checks for success in out/summary.json in CAS output.

Bug: b/258456267
Change-Id: I0da3f202a0803e9aa0ae48835a2738c0f351961c
Reviewed-on: https://fuchsia-review.googlesource.com/c/infra/infra/+/873653
Reviewed-by: Rahul Bangar <rahulbn@google.com>
Commit-Queue: Vinicius Felizardo <felizardo@google.com>
diff --git a/cmd/ftxtest/cas.go b/cmd/ftxtest/cas.go
new file mode 100644
index 0000000..e09fa8e
--- /dev/null
+++ b/cmd/ftxtest/cas.go
@@ -0,0 +1,51 @@
+// Copyright 2023 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+package main
+
+import (
+	"context"
+	"fmt"
+	"os"
+
+	rbeClient "github.com/bazelbuild/remote-apis-sdks/go/pkg/client"
+	"github.com/bazelbuild/remote-apis-sdks/go/pkg/digest"
+	"github.com/bazelbuild/remote-apis-sdks/go/pkg/filemetadata"
+	"go.chromium.org/luci/auth"
+	"go.chromium.org/luci/client/casclient"
+)
+
+const (
+	casAddr = "remotebuildexecution.googleapis.com:443"
+)
+
+type CAS struct {
+	client *rbeClient.Client
+}
+
+func NewCAS(ctx context.Context, authOpts auth.Options, luciInstance string) (*CAS, error) {
+	casInstance := fmt.Sprintf("projects/%s/instances/default_instance", luciInstance)
+	client, err := casclient.NewLegacy(ctx, casAddr, casInstance, authOpts, true)
+	if err != nil {
+		return nil, fmt.Errorf("casclient.NewLegacy: %v", err)
+	}
+	return &CAS{
+		client: client,
+	}, nil
+}
+
+func (c *CAS) Download(ctx context.Context, hash string, size int64) (string, error) {
+	outDir, err := os.MkdirTemp("", "casOut")
+	if err != nil {
+		return "", fmt.Errorf("os.MkdirTemp: %v", err)
+	}
+	d := digest.Digest{
+		Hash: hash,
+		Size: size,
+	}
+	_, _, err = c.client.DownloadDirectory(ctx, d, outDir, filemetadata.NewNoopCache())
+	if err != nil {
+		return "", fmt.Errorf("cas DownloadDirectory: %v", err)
+	}
+	return outDir, nil
+}
diff --git a/cmd/ftxtest/main.go b/cmd/ftxtest/main.go
index bd19bd0..749f3c3 100644
--- a/cmd/ftxtest/main.go
+++ b/cmd/ftxtest/main.go
@@ -21,6 +21,7 @@
 
 func getApplication(defaultAuthOpts auth.Options) *subcommands.DefaultApplication {
 	defaultAuthOpts.Scopes = []string{
+		"https://www.googleapis.com/auth/cloud-platform",
 		"https://www.googleapis.com/auth/userinfo.email",
 	}
 	return &subcommands.DefaultApplication{
diff --git a/cmd/ftxtest/run.go b/cmd/ftxtest/run.go
index ab4f82d..e095c18 100644
--- a/cmd/ftxtest/run.go
+++ b/cmd/ftxtest/run.go
@@ -73,6 +73,11 @@
 			state.SetSummaryMarkdown(fmt.Sprintf("%v: %v", buildInput.Name, err))
 			return err
 		}
+		if err := r.resultStep(ctx, bbCtx, swarming, taskId); err != nil {
+			err = fmt.Errorf("resultStep: %v", err)
+			state.SetSummaryMarkdown(fmt.Sprintf("%v: %v", buildInput.Name, err))
+			return err
+		}
 		return nil
 	})
 }
@@ -86,8 +91,11 @@
 		fmt.Fprintf(os.Stderr, "  luci-auth login -scopes %q\n", strings.Join(r.parsedAuthOpts.Scopes, " "))
 		return nil, errors.New("Not logged in.")
 	}
-	instance := instance(buildInput)
-	swarming, err := NewSwarming(httpClient, instance)
+	cas, err := NewCAS(ctx, r.parsedAuthOpts, instance(buildInput))
+	if err != nil {
+		return nil, fmt.Errorf("NewCAS: %v", err)
+	}
+	swarming, err := NewSwarming(httpClient, instance(buildInput), cas)
 	if err != nil {
 		step.End(err)
 		return nil, fmt.Errorf("NewSwarming: %v", err)
@@ -128,6 +136,16 @@
 	return nil
 }
 
+func (r *runImpl) resultStep(ctx context.Context, bbCtx context.Context, swarming *Swarming, taskId string) error {
+	step, bbCtx := build.StartStep(bbCtx, "Result")
+	if err := swarming.CheckTestFailure(ctx, taskId); err != nil {
+		step.End(err)
+		return err
+	}
+	step.End(nil)
+	return nil
+}
+
 func instance(buildInput *ftxproto.InputProperties) string {
 	if buildInput.External {
 		return "chromium-swarm"
diff --git a/cmd/ftxtest/swarming.go b/cmd/ftxtest/swarming.go
index 8fd6684..eebab54 100644
--- a/cmd/ftxtest/swarming.go
+++ b/cmd/ftxtest/swarming.go
@@ -4,8 +4,13 @@
 package main
 
 import (
+	"context"
+	"encoding/json"
+	"errors"
 	"fmt"
 	"net/http"
+	"os"
+	"path/filepath"
 	"strconv"
 	"strings"
 	"time"
@@ -17,6 +22,7 @@
 type Swarming struct {
 	instance string
 	service  *swarming.Service
+	cas      *CAS
 }
 
 const (
@@ -26,7 +32,7 @@
 	poolTaskInterval     = 10 * time.Second
 )
 
-func NewSwarming(httpClient *http.Client, instance string) (*Swarming, error) {
+func NewSwarming(httpClient *http.Client, instance string, cas *CAS) (*Swarming, error) {
 	swarmingService, err := swarming.New(httpClient)
 	swarmingService.BasePath = fmt.Sprintf("https://%s.appspot.com/_ah/api/swarming/v1/", instance)
 	if err != nil {
@@ -35,6 +41,7 @@
 	swarming := &Swarming{
 		service:  swarmingService,
 		instance: instance,
+		cas:      cas,
 	}
 	return swarming, nil
 }
@@ -132,3 +139,44 @@
 		return "turquoise:global.try"
 	}
 }
+
+// testSummary determines the data for out/summary.json
+type testSummary struct {
+	Success bool `json:"success"`
+}
+
+func (s *Swarming) CheckTestFailure(ctx context.Context, taskId string) error {
+	task, err := s.service.Task.Result(taskId).Do()
+	if err != nil {
+		return fmt.Errorf("task.request: %v", err)
+	}
+	if task.CasOutputRoot == nil || task.CasOutputRoot.Digest == nil {
+		return errors.New("Swarming task did not produce CAS output")
+	}
+	d := task.CasOutputRoot.Digest
+	dir, err := s.cas.Download(ctx, d.Hash, d.SizeBytes)
+	if err != nil {
+		return fmt.Errorf("cas.Download: %v", err)
+	}
+	summary := testSummary{}
+	err = readJSON(filepath.Join(dir, "out", "summary.json"), &summary)
+	if err != nil {
+		return fmt.Errorf("readJSON: %v", err)
+	}
+	if !summary.Success {
+		return errors.New("Test failure")
+	}
+	return nil
+}
+
+func readJSON(filename string, out any) error {
+	rawData, err := os.ReadFile(filename)
+	if err != nil {
+		return fmt.Errorf("os.ReadFile: %v", err)
+	}
+	err = json.Unmarshal(rawData, out)
+	if err != nil {
+		return fmt.Errorf("json.Unmarshal: %v", err)
+	}
+	return nil
+}
diff --git a/cmd/ftxtest/test/build.json b/cmd/ftxtest/test/build.json
index 135a4c9..e35c2a4 100644
--- a/cmd/ftxtest/test/build.json
+++ b/cmd/ftxtest/test/build.json
@@ -3,7 +3,7 @@
         "properties": {
             "name": "hello_swarming_test",
             "external": false,
-            "input_artifacts_digest": "19a21c55f6790bc5e07f9e6c3be64dfd19fa696feaed629c6a9a8e15e5e469cb/83",
+            "input_artifacts_digest": "885f75a688c8cfd7fb8de820953f4949a9c6a1a5af7870fa2d059957f718c7af/83",
             "target_dimensions": {
                 "pool": "fuchsia.dev.tests",
 		"device_type": "Vim3"