1package flux
2
3import (
4	"context"
5	"fmt"
6	"io/ioutil"
7	"net/http"
8	"net/http/httptest"
9	"path/filepath"
10	"testing"
11	"time"
12
13	"github.com/google/go-cmp/cmp"
14	"github.com/grafana/grafana-plugin-sdk-go/backend"
15	"github.com/grafana/grafana-plugin-sdk-go/data"
16	"github.com/grafana/grafana-plugin-sdk-go/experimental"
17	"github.com/grafana/grafana/pkg/components/simplejson"
18	"github.com/grafana/grafana/pkg/tsdb/influxdb/models"
19	"github.com/stretchr/testify/assert"
20	"github.com/stretchr/testify/require"
21	"github.com/xorcare/pointer"
22
23	influxdb2 "github.com/influxdata/influxdb-client-go/v2"
24	"github.com/influxdata/influxdb-client-go/v2/api"
25)
26
27//--------------------------------------------------------------
28// TestData -- reads result from saved files
29//--------------------------------------------------------------
30
31// MockRunner reads local file path for testdata.
32type MockRunner struct {
33	testDataPath string
34}
35
36func (r *MockRunner) runQuery(ctx context.Context, q string) (*api.QueryTableResult, error) {
37	bytes, err := ioutil.ReadFile(filepath.Join("testdata", r.testDataPath))
38	if err != nil {
39		return nil, err
40	}
41
42	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
43		time.Sleep(100 * time.Millisecond)
44		if r.Method == http.MethodPost {
45			w.WriteHeader(http.StatusOK)
46			_, err := w.Write(bytes)
47			if err != nil {
48				panic(fmt.Sprintf("Failed to write response: %s", err))
49			}
50		} else {
51			w.WriteHeader(http.StatusNotFound)
52		}
53	}))
54	defer server.Close()
55
56	client := influxdb2.NewClient(server.URL, "a")
57	return client.QueryAPI("x").Query(ctx, q)
58}
59
60func executeMockedQuery(t *testing.T, name string, query queryModel) *backend.DataResponse {
61	runner := &MockRunner{
62		testDataPath: name + ".csv",
63	}
64
65	dr := executeQuery(context.Background(), query, runner, 50)
66	return &dr
67}
68
69func verifyGoldenResponse(t *testing.T, name string) *backend.DataResponse {
70	dr := executeMockedQuery(t, name, queryModel{MaxDataPoints: 100})
71
72	err := experimental.CheckGoldenDataResponse(filepath.Join("testdata", fmt.Sprintf("%s.golden.txt", name)),
73		dr, true)
74	require.NoError(t, err)
75	require.NoError(t, dr.Error)
76
77	return dr
78}
79
80func TestExecuteSimple(t *testing.T) {
81	dr := verifyGoldenResponse(t, "simple")
82	require.Len(t, dr.Frames, 1)
83	require.Contains(t, dr.Frames[0].Name, "test")
84	require.Len(t, dr.Frames[0].Fields[1].Labels, 2)
85	require.Equal(t, "Time", dr.Frames[0].Fields[0].Name)
86
87	st, err := dr.Frames[0].StringTable(-1, -1)
88	require.NoError(t, err)
89	fmt.Println(st)
90	fmt.Println("----------------------")
91}
92
93func TestExecuteSingle(t *testing.T) {
94	dr := verifyGoldenResponse(t, "single")
95	require.Len(t, dr.Frames, 1)
96}
97
98func TestExecuteMultiple(t *testing.T) {
99	dr := verifyGoldenResponse(t, "multiple")
100	require.Len(t, dr.Frames, 3)
101	require.Contains(t, dr.Frames[0].Name, "test")
102	require.Len(t, dr.Frames[0].Fields[1].Labels, 2)
103	require.Equal(t, "Time", dr.Frames[0].Fields[0].Name)
104
105	st, err := dr.Frames[0].StringTable(-1, -1)
106	require.NoError(t, err)
107	fmt.Println(st)
108	fmt.Println("----------------------")
109}
110
111func TestExecuteColumnNamedTable(t *testing.T) {
112	dr := verifyGoldenResponse(t, "table")
113	require.Len(t, dr.Frames, 1)
114}
115
116func TestExecuteGrouping(t *testing.T) {
117	dr := verifyGoldenResponse(t, "grouping")
118	require.Len(t, dr.Frames, 3)
119	require.Contains(t, dr.Frames[0].Name, "system")
120	require.Len(t, dr.Frames[0].Fields[1].Labels, 1)
121	require.Equal(t, "Time", dr.Frames[0].Fields[0].Name)
122
123	st, err := dr.Frames[0].StringTable(-1, -1)
124	require.NoError(t, err)
125	fmt.Println(st)
126	fmt.Println("----------------------")
127}
128
129func TestAggregateGrouping(t *testing.T) {
130	dr := verifyGoldenResponse(t, "aggregate")
131	require.Len(t, dr.Frames, 1)
132
133	str, err := dr.Frames[0].StringTable(-1, -1)
134	require.NoError(t, err)
135	fmt.Println(str)
136
137	// 	 `Name:
138	// Dimensions: 2 Fields by 3 Rows
139	// +-------------------------------+--------------------------+
140	// | Name: Time                    | Name: Value              |
141	// | Labels:                       | Labels: host=hostname.ru |
142	// | Type: []*time.Time            | Type: []*float64         |
143	// +-------------------------------+--------------------------+
144	// | 2020-06-05 12:06:00 +0000 UTC | 8.291                    |
145	// | 2020-06-05 12:07:00 +0000 UTC | 0.534                    |
146	// | 2020-06-05 12:08:00 +0000 UTC | 0.667                    |
147	// +-------------------------------+--------------------------+
148	// `
149
150	t1 := time.Date(2020, 6, 5, 12, 6, 0, 0, time.UTC)
151	t2 := time.Date(2020, 6, 5, 12, 7, 0, 0, time.UTC)
152	t3 := time.Date(2020, 6, 5, 12, 8, 0, 0, time.UTC)
153
154	expectedFrame := data.NewFrame("",
155		data.NewField("Time", nil, []*time.Time{&t1, &t2, &t3}),
156		data.NewField("Value", map[string]string{"host": "hostname.ru"}, []*float64{
157			pointer.Float64(8.291),
158			pointer.Float64(0.534),
159			pointer.Float64(0.667),
160		}),
161	)
162	expectedFrame.Meta = &data.FrameMeta{}
163
164	diff := cmp.Diff(expectedFrame, dr.Frames[0], data.FrameTestCompareOptions()...)
165	assert.Empty(t, diff)
166}
167
168func TestNonStandardTimeColumn(t *testing.T) {
169	dr := verifyGoldenResponse(t, "non_standard_time_column")
170	require.Len(t, dr.Frames, 1)
171
172	str, err := dr.Frames[0].StringTable(-1, -1)
173	require.NoError(t, err)
174	fmt.Println(str)
175
176	// Dimensions: 3 Fields by 1 Rows
177	// +-----------------------------------------+-----------------------------------------+------------------+
178	// | Name: _start_water                      | Name: _stop_water                       | Name: _value     |
179	// | Labels: st=1                            | Labels: st=1                            | Labels: st=1     |
180	// | Type: []*time.Time                      | Type: []*time.Time                      | Type: []*float64 |
181	// +-----------------------------------------+-----------------------------------------+------------------+
182	// | 2020-06-28 17:50:13.012584046 +0000 UTC | 2020-06-29 17:50:13.012584046 +0000 UTC | 156.304          |
183	// +-----------------------------------------+-----------------------------------------+------------------+
184
185	t1 := time.Date(2020, 6, 28, 17, 50, 13, 12584046, time.UTC)
186	t2 := time.Date(2020, 6, 29, 17, 50, 13, 12584046, time.UTC)
187
188	expectedFrame := data.NewFrame("",
189		data.NewField("_start_water", map[string]string{"st": "1"}, []*time.Time{&t1}),
190		data.NewField("_stop_water", map[string]string{"st": "1"}, []*time.Time{&t2}),
191		data.NewField("_value", map[string]string{"st": "1"}, []*float64{
192			pointer.Float64(156.304),
193		}),
194	)
195	expectedFrame.Meta = &data.FrameMeta{}
196
197	diff := cmp.Diff(expectedFrame, dr.Frames[0], data.FrameTestCompareOptions()...)
198	assert.Empty(t, diff)
199}
200
201func TestBuckets(t *testing.T) {
202	verifyGoldenResponse(t, "buckets")
203}
204
205func TestBooleanTagGrouping(t *testing.T) {
206	verifyGoldenResponse(t, "boolean_tag")
207}
208
209func TestBooleanData(t *testing.T) {
210	verifyGoldenResponse(t, "boolean_data")
211}
212
213func TestGoldenFiles(t *testing.T) {
214	verifyGoldenResponse(t, "renamed")
215}
216
217func TestRealQuery(t *testing.T) {
218	t.Skip() // this is used for local testing
219
220	t.Run("Check buckets() query on localhost", func(t *testing.T) {
221		json := simplejson.New()
222		json.Set("organization", "test-org")
223
224		dsInfo := &models.DatasourceInfo{
225			URL: "http://localhost:9999", // NOTE! no api/v2
226		}
227
228		runner, err := runnerFromDataSource(dsInfo)
229		require.NoError(t, err)
230
231		dr := executeQuery(context.Background(), queryModel{
232			MaxDataPoints: 100,
233			RawQuery:      "buckets()",
234		}, runner, 50)
235		err = experimental.CheckGoldenDataResponse(filepath.Join("testdata", "buckets-real.golden.txt"), &dr, true)
236		require.NoError(t, err)
237	})
238}
239
240func assertDataResponseDimensions(t *testing.T, dr *backend.DataResponse, rows int, columns int) {
241	require.Len(t, dr.Frames, 1)
242	fields := dr.Frames[0].Fields
243	require.Len(t, fields, rows)
244	require.Equal(t, fields[0].Len(), columns)
245	require.Equal(t, fields[1].Len(), columns)
246}
247
248func TestMaxDataPointsExceededNoAggregate(t *testing.T) {
249	// unfortunately the golden-response style tests do not support
250	// responses that contain errors, so we can only do manual checks
251	// on the DataResponse
252	dr := executeMockedQuery(t, "max_data_points_exceeded", queryModel{MaxDataPoints: 2})
253
254	// it should contain the error-message
255	require.EqualError(t, dr.Error, "A query returned too many datapoints and the results have been truncated at 21 points to prevent memory issues. At the current graph size, Grafana can only draw 2. Try using the aggregateWindow() function in your query to reduce the number of points returned.")
256	assertDataResponseDimensions(t, dr, 2, 21)
257}
258
259func TestMaxDataPointsExceededWithAggregate(t *testing.T) {
260	// unfortunately the golden-response style tests do not support
261	// responses that contain errors, so we can only do manual checks
262	// on the DataResponse
263	dr := executeMockedQuery(t, "max_data_points_exceeded", queryModel{RawQuery: "aggregateWindow()", MaxDataPoints: 2})
264
265	// it should contain the error-message
266	require.EqualError(t, dr.Error, "A query returned too many datapoints and the results have been truncated at 21 points to prevent memory issues. At the current graph size, Grafana can only draw 2.")
267	assertDataResponseDimensions(t, dr, 2, 21)
268}
269
270func TestMultivalue(t *testing.T) {
271	// we await a non-labeled _time column
272	// and two value-columns named _value and _value2
273	dr := verifyGoldenResponse(t, "multivalue")
274	require.Len(t, dr.Frames, 4)
275	frame := dr.Frames[0]
276	require.Len(t, frame.Fields, 3)
277	require.Equal(t, frame.Fields[0].Name, "_time")
278	require.Equal(t, frame.Fields[0].Len(), 2)
279	require.Len(t, frame.Fields[0].Labels, 0)
280	require.Equal(t, frame.Fields[1].Name, "_value")
281	require.Len(t, frame.Fields[1].Labels, 5)
282	require.Equal(t, frame.Fields[2].Name, "_value2")
283	require.Len(t, frame.Fields[2].Labels, 5)
284}
285
286func TestMultiTime(t *testing.T) {
287	// we await three columns, _time, _time2, _value
288	// all have all labels
289	dr := verifyGoldenResponse(t, "multitime")
290	require.Len(t, dr.Frames, 4)
291	frame := dr.Frames[0]
292	require.Len(t, frame.Fields, 3)
293	require.Equal(t, frame.Fields[0].Name, "_time")
294	require.Equal(t, frame.Fields[0].Len(), 1)
295	require.Len(t, frame.Fields[0].Labels, 5)
296	require.Equal(t, frame.Fields[1].Name, "_time2")
297	require.Len(t, frame.Fields[1].Labels, 5)
298	require.Equal(t, frame.Fields[2].Name, "_value")
299	require.Len(t, frame.Fields[2].Labels, 5)
300}
301
302func TestTimestampFirst(t *testing.T) {
303	dr := verifyGoldenResponse(t, "time_first")
304	require.Len(t, dr.Frames, 1)
305	// we make sure the timestamp-column is the first column
306	// in the dataframe, even if it was not the first column
307	// in the csv.
308	require.Equal(t, "Time", dr.Frames[0].Fields[0].Name)
309	require.Equal(t, "Value", dr.Frames[0].Fields[1].Name)
310}
311