blob: 188ac6e6efe61e5508aac6af0811b7d606d9a4d3 [file]
// Copyright 2022 The LUCI Authors. All rights reserved.
// Use of this source code is governed under the Apache License, Version 2.0
// that can be found in the LICENSE file.
package main
import (
"context"
"fmt"
"path/filepath"
"strings"
"testing"
"github.com/golang/protobuf/ptypes/duration"
. "github.com/smartystreets/goconvey/convey"
"google.golang.org/protobuf/types/known/timestamppb"
configpb "go.chromium.org/chromiumos/config/go"
apipb "go.chromium.org/chromiumos/config/go/test/api"
artifactpb "go.chromium.org/chromiumos/config/go/test/artifact"
labpb "go.chromium.org/chromiumos/config/go/test/lab/api"
. "go.chromium.org/luci/common/testing/assertions"
"go.chromium.org/luci/resultdb/pbutil"
pb "go.chromium.org/luci/resultdb/proto/v1"
sinkpb "go.chromium.org/luci/resultdb/sink/proto/v1"
)
const (
// Test result JSON file with a subset of common information.
simpleTestResultFile = "test_data/cros_test_result/simple_test_result.json"
// Test result JSON file with full information.
fullTestResultFile = "test_data/cros_test_result/full_test_result.json"
// Test result JSON file with multi-DUT testing information.
multiDUTTestResultFile = "test_data/cros_test_result/multi_dut_result.json"
// Test result JSON file with skipped test results.
skippedTestResultFile = "test_data/cros_test_result/skipped_test_result.json"
// Test result JSON file with passed (with warning) test results.
warnTestResultFile = "test_data/cros_test_result/warn_test_result.json"
// Test result JSON file with failing test results.
failedTestResultFile = "test_data/cros_test_result/failed_test_result.json"
// Test result JSON file with missing test id.
missingTestIdFile = "test_data/cros_test_result/missing_test_id.json"
// Test result JSON file with warning test results and a long reason.
warnTestResultWithLongReasonFile = "test_data/cros_test_result/warn_test_result_with_long_reason.json"
// Test result JSON file with test result directory path.
testResultDirPathFile = "test_data/cros_test_result/simple_test_result_with_result_dir.json"
// The testhaus base url flag.
testhausBaseURL = "https://tests.chromeos.goog/p/chromeos/logs/unified/build-12345"
)
func TestCrosTestResultConversions(t *testing.T) {
t.Parallel()
ctx := context.Background()
testResultsJSON := ReadJSONFileToString(simpleTestResultFile)
testResult := &artifactpb.TestResult{
Version: 1,
TestInvocation: &artifactpb.TestInvocation{
PrimaryExecutionInfo: &artifactpb.ExecutionInfo{
BuildInfo: &artifactpb.BuildInfo{
Name: "hatch-cq/R106-15048.0.0",
Milestone: 106,
ChromeOsVersion: "15048.0.0",
Source: "hatch-cq",
Board: "hatch",
},
DutInfo: &artifactpb.DutInfo{
Dut: &labpb.Dut{
Id: &labpb.Dut_Id{
Value: "chromeos15-row4-rack5-host1",
},
DutType: &labpb.Dut_Chromeos{
Chromeos: &labpb.Dut_ChromeOS{
Name: "chromeos15-row4-rack5-host1",
DutModel: &labpb.DutModel{
ModelName: "nipperkin",
},
Phase: labpb.Phase_DVT_2,
},
},
},
},
},
},
TestRuns: []*artifactpb.TestRun{
{
TestCaseInfo: &artifactpb.TestCaseInfo{
TestCaseMetadata: &apipb.TestCaseMetadata{
TestCase: &apipb.TestCase{
Id: &apipb.TestCase_Id{
Value: "rlz_CheckPing",
},
Name: "rlz_CheckPing",
},
},
TestCaseResult: &apipb.TestCaseResult{
TestCaseId: &apipb.TestCase_Id{
Value: "rlz_CheckPing",
},
Verdict: &apipb.TestCaseResult_Pass_{},
StartTime: timestamppb.New(parseTime("2022-09-07T18:53:33.983328614Z")),
Duration: &duration.Duration{Seconds: 60},
},
},
LogsInfo: []*configpb.StoragePath{
{
HostType: configpb.StoragePath_GS,
Path: "gs://chromeos-test-logs/test-runner/prod/2022-09-07/98098abe-da4f-4bfa-bef5-9cbc4936da03",
},
},
},
{
TestCaseInfo: &artifactpb.TestCaseInfo{
TestCaseMetadata: &apipb.TestCaseMetadata{
TestCase: &apipb.TestCase{
Id: &apipb.TestCase_Id{
Value: "power_Resume",
},
Name: "power_Resume",
},
},
TestCaseResult: &apipb.TestCaseResult{
TestCaseId: &apipb.TestCase_Id{
Value: "power_Resume",
},
Verdict: &apipb.TestCaseResult_Fail_{},
Reason: "Test failed",
StartTime: timestamppb.New(parseTime("2022-09-07T18:53:34.983328614Z")),
Duration: &duration.Duration{Seconds: 120, Nanos: 100000000},
},
},
LogsInfo: []*configpb.StoragePath{
{
HostType: configpb.StoragePath_GS,
Path: "gs://chromeos-test-logs/test-runner/prod/2022-09-07/98098abe-da4f-4bfa-bef5-9cbc4936da04",
},
},
},
},
}
Convey(`From JSON works`, t, func() {
results := &CrosTestResult{}
err := results.ConvertFromJSON(strings.NewReader(testResultsJSON))
So(err, ShouldBeNil)
So(results.TestResult, ShouldResembleProto, testResult)
})
Convey(`ToProtos works`, t, func() {
Convey("Basic", func() {
results := &CrosTestResult{}
err := results.ConvertFromJSON(strings.NewReader(testResultsJSON))
So(err, ShouldBeNil)
testResults, err := results.ToProtos(ctx)
So(err, ShouldBeNil)
expected := []*sinkpb.TestResult{
{
TestId: "rlz_CheckPing",
Expected: true,
Status: pb.TestStatus_PASS,
StartTime: timestamppb.New(parseTime("2022-09-07T18:53:33.983328614Z")),
Duration: &duration.Duration{Seconds: 60},
Tags: SortTags([]*pb.StringPair{
pbutil.StringPair("account_id", "1"),
pbutil.StringPair("board", "hatch"),
pbutil.StringPair("build", "R106-15048.0.0"),
pbutil.StringPair("cbx", "false"),
pbutil.StringPair("hostname", "chromeos15-row4-rack5-host1"),
pbutil.StringPair("image", "hatch-cq/R106-15048.0.0"),
pbutil.StringPair("is_cft_run", "True"),
pbutil.StringPair("logs_url", "gs://chromeos-test-logs/test-runner/prod/2022-09-07/98098abe-da4f-4bfa-bef5-9cbc4936da03"),
pbutil.StringPair("model", "nipperkin"),
pbutil.StringPair("multiduts", "False"),
pbutil.StringPair("phase", "DVT_2"),
}),
},
{
TestId: "power_Resume",
Expected: false,
Status: pb.TestStatus_FAIL,
FailureReason: &pb.FailureReason{
PrimaryErrorMessage: "Test failed",
Errors: []*pb.FailureReason_Error{
{Message: "Test failed"},
},
},
StartTime: timestamppb.New(parseTime("2022-09-07T18:53:34.983328614Z")),
Duration: &duration.Duration{Seconds: 120, Nanos: 100000000},
Tags: SortTags([]*pb.StringPair{
pbutil.StringPair("account_id", "1"),
pbutil.StringPair("board", "hatch"),
pbutil.StringPair("build", "R106-15048.0.0"),
pbutil.StringPair("cbx", "false"),
pbutil.StringPair("hostname", "chromeos15-row4-rack5-host1"),
pbutil.StringPair("image", "hatch-cq/R106-15048.0.0"),
pbutil.StringPair("is_cft_run", "True"),
pbutil.StringPair("logs_url", "gs://chromeos-test-logs/test-runner/prod/2022-09-07/98098abe-da4f-4bfa-bef5-9cbc4936da04"),
pbutil.StringPair("model", "nipperkin"),
pbutil.StringPair("multiduts", "False"),
pbutil.StringPair("phase", "DVT_2"),
}),
},
}
for i, tr := range expected {
err := PopulateProperties(tr, results.TestResult.TestRuns[i])
So(err, ShouldBeNil)
}
So(testResults, ShouldHaveLength, 2)
So(testResults, ShouldResembleProto, expected)
for _, tr := range testResults {
So(tr.GetProperties().GetFields(), ShouldNotBeEmpty)
}
})
Convey("Uploads test artifacts when result dir is provided", func() {
artBaseDir := filepath.Join("test_data", "cros_test_result", "artifacts")
artName1 := "test_artifact_1.txt"
artName2 := "test_artifact_2.txt"
testCount := 2
wantArtifacts := make([]map[string]*sinkpb.Artifact, 0, testCount)
for _, testID := range []string{"rlz_CheckPing", "power_Resume"} {
wantArtifacts = append(wantArtifacts, map[string]*sinkpb.Artifact{
artName1: {
Body: &sinkpb.Artifact_FilePath{FilePath: filepath.Join(artBaseDir, artName1)},
},
artName2: {
Body: &sinkpb.Artifact_FilePath{FilePath: filepath.Join(artBaseDir, artName2)},
},
"testhaus_logs": {
Body: &sinkpb.Artifact_Contents{Contents: []byte(fmt.Sprintf("%s?treeQuery=%s&test=%s", testhausBaseURL, testID, testID))},
ContentType: "text/x-uri",
},
})
}
testResultsJSON := ReadJSONFileToString(testResultDirPathFile)
results := &CrosTestResult{testhausBaseURL: testhausBaseURL}
err := results.ConvertFromJSON(strings.NewReader(testResultsJSON))
So(err, ShouldBeNil)
gotTestResults, err := results.ToProtos(ctx)
So(err, ShouldBeNil)
So(gotTestResults, ShouldHaveLength, testCount)
gotArtifacts := make([]map[string]*sinkpb.Artifact, 0, testCount)
for _, tr := range gotTestResults {
gotArtifacts = append(gotArtifacts, tr.GetArtifacts())
}
So(gotArtifacts, ShouldResemble, wantArtifacts)
})
Convey("Skips test artifacts upload when result dir is invalid", func() {
testResultsJSON := ReadJSONFileToString(testResultDirPathFile)
results := &CrosTestResult{testhausBaseURL: testhausBaseURL}
err := results.ConvertFromJSON(strings.NewReader(testResultsJSON))
So(err, ShouldBeNil)
// Update the test case result dir to an invalid path
for _, tr := range results.TestResult.TestRuns {
tr.TestCaseInfo.TestCaseResult.ResultDirPath.Path = "invalid_dir"
}
gotTestResults, err := results.ToProtos(ctx)
So(err, ShouldBeNil)
So(gotTestResults, ShouldHaveLength, 2)
for _, tr := range gotTestResults {
So(tr.GetArtifacts(), ShouldBeEmpty)
}
})
Convey(`Check expected skip and unexpected skip tests`, func() {
testResultsJSON := ReadJSONFileToString(skippedTestResultFile)
results := &CrosTestResult{}
err := results.ConvertFromJSON(strings.NewReader(testResultsJSON))
So(err, ShouldBeNil)
testResults, err := results.ToProtos(ctx)
So(err, ShouldBeNil)
expected := []*sinkpb.TestResult{
{
TestId: "rlz_CheckPing",
Expected: true,
Status: pb.TestStatus_SKIP,
FailureReason: &pb.FailureReason{
PrimaryErrorMessage: "Test was skipped expectedly",
Errors: []*pb.FailureReason_Error{
{Message: "Test was skipped expectedly"},
},
},
StartTime: timestamppb.New(parseTime("2022-09-07T18:53:33.983328614Z")),
Duration: &duration.Duration{Seconds: 60},
Tags: SortTags([]*pb.StringPair{
pbutil.StringPair("is_cft_run", "True"),
}),
},
{
TestId: "power_Resume",
Expected: false,
Status: pb.TestStatus_SKIP,
FailureReason: &pb.FailureReason{
PrimaryErrorMessage: "Test has not run yet",
Errors: []*pb.FailureReason_Error{
{Message: "Test has not run yet"},
},
},
StartTime: timestamppb.New(parseTime("2022-09-07T18:53:34.983328614Z")),
Duration: &duration.Duration{Seconds: 120, Nanos: 100000000},
Tags: SortTags([]*pb.StringPair{
pbutil.StringPair("is_cft_run", "True"),
}),
},
}
for i, tr := range expected {
err := PopulateProperties(tr, results.TestResult.TestRuns[i])
So(err, ShouldBeNil)
}
So(testResults, ShouldHaveLength, 2)
So(testResults, ShouldResembleProto, expected)
for _, tr := range testResults {
So(tr.GetProperties().GetFields(), ShouldNotBeEmpty)
}
})
Convey(`Warning results`, func() {
testResultsJSON := ReadJSONFileToString(warnTestResultFile)
results := &CrosTestResult{}
err := results.ConvertFromJSON(strings.NewReader(testResultsJSON))
So(err, ShouldBeNil)
testResults, err := results.ToProtos(ctx)
So(err, ShouldBeNil)
expected := []*sinkpb.TestResult{
{
TestId: "rlz_CheckPing",
Expected: true,
// Warning results are reported as pass, and without
// the failure reason set. Warning messages are included
// in the test result properties.
Status: pb.TestStatus_PASS,
StartTime: timestamppb.New(parseTime("2022-09-07T18:53:33.983328614Z")),
Duration: &duration.Duration{Seconds: 60},
Tags: SortTags([]*pb.StringPair{
pbutil.StringPair("is_cft_run", "True"),
}),
},
}
for i, tr := range expected {
err := PopulateProperties(tr, results.TestResult.TestRuns[i])
So(err, ShouldBeNil)
}
So(testResults, ShouldResembleProto, expected)
for _, tr := range testResults {
So(tr.GetProperties().GetFields(), ShouldNotBeEmpty)
}
})
Convey(`Failed results`, func() {
testResultsJSON := ReadJSONFileToString(failedTestResultFile)
results := &CrosTestResult{}
err := results.ConvertFromJSON(strings.NewReader(testResultsJSON))
So(err, ShouldBeNil)
testResults, err := results.ToProtos(ctx)
So(err, ShouldBeNil)
expected := []*sinkpb.TestResult{
{
TestId: "rlz_CheckPing",
Expected: false,
Status: pb.TestStatus_ABORT,
FailureReason: &pb.FailureReason{
PrimaryErrorMessage: "Failed to start Chrome: login failed: context timeout",
Errors: []*pb.FailureReason_Error{
{Message: "Failed to start Chrome: login failed: context timeout"},
},
},
StartTime: timestamppb.New(parseTime("2022-09-07T18:53:33.983328614Z")),
Duration: &duration.Duration{Seconds: 60},
Tags: SortTags([]*pb.StringPair{
pbutil.StringPair("is_cft_run", "True"),
}),
},
{
TestId: "power_Resume",
Expected: false,
Status: pb.TestStatus_FAIL,
FailureReason: &pb.FailureReason{
PrimaryErrorMessage: "Failed to start Chrome: login failed: OOBE not dismissed, it is on screen \"signin-fatal-error\"",
Errors: []*pb.FailureReason_Error{
{Message: "Failed to start Chrome: login failed: OOBE not dismissed, it is on screen \"signin-fatal-error\""},
{Message: "Failed to clean-up Chrome: some error"},
{Message: "Error three"},
},
},
StartTime: timestamppb.New(parseTime("2022-09-07T18:53:34.983328614Z")),
Duration: &duration.Duration{Seconds: 120, Nanos: 100000000},
Tags: SortTags([]*pb.StringPair{
pbutil.StringPair("is_cft_run", "True"),
}),
},
}
for i, tr := range expected {
err := PopulateProperties(tr, results.TestResult.TestRuns[i])
So(err, ShouldBeNil)
}
So(testResults, ShouldResembleProto, expected)
for _, tr := range testResults {
So(tr.GetProperties().GetFields(), ShouldNotBeEmpty)
}
})
Convey(`Check the full list of tags`, func() {
testResultsJSON := ReadJSONFileToString(fullTestResultFile)
results := &CrosTestResult{}
err := results.ConvertFromJSON(strings.NewReader(testResultsJSON))
So(err, ShouldBeNil)
testResults, err := results.ToProtos(ctx)
So(err, ShouldBeNil)
expected := []*sinkpb.TestResult{
{
TestId: "rlz_CheckPing",
Expected: true,
Status: pb.TestStatus_PASS,
StartTime: timestamppb.New(parseTime("2022-09-07T18:53:33.983328614Z")),
Duration: &duration.Duration{Seconds: 60},
Tags: SortTags([]*pb.StringPair{
pbutil.StringPair("account_id", "4"),
pbutil.StringPair("analytics_name", "Bluetooth_Sa_Perbuild"),
pbutil.StringPair("ancestor_buildbucket_ids", "8814950840874708945,8814951792758733697"),
pbutil.StringPair("ash_version", "109.0.5391.0"),
pbutil.StringPair("board", "hatch"),
pbutil.StringPair("branch", "main"),
pbutil.StringPair("bug_component", "b:1234"),
pbutil.StringPair("build", "R106-15048.0.0"),
pbutil.StringPair("buildbucket_builder", "test_runner-dev"),
pbutil.StringPair("carrier", "CARRIER_ESIM"),
pbutil.StringPair("cbx", "true"),
pbutil.StringPair("chameleon_type", "CHAMELEON_TYPE_V2,CHAMELEON_TYPE_V3"),
pbutil.StringPair("chameleon_connection_types", "CHAMELEON_CONNECTION_TYPE_HDMI,CHAMELEON_CONNECTION_TYPE_USB"),
pbutil.StringPair("channel", "DEV"),
pbutil.StringPair("contacts", "user@google.com"),
pbutil.StringPair("criteria", "Tauto wrapper for specified tast tests"),
pbutil.StringPair("ctp_fwd_task_name", "Bluetooth_Sa_Perbuild"),
pbutil.StringPair("declared_name", "hatch-cq/R102-14632.0.0-62834-8818718496810023809/wificell-cq/tast.wificell-cq"),
pbutil.StringPair("dlm_sku_id", "16968"),
pbutil.StringPair("suite", "arc-cts-vm"),
pbutil.StringPair("drone", "skylab-drone-deployment-prod-6dc79d4f9-czjlj"),
pbutil.StringPair("drone_server", "chromeos4-row4-rack1-drone8"),
pbutil.StringPair("hostname", "chromeos15-row4-rack5-host1"),
pbutil.StringPair("hwid_sku", "katsu_MT8183_0B"),
pbutil.StringPair("image", "hatch-cq/R106-15048.0.0"),
pbutil.StringPair("is_cft_run", "True"),
pbutil.StringPair("job_name", "bb-8818737803155059937-chromeos/general/Full"),
pbutil.StringPair("kernel_version", "5.4.151-16902-g93699f4e73de"),
pbutil.StringPair("lacros_version", "109.0.5391.0"),
pbutil.StringPair("logs_url", "gs://chromeos-test-logs/test-runner/prod/2022-09-07/98098abe-da4f-4bfa-bef5-9cbc4936da03"),
pbutil.StringPair("main_builder_name", "main-release"),
pbutil.StringPair("model", "nipperkin"),
pbutil.StringPair("modem_type", "MODEM_TYPE_FIBOCOMM_L850GL"),
pbutil.StringPair("multiduts", "False"),
pbutil.StringPair("owners", "owner1@test.com,owner2@test.com"),
pbutil.StringPair("phase", "DVT_2_MPS_LTE"),
pbutil.StringPair("pool", "ChromeOSSkylab"),
pbutil.StringPair("qs_account", "unmanaged_p2"),
pbutil.StringPair("qual_bug_id", "1234"),
pbutil.StringPair("queued_time", "2022-06-03 18:53:33.983328614 +0000 UTC"),
pbutil.StringPair("requirements", "requirement 1,requirement 2"),
pbutil.StringPair("ro_fwid", "Google_Voema.13672.224.0"),
pbutil.StringPair("rw_fwid", "Google_Voema.13672.224.0"),
pbutil.StringPair("suite_task_id", "59ef5e9532bbd611"),
pbutil.StringPair("task_id", "59f0e13fe7af0710"),
pbutil.StringPair("test_args", "bug_id=12345 qual_run_id=1712172839652"),
pbutil.StringPair("test_harness", "Tast"),
pbutil.StringPair("label_pool", "DUT_POOL_QUOTA"),
pbutil.StringPair("bot_id", "cloudbots-prod-1715342009263-7kz6"),
pbutil.StringPair("ufs_zone", "ZONE_SFO36_OS"),
pbutil.StringPair("wifi_chip", "marvell"),
pbutil.StringPair("wifi_router_models", "gale"),
}),
},
}
err = PopulateProperties(expected[0], results.TestResult.TestRuns[0])
So(err, ShouldBeNil)
So(testResults, ShouldHaveLength, 1)
So(testResults, ShouldResembleProto, expected)
So(testResults[0].GetProperties().GetFields(), ShouldNotBeEmpty)
})
Convey(`Check multi DUT testing`, func() {
testResultsJSON := ReadJSONFileToString(multiDUTTestResultFile)
results := &CrosTestResult{}
err := results.ConvertFromJSON(strings.NewReader(testResultsJSON))
So(err, ShouldBeNil)
testResults, err := results.ToProtos(ctx)
So(err, ShouldBeNil)
expected := []*sinkpb.TestResult{
{
TestId: "rlz_CheckPing",
Expected: true,
Status: pb.TestStatus_PASS,
StartTime: timestamppb.New(parseTime("2022-09-07T18:53:33.983328614Z")),
Duration: &duration.Duration{Seconds: 60},
Tags: SortTags([]*pb.StringPair{
pbutil.StringPair("account_id", "1"),
pbutil.StringPair("board", "hatch"),
pbutil.StringPair("build", "R106-15048.0.0"),
pbutil.StringPair("cbx", "false"),
pbutil.StringPair("hostname", "chromeos15-row4-rack5-host1"),
pbutil.StringPair("image", "hatch-cq/R106-15048.0.0"),
pbutil.StringPair("is_cft_run", "True"),
pbutil.StringPair("logs_url", "gs://chromeos-test-logs/test-runner/prod/2022-09-07/98098abe-da4f-4bfa-bef5-9cbc4936da03"),
pbutil.StringPair("model", "nipperkin"),
pbutil.StringPair("multiduts", "True"),
pbutil.StringPair("phase", "DVT_2"),
pbutil.StringPair("primary_board", "hatch"),
pbutil.StringPair("primary_model", "nipperkin"),
pbutil.StringPair("primary_phase", "DVT_2"),
pbutil.StringPair("secondary_boards", "brya"),
pbutil.StringPair("secondary_models", "gimble"),
pbutil.StringPair("secondary_phases", "DVT_2"),
}),
},
}
err = PopulateProperties(expected[0], results.TestResult.TestRuns[0])
So(err, ShouldBeNil)
So(testResults, ShouldHaveLength, 1)
So(testResults, ShouldResembleProto, expected)
So(testResults[0].GetProperties().GetFields(), ShouldNotBeEmpty)
})
Convey(`Check with missing test id`, func() {
// There are 3 test cases in the test file. The first test case has
// both id and name while the second one only has the name. The
// third one doesn't have id and name, so it would throw an error
// to surface the problem explicitly and the first two would be
// skipped.
testResultsJSON := ReadJSONFileToString(missingTestIdFile)
results := &CrosTestResult{}
err := results.ConvertFromJSON(strings.NewReader(testResultsJSON))
So(err, ShouldBeNil)
_, err = results.ToProtos(ctx)
So(err, ShouldErrLike, "testId is unspecified due to the missing id in test case")
})
Convey(`Truncate reason field when stored in the properties of test result`, func() {
testResultsJSON := ReadJSONFileToString(warnTestResultWithLongReasonFile)
results := &CrosTestResult{}
err := results.ConvertFromJSON(strings.NewReader(testResultsJSON))
So(err, ShouldBeNil)
testResults, err := results.ToProtos(ctx)
So(err, ShouldBeNil)
expected := []*sinkpb.TestResult{
{
TestId: "rlz_CheckPing",
Expected: true,
// Warning results are reported as pass, and without
// the failure reason set. Warning messages are included
// in the test result properties.
Status: pb.TestStatus_PASS,
StartTime: timestamppb.New(parseTime("2022-09-07T18:53:33.983328614Z")),
Duration: &duration.Duration{Seconds: 60},
Tags: SortTags([]*pb.StringPair{
pbutil.StringPair("is_cft_run", "True"),
}),
},
}
for i, tr := range expected {
err := PopulateProperties(tr, results.TestResult.TestRuns[i])
So(err, ShouldBeNil)
}
So(testResults, ShouldResembleProto, expected)
for _, tr := range testResults {
So(tr.GetProperties().
GetFields()["testCaseInfo"].GetStructValue().
GetFields()["testCaseResult"].GetStructValue().
GetFields()["reason"].GetStringValue(),
ShouldHaveLength, 1024)
}
})
})
}