Skip to content

Commit

Permalink
[chore] generate tests to test component lifecycle, specifically for …
Browse files Browse the repository at this point in the history
…exporters. (open-telemetry#29284)

This relates to open-telemetry#27849
  • Loading branch information
atoulme authored Nov 16, 2023
1 parent 1d886ea commit 97e7916
Show file tree
Hide file tree
Showing 10 changed files with 474 additions and 13 deletions.
1 change: 1 addition & 0 deletions cmd/mdatagen/embeded_templates_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"): {},
Expand Down
8 changes: 8 additions & 0 deletions cmd/mdatagen/loader.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand All @@ -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) {
Expand Down
75 changes: 64 additions & 11 deletions cmd/mdatagen/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
Expand All @@ -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
}
Expand All @@ -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
}
}
Expand All @@ -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 {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
}

Expand Down
2 changes: 1 addition & 1 deletion cmd/mdatagen/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
6 changes: 6 additions & 0 deletions cmd/mdatagen/metadata-schema.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
132 changes: 132 additions & 0 deletions cmd/mdatagen/templates/component_test.go.tmpl
Original file line number Diff line number Diff line change
@@ -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 }}
Loading

0 comments on commit 97e7916

Please sign in to comment.