summaryrefslogtreecommitdiff
path: root/internal/mapr/logformat/variables_test.go
blob: 7843855757334f615e7a257e475e2662801ea31d (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
package logformat

import (
	"strings"
	"testing"

	"github.com/mimecast/dtail/internal/mapr"
)

// TestPlanVariableWarnings locks the plan-time unknown-$-variable diagnostic.
// The warning exists to surface the silent footgun where a mistyped or wrongly
// $-prefixed field (e.g. "$service" instead of the bareword "service", or
// "$time" without a "from"/logformat clause) collapses an aggregation into a
// single empty group with zero other diagnostics.
func TestPlanVariableWarnings(t *testing.T) {
	tests := []struct {
		name string
		// query is parsed and its parser resolved via EffectiveLogFormat so the
		// test exercises the same selection rule the client uses.
		query string
		// wantVars lists the $-variables that must be flagged (each appears in
		// exactly one warning line).
		wantVars []string
		// unwantVars lists names that must NOT appear in any warning.
		unwantVars []string
	}{
		{
			name:       "ys0 footgun: $-prefixed dynamic field with from clause",
			query:      "from STATS select $service,sum($bytes) group by $service",
			wantVars:   []string{"$service", "$bytes"},
			unwantVars: []string{"service", "bytes"},
		},
		{
			name:       "generic parser without from warns for default-format $time",
			query:      "select $time,$line",
			wantVars:   []string{"$time"},
			unwantVars: []string{"$line"},
		},
		{
			name:       "valid default-format variables do not warn",
			query:      "from STATS select $hostname,max($goroutines),$time group by $hostname",
			wantVars:   nil,
			unwantVars: []string{"$hostname", "$goroutines", "$time"},
		},
		{
			name:       "barewords never warn",
			query:      "from STATS select service,sum(bytes) group by service",
			wantVars:   nil,
			unwantVars: []string{"service", "bytes", "$service", "$bytes"},
		},
		{
			name:       "built-in $empty does not warn",
			query:      "from STATS select $empty,count($line) group by $empty",
			wantVars:   nil,
			unwantVars: []string{"$empty", "$line"},
		},
		{
			name:       "set-defined variable does not warn",
			query:      "from STATS select $masked group by $masked set $masked = maskdigits($line)",
			wantVars:   nil,
			unwantVars: []string{"$masked"},
		},
		{
			name:       "unknown variable referenced only in where clause warns",
			query:      "from STATS select $line where $bogus eq foo",
			wantVars:   []string{"$bogus"},
			unwantVars: []string{"$line"},
		},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			query, err := mapr.NewQuery(tc.query)
			if err != nil {
				t.Fatalf("NewQuery(%q) failed: %v", tc.query, err)
			}
			logFormat := query.EffectiveLogFormat("")
			warnings := PlanVariableWarnings(query, logFormat)

			joined := strings.Join(warnings, "\n")
			for _, want := range tc.wantVars {
				if countVarWarnings(warnings, want) != 1 {
					t.Errorf("expected exactly one warning for %q, got warnings:\n%s",
						want, joined)
				}
			}
			for _, unwant := range tc.unwantVars {
				if countVarWarnings(warnings, unwant) != 0 {
					t.Errorf("did not expect a warning for %q, got warnings:\n%s",
						unwant, joined)
				}
			}
		})
	}
}

// TestPlanVariableWarningsText locks the exact warning wording so downstream
// tooling and users can rely on it.
func TestPlanVariableWarningsText(t *testing.T) {
	query, err := mapr.NewQuery("from STATS select $service group by $service")
	if err != nil {
		t.Fatalf("NewQuery failed: %v", err)
	}
	warnings := PlanVariableWarnings(query, query.EffectiveLogFormat(""))
	if len(warnings) != 1 {
		t.Fatalf("expected exactly one warning (deduped), got %d: %v", len(warnings), warnings)
	}
	want := `warning: $service is not a known variable for log format "default"; did you mean bareword service?`
	if warnings[0] != want {
		t.Errorf("warning text mismatch:\n got: %s\nwant: %s", warnings[0], want)
	}
}

// TestPlanVariableWarningsNonEnumerable ensures that log formats whose variable
// set cannot be determined statically produce no warnings, avoiding false
// positives that would train users to ignore the diagnostic.
func TestPlanVariableWarningsNonEnumerable(t *testing.T) {
	query, err := mapr.NewQuery("from STATS select $whatever group by $whatever")
	if err != nil {
		t.Fatalf("NewQuery failed: %v", err)
	}
	for _, format := range []string{"mimecast", "custom1", "custom2", "doesnotexist"} {
		if warnings := PlanVariableWarnings(query, format); len(warnings) != 0 {
			t.Errorf("expected no warnings for non-enumerable format %q, got: %v",
				format, warnings)
		}
	}
}

// countVarWarnings counts warnings whose SUBJECT is the given variable, i.e.
// "warning: <variable> is not a known variable ...". It deliberately matches
// only the subject and not the "did you mean bareword X" suggestion, so that a
// bareword appearing in a suggestion is not mistaken for a warning about that
// bareword. The trailing " is not" also prevents "$service" from matching a
// hypothetical "$services" subject.
func countVarWarnings(warnings []string, variable string) int {
	subject := "warning: " + variable + " is not a known variable"
	count := 0
	for _, w := range warnings {
		if strings.HasPrefix(w, subject) {
			count++
		}
	}
	return count
}