| // Copyright 2023 The Go Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| package main |
| |
| import ( |
| "net/url" |
| "testing" |
| "time" |
| |
| "github.com/google/go-cmp/cmp" |
| "golang.org/x/mod/semver" |
| "golang.org/x/telemetry/internal/config" |
| "golang.org/x/telemetry/internal/telemetry" |
| ) |
| |
| var exampleReports = []telemetry.Report{ |
| { |
| Week: "2999-01-01", |
| LastWeek: "2998-01-01", |
| X: 0.1, |
| Programs: []*telemetry.ProgramReport{ |
| { |
| Program: "cmd/go", |
| Version: "go1.2.3", |
| GoVersion: "go1.2.3", |
| GOOS: "darwin", |
| GOARCH: "arm64", |
| Counters: map[string]int64{ |
| "main": 1, |
| }, |
| }, |
| { |
| Program: "example.com/mod/pkg", |
| Version: "v2.3.4", |
| GoVersion: "go1.2.3", |
| GOOS: "darwin", |
| GOARCH: "arm64", |
| Counters: map[string]int64{ |
| "main": 1, |
| "flag:a": 2, |
| "flag:b": 3, |
| }, |
| // TODO: add support for stacks |
| Stacks: map[string]int64{ |
| "panic": 4, |
| }, |
| }, |
| { |
| Program: "example.com/mod/pkg", |
| Version: "v2.3.4-pre.1", |
| GoVersion: "go1.2.3", |
| GOOS: "darwin", |
| GOARCH: "arm64", |
| Counters: map[string]int64{ |
| "flag:b": 3, |
| }, |
| // TODO: add support for stacks |
| Stacks: map[string]int64{ |
| "panic": 2, |
| }, |
| }, |
| }, |
| Config: "v0.0.1", |
| }, |
| { |
| Week: "2999-01-01", |
| LastWeek: "2998-01-01", |
| X: 0.2, |
| Programs: []*telemetry.ProgramReport{ |
| { |
| Program: "example.com/mod/pkg", |
| Version: "v1.2.3", |
| GoVersion: "go1.2.3", |
| GOOS: "darwin", |
| GOARCH: "arm64", |
| Counters: map[string]int64{ |
| "main": 1, |
| "flag:a": 2, |
| "flag:b": 3, |
| }, |
| // TODO: add support for stacks |
| Stacks: map[string]int64{ |
| "panic": 4, |
| }, |
| }, |
| { |
| Program: "example.com/mod/pkg", |
| Version: "v2.3.4", |
| GoVersion: "go1.19.0", |
| GOOS: "darwin", |
| GOARCH: "arm64", |
| Counters: map[string]int64{ |
| "main": 1, |
| "flag:a": 2, |
| "flag:b": 3, |
| }, |
| // TODO: add support for stacks |
| Stacks: map[string]int64{ |
| "panic": 4, |
| }, |
| }, |
| }, |
| Config: "v0.0.1", |
| }, |
| { |
| Week: "2999-01-01", |
| LastWeek: "2998-01-01", |
| X: 0.3, |
| Programs: []*telemetry.ProgramReport{ |
| { |
| Program: "example.com/mod/pkg", |
| Version: "v1.2.3", |
| GoVersion: "go1.2.3", |
| GOOS: "linux", |
| GOARCH: "amd64", |
| Counters: map[string]int64{ |
| "main": 4, |
| "flag:a": 5, |
| "flag:b": 6, |
| "flag:c": 1, |
| }, |
| // TODO: add support for stacks |
| Stacks: map[string]int64{ |
| "panic": 7, |
| }, |
| }, |
| }, |
| Config: "v0.0.1", |
| }, |
| } |
| |
| func TestGroup(t *testing.T) { |
| type args struct { |
| reports []telemetry.Report |
| } |
| tests := []struct { |
| name string |
| args args |
| want data |
| }{ |
| { |
| name: "single report", |
| args: args{ |
| []telemetry.Report{ |
| { |
| Week: "2999-01-01", |
| LastWeek: "2998-01-01", |
| X: 0.123456789, |
| Programs: []*telemetry.ProgramReport{ |
| { |
| Program: "example.com/mod/pkg", |
| Version: "v1.2.3", |
| GoVersion: "go1.2.3", |
| GOOS: "darwin", |
| GOARCH: "arm64", |
| Counters: map[string]int64{ |
| "main": 1, |
| "flag:a": 2, |
| "flag:b": 3, |
| }, |
| // TODO: add support for stacks |
| Stacks: map[string]int64{ |
| "panic": 4, |
| }, |
| }, |
| }, |
| Config: "v0.0.1", |
| }, |
| }, |
| }, |
| want: data{ |
| weekName("2999-01-01"): { |
| programName("example.com/mod/pkg"): { |
| graphName("Version"): { |
| bucketName("v1.2.3"): { |
| reportID(0.1234567890): 1, |
| }, |
| }, |
| graphName("GOOS"): { |
| bucketName("darwin"): { |
| reportID(0.1234567890): 1, |
| }, |
| }, |
| graphName("GOARCH"): { |
| bucketName("arm64"): { |
| reportID(0.1234567890): 1, |
| }, |
| }, |
| graphName("GoVersion"): { |
| bucketName("go1.2.3"): { |
| reportID(0.1234567890): 1, |
| }, |
| }, |
| graphName("main"): { |
| bucketName("main"): { |
| reportID(0.1234567890): 1, |
| }, |
| }, |
| graphName("flag"): { |
| bucketName("a"): { |
| reportID(0.1234567890): 2, |
| }, |
| bucketName("b"): { |
| reportID(0.1234567890): 3, |
| }, |
| }, |
| }, |
| }, |
| }, |
| }, |
| } |
| for _, tt := range tests { |
| t.Run(tt.name, func(t *testing.T) { |
| got := group(tt.args.reports) |
| if diff := cmp.Diff(tt.want, got); diff != "" { |
| t.Errorf("nest() mismatch (-want +got):\n%s", diff) |
| } |
| }) |
| } |
| } |
| |
| func TestPartition(t *testing.T) { |
| normalVersion := func(b bucketName) bucketName { |
| return bucketName(semver.MajorMinor(string(b))) |
| } |
| normalGoVersion := func(b bucketName) bucketName { |
| return bucketName(goMajorMinor(string(b))) |
| } |
| exampleData := group(exampleReports) |
| type args struct { |
| program programName |
| name graphName |
| buckets []bucketName |
| } |
| tests := []struct { |
| name string |
| data data |
| args args |
| normalize func(bucketName) bucketName |
| want *chart |
| }{ |
| { |
| name: "major.minor.patch version counter", |
| data: exampleData, |
| args: args{ |
| program: "example.com/mod/pkg", |
| name: "Version", |
| buckets: []bucketName{"v1.2.3", "v2.3.4"}, |
| }, |
| normalize: normalVersion, |
| want: &chart{ |
| ID: "charts:example.com/mod/pkg:Version", |
| Name: "Version", |
| Type: "partition", |
| Data: []*datum{ |
| { |
| Week: "2999-01-01", |
| Key: "v1.2", |
| Value: 2, |
| }, |
| { |
| Week: "2999-01-01", |
| Key: "v2.3", |
| Value: 2, |
| }, |
| }, |
| }, |
| }, |
| { |
| name: "major.minor version counter should have same result as major.minor.patch", |
| data: exampleData, |
| args: args{ |
| program: "example.com/mod/pkg", |
| name: "Version", |
| buckets: []bucketName{"v1.2.3", "v2.3.4"}, |
| }, |
| normalize: normalVersion, |
| want: &chart{ |
| ID: "charts:example.com/mod/pkg:Version", |
| Name: "Version", |
| Type: "partition", |
| Data: []*datum{ |
| { |
| Week: "2999-01-01", |
| Key: "v1.2", |
| Value: 2, |
| }, |
| { |
| Week: "2999-01-01", |
| Key: "v2.3", |
| Value: 2, |
| }, |
| }, |
| }, |
| }, |
| { |
| name: "duplicated counter should be ignored", |
| data: exampleData, |
| args: args{ |
| program: "example.com/mod/pkg", |
| name: "Version", |
| buckets: []bucketName{"v1.2.3", "v2.3.4", "v1.2.3"}, |
| }, |
| normalize: normalVersion, |
| want: &chart{ |
| ID: "charts:example.com/mod/pkg:Version", |
| Name: "Version", |
| Type: "partition", |
| Data: []*datum{ |
| { |
| Week: "2999-01-01", |
| Key: "v1.2", |
| Value: 2, |
| }, |
| { |
| Week: "2999-01-01", |
| Key: "v2.3", |
| Value: 2, |
| }, |
| }, |
| }, |
| }, |
| { |
| name: "goos counter", |
| data: exampleData, |
| args: args{ |
| program: "example.com/mod/pkg", |
| name: "GOOS", |
| buckets: []bucketName{"darwin", "linux"}, |
| }, |
| want: &chart{ |
| ID: "charts:example.com/mod/pkg:GOOS", |
| Name: "GOOS", |
| Type: "partition", |
| Data: []*datum{ |
| { |
| Week: "2999-01-01", |
| Key: "darwin", |
| Value: 2, |
| }, |
| { |
| Week: "2999-01-01", |
| Key: "linux", |
| Value: 1, |
| }, |
| }, |
| }, |
| }, |
| { |
| name: "GoVersion counter", |
| data: exampleData, |
| args: args{ |
| program: "example.com/mod/pkg", |
| name: "GoVersion", |
| buckets: []bucketName{"go1.2.3", "go2.3.4"}, |
| }, |
| normalize: normalGoVersion, |
| want: &chart{ |
| ID: "charts:example.com/mod/pkg:GoVersion", |
| Name: "GoVersion", |
| Type: "partition", |
| Data: []*datum{ |
| { |
| Week: "2999-01-01", |
| Key: "go1.2", |
| Value: 3, |
| }, |
| { |
| Week: "2999-01-01", |
| Key: "go2.3", |
| Value: 0, |
| }, |
| }, |
| }, |
| }, |
| { |
| name: "three days, multiple versions", |
| data: data{ |
| "2999-01-01": {"example.com/mod/pkg": {"Version": { |
| "v1.2.3": {0.1: 2}, |
| "v2.3.4": {0.1: 3}, |
| }, |
| }}, |
| "2999-01-04": {"example.com/mod/pkg": {"Version": { |
| "v1.2.3": {0.3: 2}, |
| "v2.3.4": {0.4: 5}, |
| }, |
| }}, |
| "2999-01-05": {"example.com/mod/pkg": {"Version": { |
| "v2.3.4": {0.5: 6}, |
| }}}, |
| }, |
| args: args{ |
| program: "example.com/mod/pkg", |
| name: "Version", |
| buckets: []bucketName{"v1.2.3", "v2.3.4"}, |
| }, |
| normalize: normalVersion, |
| want: &chart{ |
| ID: "charts:example.com/mod/pkg:Version", |
| Name: "Version", |
| Type: "partition", |
| Data: []*datum{ |
| { |
| Week: "2999-01-05", |
| Key: "v1.2", |
| Value: 2, |
| }, |
| { |
| Week: "2999-01-05", |
| Key: "v2.3", |
| Value: 3, |
| }, |
| }, |
| }, |
| }, |
| { |
| name: "three days, multiple GOOS", |
| data: data{ |
| "2999-01-01": {"example.com/mod/pkg": {"GOOS": { |
| "darwin": {0.1: 2, 0.2: 2, 0.3: 2}, |
| "linux": {0.1: 2, 0.2: 2}, |
| }, |
| }}, |
| "2999-01-02": {"example.com/mod/pkg": {"GOOS": { |
| "darwin": {0.4: 2, 0.5: 2}, |
| "linux": {0.6: 5}, |
| }, |
| }}, |
| "2999-01-03": {"example.com/mod/pkg": {"GOOS": { |
| "darwin": {0.6: 3}, |
| }, |
| }}, |
| }, |
| args: args{ |
| program: "example.com/mod/pkg", |
| name: "GOOS", |
| buckets: []bucketName{"darwin", "linux"}, |
| }, |
| want: &chart{ |
| ID: "charts:example.com/mod/pkg:GOOS", |
| Name: "GOOS", |
| Type: "partition", |
| Data: []*datum{ |
| { |
| Week: "2999-01-03", |
| Key: "darwin", |
| Value: 6, |
| }, |
| { |
| Week: "2999-01-03", |
| Key: "linux", |
| Value: 3, |
| }, |
| }, |
| }, |
| }, |
| { |
| name: "two days data, missing GOOS in first day", |
| data: data{ |
| "2999-01-01": {"example.com/mod/pkg": {"Version": { |
| "v1.2": {0.1: 2}, |
| }, |
| }}, |
| "2999-01-02": {"example.com/mod/pkg": {"GOOS": { |
| "darwin": {0.3: 2}, |
| "linux": {0.3: 2}, |
| }, |
| }}, |
| }, |
| args: args{ |
| program: "example.com/mod/pkg", |
| name: "GOOS", |
| buckets: []bucketName{"darwin", "linux"}, |
| }, |
| want: &chart{ |
| ID: "charts:example.com/mod/pkg:GOOS", |
| Name: "GOOS", |
| Type: "partition", |
| Data: []*datum{ |
| { |
| Week: "2999-01-02", |
| Key: "darwin", |
| Value: 1, |
| }, |
| { |
| Week: "2999-01-02", |
| Key: "linux", |
| Value: 1, |
| }, |
| }, |
| }, |
| }, |
| { |
| name: "three days, missing version data all days", |
| data: data{ |
| "2999-01-01": {"example.com/mod/pkg": {"GOOS": { |
| "GOOS": {0.1: 2}, |
| "GOOS:darwin": {0.1: 2}, |
| }, |
| }}, |
| "2999-01-02": {"example.com/mod/pkg": {"GOOS": { |
| "GOOS": {0.6: 5}, |
| "GOOS:linux": {0.6: 5}, |
| }, |
| }}, |
| "2999-01-03": {"example.com/mod/pkg": {"GOOS": { |
| "GOOS": {0.6: 3}, |
| "GOOS:darwin": {0.6: 3}, |
| }, |
| }}, |
| }, |
| args: args{ |
| program: "example.com/mod/pkg", |
| name: "Version", |
| buckets: []bucketName{"v1.2.3", "v2.3.4"}, |
| }, |
| normalize: normalVersion, |
| want: nil, |
| }, |
| } |
| for _, tc := range tests { |
| t.Run(tc.name, func(t *testing.T) { |
| got := tc.data.partition(tc.args.program, tc.args.name, tc.args.buckets, tc.normalize, nil) |
| if diff := cmp.Diff(tc.want, got); diff != "" { |
| t.Errorf("partition() mismatch (-want +got):\n%s", diff) |
| } |
| }) |
| } |
| } |
| |
| func TestCharts(t *testing.T) { |
| exampleData := group(exampleReports) |
| cfg := &config.Config{ |
| UploadConfig: &telemetry.UploadConfig{ |
| GOOS: []string{"darwin"}, |
| GOARCH: []string{"amd64"}, |
| GoVersion: []string{"go1.2.3", "go1.19.0"}, |
| SampleRate: 1, |
| Programs: []*telemetry.ProgramConfig{ |
| { |
| Name: "cmd/go", |
| Versions: []string{"go1.2.3"}, |
| Counters: []telemetry.CounterConfig{{ |
| Name: "main", |
| }}, |
| }, |
| { |
| Name: "cmd/compiler", |
| Versions: []string{"go1.2.3"}, |
| Counters: []telemetry.CounterConfig{{ |
| Name: "count1", |
| }}, |
| }, |
| { |
| Name: "example.com/mod/pkg", |
| // Exercise semver sorting. Notably v1.2.3 has data but is not |
| // present. |
| // |
| // TODO(rfindley): in a follow-up CL, remove the MajMin collapsing of |
| // Versions. It's actually really interesting to see detailed version |
| // information. |
| Versions: []string{"v2.3.4", "v2.3.4-pre.1", "v0.15.0"}, |
| Counters: []telemetry.CounterConfig{ |
| {Name: "count2"}, |
| {Name: "flag:{a,b,c}"}, |
| }, |
| }, |
| }, |
| }, |
| } |
| want := &chartdata{ |
| DateRange: [2]string{"2999-01-01", "2999-01-01"}, |
| Programs: []*program{ |
| { |
| ID: "charts:cmd/go", |
| Name: "cmd/go", |
| Charts: []*chart{ |
| { |
| ID: "charts:cmd/go:GOOS", |
| Name: "GOOS", |
| Type: "partition", |
| Data: []*datum{ |
| {Week: "2999-01-01", Key: "darwin", Value: 1}, |
| }, |
| }, |
| { |
| ID: "charts:cmd/go:GoVersion", |
| Name: "GoVersion", |
| Type: "partition", |
| Data: []*datum{ |
| {Week: "2999-01-01", Key: "go1.2", Value: 1}, |
| {Week: "2999-01-01", Key: "go1.19"}, |
| }, |
| }, |
| { |
| ID: "charts:cmd/go:main", |
| Name: "main", |
| Type: "partition", |
| Data: []*datum{ |
| {Week: "2999-01-01", Key: "main", Value: 1}, |
| }, |
| }, |
| }, |
| }, |
| { |
| ID: "charts:cmd/compiler", |
| Name: "cmd/compiler", |
| }, |
| { |
| ID: "charts:example.com/mod/pkg", |
| Name: "example.com/mod/pkg", |
| Charts: []*chart{ |
| { |
| ID: "charts:example.com/mod/pkg:Version", |
| Name: "Version", |
| Type: "partition", |
| Data: []*datum{ |
| {Week: "2999-01-01", Key: "v0.15", Value: 0}, |
| {Week: "2999-01-01", Key: "v2.3", Value: 2}, |
| }, |
| }, |
| { |
| ID: "charts:example.com/mod/pkg:GOOS", |
| Name: "GOOS", |
| Type: "partition", |
| Data: []*datum{ |
| {Week: "2999-01-01", Key: "darwin", Value: 2}, |
| }, |
| }, |
| { |
| ID: "charts:example.com/mod/pkg:GOARCH", |
| Name: "GOARCH", |
| Type: "partition", |
| Data: []*datum{ |
| {Week: "2999-01-01", Key: "amd64", Value: 1}, |
| }, |
| }, |
| { |
| ID: "charts:example.com/mod/pkg:GoVersion", |
| Name: "GoVersion", |
| Type: "partition", |
| Data: []*datum{ |
| {Week: "2999-01-01", Key: "go1.2", Value: 3}, |
| {Week: "2999-01-01", Key: "go1.19", Value: 1}, |
| }, |
| }, |
| { |
| ID: "charts:example.com/mod/pkg:flag", |
| Name: "flag", |
| Type: "partition", |
| Data: []*datum{ |
| {Week: "2999-01-01", Key: "a", Value: 3}, |
| {Week: "2999-01-01", Key: "b", Value: 3}, |
| {Week: "2999-01-01", Key: "c", Value: 1}, |
| }, |
| }, |
| }, |
| }, |
| }, |
| NumReports: 1, |
| } |
| got := charts(cfg, "2999-01-01", "2999-01-01", exampleData, []float64{0.12345}) |
| if diff := cmp.Diff(want, got); diff != "" { |
| t.Errorf("charts = %+v\n, (-want +got): %v", got, diff) |
| } |
| } |
| |
| func TestWriteCount(t *testing.T) { |
| type keyValue struct { |
| week weekName |
| program programName |
| chart graphName |
| bucket bucketName |
| x reportID |
| value int64 |
| } |
| testcases := []struct { |
| name string |
| inputs []keyValue |
| want []keyValue |
| }{ |
| { |
| name: "program version counter should have value", |
| inputs: []keyValue{ |
| {"2987-07-01", "golang.org/x/tools/gopls", "Version", "v0.15.3", 0.00009, 1}, |
| }, |
| want: []keyValue{ |
| {"2987-07-01", "golang.org/x/tools/gopls", "Version", "v0.15.3", 0.00009, 1}, |
| }, |
| }, |
| { |
| name: "only one count with same prefix and counter", |
| inputs: []keyValue{ |
| {"2987-06-30", "cmd/go", "go/invocations", "go/invocations", 0.86995, 84}, |
| }, |
| want: []keyValue{ |
| {"2987-06-30", "cmd/go", "go/invocations", "go/invocations", 0.86995, 84}, |
| }, |
| }, |
| { |
| name: "overwrite values when calling multiple times", |
| inputs: []keyValue{ |
| {"2987-06-30", "golang.org/x/tools/gopls", "GOOS", "windows", 0.86018, 1}, |
| {"2987-06-30", "golang.org/x/tools/gopls", "GOOS", "windows", 0.86018, 2}, |
| {"2987-06-30", "golang.org/x/tools/gopls", "GOOS", "windows", 0.86018, 3}, |
| }, |
| want: []keyValue{ |
| {"2987-06-30", "golang.org/x/tools/gopls", "GOOS", "windows", 0.86018, 3}, |
| }, |
| }, |
| { |
| name: "multiple counters", |
| inputs: []keyValue{ |
| {"2987-06-30", "golang.org/x/tools/gopls", "GOOS", "windows", 0.86018, 2}, |
| {"2987-06-30", "golang.org/x/tools/gopls", "GOOS", "linux", 0.86018, 4}, |
| }, |
| want: []keyValue{ |
| {"2987-06-30", "golang.org/x/tools/gopls", "GOOS", "windows", 0.86018, 2}, |
| {"2987-06-30", "golang.org/x/tools/gopls", "GOOS", "linux", 0.86018, 4}, |
| }, |
| }, |
| } |
| |
| for _, tc := range testcases { |
| t.Run(tc.name, func(t *testing.T) { |
| d := make(data) |
| for _, input := range tc.inputs { |
| d.writeCount(input.week, input.program, input.chart, input.bucket, input.x, input.value) |
| } |
| |
| for _, want := range tc.want { |
| got := d[want.week][want.program][want.chart][want.bucket][want.x] |
| if want.value != got { |
| t.Errorf("d[%q][%q][%q][%q][%v] = %v, want %v", want.week, want.program, want.chart, want.bucket, want.x, got, want.value) |
| } |
| } |
| }) |
| } |
| } |
| |
| func TestParseDateRange(t *testing.T) { |
| testcases := []struct { |
| name string |
| url string |
| wantStart time.Time |
| wantEnd time.Time |
| wantErr bool |
| }{ |
| { |
| name: "regular key start & end input", |
| url: "http://localhost:8082/chart/?start=2024-06-10&end=2024-06-17", |
| wantStart: time.Date(2024, 06, 10, 0, 0, 0, 0, time.UTC), |
| wantEnd: time.Date(2024, 06, 17, 0, 0, 0, 0, time.UTC), |
| }, |
| { |
| name: "regular key date input", |
| url: "http://localhost:8082/chart/?date=2024-06-11", |
| wantStart: time.Date(2024, 06, 11, 0, 0, 0, 0, time.UTC), |
| wantEnd: time.Date(2024, 06, 11, 0, 0, 0, 0, time.UTC), |
| }, |
| { |
| name: "malformatted value for start", |
| url: "http://localhost:8082/chart/?start=2024-066-01&end=2024-06-17", |
| wantErr: true, |
| }, |
| { |
| name: "malformatted value for start", |
| url: "http://localhost:8082/chart/?start=2024-06-10&end=2024-06-179", |
| wantErr: true, |
| }, |
| { |
| name: "end is earlier than start", |
| url: "http://localhost:8082/chart/?start=2024-06-17&end=2024-06-10", |
| wantErr: true, |
| }, |
| { |
| name: "have only start but missing end", |
| url: "http://localhost:8082/chart/?start=2024-06-01", |
| wantErr: true, |
| }, |
| { |
| name: "key date and start used together", |
| url: "http://localhost:8082/chart/?start=2024-06-17&date=2024-06-19", |
| wantErr: true, |
| }, |
| } |
| |
| for _, tc := range testcases { |
| t.Run(tc.name, func(t *testing.T) { |
| url, err := url.Parse(tc.url) |
| if err != nil { |
| t.Fatalf("failed to parse url %q: %v", url, err) |
| } |
| |
| gotStart, gotEnd, err := parseDateRange(url) |
| if tc.wantErr && err == nil { |
| t.Errorf("parseDateRange %v should return error but return nil", tc.url) |
| } |
| if !tc.wantErr && err != nil { |
| t.Errorf("parseDateRange %v should return nil but return error: %v", tc.url, err) |
| } |
| |
| if !tc.wantErr { |
| if !gotStart.Equal(tc.wantStart) || !gotEnd.Equal(tc.wantEnd) { |
| t.Errorf("parseDateRange(%s) = (%s, %s), want (%s, %s)", tc.url, gotStart.Format(telemetry.DateOnly), gotEnd.Format(telemetry.DateOnly), tc.wantStart.Format(telemetry.DateOnly), tc.wantEnd.Format(telemetry.DateOnly)) |
| } |
| } |
| }) |
| } |
| } |