snapshot of https://github.com/traggo/server.git (master, 2026-09-16; blobs > 131072B omitted)
Some checks failed
build / build (push) Failing after 3s
Some checks failed
build / build (push) Failing after 3s
This commit is contained in:
8
statistics/statisticsresolver.go
Normal file
8
statistics/statisticsresolver.go
Normal file
@@ -0,0 +1,8 @@
|
||||
package statistics
|
||||
|
||||
import "github.com/jinzhu/gorm"
|
||||
|
||||
// ResolverForStatistics resolves statistic things.
|
||||
type ResolverForStatistics struct {
|
||||
DB *gorm.DB
|
||||
}
|
||||
189
statistics/summary.go
Normal file
189
statistics/summary.go
Normal file
@@ -0,0 +1,189 @@
|
||||
package statistics
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/traggo/server/auth"
|
||||
"github.com/traggo/server/generated/gqlmodel"
|
||||
"github.com/traggo/server/model"
|
||||
)
|
||||
|
||||
// Stats groups the time spans by tag key.
|
||||
func (r *ResolverForStatistics) Stats(ctx context.Context, ranges []*gqlmodel.Range, tags []string, excludeTags []*gqlmodel.InputTimeSpanTag, requireTags []*gqlmodel.InputTimeSpanTag) ([]*gqlmodel.RangedStatisticsEntries, error) {
|
||||
if len(ranges) == 0 {
|
||||
return nil, errors.New("ranges may not be empty")
|
||||
}
|
||||
var variables []interface{}
|
||||
var rangesStrs []string
|
||||
for _, r := range ranges {
|
||||
if r.Start.Time().After(r.End.Time()) {
|
||||
return nil, fmt.Errorf("range start must be before range end")
|
||||
}
|
||||
rangesStrs = append(rangesStrs, "(?, ?)")
|
||||
variables = append(variables, r.Start.OmitTimeZone(), r.End.OmitTimeZone())
|
||||
}
|
||||
queryRanges := strings.Join(rangesStrs, ", ")
|
||||
|
||||
queryRequire, requireVars := build(requireTags, "1 = 1")
|
||||
variables = append(variables, requireVars...)
|
||||
|
||||
queryExclude, excludeVars := build(excludeTags, "1 != 1")
|
||||
variables = append(variables, excludeVars...)
|
||||
|
||||
query := fmt.Sprintf(`
|
||||
WITH dates(query_start, query_end) AS (
|
||||
VALUES %s
|
||||
)
|
||||
SELECT query_start as query_start,
|
||||
query_end as query_end,
|
||||
key as key,
|
||||
string_value as string_value,
|
||||
sum(round((julianday(CASE WHEN end_user_time > query_end THEN query_end ELSE end_user_time END) -
|
||||
julianday(CASE WHEN start_user_time < query_start THEN query_start ELSE start_user_time END))
|
||||
* 24 * 60 * 60, 0)) as time_spend_in_seconds
|
||||
FROM dates
|
||||
JOIN time_span_tags tst
|
||||
JOIN (
|
||||
SELECT *
|
||||
FROM time_spans tsx
|
||||
WHERE (EXISTS(SELECT tstx.key
|
||||
FROM time_span_tags as tstx
|
||||
WHERE (tstx.time_span_id = tsx.id)
|
||||
AND (%s)))
|
||||
AND (NOT EXISTS(SELECT tstx.key
|
||||
FROM time_span_tags as tstx
|
||||
WHERE (tstx.time_span_id = id)
|
||||
AND (%s)))
|
||||
) as ts on ts.id = tst.time_span_id
|
||||
WHERE (key in (?))
|
||||
AND (user_id = ?)
|
||||
AND (start_user_time <= query_end AND end_user_time >= query_start)
|
||||
GROUP BY query_start,
|
||||
key,
|
||||
string_value;
|
||||
`, queryRanges, queryRequire, queryExclude)
|
||||
variables = append(variables, tags, auth.GetUser(ctx).ID)
|
||||
|
||||
var entries []statReturn
|
||||
r.DB.Raw(query, variables...).Scan(&entries)
|
||||
|
||||
statisticsEntries, err := group(entries)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
statisticsEntries = fillEmptyRanges(ranges, statisticsEntries)
|
||||
fillEmptyTags(statisticsEntries)
|
||||
|
||||
return statisticsEntries, r.DB.Error
|
||||
}
|
||||
|
||||
func fillEmptyTags(statisticsEntries []*gqlmodel.RangedStatisticsEntries) {
|
||||
lookup := make(map[string]struct{})
|
||||
for _, entry := range statisticsEntries {
|
||||
for _, statEntry := range entry.Entries {
|
||||
lookup[key(statEntry.Key, statEntry.Value)] = struct{}{}
|
||||
}
|
||||
}
|
||||
for _, entry := range statisticsEntries {
|
||||
existing := make(map[string]struct{}, len(lookup))
|
||||
for _, statEntry := range entry.Entries {
|
||||
existing[key(statEntry.Key, statEntry.Value)] = struct{}{}
|
||||
}
|
||||
for identifier := range lookup {
|
||||
if _, ok := existing[identifier]; !ok {
|
||||
key, value := extract(identifier)
|
||||
entry.Entries = append(entry.Entries, &gqlmodel.StatisticsEntry{Key: key, Value: value, TimeSpendInSeconds: 0})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func fillEmptyRanges(ranges []*gqlmodel.Range, statisticsEntries []*gqlmodel.RangedStatisticsEntries) []*gqlmodel.RangedStatisticsEntries {
|
||||
result := make([]*gqlmodel.RangedStatisticsEntries, len(ranges))
|
||||
statsIdx := 0
|
||||
for idx, r := range ranges {
|
||||
if statsIdx < len(statisticsEntries) &&
|
||||
statisticsEntries[statsIdx].Start.UTC().Equal(r.Start.UTC()) &&
|
||||
statisticsEntries[statsIdx].End.UTC().Equal(r.End.UTC()) {
|
||||
result[idx] = statisticsEntries[statsIdx]
|
||||
statsIdx++
|
||||
continue
|
||||
}
|
||||
|
||||
result[idx] = &gqlmodel.RangedStatisticsEntries{
|
||||
Start: model.Time(r.Start.UTC()),
|
||||
End: model.Time(r.End.UTC()),
|
||||
Entries: []*gqlmodel.StatisticsEntry{},
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
type statReturn struct {
|
||||
QueryStart string
|
||||
QueryEnd string
|
||||
Key string
|
||||
StringValue string
|
||||
TimeSpendInSeconds float64
|
||||
}
|
||||
|
||||
func group(entries []statReturn) ([]*gqlmodel.RangedStatisticsEntries, error) {
|
||||
stats := map[string]*gqlmodel.RangedStatisticsEntries{}
|
||||
for _, entry := range entries {
|
||||
id := entry.QueryStart + "/" + entry.QueryEnd
|
||||
if _, ok := stats[id]; !ok {
|
||||
start, err := time.ParseInLocation("2006-01-02 15:04:05Z07:00", entry.QueryStart, time.UTC)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
end, err := time.ParseInLocation("2006-01-02 15:04:05Z07:00", entry.QueryEnd, time.UTC)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
stats[id] = &gqlmodel.RangedStatisticsEntries{Start: model.Time(start), End: model.Time(end)}
|
||||
}
|
||||
stats[id].Entries = append(stats[id].Entries, &gqlmodel.StatisticsEntry{
|
||||
Key: entry.Key,
|
||||
Value: entry.StringValue,
|
||||
TimeSpendInSeconds: entry.TimeSpendInSeconds,
|
||||
})
|
||||
}
|
||||
|
||||
var result []*gqlmodel.RangedStatisticsEntries
|
||||
for _, value := range stats {
|
||||
result = append(result, value)
|
||||
}
|
||||
|
||||
sort.SliceStable(result, func(i, j int) bool { return result[i].Start.Time().Before(result[j].End.Time()) })
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func build(tags []*gqlmodel.InputTimeSpanTag, noop string) (string, []interface{}) {
|
||||
if len(tags) == 0 {
|
||||
return noop, nil
|
||||
}
|
||||
|
||||
var have []string
|
||||
var haveParams []interface{}
|
||||
for _, tag := range tags {
|
||||
have = append(have, "(tstx.key = ? AND tstx.string_value = ?)")
|
||||
haveParams = append(haveParams, tag.Key, tag.Value)
|
||||
}
|
||||
|
||||
return strings.Join(have, " OR "), haveParams
|
||||
}
|
||||
|
||||
func key(key string, value string) string {
|
||||
return fmt.Sprintf("%s:%s", key, value)
|
||||
}
|
||||
|
||||
func extract(id string) (string, string) {
|
||||
split := strings.Split(id, ":")
|
||||
return split[0], split[1]
|
||||
}
|
||||
35
statistics/summary2.go
Normal file
35
statistics/summary2.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package statistics
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/traggo/server/generated/gqlmodel"
|
||||
"github.com/traggo/server/model"
|
||||
"github.com/traggo/server/setting"
|
||||
"github.com/traggo/server/time"
|
||||
)
|
||||
|
||||
// Stats2 another version of the stats endpoint
|
||||
func (r *ResolverForStatistics) Stats2(ctx context.Context, now model.Time, stats gqlmodel.InputStatsSelection) ([]*gqlmodel.RangedStatisticsEntries, error) {
|
||||
|
||||
settings, err := setting.Get(ctx, r.DB)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var ranges []*gqlmodel.Range
|
||||
|
||||
staticRanges, err := time.ParseRange(now.OmitTimeZone(),
|
||||
time.RelativeRange{From: stats.Range.From, To: stats.Range.To},
|
||||
time.InternalInterval(stats.Interval),
|
||||
settings.FirstDayOfTheWeekTimeWeekday(),
|
||||
settings.LastDayOfTheWeekTimeWeekday())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, r := range staticRanges {
|
||||
ranges = append(ranges, &gqlmodel.Range{Start: model.Time(r.From), End: model.Time(r.To)})
|
||||
}
|
||||
|
||||
return r.Stats(ctx, ranges, stats.Tags, stats.ExcludeTags, stats.IncludeTags)
|
||||
}
|
||||
322
statistics/summary_test.go
Normal file
322
statistics/summary_test.go
Normal file
@@ -0,0 +1,322 @@
|
||||
package statistics
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/traggo/server/generated/gqlmodel"
|
||||
"github.com/traggo/server/model"
|
||||
"github.com/traggo/server/test"
|
||||
"github.com/traggo/server/test/fake"
|
||||
)
|
||||
|
||||
func TestSummary_fails(t *testing.T) {
|
||||
db := test.InMemoryDB(t)
|
||||
defer db.Close()
|
||||
|
||||
resolver := ResolverForStatistics{DB: db.DB}
|
||||
|
||||
_, err := resolver.Stats(fake.User(1), []*gqlmodel.Range{rangex("2019-06-11T10:00:00Z", "2019-06-11T09:00:00Z")}, []string{"type"}, nil, nil)
|
||||
require.Error(t, err, "range start must be before range end ")
|
||||
}
|
||||
|
||||
func TestStats(t *testing.T) {
|
||||
data := []*testEntry{
|
||||
name("simple").
|
||||
Load(ts("2019-06-11T10:00:00Z", 10*time.Second, tag("type", "review"))).
|
||||
Keys("type").
|
||||
Ranges(rangex("2019-06-11T10:00:00Z", "2019-06-11T10:00:10Z")).
|
||||
Expected(
|
||||
result("2019-06-11T10:00:00Z", "2019-06-11T10:00:10Z",
|
||||
entry("type", "review", 10*time.Second))),
|
||||
name("boundaries 1").
|
||||
Load(ts("2019-06-11T10:00:00Z", 10*time.Second, tag("type", "review"))).
|
||||
Keys("type").
|
||||
Ranges(rangex("2019-06-11T10:00:00Z", "2019-06-11T10:00:05Z")).
|
||||
Expected(
|
||||
result("2019-06-11T10:00:00Z", "2019-06-11T10:00:05Z",
|
||||
entry("type", "review", 5*time.Second))),
|
||||
name("boundaries 2").
|
||||
Load(ts("2019-06-11T10:00:00Z", 10*time.Second, tag("type", "review"))).
|
||||
Keys("type").
|
||||
Ranges(rangex("2019-06-11T10:00:05Z", "2019-06-11T10:00:10Z")).
|
||||
Expected(
|
||||
result("2019-06-11T10:00:05Z", "2019-06-11T10:00:10Z",
|
||||
entry("type", "review", 5*time.Second))),
|
||||
name("boundaries 3").
|
||||
Load(ts("2019-06-11T10:00:00Z", 10*time.Second, tag("type", "review"))).
|
||||
Keys("type").
|
||||
Ranges(rangex("2019-06-11T10:00:04Z", "2019-06-11T10:00:06Z")).
|
||||
Expected(
|
||||
result("2019-06-11T10:00:04Z", "2019-06-11T10:00:06Z",
|
||||
entry("type", "review", 2*time.Second))),
|
||||
name("exclude 1").
|
||||
Load(ts("2019-06-11T10:00:00Z", 10*time.Second, tag("type", "review"), tag("issue", "13"))).
|
||||
Keys("type").
|
||||
Exclude(tag("issue", "13")).
|
||||
Ranges(rangex("2019-06-11T10:00:04Z", "2019-06-11T10:00:06Z")).
|
||||
Expected(
|
||||
result("2019-06-11T10:00:04Z", "2019-06-11T10:00:06Z", []*gqlmodel.StatisticsEntry{}...)),
|
||||
name("empty range").
|
||||
Load(ts("2019-06-11T10:00:00Z", 10*time.Second, tag("type", "review"), tag("issue", "13"))).
|
||||
Keys("type").
|
||||
Ranges(rangex("2019-06-11T10:00:04Z", "2019-06-11T10:00:06Z"),
|
||||
rangex("2019-06-12T10:00:04Z", "2019-06-12T10:00:06Z")).
|
||||
Expected(
|
||||
result("2019-06-11T10:00:04Z", "2019-06-11T10:00:06Z", entry("type", "review", 2*time.Second)),
|
||||
result("2019-06-12T10:00:04Z", "2019-06-12T10:00:06Z", entry("type", "review", 0*time.Second))),
|
||||
name("include 1").
|
||||
Load(ts("2019-06-11T10:00:00Z", 10*time.Second, tag("type", "review"), tag("issue", "13"))).
|
||||
Keys("type").
|
||||
Include(tag("issue", "13")).
|
||||
Ranges(rangex("2019-06-11T10:00:00Z", "2019-06-11T10:00:10Z")).
|
||||
Expected(
|
||||
result("2019-06-11T10:00:00Z", "2019-06-11T10:00:10Z",
|
||||
entry("type", "review", 10*time.Second))),
|
||||
name("multiple 1").
|
||||
Load(
|
||||
ts("2019-06-11T10:00:00Z", 10*time.Second, tag("type", "review"), tag("issue", "13")),
|
||||
ts("2019-06-11T10:01:00Z", 10*time.Second, tag("type", "review"), tag("issue", "14")),
|
||||
).Keys("type").
|
||||
Ranges(rangex("2019-06-11T10:00:00Z", "2019-06-11T10:02:00Z")).
|
||||
Expected(
|
||||
result("2019-06-11T10:00:00Z", "2019-06-11T10:02:00Z",
|
||||
entry("type", "review", 20*time.Second))),
|
||||
name("multiple 2").
|
||||
Load(
|
||||
ts("2019-06-11T10:00:00Z", 10*time.Second, tag("type", "review"), tag("issue", "13")),
|
||||
ts("2019-06-11T10:01:00Z", 10*time.Second, tag("type", "review"), tag("issue", "14")),
|
||||
).Keys("type").
|
||||
Exclude(tag("issue", "14")).
|
||||
Ranges(rangex("2019-06-11T10:00:00Z", "2019-06-11T10:02:00Z")).
|
||||
Expected(
|
||||
result("2019-06-11T10:00:00Z", "2019-06-11T10:02:00Z",
|
||||
entry("type", "review", 10*time.Second))),
|
||||
name("fill empty range").
|
||||
Load(
|
||||
ts("2019-06-11T10:00:00Z", 10*time.Second, tag("type", "review")),
|
||||
).Keys("type").
|
||||
Ranges(
|
||||
rangex("2019-06-10T09:58:00Z", "2019-06-10T09:59:59Z"),
|
||||
rangex("2019-06-10T10:00:00Z", "2019-06-11T10:02:00Z")).
|
||||
Expected(
|
||||
result("2019-06-10T09:58:00Z", "2019-06-10T09:59:59Z",
|
||||
entry("type", "review", 0*time.Second)),
|
||||
result("2019-06-10T10:00:00Z", "2019-06-11T10:02:00Z",
|
||||
entry("type", "review", 10*time.Second))),
|
||||
name("multiple 3").
|
||||
Load(
|
||||
ts("2019-06-11T10:00:00Z", 10*time.Second, tag("proj", "gotify"), tag("type", "review"), tag("issue", "13")),
|
||||
ts("2019-06-11T10:01:00Z", 60*time.Second, tag("proj", "gotify"), tag("type", "review"), tag("issue", "12")),
|
||||
ts("2019-06-11T10:02:00Z", 7*time.Second, tag("proj", "gotify"), tag("type", "review"), tag("issue", "13")),
|
||||
ts("2019-06-11T10:03:00Z", 14*time.Second, tag("proj", "gotify"), tag("type", "work"), tag("issue", "15")),
|
||||
ts("2019-06-11T10:04:00Z", 16*time.Second, tag("proj", "gotify"), tag("type", "work"), tag("issue", "14")),
|
||||
ts("2019-06-11T10:05:00Z", 33*time.Second, tag("proj", "gotify"), tag("type", "support"), tag("issue", "10")),
|
||||
ts("2019-06-11T10:05:00Z", 12*time.Second, tag("proj", "gotify"), tag("type", "support"), tag("issue", "13")),
|
||||
ts("2019-06-11T10:06:00Z", 90*time.Second, tag("type", "break")),
|
||||
ts("2019-06-11T10:08:00Z", 33*time.Second, tag("proj", "traggo"), tag("type", "work"), tag("issue", "10")),
|
||||
ts("2019-06-11T10:09:00Z", 21*time.Second, tag("proj", "traggo"), tag("type", "work"), tag("issue", "15")),
|
||||
ts("2019-06-11T10:10:00Z", 18*time.Second, tag("proj", "traggo"), tag("type", "work"), tag("issue", "12")),
|
||||
ts("2019-06-11T10:11:00Z", 56*time.Second, tag("proj", "traggo"), tag("type", "review"), tag("issue", "11")),
|
||||
ts("2019-06-11T10:12:00Z", 44*time.Second, tag("type", "break")),
|
||||
).Keys("type").
|
||||
Ranges(rangex("2019-06-11T10:00:00Z", "2019-06-11T10:15:00Z")).
|
||||
Expected(
|
||||
result("2019-06-11T10:00:00Z", "2019-06-11T10:15:00Z",
|
||||
entry("type", "break", 134*time.Second),
|
||||
entry("type", "review", 133*time.Second),
|
||||
entry("type", "support", 45*time.Second),
|
||||
entry("type", "work", 102*time.Second))),
|
||||
name("multiple 4").
|
||||
Load(
|
||||
ts("2019-06-11T10:00:00Z", 10*time.Second, tag("proj", "gotify"), tag("type", "review"), tag("issue", "13")),
|
||||
ts("2019-06-11T10:01:00Z", 60*time.Second, tag("proj", "gotify"), tag("type", "review"), tag("issue", "12")),
|
||||
ts("2019-06-11T10:02:00Z", 7*time.Second, tag("proj", "gotify"), tag("type", "review"), tag("issue", "13")),
|
||||
ts("2019-06-11T10:03:00Z", 14*time.Second, tag("proj", "gotify"), tag("type", "work"), tag("issue", "15")),
|
||||
ts("2019-06-11T10:04:00Z", 16*time.Second, tag("proj", "gotify"), tag("type", "work"), tag("issue", "14")),
|
||||
ts("2019-06-11T10:05:00Z", 33*time.Second, tag("proj", "gotify"), tag("type", "support"), tag("issue", "10")),
|
||||
ts("2019-06-11T10:05:00Z", 12*time.Second, tag("proj", "gotify"), tag("type", "support"), tag("issue", "13")),
|
||||
ts("2019-06-11T10:06:00Z", 90*time.Second, tag("type", "break")),
|
||||
ts("2019-06-11T10:08:00Z", 32*time.Second, tag("proj", "traggo"), tag("type", "work"), tag("issue", "10")),
|
||||
ts("2019-06-11T10:09:00Z", 21*time.Second, tag("proj", "traggo"), tag("type", "work"), tag("issue", "15")),
|
||||
ts("2019-06-11T10:10:00Z", 18*time.Second, tag("proj", "traggo"), tag("type", "work"), tag("issue", "12")),
|
||||
ts("2019-06-11T10:11:00Z", 56*time.Second, tag("proj", "traggo"), tag("type", "review"), tag("issue", "11")),
|
||||
ts("2019-06-11T10:12:00Z", 44*time.Second, tag("type", "break")),
|
||||
).Keys("type", "issue").
|
||||
Ranges(rangex("2019-06-11T10:00:00Z", "2019-06-11T10:15:00Z")).
|
||||
Expected(
|
||||
result("2019-06-11T10:00:00Z", "2019-06-11T10:15:00Z",
|
||||
entry("issue", "10", 65*time.Second),
|
||||
entry("issue", "11", 56*time.Second),
|
||||
entry("issue", "12", 78*time.Second),
|
||||
entry("issue", "13", 29*time.Second),
|
||||
entry("issue", "14", 16*time.Second),
|
||||
entry("issue", "15", 35*time.Second),
|
||||
entry("type", "break", 134*time.Second),
|
||||
entry("type", "review", 133*time.Second),
|
||||
entry("type", "support", 45*time.Second),
|
||||
entry("type", "work", 101*time.Second))),
|
||||
name("multiple 5").
|
||||
Load(
|
||||
ts("2019-06-11T10:00:00Z", 10*time.Second, tag("proj", "gotify"), tag("type", "review"), tag("issue", "13")),
|
||||
ts("2019-06-11T10:01:00Z", 60*time.Second, tag("proj", "gotify"), tag("type", "review"), tag("issue", "12")),
|
||||
ts("2019-06-11T10:02:00Z", 7*time.Second, tag("proj", "gotify"), tag("type", "review"), tag("issue", "13")),
|
||||
ts("2019-06-11T10:03:00Z", 14*time.Second, tag("proj", "gotify"), tag("type", "work"), tag("issue", "15")),
|
||||
ts("2019-06-11T10:04:00Z", 16*time.Second, tag("proj", "gotify"), tag("type", "work"), tag("issue", "14")),
|
||||
ts("2019-06-11T10:05:00Z", 33*time.Second, tag("proj", "gotify"), tag("type", "support"), tag("issue", "10")),
|
||||
ts("2019-06-11T10:05:00Z", 12*time.Second, tag("proj", "gotify"), tag("type", "support"), tag("issue", "13")),
|
||||
ts("2019-06-11T10:06:00Z", 90*time.Second, tag("type", "break")),
|
||||
ts("2019-06-11T10:08:00Z", 32*time.Second, tag("proj", "traggo"), tag("type", "work"), tag("issue", "10")),
|
||||
ts("2019-06-11T10:09:00Z", 21*time.Second, tag("proj", "traggo"), tag("type", "work"), tag("issue", "15")),
|
||||
ts("2019-06-11T10:10:00Z", 18*time.Second, tag("proj", "traggo"), tag("type", "work"), tag("issue", "12")),
|
||||
ts("2019-06-11T10:11:00Z", 56*time.Second, tag("proj", "traggo"), tag("type", "review"), tag("issue", "11")),
|
||||
ts("2019-06-11T10:12:00Z", 44*time.Second, tag("type", "break")),
|
||||
).Keys("type").
|
||||
Ranges(rangex("2019-06-11T10:00:00Z", "2019-06-11T10:15:00Z")).
|
||||
Include(tag("issue", "13"), tag("issue", "12")).
|
||||
Expected(
|
||||
result("2019-06-11T10:00:00Z", "2019-06-11T10:15:00Z",
|
||||
entry("type", "review", 77*time.Second),
|
||||
entry("type", "support", 12*time.Second),
|
||||
entry("type", "work", 18*time.Second))),
|
||||
name("multiple 6").
|
||||
Load(
|
||||
ts("2019-06-11T10:00:00Z", 10*time.Second, tag("proj", "gotify"), tag("type", "review"), tag("issue", "13")),
|
||||
ts("2019-06-11T10:01:00Z", 60*time.Second, tag("proj", "gotify"), tag("type", "review"), tag("issue", "12")),
|
||||
ts("2019-06-11T10:02:00Z", 7*time.Second, tag("proj", "gotify"), tag("type", "review"), tag("issue", "13")),
|
||||
ts("2019-06-11T10:03:00Z", 14*time.Second, tag("proj", "gotify"), tag("type", "work"), tag("issue", "15")),
|
||||
ts("2019-06-11T10:04:00Z", 16*time.Second, tag("proj", "gotify"), tag("type", "work"), tag("issue", "14")),
|
||||
ts("2019-06-11T10:05:00Z", 33*time.Second, tag("proj", "gotify"), tag("type", "support"), tag("issue", "10")),
|
||||
ts("2019-06-11T10:05:00Z", 12*time.Second, tag("proj", "gotify"), tag("type", "support"), tag("issue", "13")),
|
||||
ts("2019-06-11T10:06:00Z", 90*time.Second, tag("type", "break")),
|
||||
ts("2019-06-11T10:08:00Z", 32*time.Second, tag("proj", "traggo"), tag("type", "work"), tag("issue", "10")),
|
||||
ts("2019-06-11T10:09:00Z", 21*time.Second, tag("proj", "traggo"), tag("type", "work"), tag("issue", "15")),
|
||||
ts("2019-06-11T10:10:00Z", 18*time.Second, tag("proj", "traggo"), tag("type", "work"), tag("issue", "12")),
|
||||
ts("2019-06-11T10:11:00Z", 56*time.Second, tag("proj", "traggo"), tag("type", "review"), tag("issue", "11")),
|
||||
ts("2019-06-11T10:12:00Z", 44*time.Second, tag("type", "break")),
|
||||
).Keys("type").
|
||||
Ranges(rangex("2019-06-11T10:00:00Z", "2019-06-11T10:15:00Z")).
|
||||
Exclude(tag("issue", "10"), tag("issue", "11"), tag("issue", "14"), tag("issue", "15")).
|
||||
Expected(
|
||||
result("2019-06-11T10:00:00Z", "2019-06-11T10:15:00Z",
|
||||
entry("type", "break", 134*time.Second),
|
||||
entry("type", "review", 77*time.Second),
|
||||
entry("type", "support", 12*time.Second),
|
||||
entry("type", "work", 18*time.Second))),
|
||||
}
|
||||
|
||||
for _, entry := range data {
|
||||
t.Run(entry.name, func(t *testing.T) {
|
||||
db := test.InMemoryDB(t)
|
||||
defer db.Close()
|
||||
|
||||
db.User(1)
|
||||
for _, timespan := range entry.timespans {
|
||||
db.Create(×pan)
|
||||
}
|
||||
|
||||
resolver := ResolverForStatistics{DB: db.DB}
|
||||
|
||||
stats, err := resolver.Stats(fake.User(1), entry.ranges, entry.keys, entry.exclude, entry.include)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, entry.expected, stats)
|
||||
require.Len(t, stats, len(entry.expected))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func ts(start string, d time.Duration, tags ...model.TimeSpanTag) model.TimeSpan {
|
||||
end := test.Time(start).Add(d)
|
||||
return model.TimeSpan{
|
||||
StartUserTime: test.Time(start),
|
||||
EndUserTime: &end,
|
||||
UserID: 1,
|
||||
Tags: tags,
|
||||
}
|
||||
}
|
||||
|
||||
type testEntry struct {
|
||||
timespans []model.TimeSpan
|
||||
keys []string
|
||||
exclude []*gqlmodel.InputTimeSpanTag
|
||||
include []*gqlmodel.InputTimeSpanTag
|
||||
name string
|
||||
ranges []*gqlmodel.Range
|
||||
expected []*gqlmodel.RangedStatisticsEntries
|
||||
}
|
||||
|
||||
func name(name string) *testEntry {
|
||||
return &testEntry{
|
||||
name: name,
|
||||
}
|
||||
}
|
||||
|
||||
func (e *testEntry) Expected(expected ...*gqlmodel.RangedStatisticsEntries) *testEntry {
|
||||
e.expected = expected
|
||||
return e
|
||||
}
|
||||
|
||||
func (e *testEntry) Keys(keys ...string) *testEntry {
|
||||
e.keys = keys
|
||||
return e
|
||||
}
|
||||
|
||||
func (e *testEntry) Load(timespans ...model.TimeSpan) *testEntry {
|
||||
e.timespans = timespans
|
||||
return e
|
||||
}
|
||||
|
||||
func (e *testEntry) Ranges(ranges ...*gqlmodel.Range) *testEntry {
|
||||
e.ranges = ranges
|
||||
return e
|
||||
}
|
||||
|
||||
func (e *testEntry) Include(includes ...model.TimeSpanTag) *testEntry {
|
||||
for _, entry := range includes {
|
||||
e.include = append(e.include, &gqlmodel.InputTimeSpanTag{
|
||||
Key: entry.Key,
|
||||
Value: entry.StringValue,
|
||||
})
|
||||
}
|
||||
return e
|
||||
}
|
||||
func (e *testEntry) Exclude(excludes ...model.TimeSpanTag) *testEntry {
|
||||
for _, entry := range excludes {
|
||||
e.exclude = append(e.exclude, &gqlmodel.InputTimeSpanTag{
|
||||
Key: entry.Key,
|
||||
Value: entry.StringValue,
|
||||
})
|
||||
}
|
||||
return e
|
||||
}
|
||||
|
||||
func tag(key string, value string) model.TimeSpanTag {
|
||||
return model.TimeSpanTag{
|
||||
Key: key,
|
||||
StringValue: value,
|
||||
}
|
||||
}
|
||||
|
||||
func rangex(start, stop string) *gqlmodel.Range {
|
||||
return &gqlmodel.Range{
|
||||
Start: test.ModelTime(start),
|
||||
End: test.ModelTime(stop),
|
||||
}
|
||||
}
|
||||
|
||||
func result(start, stop string, entries ...*gqlmodel.StatisticsEntry) *gqlmodel.RangedStatisticsEntries {
|
||||
return &gqlmodel.RangedStatisticsEntries{
|
||||
Start: test.ModelTimeUTC(start),
|
||||
End: test.ModelTimeUTC(stop),
|
||||
Entries: entries,
|
||||
}
|
||||
}
|
||||
|
||||
func entry(key string, value string, duration time.Duration) *gqlmodel.StatisticsEntry {
|
||||
return &gqlmodel.StatisticsEntry{
|
||||
Key: key,
|
||||
Value: value,
|
||||
TimeSpendInSeconds: duration.Seconds(),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user