diff --git a/cmd/parse.go b/cmd/parse.go index 43acf743bb7..13bb569b2e4 100644 --- a/cmd/parse.go +++ b/cmd/parse.go @@ -24,10 +24,12 @@ const ( parseFormatJSON = "json" ) -var parseParams = struct { +type parseParams struct { format *util.EnumFlag jsonInclude string -}{ +} + +var configuredParseParams = parseParams{ format: util.NewEnumFlag(parseFormatPretty, []string{parseFormatPretty, parseFormatJSON}), jsonInclude: "", } @@ -43,18 +45,18 @@ var parseCommand = &cobra.Command{ return nil }, Run: func(_ *cobra.Command, args []string) { - os.Exit(parse(args, os.Stdout, os.Stderr)) + os.Exit(parse(args, &configuredParseParams, os.Stdout, os.Stderr)) }, } -func parse(args []string, stdout io.Writer, stderr io.Writer) int { +func parse(args []string, params *parseParams, stdout io.Writer, stderr io.Writer) int { if len(args) == 0 { return 0 } exposeLocation := false exposeComments := true - for _, opt := range strings.Split(parseParams.jsonInclude, ",") { + for _, opt := range strings.Split(params.jsonInclude, ",") { value := true if strings.HasPrefix(opt, "-") { value = false @@ -100,7 +102,7 @@ func parse(args []string, stdout io.Writer, stderr io.Writer) int { result.Parsed.Comments = nil } - switch parseParams.format.String() { + switch params.format.String() { case parseFormatJSON: bs, err := json.MarshalIndent(result.Parsed, "", " ") if err != nil { @@ -108,7 +110,7 @@ func parse(args []string, stdout io.Writer, stderr io.Writer) int { return 1 } - fmt.Println(string(bs)) + fmt.Fprint(stdout, string(bs)+"\n") default: if err != nil { fmt.Fprintln(stderr, err) @@ -121,8 +123,8 @@ func parse(args []string, stdout io.Writer, stderr io.Writer) int { } func init() { - parseCommand.Flags().VarP(parseParams.format, "format", "f", "set output format") - parseCommand.Flags().StringVarP(&parseParams.jsonInclude, "json-include", "", "", "select optional elements, current options: locations, comments. E.g. --json-include locations,-comments will include locations and exclude comments.") + parseCommand.Flags().VarP(configuredParseParams.format, "format", "f", "set output format") + parseCommand.Flags().StringVarP(&configuredParseParams.jsonInclude, "json-include", "", "", "select optional elements, current options: locations, comments. E.g. --json-include locations,-comments will include locations and exclude comments.") RootCommand.AddCommand(parseCommand) } diff --git a/cmd/parse_test.go b/cmd/parse_test.go index 40652b78dfe..3ef99e58c86 100644 --- a/cmd/parse_test.go +++ b/cmd/parse_test.go @@ -3,8 +3,10 @@ package cmd import ( "bytes" "path/filepath" + "strings" "testing" + "github.com/open-policy-agent/opa/util" "github.com/open-policy-agent/opa/util/test" ) @@ -16,13 +18,32 @@ func TestParseExit0(t *testing.T) { p = 1 `, } - errc, _, stderr := testParse(t, files) + errc, stdout, stderr, _ := testParse(t, files, &configuredParseParams) if errc != 0 { t.Fatalf("Expected exit code 0, got %v", errc) } if len(stderr) > 0 { t.Fatalf("Expected no stderr output, got:\n%s\n", string(stderr)) } + + expectedOutput := `module + package + ref + data + "x" + rule + head + ref + p + 1 + body + expr index=0 + true +` + + if got, want := string(stdout), expectedOutput; got != want { + t.Fatalf("Expected output\n%v\n, got\n%v", want, got) + } } func TestParseExit1(t *testing.T) { @@ -30,7 +51,7 @@ func TestParseExit1(t *testing.T) { files := map[string]string{ "x.rego": `???`, } - errc, _, stderr := testParse(t, files) + errc, _, stderr, _ := testParse(t, files, &configuredParseParams) if errc != 1 { t.Fatalf("Expected exit code 1, got %v", errc) } @@ -39,21 +60,202 @@ func TestParseExit1(t *testing.T) { } } +func TestParseJSONOutput(t *testing.T) { + + files := map[string]string{ + "x.rego": `package x + + p = 1 + `, + } + errc, stdout, stderr, _ := testParse(t, files, &parseParams{ + format: util.NewEnumFlag(parseFormatJSON, []string{parseFormatPretty, parseFormatJSON}), + }) + if errc != 0 { + t.Fatalf("Expected exit code 0, got %v", errc) + } + if len(stderr) > 0 { + t.Fatalf("Expected no stderr output, got:\n%s\n", string(stderr)) + } + + expectedOutput := `{ + "package": { + "path": [ + { + "type": "var", + "value": "data" + }, + { + "type": "string", + "value": "x" + } + ] + }, + "rules": [ + { + "body": [ + { + "index": 0, + "terms": { + "type": "boolean", + "value": true + } + } + ], + "head": { + "name": "p", + "value": { + "type": "number", + "value": 1 + }, + "ref": [ + { + "type": "var", + "value": "p" + } + ] + } + } + ] +} +` + + if got, want := string(stdout), expectedOutput; got != want { + t.Fatalf("Expected output\n%v\n, got\n%v", want, got) + } +} + +func TestParseJSONOutputWithLocations(t *testing.T) { + + files := map[string]string{ + "x.rego": `package x + + p = 1 + `, + } + errc, stdout, stderr, tempDirPath := testParse(t, files, &parseParams{ + format: util.NewEnumFlag(parseFormatJSON, []string{parseFormatPretty, parseFormatJSON}), + jsonInclude: "locations", + }) + if errc != 0 { + t.Fatalf("Expected exit code 0, got %v", errc) + } + if len(stderr) > 0 { + t.Fatalf("Expected no stderr output, got:\n%s\n", string(stderr)) + } + + expectedOutput := strings.Replace(`{ + "package": { + "location": { + "file": "TEMPDIR/x.rego", + "row": 1, + "col": 1 + }, + "path": [ + { + "location": { + "file": "TEMPDIR/x.rego", + "row": 1, + "col": 9 + }, + "type": "var", + "value": "data" + }, + { + "location": { + "file": "TEMPDIR/x.rego", + "row": 1, + "col": 9 + }, + "type": "string", + "value": "x" + } + ] + }, + "rules": [ + { + "body": [ + { + "index": 0, + "terms": { + "type": "boolean", + "value": true + } + } + ], + "head": { + "name": "p", + "value": { + "location": { + "file": "TEMPDIR/x.rego", + "row": 3, + "col": 7 + }, + "type": "number", + "value": 1 + }, + "ref": [ + { + "type": "var", + "value": "p" + } + ] + } + } + ] +} +`, "TEMPDIR", tempDirPath, -1) + + if got, want := string(stdout), expectedOutput; got != want { + t.Fatalf("Expected output\n%v\n, got\n%v", want, got) + } +} + +func TestParseJSONOutputComments(t *testing.T) { + + files := map[string]string{ + "x.rego": `package x + + # comment + p = 1 + `, + } + errc, stdout, stderr, _ := testParse(t, files, &parseParams{ + format: util.NewEnumFlag(parseFormatJSON, []string{parseFormatPretty, parseFormatJSON}), + jsonInclude: "comments", + }) + if errc != 0 { + t.Fatalf("Expected exit code 0, got %v", errc) + } + if len(stderr) > 0 { + t.Fatalf("Expected no stderr output, got:\n%s\n", string(stderr)) + } + + expectedCommentTextValue := "IGNvbW1lbnQ=" + + if !strings.Contains(string(stdout), expectedCommentTextValue) { + t.Fatalf("Comment text value %q missing in output: %s", expectedCommentTextValue, string(stdout)) + } +} + // Runs parse and returns the exit code, stdout, and stderr contents -func testParse(t *testing.T, files map[string]string) (int, []byte, []byte) { +func testParse(t *testing.T, files map[string]string, params *parseParams) (int, []byte, []byte, string) { t.Helper() stdout := new(bytes.Buffer) stderr := new(bytes.Buffer) var errc int + var tempDirUsed string test.WithTempFS(files, func(path string) { var args []string for file := range files { args = append(args, filepath.Join(path, file)) } - errc = parse(args, stdout, stderr) + errc = parse(args, params, stdout, stderr) + + tempDirUsed = path }) - return errc, stdout.Bytes(), stderr.Bytes() + return errc, stdout.Bytes(), stderr.Bytes(), tempDirUsed }