Feature-complete commandline interface (#138)

* Feature-complete commandline interface
* Make errors match cpp implementation
This commit is contained in:
Dave Cunningham 2017-11-03 22:00:30 -04:00 committed by GitHub
parent b6ee2c2f51
commit c60056c75f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 765 additions and 90 deletions

View File

@ -724,7 +724,7 @@ func serializeJSON(v interface{}, multiline bool, indent string, buf *bytes.Buff
}
func (i *interpreter) manifestAndSerializeJSON(
buf *bytes.Buffer, trace *TraceElement, v value, multiline bool, indent string) error {
buf *bytes.Buffer, trace *TraceElement, v value, multiline bool, indent string) error {
manifested, err := i.manifestJSON(trace, v)
if err != nil {
return err
@ -740,10 +740,55 @@ func (i *interpreter) manifestString(buf *bytes.Buffer, trace *TraceElement, v v
buf.WriteString(v.getString())
return nil
default:
return makeRuntimeError(fmt.Sprint("Expected string result, got: " + v.getType().name), i.getCurrentStackTrace(trace))
return makeRuntimeError(fmt.Sprintf("Expected string result, got: %s", v.getType().name), i.getCurrentStackTrace(trace))
}
}
func (i *interpreter) manifestAndSerializeMulti(trace *TraceElement, v value) (r map[string]string, err error) {
r = make(map[string]string)
json, err := i.manifestJSON(trace, v)
if err != nil {
return r, err
}
switch json := json.(type) {
case map[string]interface{}:
for filename, fileJson := range json {
var buf bytes.Buffer
serializeJSON(fileJson, true, "", &buf)
buf.WriteString("\n")
r[filename] = buf.String()
}
default:
msg := fmt.Sprintf("Multi mode: Top-level object was a %s, "+
"should be an object whose keys are filenames and values hold "+
"the JSON for that file.", v.getType().name)
return r, makeRuntimeError(msg, i.getCurrentStackTrace(trace))
}
return
}
func (i *interpreter) manifestAndSerializeYAMLStream(trace *TraceElement, v value) (r []string, err error) {
r = make([]string, 0)
json, err := i.manifestJSON(trace, v)
if err != nil {
return r, err
}
switch json := json.(type) {
case []interface{}:
for _, doc := range json {
var buf bytes.Buffer
serializeJSON(doc, true, "", &buf)
buf.WriteString("\n")
r = append(r, buf.String())
}
default:
msg := fmt.Sprintf("Stream mode: Top-level object was a %s, "+
"should be an array whose elements hold "+
"the JSON for each document in the stream.", v.getType().name)
return r, makeRuntimeError(msg, i.getCurrentStackTrace(trace))
}
return
}
func jsonToValue(e *evaluator, v interface{}) (value, error) {
switch v := v.(type) {
@ -890,13 +935,7 @@ func makeInitialEnv(filename string, baseStd valueObject) environment {
)
}
// TODO(sbarzowski) this function takes far too many arguments - build interpreter in vm instead
func evaluate(node ast.Node, ext vmExtMap, tla vmExtMap, nativeFuncs map[string]*NativeFunction,
maxStack int, importer Importer, stringOutput bool) (string, error) {
i, err := buildInterpreter(ext, nativeFuncs, maxStack, importer)
if err != nil {
return "", err
}
func evaluateAux(i *interpreter, node ast.Node, tla vmExtMap) (value, *TraceElement, error) {
evalLoc := ast.MakeLocationRangeMessage("During evaluation")
evalTrace := &TraceElement{
loc: &evalLoc,
@ -904,7 +943,7 @@ func evaluate(node ast.Node, ext vmExtMap, tla vmExtMap, nativeFuncs map[string]
env := makeInitialEnv(node.Loc().FileName, i.baseStd)
result, err := i.EvalInCleanEnv(evalTrace, &env, node, false)
if err != nil {
return "", err
return nil, nil, err
}
if len(tla) != 0 {
// If it's not a function, ignore TLA
@ -914,13 +953,13 @@ func evaluate(node ast.Node, ext vmExtMap, tla vmExtMap, nativeFuncs map[string]
for argName, pv := range toplevelArgMap {
args.named = append(args.named, namedCallArgument{name: ast.Identifier(argName), pv: pv})
}
funcLoc := ast.MakeLocationRangeMessage("Top-level-function")
funcLoc := ast.MakeLocationRangeMessage("Top-level function")
funcTrace := &TraceElement{
loc: &funcLoc,
}
result, err = f.call(args).getValue(i, funcTrace)
if err != nil {
return "", err
return nil, nil, err
}
}
}
@ -928,6 +967,23 @@ func evaluate(node ast.Node, ext vmExtMap, tla vmExtMap, nativeFuncs map[string]
manifestationTrace := &TraceElement{
loc: &manifestationLoc,
}
return result, manifestationTrace, nil
}
// TODO(sbarzowski) this function takes far too many arguments - build interpreter in vm instead
func evaluate(node ast.Node, ext vmExtMap, tla vmExtMap, nativeFuncs map[string]*NativeFunction,
maxStack int, importer Importer, stringOutput bool) (string, error) {
i, err := buildInterpreter(ext, nativeFuncs, maxStack, importer)
if err != nil {
return "", err
}
result, manifestationTrace, err := evaluateAux(i, node, tla)
if err != nil {
return "", err
}
var buf bytes.Buffer
if stringOutput {
err = i.manifestString(&buf, manifestationTrace, result)
@ -940,3 +996,37 @@ func evaluate(node ast.Node, ext vmExtMap, tla vmExtMap, nativeFuncs map[string]
buf.WriteString("\n")
return buf.String(), nil
}
// TODO(sbarzowski) this function takes far too many arguments - build interpreter in vm instead
func evaluateMulti(node ast.Node, ext vmExtMap, tla vmExtMap, nativeFuncs map[string]*NativeFunction,
maxStack int, importer Importer, stringOutput bool) (map[string]string, error) {
i, err := buildInterpreter(ext, nativeFuncs, maxStack, importer)
if err != nil {
return nil, err
}
result, manifestationTrace, err := evaluateAux(i, node, tla)
if err != nil {
return nil, err
}
return i.manifestAndSerializeMulti(manifestationTrace, result)
}
// TODO(sbarzowski) this function takes far too many arguments - build interpreter in vm instead
func evaluateStream(node ast.Node, ext vmExtMap, tla vmExtMap, nativeFuncs map[string]*NativeFunction,
maxStack int, importer Importer) ([]string, error) {
i, err := buildInterpreter(ext, nativeFuncs, maxStack, importer)
if err != nil {
return nil, err
}
result, manifestationTrace, err := evaluateAux(i, node, tla)
if err != nil {
return nil, err
}
return i.manifestAndSerializeYAMLStream(manifestationTrace, result)
}

View File

@ -18,20 +18,145 @@ package main
import (
"fmt"
"io"
"io/ioutil"
"log"
"os"
"runtime/pprof"
"sort"
"strconv"
"strings"
"github.com/google/go-jsonnet"
)
func usage() {
fmt.Println("usage: jsonnet <filename>")
func nextArg(i *int, args []string) string {
(*i)++
if (*i) >= len(args) {
fmt.Fprintln(os.Stderr, "Expected another commandline argument.")
os.Exit(1)
}
return args[*i]
}
func getVar(s string) (string, string, error) {
// simplifyArgs transforms an array of commandline arguments so that
// any -abc arg before the first -- (if any) are expanded into
// -a -b -c.
func simplifyArgs(args []string) (r []string) {
r = make([]string, 0, len(args)*2)
for i, arg := range args {
if arg == "--" {
for j := i; j < len(args); j++ {
r = append(r, args[j])
}
break
}
if len(arg) > 2 && arg[0] == '-' && arg[1] != '-' {
for j := 1; j < len(arg); j++ {
r = append(r, "-"+string(arg[j]))
}
} else {
r = append(r, arg)
}
}
return
}
func version(o io.Writer) {
fmt.Fprintf(o, "Jsonnet commandline interpreter %s\n", jsonnet.Version())
}
func usage(o io.Writer) {
version(o)
fmt.Fprintln(o)
fmt.Fprintln(o, "General commandline:")
fmt.Fprintln(o, "jsonnet [<cmd>] {<option>} { <filename> }")
fmt.Fprintln(o, "Note: <cmd> defaults to \"eval\"")
fmt.Fprintln(o)
fmt.Fprintln(o, "The eval command:")
fmt.Fprintln(o, "jsonnet eval {<option>} <filename>")
fmt.Fprintln(o, "Note: Only one filename is supported")
fmt.Fprintln(o)
fmt.Fprintln(o, "Available eval options:")
fmt.Fprintln(o, " -h / --help This message")
fmt.Fprintln(o, " -e / --exec Treat filename as code")
fmt.Fprintln(o, " -J / --jpath <dir> Specify an additional library search dir")
fmt.Fprintln(o, " -o / --output-file <file> Write to the output file rather than stdout")
fmt.Fprintln(o, " -m / --multi <dir> Write multiple files to the directory, list files on stdout")
fmt.Fprintln(o, " -y / --yaml-stream Write output as a YAML stream of JSON documents")
fmt.Fprintln(o, " -S / --string Expect a string, manifest as plain text")
fmt.Fprintln(o, " -s / --max-stack <n> Number of allowed stack frames")
fmt.Fprintln(o, " -t / --max-trace <n> Max length of stack trace before cropping")
fmt.Fprintln(o, " --version Print version")
fmt.Fprintln(o, "Available options for specifying values of 'external' variables:")
fmt.Fprintln(o, "Provide the value as a string:")
fmt.Fprintln(o, " -V / --ext-str <var>[=<val>] If <val> is omitted, get from environment var <var>")
fmt.Fprintln(o, " --ext-str-file <var>=<file> Read the string from the file")
fmt.Fprintln(o, "Provide a value as Jsonnet code:")
fmt.Fprintln(o, " --ext-code <var>[=<code>] If <code> is omitted, get from environment var <var>")
fmt.Fprintln(o, " --ext-code-file <var>=<file> Read the code from the file")
fmt.Fprintln(o, "Available options for specifying values of 'top-level arguments':")
fmt.Fprintln(o, "Provide the value as a string:")
fmt.Fprintln(o, " -A / --tla-str <var>[=<val>] If <val> is omitted, get from environment var <var>")
fmt.Fprintln(o, " --tla-str-file <var>=<file> Read the string from the file")
fmt.Fprintln(o, "Provide a value as Jsonnet code:")
fmt.Fprintln(o, " --tla-code <var>[=<code>] If <code> is omitted, get from environment var <var>")
fmt.Fprintln(o, " --tla-code-file <var>=<file> Read the code from the file")
fmt.Fprintln(o)
fmt.Fprintln(o, "The fmt command:")
fmt.Fprintln(o, "jsonnet fmt is currently not available in the Go implementation")
fmt.Fprintln(o)
fmt.Fprintln(o, "In all cases:")
fmt.Fprintln(o, "<filename> can be - (stdin)")
fmt.Fprintln(o, "Multichar options are expanded e.g. -abc becomes -a -b -c.")
fmt.Fprintln(o, "The -- option suppresses option processing for subsequent arguments.")
fmt.Fprintln(o, "Note that since filenames and jsonnet programs can begin with -, it is advised to")
fmt.Fprintln(o, "use -- if the argument is unknown, e.g. jsonnet -- \"$FILENAME\".")
}
func safeStrToInt(str string) (i int) {
i, err := strconv.Atoi(str)
if err != nil {
fmt.Fprintln(os.Stderr, "ERROR: Invalid integer \"%s\"", str)
os.Exit(1)
}
return
}
type Command int
const (
commandEval = iota
commandFmt = iota
)
type config struct {
cmd Command
inputFiles []string
outputFile string
filenameIsCode bool
// commandEval flags
evalMulti bool
evalStream bool
evalMultiOutputDir string
evalJpath []string
// commandFmt flags
// commandFmt is currently unsupported.
}
func makeConfig() config {
return config{
cmd: commandEval,
filenameIsCode: false,
evalMulti: false,
evalStream: false,
evalJpath: []string{},
}
}
func getVarVal(s string) (string, string, error) {
parts := strings.SplitN(s, "=", 2)
name := parts[0]
if len(parts) == 1 {
@ -45,6 +170,324 @@ func getVar(s string) (string, string, error) {
}
}
func getVarFile(s string) (string, string, error) {
parts := strings.SplitN(s, "=", 2)
name := parts[0]
if len(parts) == 1 {
return "", "", fmt.Errorf("ERROR: argument not in form <var>=<file> \"%s\".", s)
} else {
b, err := ioutil.ReadFile(parts[1])
if err != nil {
return "", "", err
}
return name, string(b), nil
}
}
type processArgsStatus = int
const (
processArgsStatusContinue = iota
processArgsStatusSuccessUsage = iota
processArgsStatusFailureUsage = iota
processArgsStatusSuccess = iota
processArgsStatusFailure = iota
)
func processArgs(givenArgs []string, config *config, vm *jsonnet.VM) (processArgsStatus, error) {
args := simplifyArgs(givenArgs)
remainingArgs := make([]string, 0, 0)
i := 0
if len(args) > 0 && args[i] == "fmt" {
config.cmd = commandFmt
i++
} else if len(args) > 0 && args[i] == "eval" {
config.cmd = commandEval
i++
}
for ; i < len(args); i++ {
arg := args[i]
if arg == "-h" || arg == "--help" {
return processArgsStatusSuccessUsage, nil
} else if arg == "-v" || arg == "--version" {
version(os.Stdout)
return processArgsStatusSuccess, nil
} else if arg == "-e" || arg == "--exec" {
config.filenameIsCode = true
} else if arg == "-o" || arg == "--exec" {
outputFile := nextArg(&i, args)
if len(outputFile) == 0 {
return processArgsStatusFailure, fmt.Errorf("ERROR: -o argument was empty string")
}
config.outputFile = outputFile
} else if arg == "--" {
// All subsequent args are not options.
i++
for ; i < len(args); i++ {
remainingArgs = append(remainingArgs, args[i])
}
break
} else if config.cmd == commandEval {
if arg == "-s" || arg == "--max-stack" {
l := safeStrToInt(nextArg(&i, args))
if l < 1 {
return processArgsStatusFailure, fmt.Errorf("ERROR: Invalid --max-stack value: %d", l)
}
vm.MaxStack = l
} else if arg == "-J" || arg == "--jpath" {
dir := nextArg(&i, args)
if len(dir) == 0 {
return processArgsStatusFailure, fmt.Errorf("ERROR: -J argument was empty string")
}
if dir[len(dir)-1] != '/' {
dir += "/"
}
config.evalJpath = append(config.evalJpath, dir)
} else if arg == "-V" || arg == "--ext-str" {
next := nextArg(&i, args)
name, content, err := getVarVal(next)
if err != nil {
return processArgsStatusFailure, err
}
vm.ExtVar(name, content)
} else if arg == "--ext-str-file" {
next := nextArg(&i, args)
name, content, err := getVarFile(next)
if err != nil {
return processArgsStatusFailure, err
}
vm.ExtVar(name, content)
} else if arg == "--ext-code" {
next := nextArg(&i, args)
name, content, err := getVarVal(next)
if err != nil {
return processArgsStatusFailure, err
}
vm.ExtCode(name, content)
} else if arg == "--ext-code-file" {
next := nextArg(&i, args)
name, content, err := getVarFile(next)
if err != nil {
return processArgsStatusFailure, err
}
vm.ExtCode(name, content)
} else if arg == "-A" || arg == "--tla-str" {
next := nextArg(&i, args)
name, content, err := getVarVal(next)
if err != nil {
return processArgsStatusFailure, err
}
vm.TLAVar(name, content)
} else if arg == "--tla-str-file" {
next := nextArg(&i, args)
name, content, err := getVarFile(next)
if err != nil {
return processArgsStatusFailure, err
}
vm.TLAVar(name, content)
} else if arg == "--tla-code" {
next := nextArg(&i, args)
name, content, err := getVarVal(next)
if err != nil {
return processArgsStatusFailure, err
}
vm.TLACode(name, content)
} else if arg == "--tla-code-file" {
next := nextArg(&i, args)
name, content, err := getVarFile(next)
if err != nil {
return processArgsStatusFailure, err
}
vm.TLACode(name, content)
} else if arg == "-t" || arg == "--max-trace" {
l := safeStrToInt(nextArg(&i, args))
if l < 0 {
return processArgsStatusFailure, fmt.Errorf("ERROR: Invalid --max-trace value: %d", l)
}
vm.ErrorFormatter.MaxStackTraceSize = l
} else if arg == "-m" || arg == "--multi" {
config.evalMulti = true
outputDir := nextArg(&i, args)
if len(outputDir) == 0 {
return processArgsStatusFailure, fmt.Errorf("ERROR: -m argument was empty string")
}
if outputDir[len(outputDir)-1] != '/' {
outputDir += "/"
}
config.evalMultiOutputDir = outputDir
} else if arg == "-y" || arg == "--yaml-stream" {
config.evalStream = true
} else if arg == "-S" || arg == "--string" {
vm.StringOutput = true
} else if len(arg) > 1 && arg[0] == '-' {
return processArgsStatusFailure, fmt.Errorf("ERROR: Unrecognized argument: %s", arg)
} else {
remainingArgs = append(remainingArgs, arg)
}
} else {
return processArgsStatusFailure, fmt.Errorf("The Go implementation currently does not support jsonnet fmt.")
}
}
want := "filename"
if config.filenameIsCode {
want = "code"
}
if len(remainingArgs) == 0 {
return processArgsStatusFailureUsage, fmt.Errorf("ERROR: Must give %s", want)
}
// TODO(dcunnin): Formatter allows multiple files in test and in-place mode.
multipleFilesAllowed := false
if !multipleFilesAllowed {
if len(remainingArgs) > 1 {
return processArgsStatusFailure, fmt.Errorf("ERROR: Only one %s is allowed", want)
}
}
config.inputFiles = remainingArgs
return processArgsStatusContinue, nil
}
// readInput gets Jsonnet code from the given place (file, commandline, stdin).
// It also updates the given filename to <stdin> or <cmdline> if it wasn't a real filename.
func readInput(config config, filename *string) (input string, err error) {
if config.filenameIsCode {
input, err = *filename, nil
*filename = "<cmdline>"
} else if *filename == "-" {
var bytes []byte
bytes, err = ioutil.ReadAll(os.Stdin)
input = string(bytes)
*filename = "<stdin>"
} else {
var bytes []byte
bytes, err = ioutil.ReadFile(*filename)
input = string(bytes)
}
return
}
func writeMultiOutputFiles(output map[string]string, outputDir, outputFile string) error {
// If multiple file output is used, then iterate over each string from
// the sequence of strings returned by jsonnet_evaluate_snippet_multi,
// construct pairs of filename and content, and write each output file.
var manifest *os.File
if outputFile == "" {
manifest = os.Stdout
} else {
var err error
manifest, err = os.Create(outputFile)
if err != nil {
return err
}
defer manifest.Close()
}
// Iterate through the map in order.
keys := make([]string, 0, len(output))
for k, _ := range output {
keys = append(keys, k)
}
sort.Strings(keys)
for _, key := range keys {
newContent := output[key]
filename := outputDir + key
_, err := manifest.WriteString(filename)
if err != nil {
return err
}
_, err = manifest.WriteString("\n")
if err != nil {
return err
}
if _, err := os.Stat(filename); !os.IsNotExist(err) {
existingContent, err := ioutil.ReadFile(filename)
if err != nil {
return err
}
if string(existingContent) == newContent {
// Do not bump the timestamp on the file if its content is
// the same. This may trigger other tools (e.g. make) to do
// unnecessary work.
continue
}
}
f, err := os.Create(filename)
if err != nil {
return err
}
defer f.Close()
_, err = f.WriteString(newContent)
if err != nil {
return err
}
}
return nil
}
// writeOutputStream writes the output as a YAML stream.
func writeOutputStream(output []string, outputFile string) error {
var f *os.File
if outputFile == "" {
f = os.Stdout
} else {
var err error
f, err = os.Create(outputFile)
if err != nil {
return err
}
defer f.Close()
}
for _, doc := range output {
_, err := f.WriteString("---\n")
if err != nil {
return err
}
_, err = f.WriteString(doc)
if err != nil {
return err
}
}
if len(output) > 0 {
_, err := f.WriteString("...\n")
if err != nil {
return err
}
}
return nil
}
func writeOutputFile(output string, outputFile string) error {
if outputFile == "" {
fmt.Print(output)
return nil
}
f, err := os.Create(outputFile)
if err != nil {
return err
}
defer f.Close()
_, err = f.WriteString(output)
return err
}
func main() {
// https://blog.golang.org/profiling-go-programs
var cpuprofile = os.Getenv("JSONNET_CPU_PROFILE")
@ -57,62 +500,102 @@ func main() {
defer pprof.StopCPUProfile()
}
// TODO(sbarzowski) Be consistent about error codes with C++ maybe
var filename string
vm := jsonnet.MakeVM()
for i := 1; i < len(os.Args); i++ {
arg := os.Args[i]
switch arg {
case "--tla-str":
i++
name, content, err := getVar(os.Args[i])
if err != nil {
fmt.Fprintln(os.Stderr, err.Error())
os.Exit(1)
}
vm.TLAVar(name, content)
case "--tla-code":
i++
name, content, err := getVar(os.Args[i])
if err != nil {
fmt.Fprintln(os.Stderr, err.Error())
os.Exit(1)
}
vm.TLACode(name, content)
case "--ext-code":
i++
name, content, err := getVar(os.Args[i])
if err != nil {
fmt.Fprintln(os.Stderr, err.Error())
os.Exit(1)
}
vm.ExtCode(name, content)
case "--ext-str":
i++
name, content, err := getVar(os.Args[i])
if err != nil {
fmt.Fprintln(os.Stderr, err.Error())
os.Exit(1)
}
vm.ExtVar(name, content)
default:
if filename != "" {
usage()
os.Exit(1)
}
filename = arg
}
}
snippet, err := ioutil.ReadFile(filename)
config := makeConfig()
status, err := processArgs(os.Args[1:], &config, vm)
if err != nil {
fmt.Fprintf(os.Stderr, "Error reading input file: %v\n", err.Error())
fmt.Fprintln(os.Stderr, err.Error())
}
switch status {
case processArgsStatusContinue:
break
case processArgsStatusSuccessUsage:
usage(os.Stdout)
os.Exit(0)
case processArgsStatusFailureUsage:
if err != nil {
fmt.Fprintln(os.Stderr, "")
}
usage(os.Stderr)
os.Exit(1)
case processArgsStatusSuccess:
os.Exit(0)
case processArgsStatusFailure:
os.Exit(1)
}
json, err := vm.EvaluateSnippet(filename, string(snippet))
if err != nil {
fmt.Fprintf(os.Stderr, "%v\n", err.Error())
os.Exit(2)
vm.Importer(&jsonnet.FileImporter{
JPaths: config.evalJpath,
})
if config.cmd == commandEval {
if len(config.inputFiles) != 1 {
// Should already have been caught by processArgs.
panic(fmt.Sprintf("Internal error: Expected a single input file."))
}
filename := config.inputFiles[0]
input, err := readInput(config, &filename)
if err != nil {
var op string
switch typedErr := err.(type) {
case *os.PathError:
op = typedErr.Op
err = typedErr.Err
}
if op == "open" {
fmt.Fprintf(os.Stderr, "Opening input file: %s: %s\n", filename, err.Error())
} else if op == "read" {
fmt.Fprintf(os.Stderr, "Reading input file: %s: %s\n", filename, err.Error())
} else {
fmt.Fprintf(os.Stderr, err.Error())
}
os.Exit(1)
}
var output string
var outputArray []string
var outputDict map[string]string
if config.evalMulti {
outputDict, err = vm.EvaluateSnippetMulti(filename, input)
} else if config.evalStream {
outputArray, err = vm.EvaluateSnippetStream(filename, input)
} else {
output, err = vm.EvaluateSnippet(filename, input)
}
if err != nil {
fmt.Fprintln(os.Stderr, err.Error())
os.Exit(1)
}
// Write output JSON.
if config.evalMulti {
err := writeMultiOutputFiles(outputDict, config.evalMultiOutputDir, config.outputFile)
if err != nil {
fmt.Fprintln(os.Stderr, err.Error())
os.Exit(1)
}
} else if config.evalStream {
err := writeOutputStream(outputArray, config.outputFile)
if err != nil {
fmt.Fprintln(os.Stderr, err.Error())
os.Exit(1)
}
} else {
err := writeOutputFile(output, config.outputFile)
if err != nil {
fmt.Fprintln(os.Stderr, err.Error())
os.Exit(1)
}
}
} else if config.cmd == commandFmt {
// Should already have been caught by processArgs.
panic(fmt.Sprintf("Internal error: No jsonnet fmt."))
} else {
panic(fmt.Sprintf("Internal error (please report this): Bad cmd value: %d\n", config.cmd))
}
fmt.Print(json)
}

58
jsonnet/cmd_test.go Normal file
View File

@ -0,0 +1,58 @@
/*
Copyright 2017 Google Inc. All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"testing"
)
func testEq(a, b []string) bool {
if a == nil && b == nil {
return true
}
if a == nil || b == nil {
return false
}
if len(a) != len(b) {
return false
}
for i := range a {
if a[i] != b[i] {
return false
}
}
return true
}
func testSimplifyAux(t *testing.T, name string, input, expected []string) {
t.Run(name, func(t *testing.T) {
got := simplifyArgs(input)
if !testEq(got, expected) {
t.Fail()
t.Errorf("Got %v, expected %v\n", got, expected)
}
})
}
func TestSimplifyArgs(t *testing.T) {
testSimplifyAux(t, "empty", []string{}, []string{})
testSimplifyAux(t, "-a", []string{"-a"}, []string{"-a"})
testSimplifyAux(t, "-a -b", []string{"-a", "-b"}, []string{"-a", "-b"})
testSimplifyAux(t, "-a -c -b", []string{"-a", "-c", "-b"}, []string{"-a", "-c", "-b"})
testSimplifyAux(t, "-abc", []string{"-abc"}, []string{"-a", "-b", "-c"})
testSimplifyAux(t, "-acb", []string{"-acb"}, []string{"-a", "-c", "-b"})
}

View File

@ -122,12 +122,15 @@ func TestMain(t *testing.T) {
}
input := read(test.input)
output, err := vm.evaluateSnippet(test.name, string(input))
rawOutput, err := vm.evaluateSnippet(test.name, string(input), evalKindRegular)
var output string
if err != nil {
// TODO(sbarzowski) perhaps somehow mark that we are processing
// an error. But for now we can treat them the same.
output = errFormatter.format(err)
output += "\n"
} else {
output = rawOutput.(string)
}
if *update {
err := ioutil.WriteFile(test.golden, []byte(output), 0666)
@ -169,7 +172,7 @@ type errorFormattingTest struct {
func genericTestErrorMessage(t *testing.T, tests []errorFormattingTest, format func(RuntimeError) string) {
for _, test := range tests {
vm := MakeVM()
output, err := vm.evaluateSnippet(test.name, test.input)
rawOutput, err := vm.evaluateSnippet(test.name, test.input, evalKindRegular)
var errString string
if err != nil {
switch typedErr := err.(type) {
@ -178,8 +181,8 @@ func genericTestErrorMessage(t *testing.T, tests []errorFormattingTest, format f
default:
t.Errorf("%s: unexpected error: %v", test.name, err)
}
}
output := rawOutput.(string)
if errString != test.errString {
t.Errorf("%s: error result does not match. got\n\t%+#v\nexpected\n\t%+#v",
test.name, errString, test.errString)

79
vm.go
View File

@ -31,14 +31,13 @@ import (
// VM is the core interpreter and is the touchpoint used to parse and execute
// Jsonnet.
type VM struct {
MaxStack int
MaxTrace int // The number of lines of stack trace to display (0 for all of them).
ext vmExtMap
tla vmExtMap
nativeFuncs map[string]*NativeFunction
importer Importer
ef ErrorFormatter
StringOutput bool
MaxStack int
ext vmExtMap
tla vmExtMap
nativeFuncs map[string]*NativeFunction
importer Importer
ErrorFormatter ErrorFormatter
StringOutput bool
}
// External variable or top level argument provided before execution
@ -55,12 +54,12 @@ type vmExtMap map[string]vmExt
// MakeVM creates a new VM with default parameters.
func MakeVM() *VM {
return &VM{
MaxStack: 500,
ext: make(vmExtMap),
tla: make(vmExtMap),
nativeFuncs: make(map[string]*NativeFunction),
ef: ErrorFormatter{pretty: true, colorful: true, MaxStackTraceSize: 20},
importer: &FileImporter{},
MaxStack: 500,
ext: make(vmExtMap),
tla: make(vmExtMap),
nativeFuncs: make(map[string]*NativeFunction),
ErrorFormatter: ErrorFormatter{pretty: false, colorful: false, MaxStackTraceSize: 20},
importer: &FileImporter{},
}
}
@ -89,7 +88,15 @@ func (vm *VM) Importer(i Importer) {
vm.importer = i
}
func (vm *VM) evaluateSnippet(filename string, snippet string) (output string, err error) {
type evalKind = int
const (
evalKindRegular = iota
evalKindMulti = iota
evalKindStream = iota
)
func (vm *VM) evaluateSnippet(filename string, snippet string, kind evalKind) (output interface{}, err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("(CRASH) %v\n%s", r, debug.Stack())
@ -99,7 +106,14 @@ func (vm *VM) evaluateSnippet(filename string, snippet string) (output string, e
if err != nil {
return "", err
}
output, err = evaluate(node, vm.ext, vm.tla, vm.nativeFuncs, vm.MaxStack, vm.importer, vm.StringOutput)
switch kind {
case evalKindRegular:
output, err = evaluate(node, vm.ext, vm.tla, vm.nativeFuncs, vm.MaxStack, vm.importer, vm.StringOutput)
case evalKindMulti:
output, err = evaluateMulti(node, vm.ext, vm.tla, vm.nativeFuncs, vm.MaxStack, vm.importer, vm.StringOutput)
case evalKindStream:
output, err = evaluateStream(node, vm.ext, vm.tla, vm.nativeFuncs, vm.MaxStack, vm.importer)
}
if err != nil {
return "", err
}
@ -116,11 +130,38 @@ func (vm *VM) NativeFunction(f *NativeFunction) {
//
// The filename parameter is only used for error messages.
func (vm *VM) EvaluateSnippet(filename string, snippet string) (json string, formattedErr error) {
json, err := vm.evaluateSnippet(filename, snippet)
output, err := vm.evaluateSnippet(filename, snippet, evalKindRegular)
if err != nil {
return "", errors.New(vm.ef.format(err))
return "", errors.New(vm.ErrorFormatter.format(err))
}
return json, nil
json = output.(string)
return
}
// EvaluateSnippetStream evaluates a string containing Jsonnet code to an array.
// The array is returned as an array of JSON strings.
//
// The filename parameter is only used for error messages.
func (vm *VM) EvaluateSnippetStream(filename string, snippet string) (docs []string, formattedErr error) {
output, err := vm.evaluateSnippet(filename, snippet, evalKindStream)
if err != nil {
return nil, errors.New(vm.ErrorFormatter.format(err))
}
docs = output.([]string)
return
}
// EvaluateSnippetStream evaluates a string containing Jsonnet code to an array.
// The array is returned as an array of JSON strings.
//
// The filename parameter is only used for error messages.
func (vm *VM) EvaluateSnippetMulti(filename string, snippet string) (files map[string]string, formattedErr error) {
output, err := vm.evaluateSnippet(filename, snippet, evalKindMulti)
if err != nil {
return nil, errors.New(vm.ErrorFormatter.format(err))
}
files = output.(map[string]string)
return
}
func snippetToAST(filename string, snippet string) (ast.Node, error) {