<commit_before>// Copyright 2021 The LUCI Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package e2e

import (
	"fmt"
	"sort"
	"strings"
	"testing"
	"time"

	"google.golang.org/protobuf/proto"
	"google.golang.org/protobuf/types/known/timestamppb"

	gerritpb "go.chromium.org/luci/common/proto/gerrit"
	"go.chromium.org/luci/gae/service/datastore"

	cvbqpb "go.chromium.org/luci/cv/api/bigquery/v1"
	cfgpb "go.chromium.org/luci/cv/api/config/v2"
	migrationpb "go.chromium.org/luci/cv/api/migration"
	"go.chromium.org/luci/cv/internal/common"
	gf "go.chromium.org/luci/cv/internal/gerrit/gerritfake"
	"go.chromium.org/luci/cv/internal/gerrit/trigger"
	"go.chromium.org/luci/cv/internal/migration"
	"go.chromium.org/luci/cv/internal/migration/cqdfake"
	"go.chromium.org/luci/cv/internal/run"

	. "github.com/smartystreets/goconvey/convey"
)

func TestCreatesSingularRun(t *testing.T) {
	t.Parallel()

	Convey("CV creates 1 CL Run, which gets canceled by the user", t, func() {
		/////////////////////////    Setup   ////////////////////////////////
		ct := Test{}
		ctx, cancel := ct.SetUp()
		defer cancel()

		const lProject = "infra"
		const gHost = "g-review"
		const gRepo = "re/po"
		const gRef = "refs/heads/main"
		const gChange = 33

		// TODO(tandrii): remove this once Run creation is not conditional on CV
		// managing Runs for a project.
		ct.EnableCVRunManagement(ctx, lProject)
		cfg := MakeCfgSingular("cg0", gHost, gRepo, gRef)
		ct.Cfg.Create(ctx, lProject, cfg)

		tStart := ct.Clock.Now()

		ct.GFake.AddFrom(gf.WithCIs(gHost, gf.ACLRestricted(lProject), gf.CI(
			gChange, gf.Project(gRepo), gf.Ref(gRef),
			gf.Owner("user-1"),
			gf.Updated(tStart), gf.CQ(+2, tStart, gf.U("user-2")),
		)))

		/////////////////////////    Run CV   ////////////////////////////////
		ct.LogPhase(ctx, "CV notices CL and starts the Run")
		So(ct.PMNotifier.UpdateConfig(ctx, lProject), ShouldBeNil)
		var r *run.Run
		ct.RunUntil(ctx, func() bool {
			r = ct.EarliestCreatedRunOf(ctx, lProject)
			return r != nil && r.Status == run.Status_RUNNING
		})
		So(ct.LoadGerritCL(ctx, gHost, gChange).IncompleteRuns.ContainsSorted(r.ID), ShouldBeTrue)

		// Under normal conditions, this would be done by the same TQ handler that
		// creates the Run, but with flaky Datastore this may come late.
		ct.LogPhase(ctx, "PM incorporates Run into component")
		ct.RunUntil(ctx, func() bool {
			cs := ct.LoadProject(ctx, lProject).State.GetComponents()
			return len(cs) == 1 && len(cs[0].GetPruns()) == 1
		})
		So(ct.LoadProject(ctx, lProject).State.GetComponents()[0].GetPruns()[0].GetId(), ShouldResemble, string(r.ID))

		ct.LogPhase(ctx, "User cancels the Run")
		ct.GFake.MutateChange(gHost, gChange, func(c *gf.Change) {
			gf.CQ(0, ct.Clock.Now(), "user-2")(c.Info)
			gf.Updated(ct.Clock.Now())(c.Info)
		})

		ct.LogPhase(ctx, "CV cancels the Run")
		ct.RunUntil(ctx, func() bool {
			r2 := ct.LoadRun(ctx, r.ID)
			p := ct.LoadProject(ctx, lProject)
			return r2.Status == run.Status_CANCELLED && len(p.State.GetComponents()) == 0
		})

		/////////////////////////    Verify    ////////////////////////////////
		So(ct.LoadGerritCL(ctx, gHost, gChange).IncompleteRuns.ContainsSorted(r.ID), ShouldBeFalse)
		So(ct.LoadProject(ctx, lProject).State.GetPcls(), ShouldBeEmpty)
		So(ct.LoadProject(ctx, lProject).State.GetComponents(), ShouldBeEmpty)
	})
}

func TestCreatesSingularRunCQDinCharge(t *testing.T) {
	t.Parallel()

	Convey("CV creates and finalizes a 1 CL Run while CQD is in charge", t, func() {
		/////////////////////////    Setup   ////////////////////////////////
		ct := Test{}
		ctx, cancel := ct.SetUp()
		defer cancel()

		const lProject = "e2e-always-create-runs"
		const gHost = "g-review"
		const gRepo = "re/po"
		const gRef = "refs/heads/main"
		const gChange = 33

		ct.DisableCVRunManagement(ctx)
		cfg := MakeCfgSingular("cg0", gHost, gRepo, gRef)
		ct.Cfg.Create(ctx, lProject, cfg)

		tStart := ct.Clock.Now()

		ct.GFake.AddFrom(gf.WithCIs(gHost, gf.ACLRestricted(lProject), gf.CI(
			gChange, gf.Project(gRepo), gf.Ref(gRef),
			gf.Owner("user-1"),
			gf.Updated(tStart), gf.CQ(+1, tStart, gf.U("user-2")),
		)))
		const expectedAttemptKey = "<KEY>"

		/////////////////////////    Run CV   ////////////////////////////////
		ct.LogPhase(ctx, "CV and CQD notice CL and start a Run")

		So(ct.PMNotifier.UpdateConfig(ctx, lProject), ShouldBeNil)
		ct.MustCQD(ctx, lProject).SetCandidatesClbk(func() []*migrationpb.ReportedRun {
			if ct.MaxCQVote(ctx, gHost, gChange) == 0 {
				return nil
			}
			return []*migrationpb.ReportedRun{
				{
					Id: "", // CQD isn't aware of CV Runs while computing its own candidates.
					Attempt: &cvbqpb.Attempt{
						Key:           expectedAttemptKey,
						GerritChanges: []*cvbqpb.GerritChange{{Host: gHost, Change: gChange, Mode: cvbqpb.Mode_DRY_RUN}},
						Status:        cvbqpb.AttemptStatus_STARTED,
					},
				},
			}
		})

		var r *run.Run
		ct.RunUntil(ctx, func() bool {
			r = ct.EarliestCreatedRunOf(ctx, lProject)
			as := ct.MustCQD(ctx, lProject).ActiveAttemptKeys()
			return r != nil && r.Status == run.Status_RUNNING && len(as) == 1 && as[0] == expectedAttemptKey
		})
		So(r.ID.AttemptKey(), ShouldResemble, expectedAttemptKey)

		ct.LogPhase(ctx, "Dry Run passes in CQD and removes CQ+1 vote")
		ct.MustCQD(ctx, lProject).SetVerifyClbk(func(r *migrationpb.ReportedRun, _ bool) *migrationpb.ReportedRun {
			if r.GetAttempt().GetEndTime() != nil {
				return r // already finalized
			}
			return &migrationpb.ReportedRun{
				Id: "",
				Attempt: &cvbqpb.Attempt{
					Key:                  expectedAttemptKey,
					ClGroupKey:           "beef",
					EquivalentClGroupKey: "beef",
					GerritChanges:        []*cvbqpb.GerritChange{{Host: gHost, Change: gChange, Mode: cvbqpb.Mode_DRY_RUN}},
					Status:               cvbqpb.AttemptStatus_SUCCESS,
					Builds: []*cvbqpb.Build{
						{Critical: true, Host: "bb-host", Id: 22233, Origin: cvbqpb.Build_NOT_REUSABLE},
					},
					ConfigGroup: cfg.GetConfigGroups()[0].GetName(),
					LuciProject: lProject,
					StartTime:   timestamppb.New(tStart),
					EndTime:     timestamppb.New(ct.Clock.Now()),
				},
			}
		})
		ct.RunUntil(ctx, func() bool {
			return ct.MaxCQVote(ctx, gHost, gChange) == 0
		})

		ct.LogPhase(ctx, "CV finalizes the Run")
		ct.RunUntil(ctx, func() bool {
			return ct.LoadRun(ctx, r.ID).Status == run.Status_SUCCEEDED
		})
		So(ct.LoadGerritCL(ctx, gHost, gChange).IncompleteRuns.ContainsSorted(r.ID), ShouldBeFalse)
		ct.RunUntil(ctx, func() bool {
			return len(ct.LoadProject(ctx, lProject).State.GetPcls()) == 0
		})
		// TODO(qyearsley): assert no BQ row was sent.<commit_msg>[cv] deflake the test.<commit_after>// Copyright 2021 The LUCI Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package e2e

import (
	"fmt"
	"sort"
	"strings"
	"testing"
	"time"

	"google.golang.org/protobuf/proto"
	"google.golang.org/protobuf/types/known/timestamppb"

	gerritpb "go.chromium.org/luci/common/proto/gerrit"
	"go.chromium.org/luci/gae/service/datastore"

	cvbqpb "go.chromium.org/luci/cv/api/bigquery/v1"
	cfgpb "go.chromium.org/luci/cv/api/config/v2"
	migrationpb "go.chromium.org/luci/cv/api/migration"
	"go.chromium.org/luci/cv/internal/common"
	gf "go.chromium.org/luci/cv/internal/gerrit/gerritfake"
	"go.chromium.org/luci/cv/internal/gerrit/trigger"
	"go.chromium.org/luci/cv/internal/migration"
	"go.chromium.org/luci/cv/internal/migration/cqdfake"
	"go.chromium.org/luci/cv/internal/run"

	. "github.com/smartystreets/goconvey/convey"
)

func TestCreatesSingularRun(t *testing.T) {
	t.Parallel()

	Convey("CV creates 1 CL Run, which gets canceled by the user", t, func() {
		/////////////////////////    Setup   ////////////////////////////////
		ct := Test{}
		ctx, cancel := ct.SetUp()
		defer cancel()

		const lProject = "infra"
		const gHost = "g-review"
		const gRepo = "re/po"
		const gRef = "refs/heads/main"
		const gChange = 33

		// TODO(tandrii): remove this once Run creation is not conditional on CV
		// managing Runs for a project.
		ct.EnableCVRunManagement(ctx, lProject)
		cfg := MakeCfgSingular("cg0", gHost, gRepo, gRef)
		ct.Cfg.Create(ctx, lProject, cfg)

		tStart := ct.Clock.Now()

		ct.GFake.AddFrom(gf.WithCIs(gHost, gf.ACLRestricted(lProject), gf.CI(
			gChange, gf.Project(gRepo), gf.Ref(gRef),
			gf.Owner("user-1"),
			gf.Updated(tStart), gf.CQ(+2, tStart, gf.U("user-2")),
		)))

		/////////////////////////    Run CV   ////////////////////////////////
		ct.LogPhase(ctx, "CV notices CL and starts the Run")
		So(ct.PMNotifier.UpdateConfig(ctx, lProject), ShouldBeNil)
		var r *run.Run
		ct.RunUntil(ctx, func() bool {
			r = ct.EarliestCreatedRunOf(ctx, lProject)
			return r != nil && r.Status == run.Status_RUNNING
		})
		So(ct.LoadGerritCL(ctx, gHost, gChange).IncompleteRuns.ContainsSorted(r.ID), ShouldBeTrue)

		// Under normal conditions, this would be done by the same TQ handler that
		// creates the Run, but with flaky Datastore this may come late.
		ct.LogPhase(ctx, "PM incorporates Run into component")
		ct.RunUntil(ctx, func() bool {
			cs := ct.LoadProject(ctx, lProject).State.GetComponents()
			return len(cs) == 1 && len(cs[0].GetPruns()) == 1
		})
		So(ct.LoadProject(ctx, lProject).State.GetComponents()[0].GetPruns()[0].GetId(), ShouldResemble, string(r.ID))

		ct.LogPhase(ctx, "User cancels the Run")
		ct.GFake.MutateChange(gHost, gChange, func(c *gf.Change) {
			gf.CQ(0, ct.Clock.Now(), "user-2")(c.Info)
			gf.Updated(ct.Clock.Now())(c.Info)
		})

		ct.LogPhase(ctx, "CV cancels the Run")
		ct.RunUntil(ctx, func() bool {
			r2 := ct.LoadRun(ctx, r.ID)
			p := ct.LoadProject(ctx, lProject)
			return r2.Status == run.Status_CANCELLED && len(p.State.GetComponents()) == 0
		})

		/////////////////////////    Verify    ////////////////////////////////
		So(ct.LoadGerritCL(ctx, gHost, gChange).IncompleteRuns.ContainsSorted(r.ID), ShouldBeFalse)
		So(ct.LoadProject(ctx, lProject).State.GetPcls(), ShouldBeEmpty)
		So(ct.LoadProject(ctx, lProject).State.GetComponents(), ShouldBeEmpty)
	})
}

func TestCreatesSingularRunCQDinChargeOK(t *testing.T) {
	t.Parallel()

	Convey("CV creates and finalizes a 1 CL Run while CQD is in charge", t, func() {
		/////////////////////////    Setup   ////////////////////////////////
		ct := Test{}
		ctx, cancel := ct.SetUp()
		defer cancel()

		const lProject = "e2e-always-create-runs"
		const gHost = "g-review"
		const gRepo = "re/po"
		const gRef = "refs/heads/main"
		const gChange = 33

		ct.DisableCVRunManagement(ctx)
		cfg := MakeCfgSingular("cg0", gHost, gRepo, gRef)
		ct.Cfg.Create(ctx, lProject, cfg)

		tStart := ct.Clock.Now()

		ct.GFake.AddFrom(gf.WithCIs(gHost, gf.ACLRestricted(lProject), gf.CI(
			gChange, gf.Project(gRepo), gf.Ref(gRef),
			gf.Owner("user-1"),
			gf.Updated(tStart), gf.CQ(+1, tStart, gf.U("user-2")),
		)))
		const expectedAttemptKey = "0a6686c7f19a33a3"

		/////////////////////////    Run CV   ////////////////////////////////
		ct.LogPhase(ctx, "CV and CQD notice CL and start a Run")

		So(ct.PMNotifier.UpdateConfig(ctx, lProject), ShouldBeNil)
		ct.MustCQD(ctx, lProject).SetCandidatesClbk(func() []*migrationpb.ReportedRun {
			if ct.MaxCQVote(ctx, gHost, gChange) == 0 {
				return nil
			}
			return []*migrationpb.ReportedRun{
				{
					Id: "", // CQD isn't aware of CV Runs while computing its own candidates.
					Attempt: &cvbqpb.Attempt{
						Key:           expectedAttemptKey,
						GerritChanges: []*cvbqpb.GerritChange{{Host: gHost, Change: gChange, Mode: cvbqpb.Mode_DRY_RUN}},
						Status:        cvbqpb.AttemptStatus_STARTED,
					},
				},
			}
		})

		var r *run.Run
		ct.RunUntil(ctx, func() bool {
			r = ct.EarliestCreatedRunOf(ctx, lProject)
			as := ct.MustCQD(ctx, lProject).ActiveAttemptKeys()
			return r != nil && r.Status == run.Status_RUNNING && len(as) == 1 && as[0] == expectedAttemptKey
		})
		So(r.ID.AttemptKey(), ShouldResemble, expectedAttemptKey)

		ct.LogPhase(ctx, "Dry Run passes in CQD and removes CQ+1 vote")
		var endedAt time.Time
		ct.MustCQD(ctx, lProject).SetVerifyClbk(func(r *migrationpb.ReportedRun, _ bool) *migrationpb.ReportedRun {
			if r.GetAttempt().GetEndTime() != nil {
				return r // already finalized
			}
			endedAt = ct.Clock.Now()
			ct.Clock.Add(time.Second)
			return &migrationpb.ReportedRun{
				Id: "",
				Attempt: &cvbqpb.Attempt{
					Key:                  expectedAttemptKey,
					ClGroupKey:           "beef",
					EquivalentClGroupKey: "beef",
					GerritChanges:        []*cvbqpb.GerritChange{{Host: gHost, Change: gChange, Mode: cvbqpb.Mode_DRY_RUN}},
					Status:               cvbqpb.AttemptStatus_SUCCESS,
					Builds: []*cvbqpb.Build{
						{Critical: true, Host: "bb-host", Id: 22233, Origin: cvbqpb.Build_NOT_REUSABLE},
					},
					ConfigGroup: cfg.GetConfigGroups()[0].GetName(),
					LuciProject: lProject,
					StartTime:   timestamppb.New(tStart),
					EndTime:     timestamppb.New(endedAt),
				},
			}
		})
		ct.RunUntil(ctx, func() bool {
			return ct.MaxCQVote(ctx, gHost, gChange) == 0
		})

		ct.LogPhase(ctx, "CV finalizes the Run")
		ct.RunUntil(ctx, func() bool {
			return run.IsEnded(ct.LoadRun(ctx, r.ID).Status)
		})
		// There is a race between CV and CQD here. CQD is supposed to call
		// ReportFinishedRun within O(minutes) of the removing of the Gerrit vote,
		// otherwise failsafe on CV kicks in and finalizes such Run with CANCELED.
		// Depending on test setup (e.g. flaky datastore), this test may exhibit
		// either bahavior. See TestCreatesSingularRunCQDinChargeButCrashes for a
		// test this ensures failsafe actually works.
		r = ct.LoadRun(ctx, r.ID)
		if r.Status == run.Status_SUCCEEDED {
			So(r.EndTime, ShouldEqual, datastore.RoundTime(endedAt.UTC()))
			// CV finalizes Run after CQD end time record.
			So(r.UpdateTime, ShouldHappenAfter, r.EndTime)
		} else {
			So(r.Status, ShouldEqual, run.Status_CANCELLED)
		}

		So(ct.LoadGerritCL(ctx, gHost, gChange).IncompleteRuns.ContainsSorted(r.ID), ShouldBeFalse)
		ct.RunUntil(ctx, func() bool {
			return len(ct.LoadProject(ctx, lProject).State.GetPcls()) == 0
		})
		// TODO(qyearsley): assert no BQ row was sent.
{'fraction_non_alphanumeric': 0.17600845051825445, 'fraction_numerical': 0.00706410510332079, 'mean_word_length': 3.0448598130841122, 'pattern_counts': {'":': 0, '<': 4, '<?xml version=': 0, '>': 4, 'https://': 0, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '7656701', 'n_tokens_mistral': 6171, 'n_tokens_neox': 5423, 'n_words': 1190}