diff --git a/cmd/mdatagen/embeded_templates_test.go b/cmd/mdatagen/embeded_templates_test.go index 0ffd6a1b5cba..b52850da3954 100644 --- a/cmd/mdatagen/embeded_templates_test.go +++ b/cmd/mdatagen/embeded_templates_test.go @@ -20,6 +20,7 @@ func TestEnsureTemplatesLoaded(t *testing.T) { var ( templateFiles = map[string]struct{}{ + path.Join(rootDir, "component_test.go.tmpl"): {}, path.Join(rootDir, "documentation.md.tmpl"): {}, path.Join(rootDir, "metrics.go.tmpl"): {}, path.Join(rootDir, "metrics_test.go.tmpl"): {}, diff --git a/cmd/mdatagen/loader.go b/cmd/mdatagen/loader.go index 19add14defb5..859b42cdbf36 100644 --- a/cmd/mdatagen/loader.go +++ b/cmd/mdatagen/loader.go @@ -199,6 +199,12 @@ func (a attribute) TestValue() string { return "" } +type tests struct { + Config any `mapstructure:"config"` + SkipLifecycle bool `mapstructure:"skip_lifecycle"` + ExpectConsumerError bool `mapstructure:"expect_consumer_error"` +} + type metadata struct { // Type of the component. Type string `mapstructure:"type"` @@ -218,6 +224,8 @@ type metadata struct { ScopeName string `mapstructure:"-"` // ShortFolderName is the shortened folder name of the component, removing class if present ShortFolderName string `mapstructure:"-"` + + Tests *tests `mapstructure:"tests"` } func setAttributesFullName(attrs map[attributeName]attribute) { diff --git a/cmd/mdatagen/main.go b/cmd/mdatagen/main.go index 2d35ff129362..d65b4ff51505 100644 --- a/cmd/mdatagen/main.go +++ b/cmd/mdatagen/main.go @@ -58,7 +58,7 @@ func run(ymlPath string) error { if md.Status != nil { if md.Status.Class != "cmd" && md.Status.Class != "pkg" { if err = generateFile(filepath.Join(tmplDir, "status.go.tmpl"), - filepath.Join(codeDir, "generated_status.go"), md); err != nil { + filepath.Join(codeDir, "generated_status.go"), md, "metadata"); err != nil { return err } } @@ -72,6 +72,14 @@ func run(ymlPath string) error { } } } + + if md.Tests != nil { + if err = generateFile(filepath.Join(tmplDir, "component_test.go.tmpl"), + filepath.Join(ymlDir, "generated_component_test.go"), md, md.ShortFolderName+md.Status.Class); err != nil { + return err + } + } + if len(md.Metrics) == 0 && len(md.ResourceAttributes) == 0 { return nil } @@ -80,26 +88,26 @@ func run(ymlPath string) error { return fmt.Errorf("unable to create output directory %q: %w", filepath.Join(codeDir, "testdata"), err) } if err = generateFile(filepath.Join(tmplDir, "testdata", "config.yaml.tmpl"), - filepath.Join(codeDir, "testdata", "config.yaml"), md); err != nil { + filepath.Join(codeDir, "testdata", "config.yaml"), md, "metadata"); err != nil { return err } if err = generateFile(filepath.Join(tmplDir, "config.go.tmpl"), - filepath.Join(codeDir, "generated_config.go"), md); err != nil { + filepath.Join(codeDir, "generated_config.go"), md, "metadata"); err != nil { return err } if err = generateFile(filepath.Join(tmplDir, "config_test.go.tmpl"), - filepath.Join(codeDir, "generated_config_test.go"), md); err != nil { + filepath.Join(codeDir, "generated_config_test.go"), md, "metadata"); err != nil { return err } if len(md.ResourceAttributes) > 0 { if err = generateFile(filepath.Join(tmplDir, "resource.go.tmpl"), - filepath.Join(codeDir, "generated_resource.go"), md); err != nil { + filepath.Join(codeDir, "generated_resource.go"), md, "metadata"); err != nil { return err } if err = generateFile(filepath.Join(tmplDir, "resource_test.go.tmpl"), - filepath.Join(codeDir, "generated_resource_test.go"), md); err != nil { + filepath.Join(codeDir, "generated_resource_test.go"), md, "metadata"); err != nil { return err } } @@ -109,15 +117,15 @@ func run(ymlPath string) error { } if err = generateFile(filepath.Join(tmplDir, "metrics.go.tmpl"), - filepath.Join(codeDir, "generated_metrics.go"), md); err != nil { + filepath.Join(codeDir, "generated_metrics.go"), md, "metadata"); err != nil { return err } if err = generateFile(filepath.Join(tmplDir, "metrics_test.go.tmpl"), - filepath.Join(codeDir, "generated_metrics_test.go"), md); err != nil { + filepath.Join(codeDir, "generated_metrics_test.go"), md, "metadata"); err != nil { return err } - return generateFile(filepath.Join(tmplDir, "documentation.md.tmpl"), filepath.Join(ymlDir, "documentation.md"), md) + return generateFile(filepath.Join(tmplDir, "documentation.md.tmpl"), filepath.Join(ymlDir, "documentation.md"), md, "metadata") } func templatize(tmplFile string, md metadata) *template.Template { @@ -170,6 +178,51 @@ func templatize(tmplFile string, md metadata) *template.Template { "distroURL": func(name string) string { return distros[name] }, + "isExporter": func() bool { + return md.Status.Class == "exporter" + }, + "isProcessor": func() bool { + return md.Status.Class == "processor" + }, + "isReceiver": func() bool { + return md.Status.Class == "receiver" + }, + "skipLifecycle": func() bool { + return md.Tests.SkipLifecycle + }, + "supportsLogs": func() bool { + for _, signals := range md.Status.Stability { + for _, s := range signals { + if s == "logs" { + return true + } + } + } + return false + }, + "supportsMetrics": func() bool { + for _, signals := range md.Status.Stability { + for _, s := range signals { + if s == "metrics" { + return true + } + } + } + return false + }, + "supportsTraces": func() bool { + for _, signals := range md.Status.Stability { + for _, s := range signals { + if s == "traces" { + return true + } + } + } + return false + }, + "expectConsumerError": func() bool { + return md.Tests.ExpectConsumerError + }, // ParseFS delegates the parsing of the files to `Glob` // which uses the `\` as a special character. // Meaning on windows based machines, the `\` needs to be replaced @@ -206,11 +259,11 @@ func inlineReplace(tmplFile string, outputFile string, md metadata, start string return nil } -func generateFile(tmplFile string, outputFile string, md metadata) error { +func generateFile(tmplFile string, outputFile string, md metadata, goPackage string) error { tmpl := templatize(tmplFile, md) buf := bytes.Buffer{} - if err := tmpl.Execute(&buf, templateContext{metadata: md, Package: "metadata"}); err != nil { + if err := tmpl.Execute(&buf, templateContext{metadata: md, Package: goPackage}); err != nil { return fmt.Errorf("failed executing template: %w", err) } diff --git a/cmd/mdatagen/main_test.go b/cmd/mdatagen/main_test.go index e23dcb6d129c..821894d96dc3 100644 --- a/cmd/mdatagen/main_test.go +++ b/cmd/mdatagen/main_test.go @@ -362,7 +362,7 @@ const ( t.Run(tt.name, func(t *testing.T) { tmpdir := t.TempDir() err := generateFile("templates/status.go.tmpl", - filepath.Join(tmpdir, "generated_status.go"), tt.md) + filepath.Join(tmpdir, "generated_status.go"), tt.md, "metadata") require.NoError(t, err) actual, err := os.ReadFile(filepath.Join(tmpdir, "generated_status.go")) require.NoError(t, err) diff --git a/cmd/mdatagen/metadata-schema.yaml b/cmd/mdatagen/metadata-schema.yaml index 91ab3404bcbd..60d165c616bb 100644 --- a/cmd/mdatagen/metadata-schema.yaml +++ b/cmd/mdatagen/metadata-schema.yaml @@ -104,3 +104,9 @@ metrics: input_type: string # Optional: array of attributes that were defined in the attributes section that are emitted by this metric. attributes: [string] + +# Lifecycle tests generated for this component. +tests: + config: # {} by default, specific testing configuration for lifecycle tests. + skip_lifecycle: false # false by default + expect_consumer_error: true # false by default diff --git a/cmd/mdatagen/templates/component_test.go.tmpl b/cmd/mdatagen/templates/component_test.go.tmpl new file mode 100644 index 000000000000..146169b546c6 --- /dev/null +++ b/cmd/mdatagen/templates/component_test.go.tmpl @@ -0,0 +1,132 @@ +// Code generated by mdatagen. DO NOT EDIT. + +package {{ .Package }} + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.opentelemetry.io/collector/component" + "go.opentelemetry.io/collector/component/componenttest" +{{ if isExporter }} + "go.opentelemetry.io/collector/exporter" + "go.opentelemetry.io/collector/exporter/exportertest" +{{ end }} + "go.opentelemetry.io/collector/confmap/confmaptest" + + "github.com/open-telemetry/opentelemetry-collector-contrib/internal/coreinternal/testdata" +) + +// assertNoErrorHost implements a component.Host that asserts that there were no errors. +type assertNoErrorHost struct { + component.Host + *testing.T +} + +var _ component.Host = (*assertNoErrorHost)(nil) + +// newAssertNoErrorHost returns a new instance of assertNoErrorHost. +func newAssertNoErrorHost(t *testing.T) component.Host { + return &assertNoErrorHost{ + componenttest.NewNopHost(), + t, + } +} + +func (aneh *assertNoErrorHost) ReportFatalError(err error) { + assert.NoError(aneh, err) +} + + +{{ if isExporter }} +func Test_ComponentLifecycle(t *testing.T) { + factory := NewFactory() + + tests := []struct{ + name string + createFn func(ctx context.Context, set exporter.CreateSettings, cfg component.Config) (component.Component, error) + }{ +{{ if supportsLogs }} + { + name: "logs", + createFn: func(ctx context.Context, set exporter.CreateSettings, cfg component.Config) (component.Component, error) { + return factory.CreateLogsExporter(ctx, set, cfg) + }, + }, +{{ end }} +{{ if supportsMetrics }} + { + name: "metrics", + createFn: func(ctx context.Context, set exporter.CreateSettings, cfg component.Config) (component.Component, error) { + return factory.CreateMetricsExporter(ctx, set, cfg) + }, + }, +{{ end }} +{{ if supportsTraces }} + { + name: "traces", + createFn: func(ctx context.Context, set exporter.CreateSettings, cfg component.Config) (component.Component, error) { + return factory.CreateTracesExporter(ctx, set, cfg) + }, + }, +{{ end }} + } + + cm, err := confmaptest.LoadConf("metadata.yaml") + require.NoError(t, err) + cfg := factory.CreateDefaultConfig() + sub, err := cm.Sub("tests::config") + require.NoError(t, err) + require.NoError(t, component.UnmarshalConfig(sub, cfg)) + + for _, test := range tests { + t.Run(test.name + "-shutdown", func(t *testing.T) { + c, err := test.createFn(context.Background(), exportertest.NewNopCreateSettings(), cfg) + require.NoError(t, err) + err = c.Shutdown(context.Background()) + require.NoError(t, err) + }) + + t.Run(test.name + "-lifecycle", func(t *testing.T) { + {{ if skipLifecycle }} + // TODO support lifecycle + t.SkipNow() + {{ end }} + c, err := test.createFn(context.Background(), exportertest.NewNopCreateSettings(), cfg) + require.NoError(t, err) + host := newAssertNoErrorHost(t) + err = c.Start(context.Background(), host) + require.NoError(t, err) + assert.NotPanics(t, func() { + switch e := c.(type) { + case exporter.Logs: + logs := testdata.GenerateLogsManyLogRecordsSameResource(2) + if !e.Capabilities().MutatesData { + logs.MarkReadOnly() + } + err = e.ConsumeLogs(context.Background(), logs) + case exporter.Metrics: + metrics := testdata.GenerateMetricsTwoMetrics() + if !e.Capabilities().MutatesData { + metrics.MarkReadOnly() + } + err = e.ConsumeMetrics(context.Background(), metrics) + case exporter.Traces: + traces := testdata.GenerateTracesTwoSpansSameResource() + if !e.Capabilities().MutatesData { + traces.MarkReadOnly() + } + err = e.ConsumeTraces(context.Background(), traces) + } + }) + {{ if not expectConsumerError }} + assert.NoError(t, err) + {{ end }} + err = c.Shutdown(context.Background()) + require.NoError(t, err) + }) + } +} +{{ end }} diff --git a/exporter/signalfxexporter/generated_component_test.go b/exporter/signalfxexporter/generated_component_test.go new file mode 100644 index 000000000000..44be7788b02a --- /dev/null +++ b/exporter/signalfxexporter/generated_component_test.go @@ -0,0 +1,121 @@ +// Code generated by mdatagen. DO NOT EDIT. + +package signalfxexporter + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.opentelemetry.io/collector/component" + "go.opentelemetry.io/collector/component/componenttest" + + "go.opentelemetry.io/collector/exporter" + "go.opentelemetry.io/collector/exporter/exportertest" + + "go.opentelemetry.io/collector/confmap/confmaptest" + + "github.com/open-telemetry/opentelemetry-collector-contrib/internal/coreinternal/testdata" +) + +// assertNoErrorHost implements a component.Host that asserts that there were no errors. +type assertNoErrorHost struct { + component.Host + *testing.T +} + +var _ component.Host = (*assertNoErrorHost)(nil) + +// newAssertNoErrorHost returns a new instance of assertNoErrorHost. +func newAssertNoErrorHost(t *testing.T) component.Host { + return &assertNoErrorHost{ + componenttest.NewNopHost(), + t, + } +} + +func (aneh *assertNoErrorHost) ReportFatalError(err error) { + assert.NoError(aneh, err) +} + +func Test_ComponentLifecycle(t *testing.T) { + factory := NewFactory() + + tests := []struct { + name string + createFn func(ctx context.Context, set exporter.CreateSettings, cfg component.Config) (component.Component, error) + }{ + + { + name: "logs", + createFn: func(ctx context.Context, set exporter.CreateSettings, cfg component.Config) (component.Component, error) { + return factory.CreateLogsExporter(ctx, set, cfg) + }, + }, + + { + name: "metrics", + createFn: func(ctx context.Context, set exporter.CreateSettings, cfg component.Config) (component.Component, error) { + return factory.CreateMetricsExporter(ctx, set, cfg) + }, + }, + + { + name: "traces", + createFn: func(ctx context.Context, set exporter.CreateSettings, cfg component.Config) (component.Component, error) { + return factory.CreateTracesExporter(ctx, set, cfg) + }, + }, + } + + cm, err := confmaptest.LoadConf("metadata.yaml") + require.NoError(t, err) + cfg := factory.CreateDefaultConfig() + sub, err := cm.Sub("tests::config") + require.NoError(t, err) + require.NoError(t, component.UnmarshalConfig(sub, cfg)) + + for _, test := range tests { + t.Run(test.name+"-shutdown", func(t *testing.T) { + c, err := test.createFn(context.Background(), exportertest.NewNopCreateSettings(), cfg) + require.NoError(t, err) + err = c.Shutdown(context.Background()) + require.NoError(t, err) + }) + + t.Run(test.name+"-lifecycle", func(t *testing.T) { + + c, err := test.createFn(context.Background(), exportertest.NewNopCreateSettings(), cfg) + require.NoError(t, err) + host := newAssertNoErrorHost(t) + err = c.Start(context.Background(), host) + require.NoError(t, err) + assert.NotPanics(t, func() { + switch e := c.(type) { + case exporter.Logs: + logs := testdata.GenerateLogsManyLogRecordsSameResource(2) + if !e.Capabilities().MutatesData { + logs.MarkReadOnly() + } + err = e.ConsumeLogs(context.Background(), logs) + case exporter.Metrics: + metrics := testdata.GenerateMetricsTwoMetrics() + if !e.Capabilities().MutatesData { + metrics.MarkReadOnly() + } + err = e.ConsumeMetrics(context.Background(), metrics) + case exporter.Traces: + traces := testdata.GenerateTracesTwoSpansSameResource() + if !e.Capabilities().MutatesData { + traces.MarkReadOnly() + } + err = e.ConsumeTraces(context.Background(), traces) + } + }) + + err = c.Shutdown(context.Background()) + require.NoError(t, err) + }) + } +} diff --git a/exporter/signalfxexporter/metadata.yaml b/exporter/signalfxexporter/metadata.yaml index 777f6c51b5ce..9eb2f82f65b6 100644 --- a/exporter/signalfxexporter/metadata.yaml +++ b/exporter/signalfxexporter/metadata.yaml @@ -7,3 +7,13 @@ status: distributions: [contrib, splunk, observiq, aws] codeowners: active: [dmitryax, crobert-1] +tests: + config: + access_token: "my_fake_token" + ingest_url: "http://localhost:1234" + api_url: "http://localhost:1234" + sending_queue: + enabled: false + retry_on_failure: + enabled: false + expect_consumer_error: true diff --git a/exporter/splunkhecexporter/generated_component_test.go b/exporter/splunkhecexporter/generated_component_test.go new file mode 100644 index 000000000000..23c4040fa1c3 --- /dev/null +++ b/exporter/splunkhecexporter/generated_component_test.go @@ -0,0 +1,121 @@ +// Code generated by mdatagen. DO NOT EDIT. + +package splunkhecexporter + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.opentelemetry.io/collector/component" + "go.opentelemetry.io/collector/component/componenttest" + + "go.opentelemetry.io/collector/exporter" + "go.opentelemetry.io/collector/exporter/exportertest" + + "go.opentelemetry.io/collector/confmap/confmaptest" + + "github.com/open-telemetry/opentelemetry-collector-contrib/internal/coreinternal/testdata" +) + +// assertNoErrorHost implements a component.Host that asserts that there were no errors. +type assertNoErrorHost struct { + component.Host + *testing.T +} + +var _ component.Host = (*assertNoErrorHost)(nil) + +// newAssertNoErrorHost returns a new instance of assertNoErrorHost. +func newAssertNoErrorHost(t *testing.T) component.Host { + return &assertNoErrorHost{ + componenttest.NewNopHost(), + t, + } +} + +func (aneh *assertNoErrorHost) ReportFatalError(err error) { + assert.NoError(aneh, err) +} + +func Test_ComponentLifecycle(t *testing.T) { + factory := NewFactory() + + tests := []struct { + name string + createFn func(ctx context.Context, set exporter.CreateSettings, cfg component.Config) (component.Component, error) + }{ + + { + name: "logs", + createFn: func(ctx context.Context, set exporter.CreateSettings, cfg component.Config) (component.Component, error) { + return factory.CreateLogsExporter(ctx, set, cfg) + }, + }, + + { + name: "metrics", + createFn: func(ctx context.Context, set exporter.CreateSettings, cfg component.Config) (component.Component, error) { + return factory.CreateMetricsExporter(ctx, set, cfg) + }, + }, + + { + name: "traces", + createFn: func(ctx context.Context, set exporter.CreateSettings, cfg component.Config) (component.Component, error) { + return factory.CreateTracesExporter(ctx, set, cfg) + }, + }, + } + + cm, err := confmaptest.LoadConf("metadata.yaml") + require.NoError(t, err) + cfg := factory.CreateDefaultConfig() + sub, err := cm.Sub("tests::config") + require.NoError(t, err) + require.NoError(t, component.UnmarshalConfig(sub, cfg)) + + for _, test := range tests { + t.Run(test.name+"-shutdown", func(t *testing.T) { + c, err := test.createFn(context.Background(), exportertest.NewNopCreateSettings(), cfg) + require.NoError(t, err) + err = c.Shutdown(context.Background()) + require.NoError(t, err) + }) + + t.Run(test.name+"-lifecycle", func(t *testing.T) { + + c, err := test.createFn(context.Background(), exportertest.NewNopCreateSettings(), cfg) + require.NoError(t, err) + host := newAssertNoErrorHost(t) + err = c.Start(context.Background(), host) + require.NoError(t, err) + assert.NotPanics(t, func() { + switch e := c.(type) { + case exporter.Logs: + logs := testdata.GenerateLogsManyLogRecordsSameResource(2) + if !e.Capabilities().MutatesData { + logs.MarkReadOnly() + } + err = e.ConsumeLogs(context.Background(), logs) + case exporter.Metrics: + metrics := testdata.GenerateMetricsTwoMetrics() + if !e.Capabilities().MutatesData { + metrics.MarkReadOnly() + } + err = e.ConsumeMetrics(context.Background(), metrics) + case exporter.Traces: + traces := testdata.GenerateTracesTwoSpansSameResource() + if !e.Capabilities().MutatesData { + traces.MarkReadOnly() + } + err = e.ConsumeTraces(context.Background(), traces) + } + }) + + err = c.Shutdown(context.Background()) + require.NoError(t, err) + }) + } +} diff --git a/exporter/splunkhecexporter/metadata.yaml b/exporter/splunkhecexporter/metadata.yaml index fb77ac506dfc..32856b9a76b4 100644 --- a/exporter/splunkhecexporter/metadata.yaml +++ b/exporter/splunkhecexporter/metadata.yaml @@ -6,4 +6,13 @@ status: beta: [traces, metrics, logs] distributions: [contrib, splunk, observiq] codeowners: - active: [atoulme, dmitryax] \ No newline at end of file + active: [atoulme, dmitryax] +tests: + config: + token: "my_fake_token" + endpoint: "http://localhost:0" + sending_queue: + enabled: false + retry_on_failure: + enabled: false + expect_consumer_error: true