package starlark

import (
	"fmt"
	"io/ioutil"
	"os"
	"path/filepath"
	"strings"
	"testing"
	"time"

	"github.com/influxdata/telegraf"
	"github.com/influxdata/telegraf/plugins/parsers"
	"github.com/influxdata/telegraf/testutil"
	"github.com/stretchr/testify/require"
)

// Tests for runtime errors in the processors Init function.
func TestInitError(t *testing.T) {
	tests := []struct {
		name   string
		plugin *Starlark
	}{
		{
			name: "source must define apply",
			plugin: &Starlark{
				Source: "",
				Log:    testutil.Logger{},
			},
		},
		{
			name: "apply must be a function",
			plugin: &Starlark{
				Source: `
apply = 42
`,
				Log: testutil.Logger{},
			},
		},
		{
			name: "apply function must take one arg",
			plugin: &Starlark{
				Source: `
def apply():
	pass
`,
				Log: testutil.Logger{},
			},
		},
		{
			name: "package scope must have valid syntax",
			plugin: &Starlark{
				Source: `
for
`,
				Log: testutil.Logger{},
			},
		},
		{
			name: "no source no script",
			plugin: &Starlark{
				Log: testutil.Logger{},
			},
		},
		{
			name: "source and script",
			plugin: &Starlark{
				Source: `
def apply():
	pass
`,
				Script: "testdata/ratio.star",
				Log:    testutil.Logger{},
			},
		},
		{
			name: "script file not found",
			plugin: &Starlark{
				Script: "testdata/file_not_found.star",
				Log:    testutil.Logger{},
			},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			err := tt.plugin.Init()
			require.Error(t, err)
		})
	}
}

func TestApply(t *testing.T) {
	// Tests for the behavior of the processors Apply function.
	var applyTests = []struct {
		name             string
		source           string
		input            []telegraf.Metric
		expected         []telegraf.Metric
		expectedErrorStr string
	}{
		{
			name: "drop metric",
			source: `
def apply(metric):
	return None
`,
			input: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{},
					map[string]interface{}{"time_idle": 42},
					time.Unix(0, 0),
				),
			},
			expected: []telegraf.Metric{},
		},
		{
			name: "passthrough",
			source: `
def apply(metric):
	return metric
`,
			input: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{},
					map[string]interface{}{"time_idle": 42},
					time.Unix(0, 0),
				),
			},
			expected: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{},
					map[string]interface{}{"time_idle": 42},
					time.Unix(0, 0),
				),
			},
		},
		{
			name: "read value from global scope",
			source: `
names = {
	'cpu': 'cpu2',
	'mem': 'mem2',
}

def apply(metric):
	metric.name = names[metric.name]
	return metric
`,
			input: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{},
					map[string]interface{}{
						"time_idle": 42.0,
					},
					time.Unix(0, 0),
				),
			},
			expected: []telegraf.Metric{
				testutil.MustMetric("cpu2",
					map[string]string{},
					map[string]interface{}{
						"time_idle": 42.0,
					},
					time.Unix(0, 0),
				),
			},
		},
		{
			name: "cannot write to frozen global scope",
			source: `
cache = []

def apply(metric):
	cache.append(deepcopy(metric))
	return metric
`,
			input: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{},
					map[string]interface{}{
						"time_idle": 1.0,
					},
					time.Unix(0, 0),
				),
			},
			expected:         []telegraf.Metric{},
			expectedErrorStr: "append: cannot append to frozen list",
		},
		{
			name: "cannot return multiple references to same metric",
			source: `
def apply(metric):
	# Should be return [metric, deepcopy(metric)]
	return [metric, metric]
`,
			input: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{},
					map[string]interface{}{
						"time_idle": 42.0,
					},
					time.Unix(0, 0),
				),
			},
			expected: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{},
					map[string]interface{}{
						"time_idle": 42.0,
					},
					time.Unix(0, 0),
				),
			},
		},
	}

	for _, tt := range applyTests {
		t.Run(tt.name, func(t *testing.T) {
			plugin := &Starlark{
				Source: tt.source,
				Log:    testutil.Logger{},
			}
			err := plugin.Init()
			require.NoError(t, err)

			var acc testutil.Accumulator

			err = plugin.Start(&acc)
			require.NoError(t, err)

			for _, m := range tt.input {
				err = plugin.Add(m, &acc)
				if tt.expectedErrorStr != "" {
					require.EqualError(t, err, tt.expectedErrorStr)
				} else {
					require.NoError(t, err)
				}
			}

			err = plugin.Stop()
			require.NoError(t, err)

			testutil.RequireMetricsEqual(t, tt.expected, acc.GetTelegrafMetrics())
		})
	}
}

// Tests for the behavior of the Metric type.
func TestMetric(t *testing.T) {
	var tests = []struct {
		name             string
		source           string
		input            []telegraf.Metric
		expected         []telegraf.Metric
		expectedErrorStr string
	}{
		{
			name: "create new metric",
			source: `
def apply(metric):
	m = Metric('cpu')
	m.fields['time_guest'] = 2.0
	m.time = 0
	return m
`,
			input: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{},
					map[string]interface{}{
						"time_idle": 42.0,
					},
					time.Unix(0, 0),
				),
			},
			expected: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{},
					map[string]interface{}{
						"time_guest": 2.0,
					},
					time.Unix(0, 0),
				),
			},
		},
		{
			name: "deepcopy",
			source: `
def apply(metric):
	return [metric, deepcopy(metric)]
`,
			input: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{},
					map[string]interface{}{
						"time_idle": 42.0,
					},
					time.Unix(0, 0),
				),
			},
			expected: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{},
					map[string]interface{}{
						"time_idle": 42.0,
					},
					time.Unix(0, 0),
				),
				testutil.MustMetric("cpu",
					map[string]string{},
					map[string]interface{}{
						"time_idle": 42.0,
					},
					time.Unix(0, 0),
				),
			},
		},
		{
			name: "set name",
			source: `
def apply(metric):
	metric.name = "howdy"
	return metric
`,
			input: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{},
					map[string]interface{}{"time_idle": 42.0},
					time.Unix(0, 0),
				),
			},
			expected: []telegraf.Metric{
				testutil.MustMetric("howdy",
					map[string]string{},
					map[string]interface{}{"time_idle": 42.0},
					time.Unix(0, 0),
				),
			},
		},
		{
			name: "set name wrong type",
			source: `
def apply(metric):
	metric.name = 42
	return metric
`,
			input: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{},
					map[string]interface{}{"time_idle": 42.0},
					time.Unix(0, 0),
				),
			},
			expected:         []telegraf.Metric{},
			expectedErrorStr: "type error",
		},
		{
			name: "get name",
			source: `
def apply(metric):
	metric.tags['measurement'] = metric.name
	return metric
`,
			input: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{},
					map[string]interface{}{"time_idle": 42.0},
					time.Unix(0, 0),
				),
			},
			expected: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{
						"measurement": "cpu",
					},
					map[string]interface{}{"time_idle": 42.0},
					time.Unix(0, 0),
				),
			},
		},
		{
			name: "getattr tags",
			source: `
def apply(metric):
	metric.tags
	return metric
`,
			input: []telegraf.Metric{
				testutil.MustMetric(
					"cpu",
					map[string]string{},
					map[string]interface{}{
						"time_idle": 42.0,
					},
					time.Unix(0, 0),
				),
			},
			expected: []telegraf.Metric{
				testutil.MustMetric(
					"cpu",
					map[string]string{},
					map[string]interface{}{
						"time_idle": 42.0,
					},
					time.Unix(0, 0),
				),
			},
		},
		{
			name: "setattr tags is not allowed",
			source: `
def apply(metric):
	metric.tags = {}
	return metric
		`,
			input: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{},
					map[string]interface{}{"time_idle": 42},
					time.Unix(0, 0),
				),
			},
			expected:         []telegraf.Metric{},
			expectedErrorStr: "cannot set tags",
		},
		{
			name: "empty tags are false",
			source: `
def apply(metric):
	if not metric.tags:
		return metric
	return None
`,
			input: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{},
					map[string]interface{}{
						"time_idle": 42.0,
					},
					time.Unix(0, 0),
				),
			},
			expected: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{},
					map[string]interface{}{
						"time_idle": 42.0,
					},
					time.Unix(0, 0),
				),
			},
		},
		{
			name: "non-empty tags are true",
			source: `
def apply(metric):
	if metric.tags:
		return metric
	return None
`,
			input: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{
						"host": "example.org",
					},
					map[string]interface{}{
						"time_idle": 42.0,
					},
					time.Unix(0, 0),
				),
			},
			expected: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{
						"host": "example.org",
					},
					map[string]interface{}{
						"time_idle": 42.0,
					},
					time.Unix(0, 0),
				),
			},
		},
		{
			name: "tags in operator",
			source: `
def apply(metric):
	if 'host' not in metric.tags:
		return
	return metric
`,
			input: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{
						"host": "example.org",
					},
					map[string]interface{}{"time_idle": 42.0},
					time.Unix(0, 0),
				),
			},
			expected: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{
						"host": "example.org",
					},
					map[string]interface{}{"time_idle": 42.0},
					time.Unix(0, 0),
				),
			},
		},
		{
			name: "lookup tag",
			source: `
def apply(metric):
	metric.tags['result'] = metric.tags['host']
	return metric
`,
			input: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{
						"host": "example.org",
					},
					map[string]interface{}{"time_idle": 42.0},
					time.Unix(0, 0),
				),
			},
			expected: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{
						"host":   "example.org",
						"result": "example.org",
					},
					map[string]interface{}{"time_idle": 42.0},
					time.Unix(0, 0),
				),
			},
		},
		{
			name: "lookup tag not set",
			source: `
def apply(metric):
	metric.tags['foo']
	return metric
		`,
			input: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{},
					map[string]interface{}{"time_idle": 42},
					time.Unix(0, 0),
				),
			},
			expected:         []telegraf.Metric{},
			expectedErrorStr: `key "foo" not in Tags`,
		},
		{
			name: "get tag",
			source: `
def apply(metric):
	metric.tags['result'] = metric.tags.get('host')
	return metric
`,
			input: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{
						"host": "example.org",
					},
					map[string]interface{}{"time_idle": 42.0},
					time.Unix(0, 0),
				),
			},
			expected: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{
						"host":   "example.org",
						"result": "example.org",
					},
					map[string]interface{}{"time_idle": 42.0},
					time.Unix(0, 0),
				),
			},
		},
		{
			name: "get tag default",
			source: `
def apply(metric):
	metric.tags['result'] = metric.tags.get('foo', 'example.org')
	return metric
		`,
			input: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{},
					map[string]interface{}{"time_idle": 42},
					time.Unix(0, 0),
				),
			},
			expected: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{
						"result": "example.org",
					},
					map[string]interface{}{"time_idle": 42},
					time.Unix(0, 0),
				),
			},
		},
		{
			name: "get tag not set returns none",
			source: `
def apply(metric):
	if metric.tags.get('foo') != None:
		return
	return metric
		`,
			input: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{},
					map[string]interface{}{"time_idle": 42},
					time.Unix(0, 0),
				),
			},
			expected: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{},
					map[string]interface{}{"time_idle": 42},
					time.Unix(0, 0),
				),
			},
		},
		{
			name: "set tag",
			source: `
def apply(metric):
	metric.tags['host'] = 'example.org'
	return metric
`,
			input: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{},
					map[string]interface{}{"time_idle": 42.0},
					time.Unix(0, 0),
				),
			},
			expected: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{
						"host": "example.org",
					},
					map[string]interface{}{"time_idle": 42.0},
					time.Unix(0, 0),
				),
			},
		},
		{
			name: "set tag type error",
			source: `
def apply(metric):
	metric.tags['host'] = 42
	return metric
`,
			input: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{},
					map[string]interface{}{"time_idle": 42.0},
					time.Unix(0, 0),
				),
			},
			expected:         []telegraf.Metric{},
			expectedErrorStr: "tag value must be of type 'str'",
		},
		{
			name: "pop tag",
			source: `
def apply(metric):
	metric.tags['host2'] = metric.tags.pop('host')
	return metric
`,
			input: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{
						"host": "example.org",
					},
					map[string]interface{}{"time_idle": 0},
					time.Unix(0, 0),
				),
			},
			expected: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{
						"host2": "example.org",
					},
					map[string]interface{}{"time_idle": 0},
					time.Unix(0, 0),
				),
			},
		},
		{
			name: "popitem tags",
			source: `
def apply(metric):
	metric.tags['result'] = '='.join(metric.tags.popitem())
	return metric
`,
			input: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{
						"host": "example.org",
					},
					map[string]interface{}{"time_idle": 0},
					time.Unix(0, 0),
				),
			},
			expected: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{
						"result": "host=example.org",
					},
					map[string]interface{}{"time_idle": 0},
					time.Unix(0, 0),
				),
			},
		},
		{
			name: "popitem tags empty dict",
			source: `
def apply(metric):
	metric.tags.popitem()
	return metric
`,
			input: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{},
					map[string]interface{}{"time_idle": 0},
					time.Unix(0, 0),
				),
			},
			expected:         []telegraf.Metric{},
			expectedErrorStr: "popitem(): tag dictionary is empty",
		},
		{
			name: "tags setdefault key not set",
			source: `
def apply(metric):
	metric.tags.setdefault('a', 'b')
	return metric
`,
			input: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{},
					map[string]interface{}{"time_idle": 0},
					time.Unix(0, 0),
				),
			},
			expected: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{
						"a": "b",
					},
					map[string]interface{}{"time_idle": 0},
					time.Unix(0, 0),
				),
			},
		},
		{
			name: "tags setdefault key already set",
			source: `
def apply(metric):
	metric.tags.setdefault('a', 'c')
	return metric
`,
			input: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{
						"a": "b",
					},
					map[string]interface{}{"time_idle": 0},
					time.Unix(0, 0),
				),
			},
			expected: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{
						"a": "b",
					},
					map[string]interface{}{"time_idle": 0},
					time.Unix(0, 0),
				),
			},
		},
		{
			name: "tags update list of tuple",
			source: `
def apply(metric):
	metric.tags.update([('b', 'y'), ('c', 'z')])
	return metric
`,
			input: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{
						"a": "x",
					},
					map[string]interface{}{"time_idle": 0},
					time.Unix(0, 0),
				),
			},
			expected: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{
						"a": "x",
						"b": "y",
						"c": "z",
					},
					map[string]interface{}{"time_idle": 0},
					time.Unix(0, 0),
				),
			},
		},
		{
			name: "tags update kwargs",
			source: `
def apply(metric):
	metric.tags.update(b='y', c='z')
	return metric
`,
			input: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{
						"a": "x",
					},
					map[string]interface{}{"time_idle": 0},
					time.Unix(0, 0),
				),
			},
			expected: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{
						"a": "x",
						"b": "y",
						"c": "z",
					},
					map[string]interface{}{"time_idle": 0},
					time.Unix(0, 0),
				),
			},
		},
		{
			name: "tags update dict",
			source: `
def apply(metric):
	metric.tags.update({'b': 'y', 'c': 'z'})
	return metric
`,
			input: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{
						"a": "x",
					},
					map[string]interface{}{"time_idle": 0},
					time.Unix(0, 0),
				),
			},
			expected: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{
						"a": "x",
						"b": "y",
						"c": "z",
					},
					map[string]interface{}{"time_idle": 0},
					time.Unix(0, 0),
				),
			},
		},
		{
			name: "tags update list tuple and kwargs",
			source: `
def apply(metric):
	metric.tags.update([('b', 'y'), ('c', 'z')], d='zz')
	return metric
`,
			input: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{
						"a": "x",
					},
					map[string]interface{}{"time_idle": 0},
					time.Unix(0, 0),
				),
			},
			expected: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{
						"a": "x",
						"b": "y",
						"c": "z",
						"d": "zz",
					},
					map[string]interface{}{"time_idle": 0},
					time.Unix(0, 0),
				),
			},
		},
		{
			name: "iterate tags",
			source: `
def apply(metric):
	for k in metric.tags:
		pass
	return metric
`,
			input: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{
						"host": "example.org",
						"cpu":  "cpu0",
						"foo":  "bar",
					},
					map[string]interface{}{"time_idle": 42.0},
					time.Unix(0, 0),
				),
			},
			expected: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{
						"host": "example.org",
						"cpu":  "cpu0",
						"foo":  "bar",
					},
					map[string]interface{}{"time_idle": 42.0},
					time.Unix(0, 0),
				),
			},
		},
		{
			name: "iterate tags and copy to fields",
			source: `
def apply(metric):
	for k in metric.tags:
		metric.fields[k] = k
	return metric
`,
			input: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{
						"host": "example.org",
						"cpu":  "cpu0",
					},
					map[string]interface{}{"time_idle": 42},
					time.Unix(0, 0),
				),
			},
			expected: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{
						"host": "example.org",
						"cpu":  "cpu0",
					},
					map[string]interface{}{
						"host":      "host",
						"cpu":       "cpu",
						"time_idle": 42,
					},
					time.Unix(0, 0),
				),
			},
		},
		{
			name: "iterate tag keys",
			source: `
def apply(metric):
	for k in metric.tags.keys():
		pass
	return metric
`,
			input: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{
						"host": "example.org",
						"cpu":  "cpu0",
						"foo":  "bar",
					},
					map[string]interface{}{"time_idle": 42.0},
					time.Unix(0, 0),
				),
			},
			expected: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{
						"host": "example.org",
						"cpu":  "cpu0",
						"foo":  "bar",
					},
					map[string]interface{}{"time_idle": 42.0},
					time.Unix(0, 0),
				),
			},
		},
		{
			name: "iterate tag keys and copy to fields",
			source: `
def apply(metric):
	for k in metric.tags.keys():
		metric.fields[k] = k
	return metric
`,
			input: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{
						"host": "example.org",
						"cpu":  "cpu0",
					},
					map[string]interface{}{"time_idle": 42},
					time.Unix(0, 0),
				),
			},
			expected: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{
						"host": "example.org",
						"cpu":  "cpu0",
					},
					map[string]interface{}{
						"host":      "host",
						"cpu":       "cpu",
						"time_idle": 42,
					},
					time.Unix(0, 0),
				),
			},
		},
		{
			name: "iterate tag items",
			source: `
def apply(metric):
	for k, v in metric.tags.items():
		pass
	return metric
`,
			input: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{
						"host": "example.org",
						"cpu":  "cpu0",
					},
					map[string]interface{}{"time_idle": 42},
					time.Unix(0, 0),
				),
			},
			expected: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{
						"host": "example.org",
						"cpu":  "cpu0",
					},
					map[string]interface{}{"time_idle": 42},
					time.Unix(0, 0),
				),
			},
		},
		{
			name: "iterate tag items and copy to fields",
			source: `
def apply(metric):
	for k, v in metric.tags.items():
		metric.fields[k] = v
	return metric
`,
			input: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{
						"host": "example.org",
						"cpu":  "cpu0",
					},
					map[string]interface{}{"time_idle": 42},
					time.Unix(0, 0),
				),
			},
			expected: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{
						"host": "example.org",
						"cpu":  "cpu0",
					},
					map[string]interface{}{
						"time_idle": 42,
						"host":      "example.org",
						"cpu":       "cpu0",
					},
					time.Unix(0, 0),
				),
			},
		},
		{
			name: "iterate tag values",
			source: `
def apply(metric):
	for v in metric.tags.values():
		pass
	return metric
`,
			input: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{
						"host": "example.org",
						"cpu":  "cpu0",
					},
					map[string]interface{}{"time_idle": 42},
					time.Unix(0, 0),
				),
			},
			expected: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{
						"host": "example.org",
						"cpu":  "cpu0",
					},
					map[string]interface{}{"time_idle": 42},
					time.Unix(0, 0),
				),
			},
		},
		{
			name: "iterate tag values and copy to fields",
			source: `
def apply(metric):
	for v in metric.tags.values():
		metric.fields[v] = v
	return metric
`,
			input: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{
						"host": "example.org",
						"cpu":  "cpu0",
					},
					map[string]interface{}{"time_idle": 42},
					time.Unix(0, 0),
				),
			},
			expected: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{
						"host": "example.org",
						"cpu":  "cpu0",
					},
					map[string]interface{}{
						"time_idle":   42,
						"example.org": "example.org",
						"cpu0":        "cpu0",
					},
					time.Unix(0, 0),
				),
			},
		},
		{
			name: "clear tags",
			source: `
def apply(metric):
	metric.tags.clear()
	return metric
`,
			input: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{
						"a": "b",
						"c": "d",
						"e": "f",
						"g": "h",
					},
					map[string]interface{}{"time_idle": 0},
					time.Unix(0, 0),
				),
			},
			expected: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{},
					map[string]interface{}{"time_idle": 0},
					time.Unix(0, 0),
				),
			},
		},
		{
			name: "tags cannot pop while iterating",
			source: `
def apply(metric):
	for k in metric.tags:
		metric.tags.pop(k)
	return metric
`,
			input: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{
						"a": "b",
						"c": "d",
						"e": "f",
						"g": "h",
					},
					map[string]interface{}{"time_idle": 0},
					time.Unix(0, 0),
				),
			},
			expected:         []telegraf.Metric{},
			expectedErrorStr: "pop: cannot delete during iteration",
		},
		{
			name: "tags cannot popitem while iterating",
			source: `
def apply(metric):
	for k in metric.tags:
		metric.tags.popitem()
	return metric
`,
			input: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{
						"a": "b",
						"c": "d",
						"e": "f",
						"g": "h",
					},
					map[string]interface{}{"time_idle": 0},
					time.Unix(0, 0),
				),
			},
			expected:         []telegraf.Metric{},
			expectedErrorStr: "cannot delete during iteration",
		},
		{
			name: "tags cannot clear while iterating",
			source: `
def apply(metric):
	for k in metric.tags:
		metric.tags.clear()
	return metric
`,
			input: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{
						"a": "b",
						"c": "d",
						"e": "f",
						"g": "h",
					},
					map[string]interface{}{"time_idle": 0},
					time.Unix(0, 0),
				),
			},
			expected:         []telegraf.Metric{},
			expectedErrorStr: "cannot delete during iteration",
		},
		{
			name: "tags cannot insert while iterating",
			source: `
def apply(metric):
	for k in metric.tags:
		metric.tags['i'] = 'j'
	return metric
`,
			input: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{
						"a": "b",
						"c": "d",
						"e": "f",
						"g": "h",
					},
					map[string]interface{}{"time_idle": 0},
					time.Unix(0, 0),
				),
			},
			expected:         []telegraf.Metric{},
			expectedErrorStr: "cannot insert during iteration",
		},
		{
			name: "tags can be cleared after iterating",
			source: `
def apply(metric):
	for k in metric.tags:
		pass
	metric.tags.clear()
	return metric
`,
			input: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{
						"a": "b",
					},
					map[string]interface{}{"time_idle": 0},
					time.Unix(0, 0),
				),
			},
			expected: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{},
					map[string]interface{}{"time_idle": 0},
					time.Unix(0, 0),
				),
			},
		},
		{
			name: "getattr fields",
			source: `
def apply(metric):
	metric.fields
	return metric
`,
			input: []telegraf.Metric{
				testutil.MustMetric(
					"cpu",
					map[string]string{},
					map[string]interface{}{
						"time_idle": 42.0,
					},
					time.Unix(0, 0),
				),
			},
			expected: []telegraf.Metric{
				testutil.MustMetric(
					"cpu",
					map[string]string{},
					map[string]interface{}{
						"time_idle": 42.0,
					},
					time.Unix(0, 0),
				),
			},
		},
		{
			name: "setattr fields is not allowed",
			source: `
def apply(metric):
	metric.fields = {}
	return metric
		`,
			input: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{},
					map[string]interface{}{"time_idle": 42},
					time.Unix(0, 0),
				),
			},
			expected:         []telegraf.Metric{},
			expectedErrorStr: "cannot set fields",
		},
		{
			name: "empty fields are false",
			source: `
def apply(metric):
	if not metric.fields:
		metric.fields["time_idle"] = 42
		return metric
	return None
`,
			input: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{},
					map[string]interface{}{},
					time.Unix(0, 0),
				),
			},
			expected: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{},
					map[string]interface{}{"time_idle": 42},
					time.Unix(0, 0),
				),
			},
		},
		{
			name: "non-empty fields are true",
			source: `
def apply(metric):
	if metric.fields:
		return metric
	return None
`,
			input: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{},
					map[string]interface{}{"time_idle": 42.0},
					time.Unix(0, 0),
				),
			},
			expected: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{},
					map[string]interface{}{"time_idle": 42.0},
					time.Unix(0, 0),
				),
			},
		},
		{
			name: "fields in operator",
			source: `
def apply(metric):
	if 'time_idle' not in metric.fields:
		return
	return metric
`,
			input: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{},
					map[string]interface{}{"time_idle": 42.0},
					time.Unix(0, 0),
				),
			},
			expected: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{},
					map[string]interface{}{"time_idle": 42.0},
					time.Unix(0, 0),
				),
			},
		},
		{
			name: "lookup string field",
			source: `
def apply(metric):
	value = metric.fields['value']
	if value != "xyzzy" and type(value) != "str":
		return
	return metric
`,
			input: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{},
					map[string]interface{}{"value": "xyzzy"},
					time.Unix(0, 0),
				),
			},
			expected: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{},
					map[string]interface{}{"value": "xyzzy"},
					time.Unix(0, 0),
				),
			},
		},
		{
			name: "lookup integer field",
			source: `
def apply(metric):
	value = metric.fields['value']
	if value != 42 and type(value) != "int":
		return
	return metric
`,
			input: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{},
					map[string]interface{}{"value": 42},
					time.Unix(0, 0),
				),
			},
			expected: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{},
					map[string]interface{}{"value": 42},
					time.Unix(0, 0),
				),
			},
		},
		{
			name: "lookup unsigned field",
			source: `
def apply(metric):
	value = metric.fields['value']
	if value != 42 and type(value) != "int":
		return
	return metric
`,
			input: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{},
					map[string]interface{}{"value": uint64(42)},
					time.Unix(0, 0),
				),
			},
			expected: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{},
					map[string]interface{}{"value": uint64(42)},
					time.Unix(0, 0),
				),
			},
		},
		{
			name: "lookup bool field",
			source: `
def apply(metric):
	value = metric.fields['value']
	if value != True and type(value) != "bool":
		return
	return metric
`,
			input: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{},
					map[string]interface{}{"value": true},
					time.Unix(0, 0),
				),
			},
			expected: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{},
					map[string]interface{}{"value": true},
					time.Unix(0, 0),
				),
			},
		},
		{
			name: "lookup float field",
			source: `
def apply(metric):
	value = metric.fields['value']
	if value != 42.0 and type(value) != "float":
		return
	return metric
`,
			input: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{},
					map[string]interface{}{"value": 42.0},
					time.Unix(0, 0),
				),
			},
			expected: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{},
					map[string]interface{}{"value": 42.0},
					time.Unix(0, 0),
				),
			},
		},
		{
			name: "lookup field not set",
			source: `
def apply(metric):
	metric.fields['foo']
	return metric
		`,
			input: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{},
					map[string]interface{}{"time_idle": 42},
					time.Unix(0, 0),
				),
			},
			expected:         []telegraf.Metric{},
			expectedErrorStr: `key "foo" not in Fields`,
		},
		{
			name: "get field",
			source: `
def apply(metric):
	metric.fields['result'] = metric.fields.get('time_idle')
	return metric
`,
			input: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{},
					map[string]interface{}{"time_idle": 42.0},
					time.Unix(0, 0),
				),
			},
			expected: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{},
					map[string]interface{}{
						"time_idle": 42.0,
						"result":    42.0,
					},
					time.Unix(0, 0),
				),
			},
		},
		{
			name: "get field default",
			source: `
def apply(metric):
	metric.fields['result'] = metric.fields.get('foo', 'example.org')
	return metric
		`,
			input: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{},
					map[string]interface{}{"time_idle": 42},
					time.Unix(0, 0),
				),
			},
			expected: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{},
					map[string]interface{}{
						"time_idle": 42,
						"result":    "example.org",
					},
					time.Unix(0, 0),
				),
			},
		},
		{
			name: "get field not set returns none",
			source: `
def apply(metric):
	if metric.fields.get('foo') != None:
		return
	return metric
		`,
			input: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{},
					map[string]interface{}{"time_idle": 42},
					time.Unix(0, 0),
				),
			},
			expected: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{},
					map[string]interface{}{"time_idle": 42},
					time.Unix(0, 0),
				),
			},
		},
		{
			name: "set string field",
			source: `
def apply(metric):
	metric.fields['host'] = 'example.org'
	return metric
`,
			input: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{},
					map[string]interface{}{},
					time.Unix(0, 0),
				),
			},
			expected: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{},
					map[string]interface{}{
						"host": "example.org",
					},
					time.Unix(0, 0),
				),
			},
		},
		{
			name: "set integer field",
			source: `
def apply(metric):
	metric.fields['time_idle'] = 42
	return metric
`,
			input: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{},
					map[string]interface{}{},
					time.Unix(0, 0),
				),
			},
			expected: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{},
					map[string]interface{}{
						"time_idle": 42,
					},
					time.Unix(0, 0),
				),
			},
		},
		{
			name: "set float field",
			source: `
def apply(metric):
	metric.fields['time_idle'] = 42.0
	return metric
`,
			input: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{},
					map[string]interface{}{},
					time.Unix(0, 0),
				),
			},
			expected: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{},
					map[string]interface{}{
						"time_idle": 42.0,
					},
					time.Unix(0, 0),
				),
			},
		},
		{
			name: "set bool field",
			source: `
def apply(metric):
	metric.fields['time_idle'] = True
	return metric
`,
			input: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{},
					map[string]interface{}{},
					time.Unix(0, 0),
				),
			},
			expected: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{},
					map[string]interface{}{
						"time_idle": true,
					},
					time.Unix(0, 0),
				),
			},
		},
		{
			name: "set field type error",
			source: `
def apply(metric):
	metric.fields['time_idle'] = {}
	return metric
`,
			input: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{},
					map[string]interface{}{},
					time.Unix(0, 0),
				),
			},
			expected:         []telegraf.Metric{},
			expectedErrorStr: "invalid starlark type",
		},
		{
			name: "pop field",
			source: `
def apply(metric):
	time_idle = metric.fields.pop('time_idle')
	if time_idle != 0:
		return
	return metric
`,
			input: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{},
					map[string]interface{}{
						"time_idle":  0,
						"time_guest": 0,
					},
					time.Unix(0, 0),
				),
			},
			expected: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{},
					map[string]interface{}{"time_guest": 0},
					time.Unix(0, 0),
				),
			},
		},
		{
			name: "popitem field",
			source: `
def apply(metric):
	item = metric.fields.popitem()
	if item != ("time_idle", 0):
		return
	metric.fields['time_guest'] = 0
	return metric
`,
			input: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{},
					map[string]interface{}{"time_idle": 0},
					time.Unix(0, 0),
				),
			},
			expected: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{},
					map[string]interface{}{"time_guest": 0},
					time.Unix(0, 0),
				),
			},
		},
		{
			name: "popitem fields empty dict",
			source: `
def apply(metric):
	metric.fields.popitem()
	return metric
`,
			input: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{},
					map[string]interface{}{},
					time.Unix(0, 0),
				),
			},
			expected:         []telegraf.Metric{},
			expectedErrorStr: "popitem(): field dictionary is empty",
		},
		{
			name: "fields setdefault key not set",
			source: `
def apply(metric):
	metric.fields.setdefault('a', 'b')
	return metric
`,
			input: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{},
					map[string]interface{}{},
					time.Unix(0, 0),
				),
			},
			expected: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{},
					map[string]interface{}{"a": "b"},
					time.Unix(0, 0),
				),
			},
		},
		{
			name: "fields setdefault key already set",
			source: `
def apply(metric):
	metric.fields.setdefault('a', 'c')
	return metric
`,
			input: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{},
					map[string]interface{}{"a": "b"},
					time.Unix(0, 0),
				),
			},
			expected: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{},
					map[string]interface{}{"a": "b"},
					time.Unix(0, 0),
				),
			},
		},
		{
			name: "fields update list of tuple",
			source: `
def apply(metric):
	metric.fields.update([('a', 'b'), ('c', 'd')])
	return metric
`,
			input: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{},
					map[string]interface{}{},
					time.Unix(0, 0),
				),
			},
			expected: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{},
					map[string]interface{}{
						"a": "b",
						"c": "d",
					},
					time.Unix(0, 0),
				),
			},
		},
		{
			name: "fields update kwargs",
			source: `
def apply(metric):
	metric.fields.update(a='b', c='d')
	return metric
`,
			input: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{},
					map[string]interface{}{},
					time.Unix(0, 0),
				),
			},
			expected: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{},
					map[string]interface{}{
						"a": "b",
						"c": "d",
					},
					time.Unix(0, 0),
				),
			},
		},
		{
			name: "fields update dict",
			source: `
def apply(metric):
	metric.fields.update({'a': 'b', 'c': 'd'})
	return metric
`,
			input: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{},
					map[string]interface{}{},
					time.Unix(0, 0),
				),
			},
			expected: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{},
					map[string]interface{}{
						"a": "b",
						"c": "d",
					},
					time.Unix(0, 0),
				),
			},
		},
		{
			name: "fields update list tuple and kwargs",
			source: `
def apply(metric):
	metric.fields.update([('a', 'b'), ('c', 'd')], e='f')
	return metric
`,
			input: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{},
					map[string]interface{}{},
					time.Unix(0, 0),
				),
			},
			expected: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{},
					map[string]interface{}{
						"a": "b",
						"c": "d",
						"e": "f",
					},
					time.Unix(0, 0),
				),
			},
		},
		{
			name: "iterate fields",
			source: `
def apply(metric):
	for k in metric.fields:
		pass
	return metric
`,
			input: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{},
					map[string]interface{}{
						"time_guest":  1.0,
						"time_idle":   2.0,
						"time_system": 3.0,
					},
					time.Unix(0, 0),
				),
			},
			expected: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{},
					map[string]interface{}{
						"time_guest":  1.0,
						"time_idle":   2.0,
						"time_system": 3.0,
					},
					time.Unix(0, 0),
				),
			},
		},
		{
			name: "iterate field keys",
			source: `
def apply(metric):
	for k in metric.fields.keys():
		pass
	return metric
`,
			input: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{},
					map[string]interface{}{
						"time_guest":  1.0,
						"time_idle":   2.0,
						"time_system": 3.0,
					},
					time.Unix(0, 0),
				),
			},
			expected: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{},
					map[string]interface{}{
						"time_guest":  1.0,
						"time_idle":   2.0,
						"time_system": 3.0,
					},
					time.Unix(0, 0),
				),
			},
		},
		{
			name: "iterate field keys and copy to tags",
			source: `
def apply(metric):
	for k in metric.fields.keys():
		metric.tags[k] = k
	return metric
`,
			input: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{},
					map[string]interface{}{
						"time_guest":  1.0,
						"time_idle":   2.0,
						"time_system": 3.0,
					},
					time.Unix(0, 0),
				),
			},
			expected: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{
						"time_guest":  "time_guest",
						"time_idle":   "time_idle",
						"time_system": "time_system",
					},
					map[string]interface{}{
						"time_guest":  1.0,
						"time_idle":   2.0,
						"time_system": 3.0,
					},
					time.Unix(0, 0),
				),
			},
		},
		{
			name: "iterate field items",
			source: `
def apply(metric):
	for k, v in metric.fields.items():
		pass
	return metric
`,
			input: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{},
					map[string]interface{}{
						"time_guest":  1.0,
						"time_idle":   2.0,
						"time_system": 3.0,
					},
					time.Unix(0, 0),
				),
			},
			expected: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{},
					map[string]interface{}{
						"time_guest":  1.0,
						"time_idle":   2.0,
						"time_system": 3.0,
					},
					time.Unix(0, 0),
				),
			},
		},
		{
			name: "iterate field items and copy to tags",
			source: `
def apply(metric):
	for k, v in metric.fields.items():
		metric.tags[k] = str(v)
	return metric
`,
			input: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{},
					map[string]interface{}{
						"time_guest":  1.1,
						"time_idle":   2.1,
						"time_system": 3.1,
					},
					time.Unix(0, 0),
				),
			},
			expected: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{
						"time_guest":  "1.1",
						"time_idle":   "2.1",
						"time_system": "3.1",
					},
					map[string]interface{}{
						"time_guest":  1.1,
						"time_idle":   2.1,
						"time_system": 3.1,
					},
					time.Unix(0, 0),
				),
			},
		},
		{
			name: "iterate field values",
			source: `
def apply(metric):
	for v in metric.fields.values():
		pass
	return metric
`,
			input: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{},
					map[string]interface{}{
						"a": "b",
						"c": "d",
						"e": "f",
					},
					time.Unix(0, 0),
				),
			},
			expected: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{},
					map[string]interface{}{
						"a": "b",
						"c": "d",
						"e": "f",
					},
					time.Unix(0, 0),
				),
			},
		},
		{
			name: "iterate field values and copy to tags",
			source: `
def apply(metric):
	for v in metric.fields.values():
		metric.tags[str(v)] = str(v)
	return metric
`,
			input: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{},
					map[string]interface{}{
						"a": "b",
						"c": "d",
						"e": "f",
					},
					time.Unix(0, 0),
				),
			},
			expected: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{
						"b": "b",
						"d": "d",
						"f": "f",
					},
					map[string]interface{}{
						"a": "b",
						"c": "d",
						"e": "f",
					},
					time.Unix(0, 0),
				),
			},
		},
		{
			name: "clear fields",
			source: `
def apply(metric):
	metric.fields.clear()
	metric.fields['notempty'] = 0
	return metric
`,
			input: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{},
					map[string]interface{}{
						"time_idle":   0,
						"time_guest":  0,
						"time_system": 0,
						"time_user":   0,
					},
					time.Unix(0, 0),
				),
			},
			expected: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{},
					map[string]interface{}{
						"notempty": 0,
					},
					time.Unix(0, 0),
				),
			},
		},
		{
			name: "fields cannot pop while iterating",
			source: `
def apply(metric):
	for k in metric.fields:
		metric.fields.pop(k)
	return metric
`,
			input: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{},
					map[string]interface{}{"time_idle": 0},
					time.Unix(0, 0),
				),
			},
			expected:         []telegraf.Metric{},
			expectedErrorStr: "pop: cannot delete during iteration",
		},
		{
			name: "fields cannot popitem while iterating",
			source: `
def apply(metric):
	for k in metric.fields:
		metric.fields.popitem()
	return metric
`,
			input: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{},
					map[string]interface{}{"time_idle": 0},
					time.Unix(0, 0),
				),
			},
			expected:         []telegraf.Metric{},
			expectedErrorStr: "cannot delete during iteration",
		},
		{
			name: "fields cannot clear while iterating",
			source: `
def apply(metric):
	for k in metric.fields:
		metric.fields.clear()
	return metric
`,
			input: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{},
					map[string]interface{}{"time_idle": 0},
					time.Unix(0, 0),
				),
			},
			expected:         []telegraf.Metric{},
			expectedErrorStr: "cannot delete during iteration",
		},
		{
			name: "fields cannot insert while iterating",
			source: `
def apply(metric):
	for k in metric.fields:
		metric.fields['time_guest'] = 0
	return metric
`,
			input: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{},
					map[string]interface{}{"time_idle": 0},
					time.Unix(0, 0),
				),
			},
			expected:         []telegraf.Metric{},
			expectedErrorStr: "cannot insert during iteration",
		},
		{
			name: "fields can be cleared after iterating",
			source: `
def apply(metric):
	for k in metric.fields:
		pass
	metric.fields.clear()
	metric.fields['notempty'] = 0
	return metric
`,
			input: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{},
					map[string]interface{}{"time_idle": 0},
					time.Unix(0, 0),
				),
			},
			expected: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{},
					map[string]interface{}{
						"notempty": 0,
					},
					time.Unix(0, 0),
				),
			},
		},
		{
			name: "set time",
			source: `
def apply(metric):
	metric.time = 42
	return metric
			`,
			input: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{},
					map[string]interface{}{
						"time_idle": 42,
					},
					time.Unix(0, 0).UTC(),
				),
			},
			expected: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{},
					map[string]interface{}{
						"time_idle": 42,
					},
					time.Unix(0, 42).UTC(),
				),
			},
		},
		{
			name: "set time wrong type",
			source: `
def apply(metric):
	metric.time = 'howdy'
	return metric
			`,
			input: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{},
					map[string]interface{}{
						"time_idle": 42,
					},
					time.Unix(0, 0).UTC(),
				),
			},
			expected:         []telegraf.Metric{},
			expectedErrorStr: "type error",
		},
		{
			name: "get time",
			source: `
def apply(metric):
	metric.time -= metric.time % 100000000
	return metric
			`,
			input: []telegraf.Metric{
				testutil.MustMetric(
					"cpu",
					map[string]string{},
					map[string]interface{}{
						"time_idle": 42,
					},
					time.Unix(42, 11).UTC(),
				),
			},
			expected: []telegraf.Metric{
				testutil.MustMetric(
					"cpu",
					map[string]string{},
					map[string]interface{}{
						"time_idle": 42,
					},
					time.Unix(42, 0).UTC(),
				),
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			plugin := &Starlark{
				Source: tt.source,
				Log:    testutil.Logger{},
			}
			err := plugin.Init()
			require.NoError(t, err)

			var acc testutil.Accumulator

			err = plugin.Start(&acc)
			require.NoError(t, err)

			for _, m := range tt.input {
				err = plugin.Add(m, &acc)
				if tt.expectedErrorStr != "" {
					require.EqualError(t, err, tt.expectedErrorStr)
				} else {
					require.NoError(t, err)
				}
			}

			err = plugin.Stop()
			require.NoError(t, err)

			testutil.RequireMetricsEqual(t, tt.expected, acc.GetTelegrafMetrics())
		})
	}
}

func TestScript(t *testing.T) {
	var tests = []struct {
		name             string
		plugin           *Starlark
		input            []telegraf.Metric
		expected         []telegraf.Metric
		expectedErrorStr string
	}{
		{
			name: "rename",
			plugin: &Starlark{
				Script: "testdata/rename.star",
				Log:    testutil.Logger{},
			},
			input: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{
						"lower": "0",
						"upper": "10",
					},
					map[string]interface{}{"time_idle": 42},
					time.Unix(0, 0),
				),
			},
			expected: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{
						"min": "0",
						"max": "10",
					},
					map[string]interface{}{"time_idle": 42},
					time.Unix(0, 0),
				),
			},
		},
		{
			name: "scale",
			plugin: &Starlark{
				Script: "testdata/scale.star",
				Log:    testutil.Logger{},
			},
			input: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{},
					map[string]interface{}{"time_idle": 10.0},
					time.Unix(0, 0),
				),
			},
			expected: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{},
					map[string]interface{}{"time_idle": 100.0},
					time.Unix(0, 0),
				),
			},
		},
		{
			name: "ratio",
			plugin: &Starlark{
				Script: "testdata/ratio.star",
				Log:    testutil.Logger{},
			},
			input: []telegraf.Metric{
				testutil.MustMetric("mem",
					map[string]string{},
					map[string]interface{}{
						"used":  2,
						"total": 10,
					},
					time.Unix(0, 0),
				),
			},
			expected: []telegraf.Metric{
				testutil.MustMetric("mem",
					map[string]string{},
					map[string]interface{}{
						"used":  2,
						"total": 10,
						"usage": 20.0,
					},
					time.Unix(0, 0),
				),
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			err := tt.plugin.Init()
			require.NoError(t, err)

			var acc testutil.Accumulator

			err = tt.plugin.Start(&acc)
			require.NoError(t, err)

			for _, m := range tt.input {
				err = tt.plugin.Add(m, &acc)
				if tt.expectedErrorStr != "" {
					require.EqualError(t, err, tt.expectedErrorStr)
				} else {
					require.NoError(t, err)
				}
			}

			err = tt.plugin.Stop()
			require.NoError(t, err)

			testutil.RequireMetricsEqual(t, tt.expected, acc.GetTelegrafMetrics())
		})
	}
}

// Benchmarks modify the metric in place, so the scripts shouldn't modify the
// metric.
func Benchmark(b *testing.B) {
	var tests = []struct {
		name   string
		source string
		input  []telegraf.Metric
	}{
		{
			name: "passthrough",
			source: `
def apply(metric):
	return metric
`,
			input: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{},
					map[string]interface{}{
						"time_idle": 42.0,
					},
					time.Unix(0, 0),
				),
			},
		},
		{
			name: "create new metric",
			source: `
def apply(metric):
	m = Metric('cpu')
	m.fields['time_guest'] = 2.0
	m.time = 0
	return m
`,
			input: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{},
					map[string]interface{}{
						"time_idle": 42.0,
					},
					time.Unix(0, 0),
				),
			},
		},
		{
			name: "set name",
			source: `
def apply(metric):
	metric.name = "cpu"
	return metric
`,
			input: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{},
					map[string]interface{}{"time_idle": 42.0},
					time.Unix(0, 0),
				),
			},
		},
		{
			name: "set tag",
			source: `
def apply(metric):
	metric.tags['host'] = 'example.org'
	return metric
`,
			input: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{
						"host": "example.org",
					},
					map[string]interface{}{"time_idle": 42.0},
					time.Unix(0, 0),
				),
			},
		},
		{
			name: "tag in operator",
			source: `
def apply(metric):
	if 'c' in metric.tags:
		return metric
	return None
`,
			input: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{
						"a": "b",
						"c": "d",
						"e": "f",
					},
					map[string]interface{}{"time_idle": 42.0},
					time.Unix(0, 0),
				),
			},
		},
		{
			name: "iterate tags",
			source: `
def apply(metric):
	for k in metric.tags:
		pass
	return metric
`,
			input: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{
						"a": "b",
						"c": "d",
						"e": "f",
						"g": "h",
					},
					map[string]interface{}{"time_idle": 42.0},
					time.Unix(0, 0),
				),
			},
		},
		{
			// This should be faster than calling items()
			name: "iterate tags and get values",
			source: `
def apply(metric):
	for k in metric.tags:
		v = metric.tags[k]
	return metric
`,
			input: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{
						"a": "b",
						"c": "d",
						"e": "f",
						"g": "h",
					},
					map[string]interface{}{"time_idle": 42},
					time.Unix(0, 0),
				),
			},
		},
		{
			name: "iterate tag items",
			source: `
def apply(metric):
	for k, v in metric.tags.items():
		pass
	return metric
`,
			input: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{
						"a": "b",
						"c": "d",
						"e": "f",
						"g": "h",
					},
					map[string]interface{}{"time_idle": 42},
					time.Unix(0, 0),
				),
			},
		},
		{
			name: "set string field",
			source: `
def apply(metric):
	metric.fields['host'] = 'example.org'
	return metric
`,
			input: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{},
					map[string]interface{}{
						"host": "example.org",
					},
					time.Unix(0, 0),
				),
			},
		},
		{
			name: "iterate fields",
			source: `
def apply(metric):
	for k in metric.fields:
		pass
	return metric
`,
			input: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{},
					map[string]interface{}{
						"time_idle":   42.0,
						"time_user":   42.0,
						"time_guest":  42.0,
						"time_system": 42.0,
					},
					time.Unix(0, 0),
				),
			},
		},
		{
			// This should be faster than calling items()
			name: "iterate fields and get values",
			source: `
def apply(metric):
	for k in metric.fields:
		v = metric.fields[k]
	return metric
`,
			input: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{},
					map[string]interface{}{
						"time_idle":   42.0,
						"time_user":   42.0,
						"time_guest":  42.0,
						"time_system": 42.0,
					},
					time.Unix(0, 0),
				),
			},
		},
		{
			name: "iterate field items",
			source: `
def apply(metric):
	for k, v in metric.fields.items():
		pass
	return metric
`,
			input: []telegraf.Metric{
				testutil.MustMetric("cpu",
					map[string]string{},
					map[string]interface{}{
						"a": "b",
						"c": "d",
						"e": "f",
						"g": "h",
					},
					time.Unix(0, 0),
				),
			},
		},
	}

	for _, tt := range tests {
		b.Run(tt.name, func(b *testing.B) {
			plugin := &Starlark{
				Source: tt.source,
				Log:    testutil.Logger{},
			}

			err := plugin.Init()
			require.NoError(b, err)

			var acc testutil.NopAccumulator

			err = plugin.Start(&acc)
			require.NoError(b, err)

			b.ResetTimer()
			for n := 0; n < b.N; n++ {
				for _, m := range tt.input {
					plugin.Add(m, &acc)
				}
			}

			err = plugin.Stop()
			require.NoError(b, err)
		})
	}
}

func TestAllScriptTestData(t *testing.T) {
	// can be run from multiple folders
	paths := []string{"testdata", "plugins/processors/starlark/testdata"}
	for _, testdataPath := range paths {
		filepath.Walk(testdataPath, func(path string, info os.FileInfo, err error) error {
			if info == nil || info.IsDir() {
				return nil
			}
			fn := path
			t.Run(fn, func(t *testing.T) {
				b, err := ioutil.ReadFile(fn)
				require.NoError(t, err)
				lines := strings.Split(string(b), "\n")
				inputMetrics := parseMetricsFrom(t, lines, "Example Input:")
				outputMetrics := parseMetricsFrom(t, lines, "Example Output:")
				plugin := &Starlark{
					Script: fn,
					Log:    testutil.Logger{},
				}
				require.NoError(t, plugin.Init())

				acc := &testutil.Accumulator{}

				err = plugin.Start(acc)
				require.NoError(t, err)

				for _, m := range inputMetrics {
					err = plugin.Add(m, acc)
					require.NoError(t, err)
				}

				err = plugin.Stop()
				require.NoError(t, err)

				testutil.RequireMetricsEqual(t, outputMetrics, acc.GetTelegrafMetrics(), testutil.SortMetrics(), testutil.IgnoreTime())
			})
			return nil
		})
	}
}

var parser, _ = parsers.NewInfluxParser() // literally never returns errors.

// parses metric lines out of line protocol following a header, with a trailing blank line
func parseMetricsFrom(t *testing.T, lines []string, header string) (metrics []telegraf.Metric) {
	require.NotZero(t, len(lines), "Expected some lines to parse from .star file, found none")
	startIdx := -1
	endIdx := len(lines)
	for i := range lines {
		if strings.TrimLeft(lines[i], "# ") == header {
			startIdx = i + 1
			break
		}
	}
	require.NotEqual(t, -1, startIdx, fmt.Sprintf("Header %q must exist in file", header))
	for i := startIdx; i < len(lines); i++ {
		line := strings.TrimLeft(lines[i], "# ")
		if line == "" || line == "'''" {
			endIdx = i
			break
		}
	}
	for i := startIdx; i < endIdx; i++ {
		m, err := parser.ParseLine(strings.TrimLeft(lines[i], "# "))
		require.NoError(t, err, fmt.Sprintf("Expected to be able to parse %q metric, but found error", header))
		metrics = append(metrics, m)
	}
	return metrics
}
