mirror of
https://github.com/sourcegraph/sourcegraph.git
synced 2026-02-06 20:31:48 +00:00
a8n: Implement GraphQL API for Burndown Chart (#5895)
* a8n: Add ChangesetCountsOverTime to GraphQL schema * a8n: Add first version of ChangesetCountsOverTime * a8n: Add a separate test for ChangesetCountsOverTime * Change tests for ChangesetCountsOverTime * a8n: Implement counting Open/Closed/Merged for GitHub PRs * a8n: Add a8n.ChangesetCounts type * Move ChangesetCounts and their calculation to enterprise/a8n * Create separate unit test for a8n.CalcCounts * Change structure of TestCalcCounts * Remove database heavy "unit" test * fixup! Rename variables in test * Fix import paths after rebase * Change condition in CalcCounts * a8n: Change interface of CalcCount to accept Events * Implement a8n.CalcCounts for open/closed/reopened/merged events * a8n: Ignore Closed event when Merged * a8n: Reformat CalcCounts tests * a8n: Add helper method for CalcCount tests * Add test to make sure sorting works * a8n: Count OpenApproved/OpenPending/OpenChangesRequested in CalcCounts * CalcCount: add tests for multiple changesets * a8n: Add test for CalcCount with multiple changesets in different stages * fixup! Rename helper function * Extract helper methods * Remove now unneeded test fixture * a8n: Add ChangesetReviewStateCommented and ignore it in counts * a8n: CalcCount add tests for time slicing * a8n: Add tests for CalcCounts to count reviews of same type once * a8n: Add another test for CalcCounts * Add more tests for CalcCount * a8n: Bring back "integration" test for ChangesetCountsOverTime * a8n: Extend integration test for ChangesetCountsOverTime * a8n: Use max(from, campaign.CreatedAt) in ChangesetCountsOverTime As discussed here: https://github.com/sourcegraph/sourcegraph/pull/5847/files#r332142729 * a8n: Do not use max(from, campaign.CreatedAt) in ChangesetCountsOverTime" If you create a campaign and add already existing changesets to it, you can't look at their progression over time. * Remove unused methods WasMergedAt and WasClosedAt * Do not panic when timestamp for ChangesetEvent could not be constructed * Remove unused methods * Remove comment See 104edc61fc * a8n: Fix loading of ChangesetEvents by using pagination * a8n: Strip all timezones in ChangesetCountsOverTime resolver * fixup! Cherry pick store changes * a8n: Change CalcCounts so OpenPending means "without review" Previously it meant: "gotten a review on Github with state 'pending'" Now it means: every changeset that hasn't been reviewed yet, counts towards "OpenPending" * a8n: Use zero time instead of error for Event timestamp * a8n: Extract computing of historical changeset state to function * fixup! Update test description * a8n: Specify author of github code reviews in tests * a8n: Take overall-review-state per person and per changeset into account * Clean up code and comments * a8n: Handle closing/reopening of reviewed changesets in CalcCounts * a8n: Replace booleans with switch on previous review state * a8n: Fix changesets that have been reviewed, closed and merged * a8n: Treat "merged" as final state in CalcCounts * a8n: Add early exit to CalcCounts * fixup! Simplify loop in generateTimestamps * Use zero timestamp instead of error handling * Update enterprise/pkg/a8n/counts.go Co-Authored-By: Tomás Senart <tsenart@gmail.com> * Update comment * Remove unused flag from tests * a8n: Refactor CalcCounts code to get rid of duplicated switch statements * a8n: Make recomputing new review state counts more efficient * a8n: Add GoDocs * a8n: Switch listing ChangesetEvents by CampaignID to using ChangesetIDs * fixup! Small fixes in CalcCounts * a8n: Add AddReviewState to ChangesetCounts * a8n: Get rid of unnecessary inner loops * fixup! Change GoDoc for CalcCounts * fixup! Run gofmt on a8n resolvers * fixup! Fix golangci-lint errors * fixup! Fix golangci-lint complaint
This commit is contained in:
parent
0abe3dad51
commit
cc3c1030af
@ -111,6 +111,11 @@ func (r *schemaResolver) Changesets(ctx context.Context, args *graphqlutil.Conne
|
||||
return r.a8nResolver.Changesets(ctx, args)
|
||||
}
|
||||
|
||||
type ChangesetCountsArgs struct {
|
||||
From *DateTime
|
||||
To *DateTime
|
||||
}
|
||||
|
||||
type CampaignResolver interface {
|
||||
ID() graphql.ID
|
||||
Name() string
|
||||
@ -121,6 +126,7 @@ type CampaignResolver interface {
|
||||
CreatedAt() DateTime
|
||||
UpdatedAt() DateTime
|
||||
Changesets(ctx context.Context, args struct{ graphqlutil.ConnectionArgs }) ChangesetsConnectionResolver
|
||||
ChangesetCountsOverTime(ctx context.Context, args *ChangesetCountsArgs) ([]ChangesetCountsResolver, error)
|
||||
}
|
||||
|
||||
type CampaignsConnectionResolver interface {
|
||||
@ -160,3 +166,14 @@ type ChangesetEventResolver interface {
|
||||
Changeset(ctx context.Context) (ChangesetResolver, error)
|
||||
CreatedAt() DateTime
|
||||
}
|
||||
|
||||
type ChangesetCountsResolver interface {
|
||||
Date() DateTime
|
||||
Total() int32
|
||||
Merged() int32
|
||||
Closed() int32
|
||||
Open() int32
|
||||
OpenApproved() int32
|
||||
OpenChangesRequested() int32
|
||||
OpenPending() int32
|
||||
}
|
||||
|
||||
30
cmd/frontend/graphqlbackend/schema.go
generated
30
cmd/frontend/graphqlbackend/schema.go
generated
@ -418,6 +418,36 @@ type Campaign implements Node {
|
||||
|
||||
# The changesets in this campaign.
|
||||
changesets(first: Int): ChangesetConnection!
|
||||
|
||||
# The changeset counts over time, in 1 day intervals backwards from the point in time given in 'to'.
|
||||
changesetCountsOverTime(
|
||||
# Only include changeset counts up to this point in time (inclusive).
|
||||
# Defaults to createdAt.
|
||||
from: DateTime
|
||||
# Only include changeset counts up to this point in time (inclusive).
|
||||
# Defaults to now.
|
||||
to: DateTime
|
||||
): [ChangesetCounts!]!
|
||||
}
|
||||
|
||||
# The counts of changesets in certain states at a specific point in time.
|
||||
type ChangesetCounts {
|
||||
# The point in time these counts were recorded.
|
||||
date: DateTime!
|
||||
# The total number of changesets.
|
||||
total: Int!
|
||||
# The number of merged changesets.
|
||||
merged: Int!
|
||||
# The number of closed changesets.
|
||||
closed: Int!
|
||||
# The number of open changesets (independent of review state).
|
||||
open: Int!
|
||||
# The number of changesets that are both open and approved.
|
||||
openApproved: Int!
|
||||
# The number of changesets that are both open and have requested changes.
|
||||
openChangesRequested: Int!
|
||||
# The number of changesets that are both open and are pending review.
|
||||
openPending: Int!
|
||||
}
|
||||
|
||||
# A list of campaigns.
|
||||
|
||||
@ -425,6 +425,36 @@ type Campaign implements Node {
|
||||
|
||||
# The changesets in this campaign.
|
||||
changesets(first: Int): ChangesetConnection!
|
||||
|
||||
# The changeset counts over time, in 1 day intervals backwards from the point in time given in 'to'.
|
||||
changesetCountsOverTime(
|
||||
# Only include changeset counts up to this point in time (inclusive).
|
||||
# Defaults to createdAt.
|
||||
from: DateTime
|
||||
# Only include changeset counts up to this point in time (inclusive).
|
||||
# Defaults to now.
|
||||
to: DateTime
|
||||
): [ChangesetCounts!]!
|
||||
}
|
||||
|
||||
# The counts of changesets in certain states at a specific point in time.
|
||||
type ChangesetCounts {
|
||||
# The point in time these counts were recorded.
|
||||
date: DateTime!
|
||||
# The total number of changesets.
|
||||
total: Int!
|
||||
# The number of merged changesets.
|
||||
merged: Int!
|
||||
# The number of closed changesets.
|
||||
closed: Int!
|
||||
# The number of open changesets (independent of review state).
|
||||
open: Int!
|
||||
# The number of changesets that are both open and approved.
|
||||
openApproved: Int!
|
||||
# The number of changesets that are both open and have requested changes.
|
||||
openChangesRequested: Int!
|
||||
# The number of changesets that are both open and are pending review.
|
||||
openPending: Int!
|
||||
}
|
||||
|
||||
# A list of campaigns.
|
||||
|
||||
286
enterprise/pkg/a8n/counts.go
Normal file
286
enterprise/pkg/a8n/counts.go
Normal file
@ -0,0 +1,286 @@
|
||||
package a8n
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sourcegraph/sourcegraph/internal/a8n"
|
||||
"github.com/sourcegraph/sourcegraph/internal/extsvc/github"
|
||||
)
|
||||
|
||||
// ChangesetCounts represents the states in which a given set of Changesets was
|
||||
// at a given point in time
|
||||
type ChangesetCounts struct {
|
||||
Time time.Time
|
||||
Total int32
|
||||
Merged int32
|
||||
Closed int32
|
||||
Open int32
|
||||
OpenApproved int32
|
||||
OpenChangesRequested int32
|
||||
OpenPending int32
|
||||
}
|
||||
|
||||
// AddReviewState adds n to the corresponding counter for a given
|
||||
// ChangesetReviewState
|
||||
func (c *ChangesetCounts) AddReviewState(s a8n.ChangesetReviewState, n int32) {
|
||||
switch s {
|
||||
case a8n.ChangesetReviewStatePending:
|
||||
c.OpenPending += n
|
||||
case a8n.ChangesetReviewStateApproved:
|
||||
c.OpenApproved += n
|
||||
case a8n.ChangesetReviewStateChangesRequested:
|
||||
c.OpenChangesRequested += n
|
||||
}
|
||||
}
|
||||
|
||||
func (cc *ChangesetCounts) String() string {
|
||||
return fmt.Sprintf("%s (Total: %d, Merged: %d, Closed: %d, Open: %d, OpenApproved: %d, OpenChangesRequested: %d, OpenPending: %d)",
|
||||
cc.Time.String(),
|
||||
cc.Total,
|
||||
cc.Merged,
|
||||
cc.Closed,
|
||||
cc.Open,
|
||||
cc.OpenApproved,
|
||||
cc.OpenChangesRequested,
|
||||
cc.OpenPending,
|
||||
)
|
||||
}
|
||||
|
||||
// Event is a single event that happened in the lifetime of a single Changeset,
|
||||
// for example a review or a merge.
|
||||
type Event interface {
|
||||
Timestamp() time.Time
|
||||
Type() a8n.ChangesetEventKind
|
||||
Changeset() int64
|
||||
}
|
||||
|
||||
// Events is a collection of Events that can be sorted by their Timestamps
|
||||
type Events []Event
|
||||
|
||||
func (es Events) Len() int { return len(es) }
|
||||
func (es Events) Swap(i, j int) { es[i], es[j] = es[j], es[i] }
|
||||
|
||||
// Less sorts events by their timestamps
|
||||
func (es Events) Less(i, j int) bool {
|
||||
return es[i].Timestamp().Before(es[j].Timestamp())
|
||||
}
|
||||
|
||||
// CalcCounts calculates ChangesetCounts for the given Changesets and their
|
||||
// Events in the timeframe specified by the start and end parameters. The
|
||||
// number of ChangesetCounts returned is the number of 1 day intervals between
|
||||
// start and end, with each ChangesetCounts representing a point in time at the
|
||||
// boundary of each 24h interval.
|
||||
func CalcCounts(start, end time.Time, cs []*a8n.Changeset, es ...Event) ([]*ChangesetCounts, error) {
|
||||
ts := generateTimestamps(start, end)
|
||||
counts := make([]*ChangesetCounts, len(ts))
|
||||
for i, t := range ts {
|
||||
counts[i] = &ChangesetCounts{Time: t}
|
||||
}
|
||||
|
||||
// Sort all events once by their timestamps
|
||||
events := Events(es)
|
||||
sort.Sort(events)
|
||||
|
||||
// Grouping Events by their Changeset ID
|
||||
byChangesetID := make(map[int64]Events)
|
||||
for _, e := range events {
|
||||
id := e.Changeset()
|
||||
byChangesetID[id] = append(byChangesetID[id], e)
|
||||
}
|
||||
|
||||
// Map Events to their Changeset
|
||||
byChangeset := make(map[*a8n.Changeset]Events)
|
||||
for _, c := range cs {
|
||||
byChangeset[c] = byChangesetID[c.ID]
|
||||
}
|
||||
|
||||
for changeset, csEvents := range byChangeset {
|
||||
// We don't have an event for "open", so we check when it was
|
||||
// created on codehost
|
||||
openedAt := changeset.ExternalCreatedAt()
|
||||
if openedAt.IsZero() {
|
||||
continue
|
||||
}
|
||||
|
||||
// For each changeset and its events, go through every point in time we
|
||||
// want to record and reconstruct the state of the changeset at that
|
||||
// point in time
|
||||
for _, c := range counts {
|
||||
if openedAt.After(c.Time) {
|
||||
// No need to look at events if changeset was not created yet
|
||||
continue
|
||||
}
|
||||
|
||||
err := computeCounts(c, csEvents)
|
||||
if err != nil {
|
||||
return counts, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return counts, nil
|
||||
}
|
||||
|
||||
func computeCounts(c *ChangesetCounts, csEvents Events) error {
|
||||
var (
|
||||
// Since "Merged" and "Closed" are exclusive events and cancel each others
|
||||
// effects on ChangesetCounts out, we need to keep track of when a
|
||||
// changeset was closed, so we can undo the effect of the "Closed" event
|
||||
// when we come across a "Merge" (since, on GitHub, a PR can be closed AND
|
||||
// merged)
|
||||
closed = false
|
||||
|
||||
lastReviewByAuthor = map[string]a8n.ChangesetReviewState{}
|
||||
)
|
||||
|
||||
c.Total++
|
||||
c.Open++
|
||||
c.OpenPending++
|
||||
|
||||
for _, e := range csEvents {
|
||||
et := e.Timestamp()
|
||||
if et.IsZero() {
|
||||
continue
|
||||
}
|
||||
// Event happened after point in time we're looking at, no need to look
|
||||
// at the events in future
|
||||
if et.After(c.Time) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Compute current overall review state
|
||||
currentReviewState := computeReviewState(lastReviewByAuthor)
|
||||
|
||||
switch e.Type() {
|
||||
case a8n.ChangesetEventKindGitHubClosed:
|
||||
c.Open--
|
||||
c.Closed++
|
||||
closed = true
|
||||
|
||||
c.AddReviewState(currentReviewState, -1)
|
||||
|
||||
case a8n.ChangesetEventKindGitHubReopened:
|
||||
c.Open++
|
||||
c.Closed--
|
||||
closed = false
|
||||
|
||||
c.AddReviewState(currentReviewState, 1)
|
||||
|
||||
case a8n.ChangesetEventKindGitHubMerged:
|
||||
// If it was closed, all "review counts" have been updated by the
|
||||
// closed events and we just need to reverse these two counts
|
||||
if closed {
|
||||
c.Closed--
|
||||
c.Merged++
|
||||
return nil
|
||||
}
|
||||
|
||||
c.AddReviewState(currentReviewState, -1)
|
||||
|
||||
c.Merged++
|
||||
c.Open--
|
||||
|
||||
// Merged is a final state, we return here and don't need to look at
|
||||
// other events
|
||||
return nil
|
||||
|
||||
case a8n.ChangesetEventKindGitHubReviewed:
|
||||
s, err := reviewState(e)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// We only care about "Approved" or "ChangesRequested" reviews
|
||||
if s != a8n.ChangesetReviewStateApproved && s != a8n.ChangesetReviewStateChangesRequested {
|
||||
continue
|
||||
}
|
||||
|
||||
author, err := reviewAuthor(e)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Save current review state, then insert new review and recompute
|
||||
// overall review state
|
||||
oldReviewState := currentReviewState
|
||||
lastReviewByAuthor[author] = s
|
||||
newReviewState := computeReviewState(lastReviewByAuthor)
|
||||
|
||||
if newReviewState != oldReviewState {
|
||||
// Decrement the counts increased by old review state
|
||||
c.AddReviewState(oldReviewState, -1)
|
||||
|
||||
// Increase the counts for new review state
|
||||
c.AddReviewState(newReviewState, 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func generateTimestamps(start, end time.Time) []time.Time {
|
||||
// Walk backwards from `end` to >= `start` in 1 day intervals
|
||||
// Backwards so we always end exactly on `end`
|
||||
ts := []time.Time{}
|
||||
for t := end; !t.Before(start); t = t.AddDate(0, 0, -1) {
|
||||
ts = append(ts, t)
|
||||
}
|
||||
|
||||
// Now reverse so we go from oldest to newest in slice
|
||||
for i := len(ts)/2 - 1; i >= 0; i-- {
|
||||
opp := len(ts) - 1 - i
|
||||
ts[i], ts[opp] = ts[opp], ts[i]
|
||||
}
|
||||
|
||||
return ts
|
||||
}
|
||||
|
||||
func reviewState(e Event) (a8n.ChangesetReviewState, error) {
|
||||
var s a8n.ChangesetReviewState
|
||||
changesetEvent, ok := e.(*a8n.ChangesetEvent)
|
||||
if !ok {
|
||||
return s, errors.New("Reviewed event not ChangesetEvent")
|
||||
}
|
||||
|
||||
review, ok := changesetEvent.Metadata.(*github.PullRequestReview)
|
||||
if !ok {
|
||||
return s, errors.New("ChangesetEvent metadata event not PullRequestReview")
|
||||
}
|
||||
|
||||
s = a8n.ChangesetReviewState(review.State)
|
||||
if !s.Valid() {
|
||||
return s, fmt.Errorf("invalid review state: %s", review.State)
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func reviewAuthor(e Event) (string, error) {
|
||||
changesetEvent, ok := e.(*a8n.ChangesetEvent)
|
||||
if !ok {
|
||||
return "", errors.New("Reviewed event not ChangesetEvent")
|
||||
}
|
||||
|
||||
review, ok := changesetEvent.Metadata.(*github.PullRequestReview)
|
||||
if !ok {
|
||||
return "", errors.New("ChangesetEvent metadata event not PullRequestReview")
|
||||
}
|
||||
|
||||
login := review.Author.Login
|
||||
if login == "" {
|
||||
return "", errors.New("review author is blank")
|
||||
}
|
||||
|
||||
return login, nil
|
||||
}
|
||||
|
||||
func computeReviewState(statesByAuthor map[string]a8n.ChangesetReviewState) a8n.ChangesetReviewState {
|
||||
states := make(map[a8n.ChangesetReviewState]bool)
|
||||
for _, s := range statesByAuthor {
|
||||
states[s] = true
|
||||
}
|
||||
return a8n.SelectReviewState(states)
|
||||
}
|
||||
572
enterprise/pkg/a8n/counts_test.go
Normal file
572
enterprise/pkg/a8n/counts_test.go
Normal file
@ -0,0 +1,572 @@
|
||||
package a8n
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/sourcegraph/sourcegraph/internal/a8n"
|
||||
"github.com/sourcegraph/sourcegraph/internal/extsvc/github"
|
||||
)
|
||||
|
||||
func TestCalcCounts(t *testing.T) {
|
||||
now := time.Now().Truncate(time.Microsecond)
|
||||
daysAgo := func(days int) time.Time { return now.AddDate(0, 0, -days) }
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
changesets []*a8n.Changeset
|
||||
start time.Time
|
||||
end time.Time
|
||||
events []Event
|
||||
want []*ChangesetCounts
|
||||
}{
|
||||
{
|
||||
name: "single changeset open merged",
|
||||
changesets: []*a8n.Changeset{
|
||||
ghChangeset(1, daysAgo(2)),
|
||||
},
|
||||
start: daysAgo(2),
|
||||
events: []Event{
|
||||
fakeEvent{t: daysAgo(1), kind: a8n.ChangesetEventKindGitHubMerged, id: 1},
|
||||
},
|
||||
want: []*ChangesetCounts{
|
||||
{Time: daysAgo(2), Total: 1, Open: 1, OpenPending: 1},
|
||||
{Time: daysAgo(1), Total: 1, Merged: 1},
|
||||
{Time: daysAgo(0), Total: 1, Merged: 1},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "start end time on subset of events",
|
||||
changesets: []*a8n.Changeset{
|
||||
ghChangeset(1, daysAgo(3)),
|
||||
},
|
||||
start: daysAgo(4),
|
||||
end: daysAgo(2),
|
||||
events: []Event{
|
||||
fakeEvent{t: daysAgo(1), kind: a8n.ChangesetEventKindGitHubMerged, id: 1},
|
||||
},
|
||||
want: []*ChangesetCounts{
|
||||
{Time: daysAgo(4), Total: 0, Open: 0},
|
||||
{Time: daysAgo(3), Total: 1, Open: 1, OpenPending: 1},
|
||||
{Time: daysAgo(2), Total: 1, Open: 1, OpenPending: 1},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "single changeset created and closed before start time",
|
||||
changesets: []*a8n.Changeset{
|
||||
ghChangeset(1, daysAgo(8)),
|
||||
},
|
||||
start: daysAgo(4),
|
||||
end: daysAgo(2),
|
||||
events: []Event{
|
||||
fakeEvent{t: daysAgo(7), kind: a8n.ChangesetEventKindGitHubMerged, id: 1},
|
||||
},
|
||||
want: []*ChangesetCounts{
|
||||
{Time: daysAgo(4), Total: 1, Merged: 1},
|
||||
{Time: daysAgo(3), Total: 1, Merged: 1},
|
||||
{Time: daysAgo(2), Total: 1, Merged: 1},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "start time not even x*24hours before end time",
|
||||
changesets: []*a8n.Changeset{
|
||||
ghChangeset(1, daysAgo(2)),
|
||||
},
|
||||
start: daysAgo(3),
|
||||
end: now.Add(-18 * time.Hour),
|
||||
events: []Event{
|
||||
fakeEvent{t: daysAgo(1), kind: a8n.ChangesetEventKindGitHubMerged, id: 1},
|
||||
},
|
||||
want: []*ChangesetCounts{
|
||||
{Time: daysAgo(2).Add(-18 * time.Hour), Total: 0, Merged: 0},
|
||||
{Time: daysAgo(1).Add(-18 * time.Hour), Total: 1, Open: 1, OpenPending: 1},
|
||||
{Time: now.Add(-18 * time.Hour), Total: 1, Merged: 1},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "multiple changesets open merged",
|
||||
changesets: []*a8n.Changeset{
|
||||
ghChangeset(1, daysAgo(2)),
|
||||
ghChangeset(2, daysAgo(2)),
|
||||
},
|
||||
start: daysAgo(2),
|
||||
events: []Event{
|
||||
fakeEvent{t: daysAgo(1), kind: a8n.ChangesetEventKindGitHubMerged, id: 1},
|
||||
fakeEvent{t: daysAgo(1), kind: a8n.ChangesetEventKindGitHubMerged, id: 2},
|
||||
},
|
||||
want: []*ChangesetCounts{
|
||||
{Time: daysAgo(2), Total: 2, Open: 2, OpenPending: 2},
|
||||
{Time: daysAgo(1), Total: 2, Merged: 2},
|
||||
{Time: daysAgo(0), Total: 2, Merged: 2},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "multiple changesets open merged different times",
|
||||
changesets: []*a8n.Changeset{
|
||||
ghChangeset(1, daysAgo(3)),
|
||||
ghChangeset(2, daysAgo(2)),
|
||||
},
|
||||
start: daysAgo(4),
|
||||
events: []Event{
|
||||
fakeEvent{t: daysAgo(2), kind: a8n.ChangesetEventKindGitHubMerged, id: 1},
|
||||
fakeEvent{t: daysAgo(1), kind: a8n.ChangesetEventKindGitHubMerged, id: 2},
|
||||
},
|
||||
want: []*ChangesetCounts{
|
||||
{Time: daysAgo(4), Total: 0, Open: 0},
|
||||
{Time: daysAgo(3), Total: 1, Open: 1, OpenPending: 1},
|
||||
{Time: daysAgo(2), Total: 2, Open: 1, OpenPending: 1, Merged: 1},
|
||||
{Time: daysAgo(1), Total: 2, Merged: 2},
|
||||
{Time: daysAgo(0), Total: 2, Merged: 2},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "changeset merged and closed at same time",
|
||||
changesets: []*a8n.Changeset{
|
||||
ghChangeset(1, daysAgo(2)),
|
||||
},
|
||||
start: daysAgo(2),
|
||||
events: []Event{
|
||||
fakeEvent{t: daysAgo(1), kind: a8n.ChangesetEventKindGitHubMerged, id: 1},
|
||||
fakeEvent{t: daysAgo(1), kind: a8n.ChangesetEventKindGitHubClosed, id: 1},
|
||||
},
|
||||
want: []*ChangesetCounts{
|
||||
{Time: daysAgo(2), Total: 1, Open: 1, OpenPending: 1},
|
||||
{Time: daysAgo(1), Total: 1, Merged: 1},
|
||||
{Time: daysAgo(0), Total: 1, Merged: 1},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "changeset merged and closed at same time, reversed order in slice",
|
||||
changesets: []*a8n.Changeset{
|
||||
ghChangeset(1, daysAgo(2)),
|
||||
},
|
||||
start: daysAgo(2),
|
||||
events: []Event{
|
||||
fakeEvent{t: daysAgo(1), kind: a8n.ChangesetEventKindGitHubClosed, id: 1},
|
||||
fakeEvent{t: daysAgo(1), kind: a8n.ChangesetEventKindGitHubMerged, id: 1},
|
||||
},
|
||||
want: []*ChangesetCounts{
|
||||
{Time: daysAgo(2), Total: 1, Open: 1, OpenPending: 1},
|
||||
{Time: daysAgo(1), Total: 1, Merged: 1},
|
||||
{Time: daysAgo(0), Total: 1, Merged: 1},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "single changeset open closed reopened merged",
|
||||
changesets: []*a8n.Changeset{
|
||||
ghChangeset(1, daysAgo(4)),
|
||||
},
|
||||
start: daysAgo(5),
|
||||
events: []Event{
|
||||
fakeEvent{t: daysAgo(3), kind: a8n.ChangesetEventKindGitHubClosed, id: 1},
|
||||
fakeEvent{t: daysAgo(2), kind: a8n.ChangesetEventKindGitHubReopened, id: 1},
|
||||
fakeEvent{t: daysAgo(1), kind: a8n.ChangesetEventKindGitHubMerged, id: 1},
|
||||
},
|
||||
want: []*ChangesetCounts{
|
||||
{Time: daysAgo(5), Total: 0, Open: 0},
|
||||
{Time: daysAgo(4), Total: 1, Open: 1, OpenPending: 1},
|
||||
{Time: daysAgo(3), Total: 1, Open: 0, Closed: 1},
|
||||
{Time: daysAgo(2), Total: 1, Open: 1, OpenPending: 1},
|
||||
{Time: daysAgo(1), Total: 1, Merged: 1},
|
||||
{Time: daysAgo(0), Total: 1, Merged: 1},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "multiple changesets open closed reopened merged different times",
|
||||
changesets: []*a8n.Changeset{
|
||||
ghChangeset(1, daysAgo(5)),
|
||||
ghChangeset(2, daysAgo(4)),
|
||||
},
|
||||
start: daysAgo(6),
|
||||
events: []Event{
|
||||
fakeEvent{t: daysAgo(4), kind: a8n.ChangesetEventKindGitHubClosed, id: 1},
|
||||
fakeEvent{t: daysAgo(3), kind: a8n.ChangesetEventKindGitHubClosed, id: 2},
|
||||
fakeEvent{t: daysAgo(3), kind: a8n.ChangesetEventKindGitHubReopened, id: 1},
|
||||
fakeEvent{t: daysAgo(2), kind: a8n.ChangesetEventKindGitHubReopened, id: 2},
|
||||
fakeEvent{t: daysAgo(1), kind: a8n.ChangesetEventKindGitHubMerged, id: 1},
|
||||
fakeEvent{t: daysAgo(0), kind: a8n.ChangesetEventKindGitHubMerged, id: 2},
|
||||
},
|
||||
want: []*ChangesetCounts{
|
||||
{Time: daysAgo(6), Total: 0, Open: 0},
|
||||
{Time: daysAgo(5), Total: 1, Open: 1, OpenPending: 1},
|
||||
{Time: daysAgo(4), Total: 2, Open: 1, OpenPending: 1, Closed: 1},
|
||||
{Time: daysAgo(3), Total: 2, Open: 1, OpenPending: 1, Closed: 1},
|
||||
{Time: daysAgo(2), Total: 2, Open: 2, OpenPending: 2},
|
||||
{Time: daysAgo(1), Total: 2, Open: 1, OpenPending: 1, Merged: 1},
|
||||
{Time: daysAgo(0), Total: 2, Merged: 2},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "single changeset open closed reopened merged, unsorted events",
|
||||
changesets: []*a8n.Changeset{
|
||||
ghChangeset(1, daysAgo(4)),
|
||||
},
|
||||
start: daysAgo(5),
|
||||
events: []Event{
|
||||
fakeEvent{t: daysAgo(1), kind: a8n.ChangesetEventKindGitHubMerged, id: 1},
|
||||
fakeEvent{t: daysAgo(3), kind: a8n.ChangesetEventKindGitHubClosed, id: 1},
|
||||
fakeEvent{t: daysAgo(2), kind: a8n.ChangesetEventKindGitHubReopened, id: 1},
|
||||
},
|
||||
want: []*ChangesetCounts{
|
||||
{Time: daysAgo(5), Total: 0, Open: 0},
|
||||
{Time: daysAgo(4), Total: 1, Open: 1, OpenPending: 1},
|
||||
{Time: daysAgo(3), Total: 1, Open: 0, Closed: 1},
|
||||
{Time: daysAgo(2), Total: 1, Open: 1, OpenPending: 1},
|
||||
{Time: daysAgo(1), Total: 1, Merged: 1},
|
||||
{Time: daysAgo(0), Total: 1, Merged: 1},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "single changeset open, approved, merged",
|
||||
changesets: []*a8n.Changeset{
|
||||
ghChangeset(1, daysAgo(3)),
|
||||
},
|
||||
start: daysAgo(4),
|
||||
events: []Event{
|
||||
ghReview(1, daysAgo(2), "user1", "APPROVED"),
|
||||
fakeEvent{t: daysAgo(1), kind: a8n.ChangesetEventKindGitHubMerged, id: 1},
|
||||
},
|
||||
want: []*ChangesetCounts{
|
||||
{Time: daysAgo(4), Total: 0, Open: 0},
|
||||
{Time: daysAgo(3), Total: 1, Open: 1, OpenPending: 1},
|
||||
{Time: daysAgo(2), Total: 1, Open: 1, OpenPending: 0, OpenApproved: 1},
|
||||
{Time: daysAgo(1), Total: 1, Merged: 1},
|
||||
{Time: daysAgo(0), Total: 1, Merged: 1},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "single changeset open, approved, closed, reopened",
|
||||
changesets: []*a8n.Changeset{
|
||||
ghChangeset(1, daysAgo(3)),
|
||||
},
|
||||
start: daysAgo(3),
|
||||
events: []Event{
|
||||
ghReview(1, daysAgo(2), "user1", "APPROVED"),
|
||||
fakeEvent{t: daysAgo(1), kind: a8n.ChangesetEventKindGitHubClosed, id: 1},
|
||||
fakeEvent{t: daysAgo(0), kind: a8n.ChangesetEventKindGitHubReopened, id: 1},
|
||||
},
|
||||
want: []*ChangesetCounts{
|
||||
{Time: daysAgo(3), Total: 1, Open: 1, OpenPending: 1},
|
||||
{Time: daysAgo(2), Total: 1, Open: 1, OpenApproved: 1},
|
||||
{Time: daysAgo(1), Total: 1, Closed: 1},
|
||||
{Time: daysAgo(0), Total: 1, Open: 1, OpenApproved: 1},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "single changeset open, approved, closed, merged",
|
||||
changesets: []*a8n.Changeset{
|
||||
ghChangeset(1, daysAgo(3)),
|
||||
},
|
||||
start: daysAgo(3),
|
||||
events: []Event{
|
||||
ghReview(1, daysAgo(2), "user1", "APPROVED"),
|
||||
fakeEvent{t: daysAgo(1), kind: a8n.ChangesetEventKindGitHubClosed, id: 1},
|
||||
fakeEvent{t: daysAgo(0), kind: a8n.ChangesetEventKindGitHubReopened, id: 1},
|
||||
fakeEvent{t: daysAgo(0), kind: a8n.ChangesetEventKindGitHubMerged, id: 1},
|
||||
},
|
||||
want: []*ChangesetCounts{
|
||||
{Time: daysAgo(3), Total: 1, Open: 1, OpenPending: 1},
|
||||
{Time: daysAgo(2), Total: 1, Open: 1, OpenApproved: 1},
|
||||
{Time: daysAgo(1), Total: 1, Closed: 1},
|
||||
{Time: daysAgo(0), Total: 1, Merged: 1},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "single changeset open, changes-requested, closed, reopened",
|
||||
changesets: []*a8n.Changeset{
|
||||
ghChangeset(1, daysAgo(3)),
|
||||
},
|
||||
start: daysAgo(3),
|
||||
events: []Event{
|
||||
ghReview(1, daysAgo(2), "user1", "CHANGES_REQUESTED"),
|
||||
fakeEvent{t: daysAgo(1), kind: a8n.ChangesetEventKindGitHubClosed, id: 1},
|
||||
fakeEvent{t: daysAgo(0), kind: a8n.ChangesetEventKindGitHubReopened, id: 1},
|
||||
},
|
||||
want: []*ChangesetCounts{
|
||||
{Time: daysAgo(3), Total: 1, Open: 1, OpenPending: 1},
|
||||
{Time: daysAgo(2), Total: 1, Open: 1, OpenChangesRequested: 1},
|
||||
{Time: daysAgo(1), Total: 1, Closed: 1},
|
||||
{Time: daysAgo(0), Total: 1, Open: 1, OpenChangesRequested: 1},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "single changeset open, changes-requested, closed, merged",
|
||||
changesets: []*a8n.Changeset{
|
||||
ghChangeset(1, daysAgo(3)),
|
||||
},
|
||||
start: daysAgo(3),
|
||||
events: []Event{
|
||||
ghReview(1, daysAgo(2), "user1", "CHANGES_REQUESTED"),
|
||||
fakeEvent{t: daysAgo(1), kind: a8n.ChangesetEventKindGitHubClosed, id: 1},
|
||||
fakeEvent{t: daysAgo(0), kind: a8n.ChangesetEventKindGitHubMerged, id: 1},
|
||||
},
|
||||
want: []*ChangesetCounts{
|
||||
{Time: daysAgo(3), Total: 1, Open: 1, OpenPending: 1},
|
||||
{Time: daysAgo(2), Total: 1, Open: 1, OpenChangesRequested: 1},
|
||||
{Time: daysAgo(1), Total: 1, Closed: 1},
|
||||
{Time: daysAgo(0), Total: 1, Merged: 1},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "single changeset open, comment review, approved, merged",
|
||||
changesets: []*a8n.Changeset{
|
||||
ghChangeset(1, daysAgo(3)),
|
||||
},
|
||||
start: daysAgo(4),
|
||||
events: []Event{
|
||||
ghReview(1, daysAgo(3), "user1", "COMMENTED"),
|
||||
ghReview(1, daysAgo(2), "user2", "APPROVED"),
|
||||
fakeEvent{t: daysAgo(1), kind: a8n.ChangesetEventKindGitHubMerged, id: 1},
|
||||
},
|
||||
want: []*ChangesetCounts{
|
||||
{Time: daysAgo(4), Total: 0, Open: 0},
|
||||
{Time: daysAgo(3), Total: 1, Open: 1, OpenPending: 1},
|
||||
{Time: daysAgo(2), Total: 1, Open: 1, OpenPending: 0, OpenApproved: 1},
|
||||
{Time: daysAgo(1), Total: 1, Merged: 1},
|
||||
{Time: daysAgo(0), Total: 1, Merged: 1},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "single changeset multiple approvals counting once",
|
||||
changesets: []*a8n.Changeset{
|
||||
ghChangeset(1, daysAgo(1)),
|
||||
},
|
||||
start: daysAgo(1),
|
||||
events: []Event{
|
||||
ghReview(1, daysAgo(1), "user1", "APPROVED"),
|
||||
ghReview(1, daysAgo(0), "user2", "APPROVED"),
|
||||
},
|
||||
want: []*ChangesetCounts{
|
||||
{Time: daysAgo(1), Total: 1, Open: 1, OpenPending: 0, OpenApproved: 1},
|
||||
{Time: daysAgo(0), Total: 1, Open: 1, OpenPending: 0, OpenApproved: 1},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "single changeset multiple changes-requested reviews counting once",
|
||||
changesets: []*a8n.Changeset{
|
||||
ghChangeset(1, daysAgo(1)),
|
||||
},
|
||||
start: daysAgo(1),
|
||||
events: []Event{
|
||||
ghReview(1, daysAgo(1), "user1", "CHANGES_REQUESTED"),
|
||||
ghReview(1, daysAgo(0), "user2", "CHANGES_REQUESTED"),
|
||||
},
|
||||
want: []*ChangesetCounts{
|
||||
{Time: daysAgo(1), Total: 1, Open: 1, OpenPending: 0, OpenChangesRequested: 1},
|
||||
{Time: daysAgo(0), Total: 1, Open: 1, OpenPending: 0, OpenChangesRequested: 1},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "single changeset open, changes-requested, merged",
|
||||
changesets: []*a8n.Changeset{
|
||||
ghChangeset(1, daysAgo(3)),
|
||||
},
|
||||
start: daysAgo(4),
|
||||
events: []Event{
|
||||
ghReview(1, daysAgo(2), "user1", "CHANGES_REQUESTED"),
|
||||
fakeEvent{t: daysAgo(1), kind: a8n.ChangesetEventKindGitHubMerged, id: 1},
|
||||
},
|
||||
want: []*ChangesetCounts{
|
||||
{Time: daysAgo(4), Total: 0, Open: 0},
|
||||
{Time: daysAgo(3), Total: 1, Open: 1, OpenPending: 1},
|
||||
{Time: daysAgo(2), Total: 1, Open: 1, OpenPending: 0, OpenChangesRequested: 1},
|
||||
{Time: daysAgo(1), Total: 1, Merged: 1},
|
||||
{Time: daysAgo(0), Total: 1, Merged: 1},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "multiple changesets open different review stages before merge",
|
||||
changesets: []*a8n.Changeset{
|
||||
ghChangeset(1, daysAgo(6)),
|
||||
ghChangeset(2, daysAgo(6)),
|
||||
ghChangeset(3, daysAgo(6)),
|
||||
},
|
||||
start: daysAgo(7),
|
||||
events: []Event{
|
||||
ghReview(1, daysAgo(5), "user1", "APPROVED"),
|
||||
fakeEvent{t: daysAgo(3), kind: a8n.ChangesetEventKindGitHubMerged, id: 1},
|
||||
ghReview(2, daysAgo(4), "user1", "APPROVED"),
|
||||
ghReview(2, daysAgo(3), "user2", "APPROVED"),
|
||||
fakeEvent{t: daysAgo(2), kind: a8n.ChangesetEventKindGitHubMerged, id: 2},
|
||||
ghReview(3, daysAgo(2), "user1", "CHANGES_REQUESTED"),
|
||||
ghReview(3, daysAgo(1), "user2", "CHANGES_REQUESTED"),
|
||||
fakeEvent{t: daysAgo(1), kind: a8n.ChangesetEventKindGitHubMerged, id: 3},
|
||||
},
|
||||
want: []*ChangesetCounts{
|
||||
{Time: daysAgo(7), Total: 0, Open: 0},
|
||||
{Time: daysAgo(6), Total: 3, Open: 3, OpenPending: 3},
|
||||
{Time: daysAgo(5), Total: 3, Open: 3, OpenPending: 2, OpenApproved: 1},
|
||||
{Time: daysAgo(4), Total: 3, Open: 3, OpenPending: 1, OpenApproved: 2},
|
||||
{Time: daysAgo(3), Total: 3, Open: 2, OpenPending: 1, OpenApproved: 1, Merged: 1},
|
||||
{Time: daysAgo(2), Total: 3, Open: 1, OpenPending: 0, OpenChangesRequested: 1, Merged: 2},
|
||||
{Time: daysAgo(1), Total: 3, Merged: 3},
|
||||
{Time: daysAgo(0), Total: 3, Merged: 3},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "time slice of multiple changesets in different stages before merge",
|
||||
changesets: []*a8n.Changeset{
|
||||
ghChangeset(1, daysAgo(6)),
|
||||
ghChangeset(2, daysAgo(6)),
|
||||
ghChangeset(3, daysAgo(6)),
|
||||
},
|
||||
// Same test as above, except we only look at 3 days in the middle
|
||||
start: daysAgo(4),
|
||||
end: daysAgo(2),
|
||||
events: []Event{
|
||||
ghReview(1, daysAgo(5), "user1", "APPROVED"),
|
||||
fakeEvent{t: daysAgo(3), kind: a8n.ChangesetEventKindGitHubMerged, id: 1},
|
||||
ghReview(2, daysAgo(4), "user1", "APPROVED"),
|
||||
fakeEvent{t: daysAgo(2), kind: a8n.ChangesetEventKindGitHubMerged, id: 2},
|
||||
ghReview(3, daysAgo(2), "user1", "CHANGES_REQUESTED"),
|
||||
fakeEvent{t: daysAgo(1), kind: a8n.ChangesetEventKindGitHubMerged, id: 3},
|
||||
},
|
||||
want: []*ChangesetCounts{
|
||||
{Time: daysAgo(4), Total: 3, Open: 3, OpenPending: 1, OpenApproved: 2},
|
||||
{Time: daysAgo(3), Total: 3, Open: 2, OpenPending: 1, OpenApproved: 1, Merged: 1},
|
||||
{Time: daysAgo(2), Total: 3, Open: 1, OpenPending: 0, OpenChangesRequested: 1, Merged: 2},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "single changeset with changes-requested then approved by same person",
|
||||
changesets: []*a8n.Changeset{
|
||||
ghChangeset(1, daysAgo(1)),
|
||||
},
|
||||
start: daysAgo(1),
|
||||
events: []Event{
|
||||
ghReview(1, daysAgo(1), "user1", "CHANGES_REQUESTED"),
|
||||
ghReview(1, daysAgo(0), "user1", "APPROVED"),
|
||||
},
|
||||
want: []*ChangesetCounts{
|
||||
{Time: daysAgo(1), Total: 1, Open: 1, OpenChangesRequested: 1},
|
||||
{Time: daysAgo(0), Total: 1, Open: 1, OpenApproved: 1},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "single changeset with approved then changes-requested by same person",
|
||||
changesets: []*a8n.Changeset{
|
||||
ghChangeset(1, daysAgo(1)),
|
||||
},
|
||||
start: daysAgo(1),
|
||||
events: []Event{
|
||||
ghReview(1, daysAgo(1), "user1", "APPROVED"),
|
||||
ghReview(1, daysAgo(0), "user1", "CHANGES_REQUESTED"),
|
||||
},
|
||||
want: []*ChangesetCounts{
|
||||
{Time: daysAgo(1), Total: 1, Open: 1, OpenApproved: 1},
|
||||
{Time: daysAgo(0), Total: 1, Open: 1, OpenChangesRequested: 1},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "single changeset with approval by one person then changes-requested by another",
|
||||
changesets: []*a8n.Changeset{
|
||||
ghChangeset(1, daysAgo(1)),
|
||||
},
|
||||
start: daysAgo(1),
|
||||
events: []Event{
|
||||
ghReview(1, daysAgo(1), "user1", "APPROVED"),
|
||||
ghReview(1, daysAgo(0), "user2", "CHANGES_REQUESTED"), // This has higher precedence
|
||||
},
|
||||
want: []*ChangesetCounts{
|
||||
{Time: daysAgo(1), Total: 1, Open: 1, OpenApproved: 1},
|
||||
{Time: daysAgo(0), Total: 1, Open: 1, OpenChangesRequested: 1},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "single changeset with changes-requested by one person then approval by another",
|
||||
changesets: []*a8n.Changeset{
|
||||
ghChangeset(1, daysAgo(1)),
|
||||
},
|
||||
start: daysAgo(1),
|
||||
events: []Event{
|
||||
ghReview(1, daysAgo(1), "user1", "CHANGES_REQUESTED"),
|
||||
ghReview(1, daysAgo(0), "user2", "APPROVED"),
|
||||
},
|
||||
want: []*ChangesetCounts{
|
||||
{Time: daysAgo(1), Total: 1, Open: 1, OpenChangesRequested: 1},
|
||||
{Time: daysAgo(0), Total: 1, Open: 1, OpenChangesRequested: 1},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "single changeset with changes-requested by one person, approval by another, then approval by first person",
|
||||
changesets: []*a8n.Changeset{
|
||||
ghChangeset(1, daysAgo(2)),
|
||||
},
|
||||
start: daysAgo(2),
|
||||
events: []Event{
|
||||
ghReview(1, daysAgo(2), "user1", "CHANGES_REQUESTED"),
|
||||
ghReview(1, daysAgo(1), "user2", "APPROVED"),
|
||||
ghReview(1, daysAgo(0), "user1", "APPROVED"),
|
||||
},
|
||||
want: []*ChangesetCounts{
|
||||
{Time: daysAgo(2), Total: 1, Open: 1, OpenChangesRequested: 1},
|
||||
{Time: daysAgo(1), Total: 1, Open: 1, OpenChangesRequested: 1},
|
||||
{Time: daysAgo(0), Total: 1, Open: 1, OpenApproved: 1},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "single changeset with approval by one person, changes-requested by another, then changes-requested by first person",
|
||||
changesets: []*a8n.Changeset{
|
||||
ghChangeset(1, daysAgo(2)),
|
||||
},
|
||||
start: daysAgo(2),
|
||||
events: []Event{
|
||||
ghReview(1, daysAgo(2), "user1", "APPROVED"),
|
||||
ghReview(1, daysAgo(1), "user2", "CHANGES_REQUESTED"),
|
||||
ghReview(1, daysAgo(0), "user1", "CHANGES_REQUESTED"),
|
||||
},
|
||||
want: []*ChangesetCounts{
|
||||
{Time: daysAgo(2), Total: 1, Open: 1, OpenApproved: 1},
|
||||
{Time: daysAgo(1), Total: 1, Open: 1, OpenChangesRequested: 1},
|
||||
{Time: daysAgo(0), Total: 1, Open: 1, OpenChangesRequested: 1},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
if tc.end.IsZero() {
|
||||
tc.end = now
|
||||
}
|
||||
|
||||
have, err := CalcCounts(tc.start, tc.end, tc.changesets, tc.events...)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(have, tc.want) {
|
||||
t.Errorf("wrong counts calculated. diff=%s", cmp.Diff(have, tc.want))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type fakeEvent struct {
|
||||
t time.Time
|
||||
kind a8n.ChangesetEventKind
|
||||
id int64
|
||||
}
|
||||
|
||||
func (e fakeEvent) Timestamp() time.Time { return e.t }
|
||||
func (e fakeEvent) Type() a8n.ChangesetEventKind { return e.kind }
|
||||
func (e fakeEvent) Changeset() int64 { return e.id }
|
||||
|
||||
func ghChangeset(id int64, t time.Time) *a8n.Changeset {
|
||||
return &a8n.Changeset{ID: id, Metadata: &github.PullRequest{CreatedAt: t}}
|
||||
}
|
||||
|
||||
func ghReview(id int64, t time.Time, login, state string) *a8n.ChangesetEvent {
|
||||
return &a8n.ChangesetEvent{
|
||||
ChangesetID: id,
|
||||
Kind: a8n.ChangesetEventKindGitHubReviewed,
|
||||
Metadata: &github.PullRequestReview{
|
||||
UpdatedAt: t,
|
||||
State: state,
|
||||
Author: github.Actor{
|
||||
Login: login,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
@ -5,6 +5,7 @@ import (
|
||||
"database/sql"
|
||||
"path"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/graph-gophers/graphql-go"
|
||||
"github.com/graph-gophers/graphql-go/relay"
|
||||
@ -353,6 +354,73 @@ func (r *campaignResolver) Changesets(ctx context.Context, args struct {
|
||||
}
|
||||
}
|
||||
|
||||
func (r *campaignResolver) ChangesetCountsOverTime(
|
||||
ctx context.Context,
|
||||
args *graphqlbackend.ChangesetCountsArgs,
|
||||
) ([]graphqlbackend.ChangesetCountsResolver, error) {
|
||||
// 🚨 SECURITY: Only site admins may access the counts for now
|
||||
if err := backend.CheckCurrentUserIsSiteAdmin(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resolvers := []graphqlbackend.ChangesetCountsResolver{}
|
||||
|
||||
opts := ee.ListChangesetsOpts{CampaignID: r.Campaign.ID}
|
||||
cs, _, err := r.store.ListChangesets(ctx, opts)
|
||||
if err != nil {
|
||||
return resolvers, err
|
||||
}
|
||||
|
||||
start := r.Campaign.CreatedAt.UTC()
|
||||
if args.From != nil {
|
||||
start = args.From.Time.UTC()
|
||||
}
|
||||
|
||||
end := time.Now().UTC()
|
||||
if args.To != nil && args.To.Time.Before(end) {
|
||||
end = args.To.Time.UTC()
|
||||
}
|
||||
|
||||
changesetIDs := make([]int64, len(cs))
|
||||
for i, c := range cs {
|
||||
changesetIDs[i] = c.ID
|
||||
}
|
||||
var (
|
||||
events []ee.Event
|
||||
eventsOpts = ee.ListChangesetEventsOpts{
|
||||
ChangesetIDs: changesetIDs,
|
||||
Limit: 1000,
|
||||
}
|
||||
)
|
||||
|
||||
for {
|
||||
es, next, err := r.store.ListChangesetEvents(ctx, eventsOpts)
|
||||
if err != nil {
|
||||
return resolvers, err
|
||||
}
|
||||
|
||||
for _, e := range es {
|
||||
events = append(events, e)
|
||||
}
|
||||
|
||||
if next == 0 {
|
||||
break
|
||||
}
|
||||
eventsOpts.Cursor = next
|
||||
}
|
||||
|
||||
counts, err := ee.CalcCounts(start, end, cs, events...)
|
||||
if err != nil {
|
||||
return resolvers, err
|
||||
}
|
||||
|
||||
for _, c := range counts {
|
||||
resolvers = append(resolvers, &changesetCountsResolver{counts: c})
|
||||
}
|
||||
|
||||
return resolvers, nil
|
||||
}
|
||||
|
||||
func (r *Resolver) CreateChangesets(ctx context.Context, args *graphqlbackend.CreateChangesetsArgs) (_ []graphqlbackend.ChangesetResolver, err error) {
|
||||
// 🚨 SECURITY: Only site admins may create changesets for now
|
||||
if err := backend.CheckCurrentUserIsSiteAdmin(ctx); err != nil {
|
||||
@ -587,8 +655,8 @@ func (r *changesetResolver) Events(ctx context.Context, args *struct {
|
||||
store: r.store,
|
||||
changeset: r.Changeset,
|
||||
opts: ee.ListChangesetEventsOpts{
|
||||
ChangesetID: r.Changeset.ID,
|
||||
Limit: int(args.ConnectionArgs.GetFirst()),
|
||||
ChangesetIDs: []int64{r.Changeset.ID},
|
||||
Limit: int(args.ConnectionArgs.GetFirst()),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
@ -618,7 +686,7 @@ func (r *changesetEventsConnectionResolver) Nodes(ctx context.Context) ([]graphq
|
||||
}
|
||||
|
||||
func (r *changesetEventsConnectionResolver) TotalCount(ctx context.Context) (int32, error) {
|
||||
opts := ee.CountChangesetEventsOpts{ChangesetID: r.opts.ChangesetID}
|
||||
opts := ee.CountChangesetEventsOpts{ChangesetID: r.opts.ChangesetIDs[0]}
|
||||
count, err := r.store.CountChangesetEvents(ctx, opts)
|
||||
return int32(count), err
|
||||
}
|
||||
@ -667,3 +735,18 @@ func unmarshalRepositoryID(id graphql.ID) (repo api.RepoID, err error) {
|
||||
err = relay.UnmarshalSpec(id, &repo)
|
||||
return
|
||||
}
|
||||
|
||||
type changesetCountsResolver struct {
|
||||
counts *ee.ChangesetCounts
|
||||
}
|
||||
|
||||
func (r *changesetCountsResolver) Date() graphqlbackend.DateTime {
|
||||
return graphqlbackend.DateTime{Time: r.counts.Time}
|
||||
}
|
||||
func (r *changesetCountsResolver) Total() int32 { return r.counts.Total }
|
||||
func (r *changesetCountsResolver) Merged() int32 { return r.counts.Merged }
|
||||
func (r *changesetCountsResolver) Closed() int32 { return r.counts.Closed }
|
||||
func (r *changesetCountsResolver) Open() int32 { return r.counts.Open }
|
||||
func (r *changesetCountsResolver) OpenApproved() int32 { return r.counts.OpenApproved }
|
||||
func (r *changesetCountsResolver) OpenChangesRequested() int32 { return r.counts.OpenChangesRequested }
|
||||
func (r *changesetCountsResolver) OpenPending() int32 { return r.counts.OpenPending }
|
||||
|
||||
@ -19,9 +19,11 @@ import (
|
||||
graphql "github.com/graph-gophers/graphql-go"
|
||||
"github.com/graph-gophers/graphql-go/errors"
|
||||
"github.com/sourcegraph/sourcegraph/cmd/frontend/backend"
|
||||
"github.com/sourcegraph/sourcegraph/cmd/frontend/db"
|
||||
"github.com/sourcegraph/sourcegraph/cmd/frontend/graphqlbackend"
|
||||
"github.com/sourcegraph/sourcegraph/cmd/repo-updater/repos"
|
||||
"github.com/sourcegraph/sourcegraph/enterprise/pkg/a8n"
|
||||
ee "github.com/sourcegraph/sourcegraph/enterprise/pkg/a8n"
|
||||
"github.com/sourcegraph/sourcegraph/internal/a8n"
|
||||
"github.com/sourcegraph/sourcegraph/internal/actor"
|
||||
"github.com/sourcegraph/sourcegraph/internal/api"
|
||||
"github.com/sourcegraph/sourcegraph/internal/db/dbconn"
|
||||
@ -57,7 +59,7 @@ func TestCampaigns(t *testing.T) {
|
||||
}
|
||||
|
||||
sr := &Resolver{
|
||||
store: a8n.NewStoreWithClock(dbconn.Global, clock),
|
||||
store: ee.NewStoreWithClock(dbconn.Global, clock),
|
||||
httpFactory: cf,
|
||||
}
|
||||
|
||||
@ -433,15 +435,27 @@ func TestCampaigns(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
type ChangesetCounts struct {
|
||||
Date graphqlbackend.DateTime
|
||||
Total int32
|
||||
Merged int32
|
||||
Closed int32
|
||||
Open int32
|
||||
OpenApproved int32
|
||||
OpenChangesRequested int32
|
||||
OpenPending int32
|
||||
}
|
||||
|
||||
type CampaignWithChangesets struct {
|
||||
ID string
|
||||
Name string
|
||||
Description string
|
||||
Author User
|
||||
CreatedAt string
|
||||
UpdatedAt string
|
||||
Namespace UserOrg
|
||||
Changesets ChangesetConnection
|
||||
ID string
|
||||
Name string
|
||||
Description string
|
||||
Author User
|
||||
CreatedAt string
|
||||
UpdatedAt string
|
||||
Namespace UserOrg
|
||||
Changesets ChangesetConnection
|
||||
ChangesetCountsOverTime []ChangesetCounts
|
||||
}
|
||||
|
||||
var addChangesetsResult struct{ Campaign CampaignWithChangesets }
|
||||
@ -451,6 +465,11 @@ func TestCampaigns(t *testing.T) {
|
||||
changesetIDs = append(changesetIDs, c.ID)
|
||||
}
|
||||
|
||||
// Date when PR #999 from above was created
|
||||
countsFrom := parseJSONTime(t, "2018-11-14T22:07:45Z")
|
||||
// Date when PR #999 from above was merged
|
||||
countsTo := parseJSONTime(t, "2018-12-04T08:10:07Z")
|
||||
|
||||
mustExec(ctx, t, s, nil, &addChangesetsResult, fmt.Sprintf(`
|
||||
fragment u on User { id, databaseID, siteAdmin }
|
||||
fragment o on Org { id, name }
|
||||
@ -483,13 +502,28 @@ func TestCampaigns(t *testing.T) {
|
||||
totalCount
|
||||
pageInfo { hasNextPage }
|
||||
}
|
||||
changesetCountsOverTime(from: %s, to: %s) {
|
||||
date
|
||||
total
|
||||
merged
|
||||
closed
|
||||
open
|
||||
openApproved
|
||||
openChangesRequested
|
||||
openPending
|
||||
}
|
||||
}
|
||||
mutation() {
|
||||
campaign: addChangesetsToCampaign(campaign: %q, changesets: %s) {
|
||||
...c
|
||||
}
|
||||
}
|
||||
`, campaigns.Admin.ID, marshalJSON(t, changesetIDs)))
|
||||
`,
|
||||
marshalDateTime(t, countsFrom),
|
||||
marshalDateTime(t, countsTo),
|
||||
campaigns.Admin.ID,
|
||||
marshalJSON(t, changesetIDs),
|
||||
))
|
||||
|
||||
{
|
||||
have := addChangesetsResult.Campaign.Changesets.TotalCount
|
||||
@ -527,6 +561,21 @@ func TestCampaigns(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
counts := addChangesetsResult.Campaign.ChangesetCountsOverTime
|
||||
|
||||
// There's 20 1-day intervals between countsFrom and including countsTo
|
||||
if have, want := len(counts), 20; have != want {
|
||||
t.Errorf("wrong changeset counts length %d, have=%d", want, have)
|
||||
}
|
||||
|
||||
for _, c := range counts {
|
||||
if have, want := c.Total, int32(1); have != want {
|
||||
t.Errorf("wrong changeset counts total %d, have=%d", want, have)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
deleteInput := map[string]interface{}{"id": campaigns.Admin.ID}
|
||||
mustExec(ctx, t, s, deleteInput, &struct{}{}, `
|
||||
mutation($id: ID!){
|
||||
@ -551,6 +600,157 @@ func TestCampaigns(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestChangesetCountsOverTime(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip()
|
||||
}
|
||||
|
||||
ctx := backend.WithAuthzBypass(context.Background())
|
||||
dbtesting.SetupGlobalTestDB(t)
|
||||
rcache.SetupForTest(t)
|
||||
|
||||
cf, save := newGithubClientFactory(t, "test-changeset-counts-over-time")
|
||||
defer save()
|
||||
|
||||
now := time.Now().UTC().Truncate(time.Microsecond)
|
||||
clock := func() time.Time {
|
||||
return now.UTC().Truncate(time.Microsecond)
|
||||
}
|
||||
|
||||
u, err := db.Users.Create(ctx, db.NewUser{
|
||||
Email: "thorsten@sourcegraph.com",
|
||||
Username: "thorsten",
|
||||
DisplayName: "thorsten",
|
||||
Password: "1234",
|
||||
EmailVerificationCode: "foobar",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
repoStore := repos.NewDBStore(dbconn.Global, sql.TxOptions{})
|
||||
githubExtSvc := &repos.ExternalService{
|
||||
Kind: "GITHUB",
|
||||
DisplayName: "GitHub",
|
||||
Config: marshalJSON(t, &schema.GitHubConnection{
|
||||
Url: "https://github.com",
|
||||
Token: os.Getenv("GITHUB_TOKEN"),
|
||||
Repos: []string{"sourcegraph/sourcegraph"},
|
||||
}),
|
||||
}
|
||||
|
||||
err = repoStore.UpsertExternalServices(ctx, githubExtSvc)
|
||||
if err != nil {
|
||||
t.Fatal(t)
|
||||
}
|
||||
|
||||
githubSrc, err := repos.NewGithubSource(githubExtSvc, cf)
|
||||
if err != nil {
|
||||
t.Fatal(t)
|
||||
}
|
||||
|
||||
githubRepo, err := githubSrc.GetRepo(ctx, "sourcegraph/sourcegraph")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = repoStore.UpsertRepos(ctx, githubRepo)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
store := ee.NewStoreWithClock(dbconn.Global, clock)
|
||||
|
||||
campaign := &a8n.Campaign{
|
||||
Name: "Test campaign",
|
||||
Description: "Testing changeset counts",
|
||||
AuthorID: u.ID,
|
||||
NamespaceUserID: u.ID,
|
||||
}
|
||||
|
||||
err = store.CreateCampaign(ctx, campaign)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
changesets := []*a8n.Changeset{
|
||||
{
|
||||
RepoID: int32(githubRepo.ID),
|
||||
ExternalID: "5834",
|
||||
ExternalServiceType: githubRepo.ExternalRepo.ServiceType,
|
||||
CampaignIDs: []int64{campaign.ID},
|
||||
},
|
||||
{
|
||||
RepoID: int32(githubRepo.ID),
|
||||
ExternalID: "5849",
|
||||
ExternalServiceType: githubRepo.ExternalRepo.ServiceType,
|
||||
CampaignIDs: []int64{campaign.ID},
|
||||
},
|
||||
}
|
||||
|
||||
err = store.CreateChangesets(ctx, changesets...)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
syncer := ee.ChangesetSyncer{
|
||||
ReposStore: repoStore,
|
||||
Store: store,
|
||||
HTTPFactory: cf,
|
||||
}
|
||||
err = syncer.SyncChangesets(ctx, changesets...)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
for _, c := range changesets {
|
||||
campaign.ChangesetIDs = append(campaign.ChangesetIDs, c.ID)
|
||||
}
|
||||
err = store.UpdateCampaign(ctx, campaign)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Date when PR #5834 was created: "2019-10-02T14:49:31Z"
|
||||
// We start exactly one day earlier
|
||||
// Date when PR #5849 was created: "2019-10-03T15:03:21Z"
|
||||
start := parseJSONTime(t, "2019-10-01T14:49:31Z")
|
||||
// Date when PR #5834 was merged: "2019-10-07T13:13:45Z"
|
||||
// Date when PR #5849 was merged: "2019-10-04T08:55:21Z"
|
||||
end := parseJSONTime(t, "2019-10-07T13:13:45Z")
|
||||
daysBeforeEnd := func(days int) time.Time {
|
||||
return end.AddDate(0, 0, -days)
|
||||
}
|
||||
|
||||
r := &campaignResolver{store: store, Campaign: campaign}
|
||||
rs, err := r.ChangesetCountsOverTime(ctx, &graphqlbackend.ChangesetCountsArgs{
|
||||
From: &graphqlbackend.DateTime{Time: start},
|
||||
To: &graphqlbackend.DateTime{Time: end},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("ChangsetCountsOverTime failed with error: %s", err)
|
||||
}
|
||||
|
||||
have := make([]*ee.ChangesetCounts, 0, len(rs))
|
||||
for _, cr := range rs {
|
||||
r := cr.(*changesetCountsResolver)
|
||||
have = append(have, r.counts)
|
||||
}
|
||||
|
||||
want := []*ee.ChangesetCounts{
|
||||
{Time: daysBeforeEnd(5), Total: 0, Open: 0},
|
||||
{Time: daysBeforeEnd(4), Total: 1, Open: 1, OpenPending: 1},
|
||||
{Time: daysBeforeEnd(3), Total: 2, Open: 1, OpenPending: 1, Merged: 1},
|
||||
{Time: daysBeforeEnd(2), Total: 2, Open: 1, OpenPending: 1, Merged: 1},
|
||||
{Time: daysBeforeEnd(1), Total: 2, Open: 1, OpenPending: 1, Merged: 1},
|
||||
{Time: end, Total: 2, Merged: 2},
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(have, want) {
|
||||
t.Errorf("wrong counts listed. diff=%s", cmp.Diff(have, want))
|
||||
}
|
||||
}
|
||||
|
||||
func mustExec(
|
||||
ctx context.Context,
|
||||
t testing.TB,
|
||||
@ -651,6 +851,30 @@ func marshalJSON(t testing.TB, v interface{}) string {
|
||||
return string(bs)
|
||||
}
|
||||
|
||||
func marshalDateTime(t testing.TB, ts time.Time) string {
|
||||
t.Helper()
|
||||
|
||||
dt := graphqlbackend.DateTime{Time: ts}
|
||||
|
||||
bs, err := dt.MarshalJSON()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
return string(bs)
|
||||
}
|
||||
|
||||
func parseJSONTime(t testing.TB, ts string) time.Time {
|
||||
t.Helper()
|
||||
|
||||
timestamp, err := time.Parse(time.RFC3339, ts)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
return timestamp
|
||||
}
|
||||
|
||||
func getBitbucketServerRepos(t testing.TB, ctx context.Context, src *repos.BitbucketServerSource) []*repos.Repo {
|
||||
results := make(chan repos.SourceResult)
|
||||
|
||||
|
||||
254
enterprise/pkg/a8n/resolvers/testdata/vcr/test-changeset-counts-over-time.yaml
vendored
Normal file
254
enterprise/pkg/a8n/resolvers/testdata/vcr/test-changeset-counts-over-time.yaml
vendored
Normal file
File diff suppressed because one or more lines are too long
@ -502,9 +502,9 @@ func getChangesetEventQuery(opts *GetChangesetEventOpts) *sqlf.Query {
|
||||
// ListChangesetEventsOpts captures the query options needed for
|
||||
// listing changeset events.
|
||||
type ListChangesetEventsOpts struct {
|
||||
ChangesetID int64
|
||||
Cursor int64
|
||||
Limit int
|
||||
ChangesetIDs []int64
|
||||
Cursor int64
|
||||
Limit int
|
||||
}
|
||||
|
||||
// ListChangesetEvents lists ChangesetEvents with the given filters.
|
||||
@ -555,8 +555,15 @@ func listChangesetEventsQuery(opts *ListChangesetEventsOpts) *sqlf.Query {
|
||||
sqlf.Sprintf("id >= %s", opts.Cursor),
|
||||
}
|
||||
|
||||
if opts.ChangesetID != 0 {
|
||||
preds = append(preds, sqlf.Sprintf("changeset_id = %s", opts.ChangesetID))
|
||||
if len(opts.ChangesetIDs) != 0 {
|
||||
ids := make([]*sqlf.Query, 0, len(opts.ChangesetIDs))
|
||||
for _, id := range opts.ChangesetIDs {
|
||||
if id != 0 {
|
||||
ids = append(ids, sqlf.Sprintf("%d", id))
|
||||
}
|
||||
}
|
||||
preds = append(preds,
|
||||
sqlf.Sprintf("changeset_id IN (%s)", sqlf.Join(ids, ",")))
|
||||
}
|
||||
|
||||
return sqlf.Sprintf(
|
||||
|
||||
@ -682,58 +682,84 @@ func testStore(db *sql.DB) func(*testing.T) {
|
||||
})
|
||||
|
||||
t.Run("List", func(t *testing.T) {
|
||||
for i := 1; i <= len(events); i++ {
|
||||
opts := ListChangesetEventsOpts{ChangesetID: int64(i)}
|
||||
t.Run("ByChangesetIDs", func(t *testing.T) {
|
||||
for i := 1; i <= len(events); i++ {
|
||||
opts := ListChangesetEventsOpts{ChangesetIDs: []int64{int64(i)}}
|
||||
|
||||
ts, next, err := s.ListChangesetEvents(ctx, opts)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if have, want := next, int64(0); have != want {
|
||||
t.Fatalf("opts: %+v: have next %v, want %v", opts, have, want)
|
||||
}
|
||||
|
||||
have, want := ts, events[i-1:i]
|
||||
if len(have) != len(want) {
|
||||
t.Fatalf("listed %d events, want: %d", len(have), len(want))
|
||||
}
|
||||
|
||||
if diff := cmp.Diff(have, want); diff != "" {
|
||||
t.Fatalf("opts: %+v, diff: %s", opts, diff)
|
||||
}
|
||||
}
|
||||
|
||||
for i := 1; i <= len(events); i++ {
|
||||
cs, next, err := s.ListChangesetEvents(ctx, ListChangesetEventsOpts{Limit: i})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
{
|
||||
have, want := next, int64(0)
|
||||
if i < len(events) {
|
||||
want = events[i].ID
|
||||
ts, next, err := s.ListChangesetEvents(ctx, opts)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if have != want {
|
||||
t.Fatalf("limit: %v: have next %v, want %v", i, have, want)
|
||||
if have, want := next, int64(0); have != want {
|
||||
t.Fatalf("opts: %+v: have next %v, want %v", opts, have, want)
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
have, want := cs, events[:i]
|
||||
have, want := ts, events[i-1:i]
|
||||
if len(have) != len(want) {
|
||||
t.Fatalf("listed %d events, want: %d", len(have), len(want))
|
||||
}
|
||||
|
||||
if diff := cmp.Diff(have, want); diff != "" {
|
||||
t.Fatal(diff)
|
||||
t.Fatalf("opts: %+v, diff: %s", opts, diff)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
{
|
||||
opts := ListChangesetEventsOpts{ChangesetIDs: []int64{}}
|
||||
|
||||
for i := 1; i <= len(events); i++ {
|
||||
opts.ChangesetIDs = append(opts.ChangesetIDs, int64(i))
|
||||
}
|
||||
|
||||
ts, next, err := s.ListChangesetEvents(ctx, opts)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if have, want := next, int64(0); have != want {
|
||||
t.Fatalf("opts: %+v: have next %v, want %v", opts, have, want)
|
||||
}
|
||||
|
||||
have, want := ts, events
|
||||
if len(have) != len(want) {
|
||||
t.Fatalf("listed %d events, want: %d", len(have), len(want))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("WithLimit", func(t *testing.T) {
|
||||
for i := 1; i <= len(events); i++ {
|
||||
cs, next, err := s.ListChangesetEvents(ctx, ListChangesetEventsOpts{Limit: i})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
{
|
||||
have, want := next, int64(0)
|
||||
if i < len(events) {
|
||||
want = events[i].ID
|
||||
}
|
||||
|
||||
if have != want {
|
||||
t.Fatalf("limit: %v: have next %v, want %v", i, have, want)
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
have, want := cs, events[:i]
|
||||
if len(have) != len(want) {
|
||||
t.Fatalf("listed %d events, want: %d", len(have), len(want))
|
||||
}
|
||||
|
||||
if diff := cmp.Diff(have, want); diff != "" {
|
||||
t.Fatal(diff)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("WithCursor", func(t *testing.T) {
|
||||
var cursor int64
|
||||
for i := 1; i <= len(events); i++ {
|
||||
opts := ListChangesetEventsOpts{Cursor: cursor, Limit: 1}
|
||||
@ -749,7 +775,7 @@ func testStore(db *sql.DB) func(*testing.T) {
|
||||
|
||||
cursor = next
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@ -241,7 +241,7 @@ func testGitHubWebhook(db *sql.DB) func(*testing.T) {
|
||||
}
|
||||
|
||||
req.Header.Set("X-Github-Event", tc.event.name)
|
||||
req.Header.Set("X-Hub-Signature", sign(body, []byte(tc.secret)))
|
||||
req.Header.Set("X-Hub-Signature", sign(t, body, []byte(tc.secret)))
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
hook.ServeHTTP(rec, req)
|
||||
@ -309,9 +309,16 @@ func loadFixtures(t testing.TB) map[string]event {
|
||||
return fs
|
||||
}
|
||||
|
||||
func sign(message, secret []byte) string {
|
||||
func sign(t *testing.T, message, secret []byte) string {
|
||||
t.Helper()
|
||||
|
||||
mac := hmac.New(sha256.New, secret)
|
||||
mac.Write(message)
|
||||
|
||||
_, err := mac.Write(message)
|
||||
if err != nil {
|
||||
t.Fatalf("writing hmac message failed: %s", err)
|
||||
}
|
||||
|
||||
return "sha256=" + hex.EncodeToString(mac.Sum(nil))
|
||||
}
|
||||
|
||||
|
||||
@ -58,6 +58,7 @@ const (
|
||||
ChangesetReviewStateApproved ChangesetReviewState = "APPROVED"
|
||||
ChangesetReviewStateChangesRequested ChangesetReviewState = "CHANGES_REQUESTED"
|
||||
ChangesetReviewStatePending ChangesetReviewState = "PENDING"
|
||||
ChangesetReviewStateCommented ChangesetReviewState = "COMMENTED"
|
||||
)
|
||||
|
||||
// Valid returns true if the given Changeset is valid.
|
||||
@ -65,7 +66,8 @@ func (s ChangesetReviewState) Valid() bool {
|
||||
switch s {
|
||||
case ChangesetReviewStateApproved,
|
||||
ChangesetReviewStateChangesRequested,
|
||||
ChangesetReviewStatePending:
|
||||
ChangesetReviewStatePending,
|
||||
ChangesetReviewStateCommented:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
@ -104,6 +106,20 @@ func (t *Changeset) Title() (string, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// ExternalCreatedAt is when the Changeset was created on the codehost. When it
|
||||
// cannot be determined when the changeset was created, a zero-value timestamp
|
||||
// is returned.
|
||||
func (t *Changeset) ExternalCreatedAt() time.Time {
|
||||
switch m := t.Metadata.(type) {
|
||||
case *github.PullRequest:
|
||||
return m.CreatedAt
|
||||
case *bitbucketserver.PullRequest:
|
||||
return unixMilliToTime(int64(m.CreatedDate))
|
||||
default:
|
||||
return time.Time{}
|
||||
}
|
||||
}
|
||||
|
||||
// Body of the Changeset.
|
||||
func (t *Changeset) Body() (string, error) {
|
||||
switch m := t.Metadata.(type) {
|
||||
@ -176,7 +192,7 @@ func (t *Changeset) ReviewState() (s ChangesetReviewState, err error) {
|
||||
return "", errors.New("unknown changeset type")
|
||||
}
|
||||
|
||||
return selectReviewState(states), nil
|
||||
return SelectReviewState(states), nil
|
||||
}
|
||||
|
||||
// Events returns the list of ChangesetEvents from the Changeset's metadata.
|
||||
@ -207,7 +223,11 @@ func (t *Changeset) Events() (events []*ChangesetEvent) {
|
||||
return events
|
||||
}
|
||||
|
||||
func selectReviewState(states map[ChangesetReviewState]bool) ChangesetReviewState {
|
||||
// SelectReviewState computes the single review state for a given set of
|
||||
// ChangesetReviewStates. Since a pull request, for example, can have multiple
|
||||
// reviews with different states, we need a function to determine what the
|
||||
// state for the pull request is.
|
||||
func SelectReviewState(states map[ChangesetReviewState]bool) ChangesetReviewState {
|
||||
// If any review requested changes, that state takes precedence over all
|
||||
// other review states, followed by explicit approval. Everything else is
|
||||
// considered pending.
|
||||
@ -241,6 +261,51 @@ func (e *ChangesetEvent) Clone() *ChangesetEvent {
|
||||
return &ee
|
||||
}
|
||||
|
||||
// Type returns the ChangesetEventKind of the ChangesetEvent.
|
||||
func (e *ChangesetEvent) Type() ChangesetEventKind {
|
||||
return e.Kind
|
||||
}
|
||||
|
||||
// Changeset returns the changeset ID of the ChangesetEvent.
|
||||
func (e *ChangesetEvent) Changeset() int64 {
|
||||
return e.ChangesetID
|
||||
}
|
||||
|
||||
// Timestamp returns the time when the ChangesetEvent happened (or was updated)
|
||||
// on the codehost, not when it was created in Sourcegraph's database.
|
||||
func (e *ChangesetEvent) Timestamp() time.Time {
|
||||
var t time.Time
|
||||
|
||||
switch e := e.Metadata.(type) {
|
||||
case *github.AssignedEvent:
|
||||
t = e.CreatedAt
|
||||
case *github.ClosedEvent:
|
||||
t = e.CreatedAt
|
||||
case *github.IssueComment:
|
||||
t = e.UpdatedAt
|
||||
case *github.RenamedTitleEvent:
|
||||
t = e.CreatedAt
|
||||
case *github.MergedEvent:
|
||||
t = e.CreatedAt
|
||||
case *github.PullRequestReview:
|
||||
t = e.UpdatedAt
|
||||
case *github.PullRequestReviewComment:
|
||||
t = e.UpdatedAt
|
||||
case *github.ReopenedEvent:
|
||||
t = e.CreatedAt
|
||||
case *github.ReviewDismissedEvent:
|
||||
t = e.CreatedAt
|
||||
case *github.ReviewRequestRemovedEvent:
|
||||
t = e.CreatedAt
|
||||
case *github.ReviewRequestedEvent:
|
||||
t = e.CreatedAt
|
||||
case *github.UnassignedEvent:
|
||||
t = e.CreatedAt
|
||||
}
|
||||
|
||||
return t
|
||||
}
|
||||
|
||||
// Update updates the metadata of e with new metadata in o.
|
||||
func (e *ChangesetEvent) Update(o *ChangesetEvent) {
|
||||
if e.ChangesetID != o.ChangesetID || e.Kind != o.Kind || e.Key != o.Key {
|
||||
@ -621,3 +686,7 @@ const (
|
||||
// - UNAPPROVED
|
||||
// - UPDATED
|
||||
)
|
||||
|
||||
func unixMilliToTime(ms int64) time.Time {
|
||||
return time.Unix(0, ms*int64(time.Millisecond))
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user