github.com/jpreese/tflint@v0.19.2-0.20200908152133-b01686250fb6/tflint/runner_test.go (about)

     1  package tflint
     2  
     3  import (
     4  	"errors"
     5  	"path/filepath"
     6  	"testing"
     7  
     8  	"github.com/google/go-cmp/cmp"
     9  	"github.com/google/go-cmp/cmp/cmpopts"
    10  	hcl "github.com/hashicorp/hcl/v2"
    11  	"github.com/hashicorp/hcl/v2/hclsyntax"
    12  	"github.com/hashicorp/terraform/configs"
    13  	"github.com/zclconf/go-cty/cty"
    14  )
    15  
    16  func Test_NewModuleRunners_noModules(t *testing.T) {
    17  	withinFixtureDir(t, "no_modules", func() {
    18  		runner := testRunnerWithOsFs(t, moduleConfig())
    19  
    20  		runners, err := NewModuleRunners(runner)
    21  		if err != nil {
    22  			t.Fatalf("Unexpected error occurred: %s", err)
    23  		}
    24  
    25  		if len(runners) > 0 {
    26  			t.Fatal("`NewModuleRunners` must not return runners when there is no module")
    27  		}
    28  	})
    29  }
    30  
    31  func Test_NewModuleRunners_nestedModules(t *testing.T) {
    32  	withinFixtureDir(t, "nested_modules", func() {
    33  		runner := testRunnerWithOsFs(t, moduleConfig())
    34  
    35  		runners, err := NewModuleRunners(runner)
    36  		if err != nil {
    37  			t.Fatalf("Unexpected error occurred: %s", err)
    38  		}
    39  
    40  		if len(runners) != 2 {
    41  			t.Fatal("This function must return 2 runners because the config has 2 modules")
    42  		}
    43  
    44  		expectedVars := map[string]map[string]*configs.Variable{
    45  			"module.root": {
    46  				"override": {
    47  					Name:        "override",
    48  					Default:     cty.StringVal("foo"),
    49  					Type:        cty.DynamicPseudoType,
    50  					ParsingMode: configs.VariableParseLiteral,
    51  					DeclRange: hcl.Range{
    52  						Filename: filepath.Join("module", "module.tf"),
    53  						Start:    hcl.Pos{Line: 1, Column: 1},
    54  						End:      hcl.Pos{Line: 1, Column: 20},
    55  					},
    56  				},
    57  				"no_default": {
    58  					Name:        "no_default",
    59  					Default:     cty.StringVal("bar"),
    60  					Type:        cty.DynamicPseudoType,
    61  					ParsingMode: configs.VariableParseLiteral,
    62  					DeclRange: hcl.Range{
    63  						Filename: filepath.Join("module", "module.tf"),
    64  						Start:    hcl.Pos{Line: 4, Column: 1},
    65  						End:      hcl.Pos{Line: 4, Column: 22},
    66  					},
    67  				},
    68  				"unknown": {
    69  					Name:        "unknown",
    70  					Default:     cty.UnknownVal(cty.DynamicPseudoType),
    71  					Type:        cty.DynamicPseudoType,
    72  					ParsingMode: configs.VariableParseLiteral,
    73  					DeclRange: hcl.Range{
    74  						Filename: filepath.Join("module", "module.tf"),
    75  						Start:    hcl.Pos{Line: 5, Column: 1},
    76  						End:      hcl.Pos{Line: 5, Column: 19},
    77  					},
    78  				},
    79  			},
    80  			"module.root.module.test": {
    81  				"override": {
    82  					Name:        "override",
    83  					Default:     cty.StringVal("foo"),
    84  					Type:        cty.DynamicPseudoType,
    85  					ParsingMode: configs.VariableParseLiteral,
    86  					DeclRange: hcl.Range{
    87  						Filename: filepath.Join("module", "module1", "resource.tf"),
    88  						Start:    hcl.Pos{Line: 1, Column: 1},
    89  						End:      hcl.Pos{Line: 1, Column: 20},
    90  					},
    91  				},
    92  				"no_default": {
    93  					Name:        "no_default",
    94  					Default:     cty.StringVal("bar"),
    95  					Type:        cty.DynamicPseudoType,
    96  					ParsingMode: configs.VariableParseLiteral,
    97  					DeclRange: hcl.Range{
    98  						Filename: filepath.Join("module", "module1", "resource.tf"),
    99  						Start:    hcl.Pos{Line: 4, Column: 1},
   100  						End:      hcl.Pos{Line: 4, Column: 22},
   101  					},
   102  				},
   103  				"unknown": {
   104  					Name:        "unknown",
   105  					Default:     cty.UnknownVal(cty.DynamicPseudoType),
   106  					Type:        cty.DynamicPseudoType,
   107  					ParsingMode: configs.VariableParseLiteral,
   108  					DeclRange: hcl.Range{
   109  						Filename: filepath.Join("module", "module1", "resource.tf"),
   110  						Start:    hcl.Pos{Line: 5, Column: 1},
   111  						End:      hcl.Pos{Line: 5, Column: 19},
   112  					},
   113  				},
   114  			},
   115  		}
   116  
   117  		for _, runner := range runners {
   118  			expected, exists := expectedVars[runner.TFConfig.Path.String()]
   119  			if !exists {
   120  				t.Fatalf("`%s` is not found in module runners", runner.TFConfig.Path)
   121  			}
   122  
   123  			opts := []cmp.Option{
   124  				cmpopts.IgnoreUnexported(cty.Type{}, cty.Value{}),
   125  				cmpopts.IgnoreFields(hcl.Pos{}, "Byte"),
   126  			}
   127  			if !cmp.Equal(expected, runner.TFConfig.Module.Variables, opts...) {
   128  				t.Fatalf("`%s` module variables are unmatched: Diff=%s", runner.TFConfig.Path, cmp.Diff(expected, runner.TFConfig.Module.Variables, opts...))
   129  			}
   130  		}
   131  	})
   132  }
   133  
   134  func Test_NewModuleRunners_modVars(t *testing.T) {
   135  	withinFixtureDir(t, "nested_module_vars", func() {
   136  		runner := testRunnerWithOsFs(t, moduleConfig())
   137  
   138  		runners, err := NewModuleRunners(runner)
   139  		if err != nil {
   140  			t.Fatalf("Unexpected error occurred: %s", err)
   141  		}
   142  
   143  		if len(runners) != 2 {
   144  			t.Fatal("This function must return 2 runners because the config has 2 modules")
   145  		}
   146  
   147  		child := runners[0]
   148  		if child.TFConfig.Path.String() != "module.module1" {
   149  			t.Fatalf("Expected child config path name is `module.module1`, but get `%s`", child.TFConfig.Path.String())
   150  		}
   151  
   152  		expected := map[string]*moduleVariable{
   153  			"foo": {
   154  				Root: true,
   155  				DeclRange: hcl.Range{
   156  					Filename: "main.tf",
   157  					Start:    hcl.Pos{Line: 4, Column: 9},
   158  					End:      hcl.Pos{Line: 4, Column: 14},
   159  				},
   160  			},
   161  			"bar": {
   162  				Root: true,
   163  				DeclRange: hcl.Range{
   164  					Filename: "main.tf",
   165  					Start:    hcl.Pos{Line: 5, Column: 9},
   166  					End:      hcl.Pos{Line: 5, Column: 14},
   167  				},
   168  			},
   169  		}
   170  		opts := []cmp.Option{cmpopts.IgnoreFields(hcl.Pos{}, "Byte")}
   171  		if !cmp.Equal(expected, child.modVars, opts...) {
   172  			t.Fatalf("`%s` module variables are unmatched: Diff=%s", child.TFConfig.Path.String(), cmp.Diff(expected, child.modVars, opts...))
   173  		}
   174  
   175  		grandchild := runners[1]
   176  		if grandchild.TFConfig.Path.String() != "module.module1.module.module2" {
   177  			t.Fatalf("Expected child config path name is `module.module1.module.module2`, but get `%s`", grandchild.TFConfig.Path.String())
   178  		}
   179  
   180  		expected = map[string]*moduleVariable{
   181  			"red": {
   182  				Root:    false,
   183  				Parents: []*moduleVariable{expected["foo"], expected["bar"]},
   184  				DeclRange: hcl.Range{
   185  					Filename: filepath.Join("module", "main.tf"),
   186  					Start:    hcl.Pos{Line: 8, Column: 11},
   187  					End:      hcl.Pos{Line: 8, Column: 34},
   188  				},
   189  			},
   190  			"blue": {
   191  				Root:    false,
   192  				Parents: []*moduleVariable{},
   193  				DeclRange: hcl.Range{
   194  					Filename: filepath.Join("module", "main.tf"),
   195  					Start:    hcl.Pos{Line: 9, Column: 11},
   196  					End:      hcl.Pos{Line: 9, Column: 17},
   197  				},
   198  			},
   199  			"green": {
   200  				Root:    false,
   201  				Parents: []*moduleVariable{expected["foo"]},
   202  				DeclRange: hcl.Range{
   203  					Filename: filepath.Join("module", "main.tf"),
   204  					Start:    hcl.Pos{Line: 10, Column: 11},
   205  					End:      hcl.Pos{Line: 10, Column: 49},
   206  				},
   207  			},
   208  		}
   209  		opts = []cmp.Option{cmpopts.IgnoreFields(hcl.Pos{}, "Byte")}
   210  		if !cmp.Equal(expected, grandchild.modVars, opts...) {
   211  			t.Fatalf("`%s` module variables are unmatched: Diff=%s", grandchild.TFConfig.Path.String(), cmp.Diff(expected, grandchild.modVars, opts...))
   212  		}
   213  	})
   214  }
   215  
   216  func Test_NewModuleRunners_ignoreModules(t *testing.T) {
   217  	withinFixtureDir(t, "nested_modules", func() {
   218  		config := moduleConfig()
   219  		config.IgnoreModules["./module"] = true
   220  		runner := testRunnerWithOsFs(t, config)
   221  
   222  		runners, err := NewModuleRunners(runner)
   223  		if err != nil {
   224  			t.Fatalf("Unexpected error occurred: %s", err)
   225  		}
   226  
   227  		if len(runners) != 0 {
   228  			t.Fatalf("This function must not return runners because `ignore_module` is set. Got `%d` runner(s)", len(runners))
   229  		}
   230  	})
   231  }
   232  
   233  func Test_NewModuleRunners_withInvalidExpression(t *testing.T) {
   234  	withinFixtureDir(t, "invalid_module_attribute", func() {
   235  		runner := testRunnerWithOsFs(t, moduleConfig())
   236  
   237  		_, err := NewModuleRunners(runner)
   238  
   239  		expected := Error{
   240  			Code:    EvaluationError,
   241  			Level:   ErrorLevel,
   242  			Message: "Failed to eval an expression in module.tf:4; Invalid \"terraform\" attribute: The terraform.env attribute was deprecated in v0.10 and removed in v0.12. The \"state environment\" concept was rename to \"workspace\" in v0.12, and so the workspace name can now be accessed using the terraform.workspace attribute.",
   243  		}
   244  		AssertAppError(t, expected, err)
   245  	})
   246  }
   247  
   248  func Test_NewModuleRunners_withNotAllowedAttributes(t *testing.T) {
   249  	withinFixtureDir(t, "not_allowed_module_attribute", func() {
   250  		runner := testRunnerWithOsFs(t, moduleConfig())
   251  
   252  		_, err := NewModuleRunners(runner)
   253  
   254  		expected := Error{
   255  			Code:    UnexpectedAttributeError,
   256  			Level:   ErrorLevel,
   257  			Message: "Attribute of module not allowed was found in module.tf:1; module.tf:4,3-10: Unexpected \"invalid\" block; Blocks are not allowed here.",
   258  		}
   259  		AssertAppError(t, expected, err)
   260  	})
   261  }
   262  
   263  func Test_RunnerFiles(t *testing.T) {
   264  	runner := TestRunner(t, map[string]string{
   265  		"main.tf": "",
   266  	})
   267  	runner.files["child/main.tf"] = &hcl.File{}
   268  
   269  	expected := map[string]*hcl.File{
   270  		"main.tf": {
   271  			Body:  hcl.EmptyBody(),
   272  			Bytes: []byte{},
   273  		},
   274  	}
   275  
   276  	files := runner.Files()
   277  
   278  	opt := cmpopts.IgnoreFields(hcl.File{}, "Body", "Nav")
   279  	if !cmp.Equal(expected, files, opt) {
   280  		t.Fatalf("Failed test: diff: %s", cmp.Diff(expected, files, opt))
   281  	}
   282  }
   283  
   284  func Test_LookupResourcesByType(t *testing.T) {
   285  	content := `
   286  resource "aws_instance" "web" {
   287    ami           = "${data.aws_ami.ubuntu.id}"
   288    instance_type = "t2.micro"
   289  
   290    tags {
   291      Name = "HelloWorld"
   292    }
   293  }
   294  
   295  resource "aws_route53_zone" "primary" {
   296    name = "example.com"
   297  }
   298  
   299  resource "aws_route" "r" {
   300    route_table_id            = "rtb-4fbb3ac4"
   301    destination_cidr_block    = "10.0.1.0/22"
   302    vpc_peering_connection_id = "pcx-45ff3dc1"
   303    depends_on                = ["aws_route_table.testing"]
   304  }`
   305  
   306  	runner := TestRunner(t, map[string]string{"resource.tf": content})
   307  	resources := runner.LookupResourcesByType("aws_instance")
   308  
   309  	if len(resources) != 1 {
   310  		t.Fatalf("Expected resources size is `1`, but get `%d`", len(resources))
   311  	}
   312  	if resources[0].Type != "aws_instance" {
   313  		t.Fatalf("Expected resource type is `aws_instance`, but get `%s`", resources[0].Type)
   314  	}
   315  }
   316  
   317  func Test_LookupIssues(t *testing.T) {
   318  	runner := TestRunner(t, map[string]string{})
   319  	runner.Issues = Issues{
   320  		{
   321  			Rule:    &testRule{},
   322  			Message: "This is test rule",
   323  			Range: hcl.Range{
   324  				Filename: "template.tf",
   325  				Start:    hcl.Pos{Line: 1},
   326  			},
   327  		},
   328  		{
   329  			Rule:    &testRule{},
   330  			Message: "This is test rule",
   331  			Range: hcl.Range{
   332  				Filename: "resource.tf",
   333  				Start:    hcl.Pos{Line: 1},
   334  			},
   335  		},
   336  	}
   337  
   338  	ret := runner.LookupIssues("template.tf")
   339  	expected := Issues{
   340  		{
   341  			Rule:    &testRule{},
   342  			Message: "This is test rule",
   343  			Range: hcl.Range{
   344  				Filename: "template.tf",
   345  				Start:    hcl.Pos{Line: 1},
   346  			},
   347  		},
   348  	}
   349  
   350  	if !cmp.Equal(expected, ret) {
   351  		t.Fatalf("Failed test: diff: %s", cmp.Diff(expected, ret))
   352  	}
   353  }
   354  
   355  func Test_EnsureNoError(t *testing.T) {
   356  	cases := []struct {
   357  		Name      string
   358  		Error     error
   359  		ErrorText string
   360  	}{
   361  		{
   362  			Name:      "no error",
   363  			Error:     nil,
   364  			ErrorText: "function called",
   365  		},
   366  		{
   367  			Name:      "native error",
   368  			Error:     errors.New("Error occurred"),
   369  			ErrorText: "Error occurred",
   370  		},
   371  		{
   372  			Name: "warning error",
   373  			Error: &Error{
   374  				Code:    UnknownValueError,
   375  				Level:   WarningLevel,
   376  				Message: "Warning error",
   377  			},
   378  		},
   379  		{
   380  			Name: "app error",
   381  			Error: &Error{
   382  				Code:    TypeMismatchError,
   383  				Level:   ErrorLevel,
   384  				Message: "App error",
   385  			},
   386  			ErrorText: "App error",
   387  		},
   388  	}
   389  
   390  	for _, tc := range cases {
   391  		runner := TestRunner(t, map[string]string{})
   392  
   393  		err := runner.EnsureNoError(tc.Error, func() error {
   394  			return errors.New("function called")
   395  		})
   396  		if err == nil {
   397  			if tc.ErrorText != "" {
   398  				t.Fatalf("Failed `%s` test: expected error is not occurred `%s`", tc.Name, tc.ErrorText)
   399  			}
   400  		} else if err.Error() != tc.ErrorText {
   401  			t.Fatalf("Failed `%s` test: expected error is %s, but get %s", tc.Name, tc.ErrorText, err)
   402  		}
   403  	}
   404  }
   405  
   406  func Test_IsNullExpr(t *testing.T) {
   407  	cases := []struct {
   408  		Name     string
   409  		Content  string
   410  		Expected bool
   411  		Error    string
   412  	}{
   413  		{
   414  			Name: "non null literal",
   415  			Content: `
   416  resource "null_resource" "test" {
   417    key = "string"
   418  }`,
   419  			Expected: false,
   420  		},
   421  		{
   422  			Name: "non null variable",
   423  			Content: `
   424  variable "value" {
   425    default = "string"
   426  }
   427  
   428  resource "null_resource" "test" {
   429    key = var.value
   430  }`,
   431  			Expected: false,
   432  		},
   433  		{
   434  			Name: "null literal",
   435  			Content: `
   436  resource "null_resource" "test" {
   437    key = null
   438  }`,
   439  			Expected: true,
   440  		},
   441  		{
   442  			Name: "null variable",
   443  			Content: `
   444  variable "value" {
   445    default = null
   446  }
   447  	
   448  resource "null_resource" "test" {
   449    key = var.value
   450  }`,
   451  			Expected: true,
   452  		},
   453  		{
   454  			Name: "unknown variable",
   455  			Content: `
   456  variable "value" {}
   457  	
   458  resource "null_resource" "test" {
   459    key = var.value
   460  }`,
   461  			Expected: false,
   462  		},
   463  		{
   464  			Name: "unevaluable reference",
   465  			Content: `
   466  resource "null_resource" "test" {
   467    key = aws_instance.id
   468  }`,
   469  			Expected: false,
   470  		},
   471  		{
   472  			Name: "including null literal",
   473  			Content: `
   474  resource "null_resource" "test" {
   475    key = "${null}-1"
   476  }`,
   477  			Expected: false,
   478  			Error:    "Invalid template interpolation value: The expression result is null. Cannot include a null value in a string template.",
   479  		},
   480  		{
   481  			Name: "invalid references",
   482  			Content: `
   483  resource "null_resource" "test" {
   484    key = invalid
   485  }`,
   486  			Expected: false,
   487  			Error:    "Invalid reference: A reference to a resource type must be followed by at least one attribute access, specifying the resource name.",
   488  		},
   489  	}
   490  
   491  	for _, tc := range cases {
   492  		runner := TestRunner(t, map[string]string{"main.tf": tc.Content})
   493  
   494  		err := runner.WalkResourceAttributes("null_resource", "key", func(attribute *hcl.Attribute) error {
   495  			ret, err := runner.IsNullExpr(attribute.Expr)
   496  			if err != nil && tc.Error == "" {
   497  				t.Fatalf("Failed `%s` test: unexpected error occurred: %s", tc.Name, err)
   498  			}
   499  			if err == nil && tc.Error != "" {
   500  				t.Fatalf("Failed `%s` test: expected error is %s, but no errors", tc.Name, tc.Error)
   501  			}
   502  			if err != nil && tc.Error != "" && err.Error() != tc.Error {
   503  				t.Fatalf("Failed `%s` test: expected error is %s, but got %s", tc.Name, tc.Error, err)
   504  			}
   505  			if tc.Expected != ret {
   506  				t.Fatalf("Failed `%s` test: expected value is %t, but get %t", tc.Name, tc.Expected, ret)
   507  			}
   508  			return nil
   509  		})
   510  
   511  		if err != nil {
   512  			t.Fatalf("Failed `%s` test: `%s` occurred", tc.Name, err)
   513  		}
   514  	}
   515  }
   516  
   517  func Test_EachStringSliceExprs(t *testing.T) {
   518  	cases := []struct {
   519  		Name    string
   520  		Content string
   521  		Vals    []string
   522  		Lines   []int
   523  	}{
   524  		{
   525  			Name: "literal list",
   526  			Content: `
   527  resource "null_resource" "test" {
   528    value = [
   529      "text",
   530      "element",
   531    ]
   532  }`,
   533  			Vals:  []string{"text", "element"},
   534  			Lines: []int{4, 5},
   535  		},
   536  		{
   537  			Name: "literal list",
   538  			Content: `
   539  variable "list" {
   540    default = [
   541      "text",
   542      "element",
   543    ]
   544  }
   545  
   546  resource "null_resource" "test" {
   547    value = var.list
   548  }`,
   549  			Vals:  []string{"text", "element"},
   550  			Lines: []int{10, 10},
   551  		},
   552  		{
   553  			Name: "for expressions",
   554  			Content: `
   555  variable "list" {
   556    default = ["text", "element", "ignored"]
   557  }
   558  
   559  resource "null_resource" "test" {
   560    value = [
   561  	for e in var.list:
   562  	e
   563  	if e != "ignored"
   564    ]
   565  }`,
   566  			Vals:  []string{"text", "element"},
   567  			Lines: []int{7, 7},
   568  		},
   569  	}
   570  
   571  	for _, tc := range cases {
   572  		runner := TestRunner(t, map[string]string{"main.tf": tc.Content})
   573  
   574  		vals := []string{}
   575  		lines := []int{}
   576  		err := runner.WalkResourceAttributes("null_resource", "value", func(attribute *hcl.Attribute) error {
   577  			return runner.EachStringSliceExprs(attribute.Expr, func(val string, expr hcl.Expression) {
   578  				vals = append(vals, val)
   579  				lines = append(lines, expr.Range().Start.Line)
   580  			})
   581  		})
   582  		if err != nil {
   583  			t.Fatalf("Failed `%s` test: %s", tc.Name, err)
   584  		}
   585  
   586  		if !cmp.Equal(vals, tc.Vals) {
   587  			t.Fatalf("Failed `%s` test: diff=%s", tc.Name, cmp.Diff(vals, tc.Vals))
   588  		}
   589  		if !cmp.Equal(lines, tc.Lines) {
   590  			t.Fatalf("Failed `%s` test: diff=%s", tc.Name, cmp.Diff(lines, tc.Lines))
   591  		}
   592  	}
   593  }
   594  
   595  type testRule struct{}
   596  
   597  func (r *testRule) Name() string {
   598  	return "test_rule"
   599  }
   600  func (r *testRule) Severity() string {
   601  	return ERROR
   602  }
   603  func (r *testRule) Link() string {
   604  	return ""
   605  }
   606  
   607  func Test_EmitIssue(t *testing.T) {
   608  	cases := []struct {
   609  		Name        string
   610  		Rule        Rule
   611  		Message     string
   612  		Location    hcl.Range
   613  		Annotations map[string]Annotations
   614  		Expected    Issues
   615  	}{
   616  		{
   617  			Name:    "basic",
   618  			Rule:    &testRule{},
   619  			Message: "This is test message",
   620  			Location: hcl.Range{
   621  				Filename: "test.tf",
   622  				Start:    hcl.Pos{Line: 1},
   623  			},
   624  			Annotations: map[string]Annotations{},
   625  			Expected: Issues{
   626  				{
   627  					Rule:    &testRule{},
   628  					Message: "This is test message",
   629  					Range: hcl.Range{
   630  						Filename: "test.tf",
   631  						Start:    hcl.Pos{Line: 1},
   632  					},
   633  				},
   634  			},
   635  		},
   636  		{
   637  			Name:    "ignore",
   638  			Rule:    &testRule{},
   639  			Message: "This is test message",
   640  			Location: hcl.Range{
   641  				Filename: "test.tf",
   642  				Start:    hcl.Pos{Line: 1},
   643  			},
   644  			Annotations: map[string]Annotations{
   645  				"test.tf": {
   646  					{
   647  						Content: "test_rule",
   648  						Token: hclsyntax.Token{
   649  							Type: hclsyntax.TokenComment,
   650  							Range: hcl.Range{
   651  								Filename: "test.tf",
   652  								Start:    hcl.Pos{Line: 1},
   653  							},
   654  						},
   655  					},
   656  				},
   657  			},
   658  			Expected: Issues{},
   659  		},
   660  	}
   661  
   662  	for _, tc := range cases {
   663  		runner := testRunnerWithAnnotations(t, map[string]string{}, tc.Annotations)
   664  
   665  		runner.EmitIssue(tc.Rule, tc.Message, tc.Location)
   666  
   667  		if !cmp.Equal(runner.Issues, tc.Expected) {
   668  			t.Fatalf("Failed `%s` test: diff=%s", tc.Name, cmp.Diff(runner.Issues, tc.Expected))
   669  		}
   670  	}
   671  }
   672  
   673  func Test_DecodeRuleConfig(t *testing.T) {
   674  	type ruleSchema struct {
   675  		Foo string `hcl:"foo"`
   676  	}
   677  	options := ruleSchema{}
   678  
   679  	file, diags := hclsyntax.ParseConfig([]byte(`foo = "bar"`), "test.hcl", hcl.Pos{})
   680  	if diags.HasErrors() {
   681  		t.Fatalf("Failed to parse test config: %s", diags)
   682  	}
   683  
   684  	cfg := EmptyConfig()
   685  	cfg.Rules["test"] = &RuleConfig{
   686  		Name:    "test",
   687  		Enabled: true,
   688  		Body:    file.Body,
   689  	}
   690  
   691  	runner := TestRunnerWithConfig(t, map[string]string{}, cfg)
   692  	if err := runner.DecodeRuleConfig("test", &options); err != nil {
   693  		t.Fatalf("Failed to decode rule config: %s", err)
   694  	}
   695  
   696  	expected := ruleSchema{Foo: "bar"}
   697  	if !cmp.Equal(options, expected) {
   698  		t.Fatalf("Failed to decode rule config: diff=%s", cmp.Diff(options, expected))
   699  	}
   700  }
   701  
   702  func Test_DecodeRuleConfig_emptyBody(t *testing.T) {
   703  	type ruleSchema struct {
   704  		Foo string `hcl:"foo"`
   705  	}
   706  	options := ruleSchema{}
   707  
   708  	cfg := EmptyConfig()
   709  	cfg.Rules["test"] = &RuleConfig{
   710  		Name:    "test",
   711  		Enabled: true,
   712  		Body:    hcl.EmptyBody(),
   713  	}
   714  
   715  	runner := TestRunnerWithConfig(t, map[string]string{}, cfg)
   716  	err := runner.DecodeRuleConfig("test", &options)
   717  	if err == nil {
   718  		t.Fatal("Expected to fail to decode rule config, but not")
   719  	}
   720  
   721  	expected := "This rule cannot be enabled with the `--enable-rule` option because it lacks the required configuration"
   722  	if err.Error() != expected {
   723  		t.Fatalf("Expected error message is %s, but got %s", expected, err.Error())
   724  	}
   725  }